C#10 新功能
阅读原文时间:2023年07月08日阅读:4

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

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 来表示一个类的记录类型。

首先自然是 record struct,解决了 record 只能给 class 而不能给 struct 用的问题:
record struct Point(int X, int Y);
用 record 定义 struct 的好处其实有很多,例如你无需重写 GetHashCode 和 Equals 之类的方法了。
sealed record ToString 方法
之前 record 的 ToString 是不能修饰为 sealed 的,因此如果你继承了一个 record,相应的 ToString 行为也会被改变,因此这是个虚方法。
但是现在你可以把 record 里的 ToString 方法标记成 sealed,这样你的 ToString 方法就不会被重写了。

struct S0
{
object x = null;
object y;
// ok
}

struct S1
{
object x = null;
object y;
S() { } // error: field 'y' must be assigned
}

struct S2
{
object x = null;
object y;
S() : this(null) { } // ok
S(object y) { this.y = y; } // ok
}
struct Foo
{
public int X;
public Foo() { X = 1; }
}

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

这个例子展示了无参构造器的使用方式。显然,跟引用类型是基本一样的,只是多了一点:如果你不定义的话,默认会生成一个无参构造器(S0 结构,y 会被自动赋值默认数值 default(object),也就是 null);但一经定义,就必须给所有没有赋值的字段和属性赋值。

当然,此时的构造器既然允许自定义了,这样就使得构造器可以定义和修改访问修饰符了。如果访问修饰符设置为 private 的话,那么外部就无法使用 new 来实例化该类型的对象了。这一点和引用类型还是一样的。

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

2.1、结构的字段初始化器

可以看到,这个特性一旦出现,就相当于诞生了另外一个特性:结构的字段初始化器。C# 早期同样是不允许你给字段设置默认数值的;相反,你必须在构造器里赋值,还不能是默认的无参构造器里。

2.2、default(T) 表达式

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

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
表达式的时候,就算你定义了无参构造器,编译器也会始终忽略它。

C# 6 诞生的内插字符串并不能直接当成常量使用。举个例子:

const string A = "Sunnie";
const string B = $"Hello, {A}"; // Here

这个写法对吗?从道理上是说得通的。但是,这个写法不对。因为内插字符串没有得到编译器认为其是常量的允许,因此 B 这里会产生编译器错误。所以,你不得不改成加法运算

const string B = "Hello, " + A; // OK.

C# 10 开始允许这一点,内插字符串也可以认为是常量了;当然前提是,内插的部分也都得是常量才行;而且内插的对象必须也得是 string 才行。

C# 10 增加了对自定义内插字符串处理程序的支持。 内插字符串处理程序是处理内插字符串中的占位符表达式的一种类型。 如果没有自定义处理程序,占位符的处理方式与 String.Format 类似。 每个占位符的格式设置为文本,然后将各组成部分串联起来,形成最终的字符串。

详细请看:https://docs.microsoft.com/zh-cn/dotnet/csharp/whats-new/tutorials/interpolated-string-handler

可将 global 修饰符添加到任何 using 指令,以指示编译器该指令适用于编译中的所有源文件。 这通常是项目中的所有源文件。

global using IntegerList = System.Collections.Generic;

那么,整个项目就都可以使用这个 IntegerList 了。

C# 10 开始你将能够在文件顶部指定该文件的 namespace,一个源文件只能包含一个namespace MyProject;声明。而不需要写一个 namespace 然后把其他代码都嵌套在大括号里面,毕竟绝大多数情况下,我们在写代码时一个文件里确实只会写一个 namespace,这样可以减少一层嵌套也是很不错的。

但是,原始的写法是允许嵌套命名空间和并排命名空间的,而一旦使用 namespace 指令后,就不可在文件里多次声明嵌套或并行的命名空间了。

namespace MyProject;

class MyClass
{
// …
}

这句话不好理解。举个例子。
int age;
(name, age) = student;
假如,name 是本身就有的东西(它可能是属性,或者是字段,或者是临时变量),而 age 仅仅是一个临时变量。如果你要把右侧的 student 变量解构了的话,由于 age 的定义变量语句无法写到赋值语句里面去,所以只能分开成两行书写。
C# 10 将允许你内嵌定义语句到值元组赋值的语句里去。
(name, int age) = student;
这样就合二为一了。name 照旧赋值,而 age 则完成了定义变量和赋值两个操作,且都在这一行里就可以完成。方便了不少。

这个改进可以说是非常大,我分多点介绍。

1、支持 attributes

lambda 可以带 attribute 了:

f = [Foo] (x) => x; // 给 lambda 设置f = [return: Foo] (x) => x; // 给 lambda 返回值设置f = ([Foo] x) => x; // 给 lambda 参数设置

2、支持指定返回值类型

此前 C# 的 lambda 返回值类型靠推导,C# 10 开始允许在参数列表最前面显示指定 lambda 类型了:

f = int () => 4;

3、支持 ref 、in 、out 等修饰

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

4、头等函数

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

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

5、自然委托类型

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

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

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

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

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

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

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

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

8、C#新写法,默认文件内容都是写在Main函数中,C#支持本地函数。所以里面文件中函数其实是本地函数

Console.WriteLine("该程序接收多个参数 用空格 来做参数分隔符");
Console.WriteLine($"main函数参数agrs 的长度{args.Length} ");
int i = 1;
foreach (var s1 in args)
{
Console.WriteLine($"参数{i++}:{s1}");

}

Console.WriteLine(args);
Console.ReadKey();
Count();
void Count()
{

Console.WriteLine("sdfsdf");

}

void Count1(string ser)
{

Console.WriteLine("sdfsdf");

}

通过IL反编译后结果