Always keep a beg|

石起起

园龄:1年10个月粉丝:1关注:0

2025-02-05 17:53阅读: 14评论: 0推荐: 0

C# 版本 10 新增特性

发布时间:2021 年 11 月

C# 10 向 C# 语言添加了以下功能和增强功能:


参考文章:
⭐⭐ C# 10 完整特性介绍 - hez2010 - 博客园
一探即将到来的 C# 10 - hez2010 - 博客园
C# 10.0 新特性解析-腾讯云开发者社区-腾讯云
.net - C# 10的新特性 - 个人文章 - SegmentFault 思否
C# 10 特性一览 - 哔哩哔哩


记录结构

C# 9 有一个新的数据类型,叫做记录(Record)。这个类型是一种特殊的引用类型,我们只需要给出一个东西的具体属性,就可以自动为这个类型生成指定的比较器(Equals 方法、比较运算符 operator == 和 operator !=、GetHashCode 方法,甚至是 ToString 方法等等)。

举个例子,我们可以这么写:

public sealed record Point(int X, int Y);

这就等价于一个类 Point,然后生成 X 和 Y 属性,以及相关的方法:

public sealed class Point
{
    public int X { get; init; }
    public int Y { get; init; }

    public override string ToString() => $"Point {{ X = {X}, Y = {Y} }}";
    public override bool Equals(object? obj) => obj is Point p && p.X == X && p.Y == y;
    public override int GetHashCode() => HashCode.Combine(X, Y);

    public static bool operator ==(Point l, Point r) => l.Equals(r);
    public static bool operator !=(Point l, Point r) => !(l == r);
}

这样的东西。你看看,就写一句话就能生成一系列的内容,是不是很方便。

不过,Point 类型就俩属性,显然没有必要定义成类,因为它太轻量级了。因此,C# 10 开始允许结构记录类型。

public record struct Point(int X, int Y);

这样就好比把前文的 sealed class 改写成 struct。因此 C# 10 开始允许结构的记录类型,所以更轻量级,灵活度更高了。当然,C# 10 依然允许引用类型(类)的记录类型,你可以使用 record 或者 record class 来表示一个类的记录类型。

前文用到的 init 属性是 C# 9 里诞生的、用来表示属性只在初始化器和构造器里才可赋值的一种属性赋值模型。它比 set 的使用范围要少,set 随时随地都可以赋值(最多加一个访问修饰符,但并不能阻止内部的任何时候的赋值)。init 仅允许初始化器和构造器里使用赋值,其它任何地方都不能赋值。这样做就避免了很多地方的安全问题,同时也提供了一种语法上的约束。

结构类型的改进

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

内插字符串处理程序

❓ 本条表达不清晰,什么是处理程序?以下的笔记貌似与标题不匹配。

常量字符串插值 (❓和下主题重复)

你可以给 const string 使用字符串插值了,非常方便:

const string x = "hello"; 
const string y = $"{x}, world!";

用法扩展

简单内插字符串
int age = 30; 
string result = $"I am {age} years old."; 
Console.WriteLine(result); // 输出: I am 30 years old.
内插表达式
int a = 5;
int b = 10;
string result = $"The sum of {a} and {b} is {a + b}.";
Console.WriteLine(result);  // 输出: The sum of 5 and 10 is 15.
内插字符串中的格式化

你可以使用格式说明符来控制内插字符串中数字的格式。

double pi = 3.141592653589793;
string result = $"Pi rounded to 2 decimal places is {pi:F2}.";
Console.WriteLine(result);  // 输出: Pi rounded to 2 decimal places is 3.14.
内插字符串中的条件表达式
int score = 85;
string grade = score >= 90 ? "A" : (score >= 80 ? "B" : (score >= 70 ? "C" : "F"));
string result = $"You scored {score} and received a grade of {grade}.";
Console.WriteLine(result);  // 输出: You scored 85 and received a grade of B.
内插字符串中的对象属性
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
 
Person person = new Person { Name = "Alice", Age = 30 };
string result = $"The person's name is {person.Name} and they are {person.Age} years old.";
Console.WriteLine(result);  // 输出: The person's name is Alice and they are 30 years old.
内插字符串中的文化信息

你可以使用IFormatProvider参数来指定文化信息。

double money = 1234567.89;
string result = string.Format(new System.Globalization.CultureInfo("fr-FR"), 
                              $"You have {money:C} in your bank account.");
Console.WriteLine(result);  // 输出: You have 1 234 567.89 € in your bank account.

global using 指令

利用全局 using 可以给整个项目启用 usings,不再需要每个文件都写一份。比如你可以创建一个 Import.cs,然后里面写:

using System; using i32 = System.Int32;

然后你整个项目都无需再 using System,并且可以用 i32 了。


如果关键字 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
{ ... } 

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

扩展属性模式

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");

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

在模式匹配文章中了解有关扩展属性模式的更多信息。


也叫:👉嵌套属性模式匹配改进
以前在匹配嵌套属性的时候需要这么写:
if (a is { X: { Y: { Z: 4 } } }) { ... }
现在只需要简单的:
if (a is { X.Y.Z: 4 }) { ... }
就可以了。

对 Lambda 表达式的改进 ⭐⭐⭐

lambda 的自然类型

说明:此改进也叫 lambda 支持自然委托类型

lambda 现在会自动创建自然委托类型,于是不再需要写出类型了。

var f = () => 1; // Func<int> 
var g = string (int x, string y) => $"{y}{x}"; // Func<int, string, string> 
var h = "test".GetHashCode; // Func<int>

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 的返回类型

说明: 支持返回类型

f = int () => 4;

在前面的示例中,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 上的属性(特性❓)

说明: lambda 可以带 attribute

f = [Foo] (x) => x; // 给 lambda 设置 
f = [return: Foo] (x) => x; // 给 lambda 返回值设置 
f = ([Foo] x) => x; // 给 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 上的属性对于代码分析仍然有用,并且可以通过反射发现它们。

支持 ref、in、out 等修饰

f = ref int (ref int x) => ref x; // 返回一个参数的引用

头等函数

头等函数:头等函数的概念指的是函数能够像其他数据类型一样被当作一等公民来使用。即:函数可被赋值给变量;函数作为参数传递给其他函数;从其他函数返回。

函数可以隐式转换到 delegate,于是函数上升至头等函数

void Foo() { Console.WriteLine("hello"); } 
var x = Foo; 
x(); // hello

可使用 const 内插字符串

你可以给 const string 使用字符串插值了,非常方便:

const string x = "hello"; 
const string y = $"{x}, world!";

记录类型可密封 ToString()

之前 record 的 ToString 是不能修饰为 sealed 的,因此如果你继承了一个 record,相应的 ToString 行为也会被改变,因此这是个虚方法。

但是现在你可以把 record 里的 ToString 方法标记成 sealed,这样你的 ToString 方法就不会被重写了。

改进型明确赋值

如果您使用尚未明确分配的值,C# 会产生错误。 C# 10 可以更好地理解您的代码并且产生更少的虚假错误。 这些相同的改进还意味着您将看到更少的针对空引用的虚假错误和警告。

在 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 

在有关解构的文章中了解更多信息。

可在方法上使用 AsyncMethodBuilder 属性

C# 10 将允许方法上使用 [AsyncMethodBuilder(...)] 来使用你自己实现的 async method builder,代替自带的 Task 或者 ValueTask 的异步方法构造器。这也有助于你自己实现零开销的异步方法。

CallerArgumentExpression 属性

调用者表达式属性

现在,CallerArgumentExpression 这个 attribute 终于有用了。借助这个 attribute,编译器会自动填充调用参数的表达式字符串,例如:

void Foo(int value, [CallerArgumentExpression("value")] string? expression = null) 
{     
	Console.WriteLine(expression + " = " + value);
}

当你调用 Foo(4 + 5) 时,会输出 4 + 5 = 9。这对测试框架极其有用,因为你可以输出 assert 的原表达式了:

static void Assert(bool value, [CallerArgumentExpression("value")] string? expr = null) 
{     
	if (!value) throw new AssertFailureException(expr);
}

增强的 #line pragma

以前 #line 只能用来指定一个文件中的某一行,现在可以指定行列和范围了,这对写编译器和代码生成器的人非常有用:

#line (startLine, startChar) - (endLine, endChar) charOffset "fileName" 
// 比如 #line (1, 1) - (2, 2) 3 "test.cs"

其它主题

struct 支持无参构造

一直以来 struct 不支持无参构造函数,现在支持了:

struct Foo { public int X; public Foo() { X = 1; } }

但是使用的时候就要注意了,因为无参构造函数的存在使得 new struct() 和 default(struct) 的语义不一样了,例如 new Foo().X == default(Foo).X 在上面这个例子中将会得出 false

匿名对象的 with

可以用 with 来根据已有的匿名对象创建新的匿名对象了:

var x = new { A = 1, B = 2 };
var y = x with { A = 3 };

这里 y.A 将会是 3 。

tuple 支持混合定义和使用

比如:

int y = 0; (var x, y, var z) = (1, 2, 3);

于是 y 就变成 2 了,同时还创建了两个变量 x 和 z,分别是 1 和 3 。

接口支持抽象静态方法 ❓

这个特性将会在 .NET 6 作为 preview 特性放出,意味着默认是不启用的,需要设置 <LangVersion>preview</LangVersion> 和 <EnablePreviewFeatures>true</EnablePreviewFeatures>,然后引入一个官方的 nuget 包 System.Runtime.Experimental 来启用。

然后接口就可以声明抽象静态成员了,.NET 的类型系统正式具备虚静态方法分发能力。

例如,你想定义一个可加而且有零的接口 IMonoid<T>

interface IMonoid<T> where T : IMonoid<T> 
{ 
	abstract static T Zero { get; } 
	abstract static T operator+(T l, T r); 
}

然后可以对其进行实现,例如这里的 MyInt:

public class MyInt : IMonoid<MyInt> 
{ 
	public MyInt(int val) { Value = val; } 
	public static MyInt Zero { get; } = new MyInt(0);
	public static MyInt operator+(MyInt l, MyInt r) => new MyInt(l.Value + r.Value); 
	 
	public int Value { get; } 
}

然后就能写出一个方法对 IMoniod<T> 进行求和了,这里为了方便写成扩展方法:

public static class IMonoidExtensions 
{ 
	public static T Sum<T>(this IEnumerable<T> t) where T : IMonoid<T>
	 { 
		 var result = T.Zero; 
		 foreach (var i in t) result += i;
		  return result; 
	 }
}

最后调用:

List<MyInt> list = new() { new(1), new(2), new(3) }; Console.WriteLine(list.Sum().Value); // 6

你可能会问为什么要引入一个 System.Runtime.Experimental,因为这个包里面包含了 .NET 基础类型的改进:给所有的基础类型都实现了相应的接口,比如给数值类型都实现了 INumber<T>,给可以加的东西都实现了 IAdditionOperators<TLeft, TRight, TResult> 等等,用起来将会非常方便,比如你想写一个函数,这个函数用来把能相加的东西加起来:

T Add<T>(T left, T right) where T : IAdditionOperators<T, T, T>
{ 
	return left + right;
}

就搞定了。

接口的静态抽象方法支持和未来 C# 将会加入的 shape 特性是相辅相成的,届时 C# 将利用 interface 和 shape 支持 Haskell 的 class、Rust 的 trait 那样的 type classes,将类型系统上升到一个新的层次。

泛型的 attribute

是的你没有看错,C# 的 attributes 支持泛型了,不过 .NET 6 中将以预览特定放出,因此需要 <LangVersion>preview</LangVersion>

class TestAttribute<T> : Attribute
{ 
	public T Data { get; } 
	public TestAttribute(T data) { Data = data; } 
}

然后你就能这么用了:

[Test<int>(3)] 
[Test<float>(4.5f)] 
[Test<string>("hello")]

推广到集合类型的 params 参数

在早期的c#,params 参数后的参数类型必须是数组。由于数组本身比较笨重的缘故(引用类型),所以就很不利于扩展性能。因此,c# 10 开始允许用户使用 params 集合的模式来声明参数。比如说:

static class OneAllocation
{
	static void Use(params Span<string> spans)
	{
		//...
	}

	static void Go()
	{
		Use("jaredpar");
		Use("hello", "world");
		Use("a", "longer", "set");
	}
}

如例子所示,我们允许参数使用 params Span<string> 这样的东西,就可以对字符串进行一些操作,还可以使用变长参数的模型,因此相当方便且有可读性。

另外,按照次序,如果有这样四个方法的重载的话,C# 会优先采用效率高的 ReadOnlySpan<T> 类型作为调用方,然后是 Span<string>T[],最后才是 `IEnumerable

其实也很好记。ReadOnlySpan<T>Span<T> 一定是效率最高的,因为它们仅存储在栈内存里;但是,ReadOnlySpan<T> 不可变,所以相较于后者来说,它更为安全;接着,T[]IEnumerable<T> 里,显然是数组更好用,然后才是迭代集合 IEnumerable<T>,因为迭代集合要使用很复杂的迭代器模式来迭代,是一种耗费性能较高的类型;但为了可读性,C# 依旧允许了这一个类型作为 params 参数。

更高效、不装箱拆箱的内插字符串 ❓

在允许了前文给的特性后,内插字符串就可以使用更为高级的语法,来避免装箱和拆箱了。在 C# 6 里,内插字符串会被自动翻译为 string.Format 的调用。而很遗憾的是,这个方法的参数都是 object 类型的可变参数序列,因而值类型在传入的时候必然导致装箱,损失了性能。在允许了 [推广到集合类型的params参数] 后,string.Format 就有了新的实现方式:

string Format(string format, params ReadOnlySpan<Variant> arguments)
{
	//...
}

这里的 Variant 类型在 C# 团队实现后才会给出。这里你可以理解成一个“任何类型都可以兼容的值类型”。换句话说,任何类型都可以转这个类型来存储,类似 object,只是这里用 Variant 表达出来可以避免装箱(因为是值类型);至于引用类型,传过来就会被改成地址什么的,总之不会装箱就对了。

其它的问题和解答,你可以查看这篇文档(英文原文档)。

default(T) 表达式

另外,由于值类型和引用类型的默认数值不同的关系,定义了无参构造器必然会影响到它的默认数值 default(T) 表达式。实际上真的是这样吗?并不是。还是拿 Point 类型举例。即使你给出了默认构造器的调用,default(Point) 依旧还是原始数据的原始数据类型的默认数值构造成的实例的结果。

default(T) 返回的是类型的默认值?👀 需要查资料学习一下。

readonly struct Point
{
	public Point() { X = int.MinValue; Y = int.MinValue; }
	public int X { get; }
	public int Y { get; }
}

那么,default(Point) 的结果是 Point { X = 0, Y = 0}, 而不是 Point { X = -2147483647, Y = -2147483647 }

我们把“原始数据的原始数据类型的默认数值构造成的实例”叫做零初始化实例(Zeroed Instance),那么,default(T) 的定义就可以缩减为“该类型的零初始化实例”;换句话说,该类型的零初始化实例就是这个值类型的默认数值。那么,使用 default 表达式的时候,就算你定义了无参构造器,编译器也会始终忽略它。

参数的 nameof 表达式

[CheckNull(nameof(obj))]
static void ThrowIfNull(object? obj)
{
	//..
}

增强对是否为 null 的对象的代码分析 ❓

C# 8 诞生了可空引用类型的概念,并提供了基本的分析模型。但很遗憾的是,很多时候,编译器依旧无法识别对象已经不可能是 null 的情况,进而产生语义分析上的 bug。C# 10 提出了增强分析的概念,这样的话,很多原本是 bug 的情况就得到了解决。举个例子。

if ((c != null && c.M(out object? obj1)) == true)
{
    obj1.ToString(); // undesired error
}

bool M([NotNullWhen(true)] out object? obj)
{
    // ...
}

比如这个例子下,c 不可空后,调用 M 方法后,如果返回值为 true 的话,参数 obj1 就不能为空了。而编译器暂时无法识别这种变量的传递(最开始是从 c != null 开始的),因此分析这个地方的时候,obj1 仍然不知道是不是为 null。

C# 10 会对这样类似的场景的分析进行修复。

ref 和 partial 关键字的顺序

在诞生了 ref struct 这种类型之后,C# 由于没有考虑到语法的灵活性,因此如果 ref struct 是分部类型的话,就必须写成 ref partial struct,而调转顺序 partial ref struct 则是错误的写法。

C# 10 将对这个问题进行修复。

可空校验标记符 !! ❓❗

如果一个参数为 null,我们期望使用 throw 语句来产生异常信息。于是,代码大概就长这样:

void SomeMethod(string str)
{
    if (str is null)
    {
        throw new ArgumentNullException(nameof(str));
    }

    // ...
}

C# 10 将允许我们直接添加双叹号到 str 参数声明上,这样的话,if 和异常抛出的语句就可以不写了。

void SomeMethod(string str!!) // Here.
{
    // ...
}

双叹号❓ 目前使用的时候发现在变量之后使用一个叹号即可,是不是c#11或12又更新这个内容了——变量之后的一个叹号表示非空标记
扩展:::又冒号用于对名称空间起别名

对于值类型的可空引用类型的可空校验

同上。

解构 default 表达式

如果我们有这么一句话在 C# 里是允许的话:

(int arg1, double arg2) = default;

你觉得,arg1 和 arg2 会是多少呢?C# 10 里将允许这个写法,这样的话,arg1 和 arg2 就会默认赋值 0。

元组表达式里内联变量定义

这句话不好理解。举个例子。

int age;
(name, age) = student;

假如,name 是本身就有的东西(它可能是属性,或者是字段,或者是临时变量),而 age 仅仅是一个临时变量。如果你要把右侧的 student 变量解构了的话,由于 age 的定义变量语句无法写到赋值语句里面去,所以只能分开成两行书写。

C# 10 将允许你内嵌定义语句到值元组赋值的语句里去。

(name, int age) = student;

这样就合二为一了。name 照旧赋值,而 age 则完成了定义变量和赋值两个操作,且都在这一行里就可以完成。方便了不少。

模式匹配:集合模式

从 C# 7 开始,模式匹配就是一种特别高大上的语法,搞得别的编程语言纷纷效仿。C# 7 里允许了在 switch 语句里使用 when 从句,并同时允许了 is 表达式里内联变量定义;C# 8 里允许了递归模式匹配;C# 9 里则又多了 and、not 和 or 的逻辑模式匹配。C# 10 这次带来的是集合模式匹配。

集合模式匹配可以对集合的元素进行解构、处理和判别。

长度模式

长度模式听起来好像是在判断集合的长度,但是这一点不是可以用 Length 属性的递归模式作为判断对象吗?是的,长度模式并不是这个意思。

长度模式用中括号来获取数据元素,然后通过冒号和模式来表达一个对象的指定索引位置上的数据是不是满足这个模式。举个例子:

if (arr is { [2]: int val and not -1 })
{
    // ...
}

当 arr 必须是带 Length 或 Count 属性的类型,且拥有一个以 int 类型作为参数的索引器时候,就可以用这个模式写法。这句话表示,当 arr 数据的第 3 个元素是一个整数,且不是 -1 的时候,满足条件。

如果 arr 在这里不能确定和断定是不是包含 Length 或 Count 属性,或者是 int 类型作参数的索引器的时候,那么编译器就会告诉你,arr 由于无法断定类型,所以无法使用该模式来对数据进行校验。

上方语法等价于 arr[2] is int val && val != -1。你可能不一定能理解,为什么 arr[2] is int val 这句话要写出来。因为 arr 的元素类型尚不清楚,换句话说就是,arr 可能是一个 object?[],所以无法直接取数据;而使用 == 会引起数据比较的错误:因为左侧是 object 类型,右侧是 int 类型,等号就会认为是俩 object 的引用比较,所以是错误的用法。

总结一下。只要类型:

  • 有一个 this[int] 的索引器,且必须有 get 语句;
  • 有一个叫做 Length 或者是 Count 的属性。

那么类型就可以使用长度模式对具体某个元素作匹配。

集合模式

长度模式我们说完了,接下来说一下集合模式。当长度模式要连续拼接多个元素判断的时候,长度模式就显得很麻烦了。那么,集合模式就出现了。

举个例子。如果我们拿到了一个集合,但我们不知道集合的元素都有些什么,那么,我们最初的写法可能就成这样了:

if (obj is int[] arr && arr[0] == 1 && arr[1] == 2 && arr[2] == 3)
{
    // OK.
}

显然,这种写法就很让人头疼。因为不知道 obj 的类型就开始判断对象的数据了,显然是很麻烦的。C# 10 提供了一种轻快的语法:

if (obj is int[] { 1, 2, 3, .. })
{
    // OK.
}

我们来说一下,这个写法是啥意思。首先,{ 1, 2, 3, .. } 是集合模式。它表示判别的序列,要满足前三个元素和这里的顺序一一匹配的 1、2、3。双小数点记号(范围记号,下面的切片模式会用到这个符号) .. 在 C# 8 里就有,它表示取序列的一部分。在模式匹配里(就是这里),这个记号表达的是“后面还有别的数据,不过我们不作验证了”。换句话说,{ 1, 2, 3 } 和 { 1, 2, 3, .. } 是两个不同的意思:前者表示必须序列只有三个元素,且必须依次是 1、2、3;而后者则可表示元素至少有三个,只要前三个顺次是 1、2、3 就可以了;后面不管是啥都行。

啰嗦一下。由于 int[] 和 { 1, 2, 3, .. } 这两个模式是分开的两个匹配逻辑(一个是类型判断,一个是数据判断),且它们是且的关系,因此按道理来说,是可以加 and 在中间的:is int[] and { 1, 2, 3, .. }。实际上可以吗?可以的。但是没有必要,因为 C# 知道这里是两个判断的关系。所以这里的 int[] { 1, 2, 3, .. } 虽然看起来有点像数组的初始化器,但编译器是知道这里不是在初始化数组,而是一个判断的两个条件(一个判断类型的条件和一个判断数据的条件)。

不过,啥样的数据类型可以使用和校验呢?既然对象可以解析数据,那么前面要满足长度模式的要求必须都得满足。因此:

  • 有一个 this[int] 的索引器,且必须有 get 语句;
  • 有一个叫做 Length 或者是 Count 的属性。

依然是这样的条件。

切片模式

那么,集合貌似有了判断模式了,好像差不多可以结束了。C# 10 还提供了一种新鲜的语法,除了以数据为单位判断,还可以以数据序列作为切片。

C# 8 里有这么一种语法:arr[0..10],这表示把 arr 的前 10 个元素取出来,而这个语法叫切片(Slice)。只要有一个方法 Slice(int, int) 定义切片的逻辑,那么对象就可以切片了。C# 10 里允许我们使用切片到模式匹配里。举个例子,expr is { 1, .. var s, 3 } 就表示我们对中间的序列作切片,然后切片结果用 s 变量表示,因此,这个写法等价于 expr.Length >= 2 && expr[0] == 1 && expr[^1] == 3 && expr[1..^1] is var s。特别要注意的是,这里第一个条件并不是直接数值判断,而是长度判断:expr.Length >= 2。这是因为后续的条件无法保证对象的长度是多少,贸然取值会导致 IndexOutOfRangeException 的异常。

那么,给一些例子给你看看。

expr is { _, .., 1 } // expr.Length is >= 2 && expr[^1] is 1
expr is { .., _, 1 } // expr.Length is >= 2 && expr[^1] is 1
expr is { _, 1, .. } // expr.Length is >= 2 && expr[1] is 1
expr is { .., 1, _ } // expr.Length is >= 2 && expr[^2] is 1

这里的下划线是弃元(Discard)语法。我们不在意这里的数据是多少,但必须要占位来表示这里是集合的第几个元素,就使用弃元来表达(后两种情况就用到了占位这个概念)。 另请注意,这里 expr 是不知道啥类型的,所以可能集合内的元素都是 object。因此在注释里用的是 is 而不是 ==。

当然了,如果只有一个切片的范围记号 ..,而不判断数据的话(即 { .. }),就等价于 obj.Length >= 0 这个条件,或者 obj is { Length: >= 0 }。另外,后者这个写法还比前者多判断一下 null:obj is not null and { Length: >= 0 };而前者只判断 Length 属性是不是至少为 0。

混用模式匹配

我们来总结一下 C# 7 开始允许的所有模式匹配:

  • C# 7

  • expr is T value(增强 is 模式)

  • expr is var variable(var 模式)

  • expr is var (value1, value2, value3)(解构模式)

  • expr is var (_, _, value3, _)(弃元模式)

  • C# 8

  • expr is { Property1: value1, Property2: { InnerProperty: value2 } }(递归模式)

  • expr is { } notNullResult(空递归模式/不可空校验模式)

  • C# 9

  • expr is var value and not (value1 or value2)(逻辑模式)

  • C# 10

  • expr is { [index]: value }(长度模式)

  • expr is { value1, value2, _, .., value3, .., value4, _ }(集合模式)

  • expr is { _, _, .. var slice, _, _ }(切片模式)

当我们需要混用的时候,需要注意一下要求。由于很多的模式匹配上都是用大括号来表达和标记模式匹配的范围和界限,因此如果混用可能就导致语法不清晰。因此,C# 10 只能让我们把集合和切片模式写在整体的最后面;而前面则是 C# 7 到 9 里的基本模式匹配。

expr is
{
    Prop1: 10,
    Prop2: var val and not null,
    Prop3: { InnerProp: { } inst }
    [1]: 18,
    { _, _, .. var slice, _, _ }
}

这样的写法。


来自官网的说明

_预览_模式下提供了更多功能。 为了使用这些功能,需要在项目中将 <LangVersion> 设置为 Preview

C# 10 继续致力于删除不必要的模式、将数据与算法分离以及提高 .NET 运行时的性能等主题。

许多功能意味着可以通过键入更少的代码来表达相同的概念。 _记录结构_合并了许多_记录类_所使用的相同方法。 结构和匿名类型支持 with 表达式。 _全局 using 指令_和_文件范围的命名空间声明_意味着可以更清楚地表达依赖项和命名空间组织。 进行 Lambda 改进后,就可以在使用 Lambda 表达式时更容易地声明它们。 新的属性模式和析构改进可创建更简洁的代码。

新的内插字符串处理程序和 AsyncMethodBuilder 行为可提高性能。 在 .NET 运行时中应用了这些语言功能来实现 .NET 6 中的性能改进。

C# 10 还标志着每年 .NET 发布节奏的更多转变。 因为不是每项功能都可以在一年内完成,因此你可以尝试 C# 10 中的几项“预览”功能。 泛型属性和接口中的静态抽象成员均可使用,但这些预览功能在最终发布之前可能会发生更改。

本文作者:石起起

本文链接:https://www.cnblogs.com/myshiqiqi/p/18699916

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   石起起  阅读(14)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起