C# 10.0 向 C# 语言添加了以下功能和增强功能:
- 记录结构
- 结构类型的改进
- 内插字符串处理程序
global using
指令- 文件范围的命名空间声明
- 扩展属性模式
- 可使用
const
内插字符串 - 记录类型可密封
ToString()
- 在同一析构中可同时进行赋值和声明
- 可在方法上使用
AsyncMethodBuilder
属性
record struct(记录结构)
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 方法就不会被重写了。
常量字符串插值
const string a = "foo";
const string b = $"{a}_bar"; // foo_bar
命名空间
C# 10 开始你将能够在文件顶部指定该文件的 namespace,一个源文件只能包含一个namespace MyProject;声明。而不需要写一个 namespace 然后把其他代码都嵌套在大括号里面,毕竟绝大多数情况下,我们在写代码时一个文件里确实只会写一个 namespace,这样可以减少一层嵌套也是很不错的。
但是,原始的写法是允许嵌套命名空间和并排命名空间的,而一旦使用 namespace 指令后,就不可在文件里多次声明嵌套或并行的命名空间了。
namespace MyProject; class MyClass { // ... }
struct 无参构造函数
一直以来 struct 不支持无参构造函数,现在支持了:
struct Foo
{
public int X;
public Foo() { X = 1; }
}
但是使用的时候就要注意了,因为无参构造函数的存在使得 new struct()
和 default(struct)
的语义不一样了,例如 new Foo().X == default(Foo).X
在上面这个例子中将会得出 false
。
全局的 using
可将 global
修饰符添加到任何 using 指令,以指示编译器该指令适用于编译中的所有源文件。 这通常是项目中的所有源文件。
global using IntegerList = System.Collections.Generic<int>;
那么,整个项目就都可以使用这个 IntegerList 了。
在同一析构中进行赋值和声明
此更改取消了早期 C# 版本中的限制。 以前,析构可以将所有值赋给现有变量,或将新声明的变量初始化:
// Initialization: (int x, int y) = point; // assignment: int x1 = 0; int y1 = 0; (x1, y1) = point;
C# 10.0 取消了此限制:
int x = 0; (x, int y) = point;
lambda 改进
这个改进可以说是非常大,我分多点介绍。
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 f = () => 1; // Func<int>
var g = string (int x, string y) => $"{y}{x}"; // Func<int, string, string>
var h = "test".GetHashCode; // Func<int>
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);
}
tuple 支持混合定义和使用
比如:
int y = 0;
(var x, y, var z) = (1, 2, 3);
于是 y 就变成 2 了,同时还创建了两个变量 x 和 z,分别是 1 和 3 。