关于c#:你所不知道的-C-10新特性

40次阅读

共计 7752 个字符,预计需要花费 20 分钟才能阅读完成。

咱们很快乐地发表 C# 10 作为 .NET 6 和 Visual Studio 2022 的一部分曾经公布了。在这篇文章中,咱们将介绍 C# 10 的许多新性能,这些性能使您的代码更丑陋、更具表现力和更快 .

浏览 Visual Studio 2022 布告 和 .NET 6 布告 以理解更多信息,包含如何装置。

全局和隐式 usings

using 指令简化了您应用命名空间的形式。C# 10 包含一个新的全局 using 指令和隐式 usings,以缩小您须要在每个文件顶部指定的 usings 数量。

全局 using 指令

如果关键字 global 呈现在 using 指令之前,则 using 实用于整个我的项目:

global using System;

您能够在全局 using 指令中应用 using 的任何性能。例如,增加动态导入类型并使该类型的成员和嵌套类型在整个我的项目中可用。如果您在 using 指令中应用别名,该别名也会影响您的整个我的项目:

global using static System.Console;
global using Env = System.Environment;

您能够将全局应用放在任何 .cs 文件中,包含 Program.cs 或专门命名的文件,如 globalusings.cs。全局 usings 的范畴是以后编译,个别对应以后我的项目。

无关详细信息,请参阅 全局 using 指令。

隐式 usings

隐式 usings 性能会主动为您正在构建的我的项目类型增加通用的全局 using 指令。要启用隐式 usings,请在 .csproj 文件中设置 ImplicitUsings 属性:

<PropertyGroup>
    <!-- Other properties like OutputType and TargetFramework -->
    <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

在新的 .NET 6 模板中启用了隐式 usings。在此博客文章中浏览无关 .NET 6 模板更改的更多信息。

一些特定全局 using 指令集取决于您正在构建的应用程序的类型。例如,控制台应用程序或类库的隐式 usings 不同于 ASP.NET 应用程序的隐式 usings。

无关详细信息,请参阅此隐式 usings 文章。

Combining using 性能

文件顶部的传统 using 指令、全局 using 指令和隐式 using 能够很好地协同工作。隐式 using 容许您在我的项目文件中蕴含适宜您正在构建的我的项目类型的 .NET 命名空间。全局 using 指令容许您蕴含其余命名空间,以使它们在整个我的项目中可用。代码文件顶部的 using 指令容许您蕴含我的项目中仅多数文件应用的命名空间。

无论它们是如何定义的,额定的 using 指令都会减少名称解析中呈现歧义的可能性。如果遇到这种状况,请思考增加别名或缩小要导入的命名空间的数量。例如,您能够将全局 using 指令替换为文件子集顶部的显式 using 指令。

如果您须要删除通过隐式 usings 蕴含的命名空间,您能够在我的项目文件中指定它们:

<ItemGroup>
  <Using Remove="System.Threading.Tasks" />
</ItemGroup>

您还能够增加命名空间,就像它们是全局 using 指令一样,您能够将 Using 项增加到我的项目文件中,例如:

<ItemGroup>
  <Using Include="System.IO.Pipes" />
</ItemGroup>

文件范畴的命名空间

许多文件蕴含单个命名空间的代码。从 C# 10 开始,您能够将命名空间作为语句蕴含在内,后跟分号且不带花括号:

namespace MyCompany.MyNamespace;

class MyClass // Note: no indentation
{...} 

他简化了代码并删除了嵌套级别。只容许一个文件范畴的命名空间申明,并且它必须在申明任何类型之前呈现。
无关文件范畴命名空间的更多信息,请参阅命名空间关键字文章。

对 lambda 表达式和办法组的改良

咱们对 lambda 的语法 和类型进行了多项改良。咱们预计这些将宽泛有用,并且驱动计划之一是使 ASP.NET Minimal API 更加简略。

lambda 的天然类型

Lambda 表达式当初有时具备“天然”类型。这意味着编译器通常能够推断出 lambda 表达式的类型。

到目前为止,必须将 lambda 表达式转换为委托或表达式类型。在大多数状况下,您会在 BCL 中应用重载的 Func<…> 或 Action<…> 委托类型之一:

Func<string, int> parse = (string s) => int.Parse(s);

然而,从 C# 10 开始,如果 lambda 没有这样的“指标类型”,咱们将尝试为您计算一个:

var parse = (string s) => int.Parse(s);

您能够在您最喜爱的编辑器中将鼠标悬停在 var parse 上,而后查看类型依然是 Func<string, int>。一般来说,编译器将应用可用的 Func 或 Action 委托(如果存在适合的委托)。否则,它将合成一个委托类型(例如,当您有 ref 参数或有大量参数时)。

并非所有 lambda 表达式都有天然类型——有些只是没有足够的类型信息。例如,放弃参数类型将使编译器无奈决定应用哪种委托类型:

var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda

lambda 的天然类型意味着它们能够调配给较弱的类型,例如 object 或 Delegate:

object parse = (string s) => int.Parse(s);   // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>

当波及到表达式树时,咱们联合了“指标”和“天然”类型。如果指标类型是 LambdaExpression 或非泛型 Expression(所有表达式树的基类型)并且 lambda 具备天然委托类型 D,咱们将改为生成 Expression<D>:

LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s);       // Expression<Func<string, int>>

办法组的天然类型

办法组(即没有参数列表的办法名称)当初有时也具备天然类型。您始终可能将办法组转换为兼容的委托类型:

Func<int> read = Console.Read;
Action<string> write = Console.Write;

当初,如果办法组只有一个重载,它将具备天然类型:

var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose

lambda 的返回类型

在后面的示例中,lambda 表达式的返回类型是不言而喻的,并被推断进去的。状况并非总是如此:

var choose = (bool b) => b ? 1 : "two"; // ERROR: Can't infer return type

在 C# 10 中,您能够在 lambda 表达式上指定显式返回类型,就像在办法或本地函数上一样。返回类型在参数之前。当你指定一个显式的返回类型时,参数必须用括号括起来,这样编译器或其余开发人员不会太混同:

var choose = object (bool b) => b ? 1 : "two"; // Func<bool, object>

lambda 上的属性

从 C# 10 开始,您能够将属性放在 lambda 表达式上,就像对办法和本地函数一样。当有属性时,lambda 的参数列表必须用括号括起来:

Func<string, int> parse = [Example(1)] (s) => int.Parse(s);
var choose = [Example(2)][Example(3)] object (bool b) => b ? 1 : "two";

就像本地函数一样,如果属性在 AttributeTargets.Method 上无效,则能够将属性利用于 lambda。

Lambda 的调用形式与办法和本地函数不同,因而在调用 lambda 时属性没有任何影响。然而,lambdas 上的属性对于代码剖析依然有用,并且能够通过反射发现它们。

structs 的改良

C# 10 为 structs 引入了性能,可在 structs(构造)和类之间提供更好的奇偶性。这些新性能包含无参数构造函数、字段初始值设定项、记录构造和 with 表达式。

无参数构造构造函数和字段初始值设定项
在 C# 10 之前,每个构造都有一个隐式的公共无参数构造函数,该构造函数将构造的字段设置为默认值。在结构上创立无参数构造函数是谬误的。

从 C# 10 开始,您能够蕴含本人的无参数构造构造函数。如果您不提供,则将提供隐式无参数构造函数以将所有字段设置为默认值。您在构造中创立的无参数构造函数必须是公共的并且不能是局部的:

public struct Address
{public Address()
    {City = "<unknown>";}
    public string City {get; init;}
}

您能够如上所述在无参数构造函数中初始化字段,也能够通过字段或属性初始化程序初始化它们:

public struct Address
{public string City { get; init;} = "<unknown>";
}

通过默认创立或作为数组调配的一部分创立的构造会疏忽显式无参数构造函数,并始终将构造成员设置为其默认值。无关构造中无参数构造函数的更多信息,请参阅构造类型。

Record structs
从 C# 10 开始,当初能够应用 record struct 定义 record。这些相似于 C# 9 中引入的 record 类:

public record struct Person
{public string FirstName { get; init;}
    public string LastName {get; init;}
}

您能够持续应用 record 定义记录类,也能够应用 record 类来分明地阐明。

构造曾经具备值相等——当你比拟它们时,它是按值。记录构造增加 IEquatable<T> 反对和 == 运算符。记录构造提供 IEquatable<T> 的自定义实现以防止反射的性能问题,并且它们包含记录性能,如 ToString() 笼罩。

记录构造能够是地位的,主构造函数隐式申明公共成员:

public record struct Person(string FirstName, string LastName);

主构造函数的参数成为记录构造的公共主动实现属性。与 record 类不同,隐式创立的属性是读 / 写的。这使得将元组转换为命名类型变得更加容易。将返回类型从 (string FirstName, string LastName) 之类的元组更改为 Person 的命名类型能够清理您的代码并保障成员名称统一。申明地位记录构造很容易并放弃可变语义。

如果您申明一个与次要结构函数参数同名的属性或字段,则不会合成任何主动属性并应用您的。

要创立不可变的记录构造,请将 readonly 增加到构造(就像您能够增加到任何构造一样)或将 readonly 利用于单个属性。对象初始化器是能够设置只读属性的结构阶段的一部分。这只是应用不可变记录构造的一种办法:

var person = new Person {FirstName = "Mads", LastName = "Torgersen"};

public readonly record struct Person
{public string FirstName { get; init;}
    public string LastName {get; init;}
}

在本文中理解无关记录构造的更多信息。

Record 类中 ToString() 上的密封修饰符
记录类也失去了改良。从 C# 10 开始,ToString() 办法能够蕴含 seal 修饰符,这会阻止编译器为任何派生记录合成 ToString 实现。

在本文中的记录中理解无关 ToString() 的更多信息。

构造和匿名类型的表达式
C# 10 反对所有构造的 with 表达式,包含记录构造,以及匿名类型:

var person2 = person with {LastName = "Kristensen"};

这将返回一个具备新值的新实例。您能够更新任意数量的值。您未设置的值将保留与初始实例雷同的值。

在本文中理解无关 with 的更多信息

内插字符串改良

当咱们在 C# 中增加内插字符串时,咱们总感觉在性能和表现力方面,应用该语法能够做更多事件。

内插字符串处理程序
明天,编译器将内插字符串转换为对 string.Format 的调用。这会导致很多调配——参数的装箱、参数数组的调配,当然还有后果字符串自身。此外,它在理论插值的含意上没有任何回旋余地。

在 C# 10 中,咱们增加了一个库模式,容许 API“接管”对内插字符串参数表达式的解决。例如,思考 StringBuilder.Append:

var sb = new StringBuilder();
sb.Append($"Hello {args[0]}, how are you?");

到目前为止,这将应用新调配和计算的字符串调用 Append(string? value) 重载,将其附加到 StringBuilder 的一个块中。然而,Append 当初有一个新的重载 Append(ref StringBuilder.AppendInterpolatedStringHandler handler),当应用内插字符串作为参数时,它优先于字符串重载。

通常,当您看到 SomethingInterpolatedStringHandler 模式的参数类型时,API 作者在幕后做了一些工作,以更失当地解决插值字符串以满足其目标。在咱们的 Append 示例中,字符串“Hello”、args[0] 和“,how are you?”将独自附加到 StringBuilder 中,这样效率更高且后果雷同。

有时您只想在特定条件下实现构建字符串的工作。一个例子是 Debug.Assert:

Debug.Assert(condition, $"{SomethingExpensiveHappensHere()}");

在大多数状况下,条件为真,第二个参数未应用。然而,每次调用都会计算所有参数,从而不必要地减慢执行速度。Debug.Assert 当初有一个带有自定义插值字符串构建器的重载,它确保第二个参数甚至不被评估,除非条件为假。

最初,这是一个在给定调用中理论更改字符串插值行为的示例:String.Create() 容许您指定 IFormatProvider 用于格式化插值字符串参数自身的洞中的表达式:
String.Create(CultureInfo.InvariantCulture, $"The result is {result}");

您能够在本文和无关创立自定义处理程序的本教程中理解无关内插字符串处理程序的更多信息。

常量内插字符串

如果内插字符串的所有洞都是常量字符串,那么生成的字符串当初也是常量。这使您能够在更多中央应用字符串插值语法,例如属性:

[Obsolete($"Call {nameof(Discard)} instead")]

请留神,必须用常量字符串填充洞。其余类型,如数字或日期值,不能应用,因为它们对文化敏感,并且不能在编译时计算。

其余改良

C# 10 对整个语言进行了许多较小的改良。其中一些只是使 C# 以您冀望的形式工作。

在解构中混合申明和变量

在 C# 10 之前,解构要求所有变量都是新的,或者所有变量都必须当时申明。在 C# 10 中,您能够混合:

int x2;
int y2;
(x2, y2) = (0, 1);       // Works in C# 9
(var x, var y) = (0, 1); // Works in C# 9
(x2, var y3) = (0, 1);   // Works in C# 10 onwards

在无关解构的文章中理解更多信息。

改良的明确调配

如果您应用尚未明确调配的值,C# 会产生谬误。C# 10 能够更好地了解您的代码并且产生更少的虚伪谬误。这些雷同的改良还意味着您将看到更少的针对空援用的虚伪谬误和正告。

在 C# 10 中的新增性能文章中理解无关 C# 确定赋值的更多信息。

扩大的属性模式

C# 10 增加了扩大属性模式,以便更轻松地拜访模式中的嵌套属性值。例如,如果咱们在下面的 Person 记录中增加一个地址,咱们能够通过以下两种形式进行模式匹配:

object obj = new Person
{
    FirstName = "Kathleen",
    LastName = "Dollard",
    Address = new Address {City = "Seattle"}
};

if (obj is Person { Address: { City: "Seattle"} })
    Console.WriteLine("Seattle");

if (obj is Person { Address.City: "Seattle"}) // Extended property pattern
    Console.WriteLine("Seattle");

扩大属性模式简化了代码并使其更易于浏览,尤其是在匹配多个属性时。

在模式匹配文章中理解无关扩大属性模式的更多信息。

调用者表达式属性

CallerArgumentExpressionAttribute 提供无关办法调用上下文的信息。与其余 CompilerServices 属性一样,此属性利用于可选参数。在这种状况下,一个字符串:

void CheckExpression(bool condition, 
    [CallerArgumentExpression("condition")] string? message = null )
{Console.WriteLine($"Condition: {message}");
}

传递给 CallerArgumentExpression 的参数名称是不同参数的名称。作为参数传递给该参数的表达式将蕴含在字符串中。例如,

var a = 6;
var b = true;
CheckExpression(true);
CheckExpression(b);
CheckExpression(a > 5);

// Output:
// Condition: true
// Condition: b
// Condition: a > 5

ArgumentNullException.ThrowIfNull() 是如何应用此属性的一个很好的示例。它通过默认提供的值来防止必须传入参数名称:

void MyMethod(object value)
{ArgumentNullException.ThrowIfNull(value);
}

理解无关 CallerArgumentExpressionAttribute 的更多信息

完结

装置 .NET 6 或 Visual Studio 2022,享受 C# 10,并通知咱们您的想法!

正文完
 0