C# 更新记录

在此记录C#版本更新时带来的新特性,并给出C#版本和使用场景的对应关系。

C# 8.0

可空引用类型

除了int?、float?这样的可空值类型,引入了string?这样的可空引用类型。但是由于引用变量本身就可以为null,因此可空引用类型并不是一个新的类型,只是用于辅助编译器检查潜在的null错误。

不可为 null 的引用类型在取消引用时应该始终是安全的,因为它们的 null-state 是 not-null。 若强制执行该规则,如果不可为 null 的引用类型没有初始化(以任何形式)为非 null 值,编译器将发出警告。

对比而言,可为null的引用类型必须在取消对变量的引用之前确定该变量的状态为 not-null(使用!= null或者is not null)。 如果可为 null 的引用的状态确定为 maybe-null,将其分配给不可为 Null 的引用变量会生成编译器警告。

对于将非not-null状态的值赋值给not-null状态的引用变量时:

  • 如果要赋值为null,需要使用null!来向编译器强调。
  • 如果要赋值为可为null的引用类型,需要在变量名后面加上!来强调。

同时需要自行维护因为暂时地违反规则而导致的空引用问题。

成员访问

Null运算符?.?[]

仅当操作数的计算结果为非 NULL 时,NULL 条件运算符才对其操作数应用成员访问 (?.) 或元素访问 (?[]) 操作;否则,它会返回 null。 换句话说:

  • 如果 a 的计算结果为 null,则 a?.xa?[x] 的结果为 null
  • 如果 a 的计算结果为非 null,则 a?.xa?[x] 的结果将分别与 a.xa[x] 的结果相同。

^..选择符

^..选择符只能用于单维数组。

^选择符指示序列末尾的元素位置,比如^1指向最后一个元素。

..选择符指定索引范围的开始和结束作为其操作数, 左侧操作数是范围的包含性开头。 右侧操作数是范围的包含性末尾。通过省略 .. 运算符的任何操作数,可以获得一个开放区间:

  • a..a..^0 等效
  • ..b0..b 等效
  • ..0..^0 等效

C# 9.0

记录record

record是引用修饰符,用于提供封装数据的内置功能。record class 语法等价于recordrecord struct 则用于定义一种具有类似功能的值类型。比如:

// 位置参数语法形式
public record Person(string FirstName, string LastName);
// 等价于
public record Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
};

public record struct Point
{
    public double X { get; init; }
    public double Y { get; init; }
    public double Z { get; init; }
}

同时不强制属性为只读,任何不符合需求的自动生成样式都可以通过声明相同的字段、属性来更改,比如:

public record Person(string FirstName, string LastName, string Id)
{
    internal string Id { get; init; } = Id;
}

record关键字主要提供了以下内容:

  • 值相等性比较,相比引用值比较,record默认使用值相等比较,同时额外比较类型,如果类型不相同就不等。

  • 内置的输出格式,将以<type> {xx : xx, xx : xx}的格式默认重载ToString函数

  • 非破坏性变化声明(with),例如:

    public record Person(string FirstName, string LastName)
    {
        public string[] PhoneNumbers { get; init; }
    }
    Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };
    Console.WriteLine(person1);
    Person person2 = person1 with { FirstName = "John" };
    
  • 引用型的浅不可变性,即在多线程环境下不会发生值的改变,但是引用属性的指向可以发生改变。

record class之间可以继承,比如:

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);

但是在使用with表达式时,你只能编辑声明的变量的类型的字段,而不是with的对象的实际类型

init关键字

init 关键字在属性或索引器中定义访问器方法。 init-only 资源库仅在对象构造期间为属性或索引器元素赋值。 init 强制实施不可变性,因此,一旦初始化对象,将无法更改,必须通过调用构造函数来初始化。

init访问器不强制提供值,如果没有显示给出值,将初始化为该类型的默认值。与private setreadonly的区别是,init不要求一定在构造函数中,也可以使用对象初始值设定项(new <type>() {xx = xx, xx = xx}

C# 11.0

原始字符串

原始字符串字面量以至少三个双引号 (""") 字符开头。 它以相同数量的双引号字符结尾,无需转义序列。 比如:

string longMessage = """
    This is a long message.
    It has several lines.
        Some are indented
                more than others.
    Some should start at the first column.
    Some have "quoted text" in them.
    """;
var location = $$"""
    You are at {{{Longitude}}, {{Latitude}}}
    """;

如果要使用内插,使用多少个$就需要多少个大括号{}表示内插。

泛型Attirbute

Attribute可以使用泛型定义,但是在使用的时候比如给出所有的类型参数,而不能使用另一个泛型参数,比如:

public class GenericAttribute<T> : Attribute { }
public class GenericType<T>
{
   [GenericAttribute<T>()] // 不合法,T不是具体类型
   public string Method() => default;
}

required修饰符

required修饰符强制字段或属性使用对象初始值设定项进行初始化,该字段的可见性应该至少与其包含类型一样可见,且不可被隐藏。对于record中,必须显示指定required属性/字段的定义,而不是使用位置参数声明。

C# 12.0

主构造函数

在定义类的时候,可以直接给出一个构造函数的参数列表,这些参数可以当作局部变量用于整个类,比如:

public class Employee(int id, string name)
{
    public int Id { get; } = id;
    public string Name { get; } = name;
    
    public void Display()
    {
        Console.WriteLine($"Employee ID: {id}, Name: {name}");
    }
}

集合表达式

集合表达式中的 分布元素..e 添加该表达式中的所有元素。 参数必须是集合类型。比如:

int[] row0 = [1, 2, 3];
int[] row1 = [4, 5, 6];
int[] row2 = [7, 8, 9];
int[] single = [.. row0, .. row1, .. row2];

..元素计算枚举表达式的每个元素。 每个元素都包含在输出集合中。