一、记录(record)
C# 9.0 引入了记录类型。 可使用 record
关键字定义一个引用类型,以最简的方式创建不可变类型。这种类型是线程安全的,不需要进行线程同步,非常适合并行计算的数据共享。
它减少了更新对象会引起各种bug的风险,更为安全。System.DateTime 和 string 也是不可变类型非常经典的代表。
与类不同的是,它是基于值相等而不是唯一的标识符--对象的引用。
通过使用位置参数或标准属性语法,可以创建具有不可变属性的记录类型,整个对象都是不可变的,且行为像一个值。
优点:
1、在构造不可变的数据结构时,它的语法简单易用;
2、record 为引用类型,不用像值类型在传递时需要内存分配,还可整体拷贝;
3、构造函数和结构函数为一体的、简化的位置记录;
4、有力的相等性支持 —— 重写了 Equals(object), IEquatable, 和 GetHashCode() 这些基本方法。
record 类型可以定义为可变的,也可以是不可变的。
// 没有 set 访问器,创建后不可更改,叫不可变类型
public record Person
{
// 要支持用对象初始化器进行初始化,则在属性中使用 init 关键字
// 或者以构造函数的方式
public string? FirstName { get; init; }
public string? LastName { get; init; }
}
// 可变类型的 record
// 因为有 set 访问器,所以它支持用对象初始化器进行初始化
public record Person
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
记录(record)和类一样,在面向对象方面,支持继承,多态等所有特性。除过前面提到的 record 专有的特性,其他语法写法跟类也是一样。同其他类型一样,record 的基类依然是 object。
注意:
1、记录只能从记录继承,不能从类继承,也不能被任何类继承;
2、record 不能定义为 static 的,但是可以有 static 成员。
从本质上来讲,record 仍然是一个类,但是关键字 record 赋予这个类额外的几个像值的行为:
1、基于值相等性的比较方法,如 Equals,==,!=,EqualityContract 等;
2、重写 GetHashCode();
3、拷贝和克隆成员;
4、PrintMembers 和 ToString() 方法。
应用场景:
1、用于 web api 返回的数据,通常作为一种一次性的传输型数据,不需要是可变的,因此适合使用 record;
2、作为不可变数据类型 record 对于并行计算和多线程之间的数据共享非常适合,安全可靠;
3、record 本身的不可变性和 ToString 的数据内容的输出,不需要人工编写很多代码,就适合进行日志处理;
4、其他涉及到有大量基于值类型比较和复制的场景,也是 record 的常用的使用场景。
with 表达式
当使用不可变的数据时,一个常见的模式是从现存的值创建新值来呈现一个新状态。
例如,如果 Person 打算改变他的姓氏(last name),我们就需要通过拷贝原来数据,并赋予一个不同的 last name 值来呈现一个新 Person。这种技术被称为非破坏性改变。作为描绘随时间变化的 person,record 呈现了一个特定时间的 person 的状态。
为了帮助进行这种类型的编程,针对 records 就提出了 with 表达式,用于拷贝原有对象,并对特定属性进行修改
// 修改特定属性后复制给新的 record
var person = new Person { FirstName = "Mads", LastName = "Nielsen" };
var otherPerson = person with { LastName = "Torgersen" };
// 只是进行拷贝,不需要修改属性,那么无须指定任何属性修改
Person clone = person with { };
with 表达式使用初始化语法来说明新对象在哪里与原有对象不同。with 表达式实际上是拷贝原来对象的整个状态值到新对象,然后根据对象初始化器来改变指定值。这意味着属性必须有 init 或者 set 访问器,才能用 with 表达式进行更改。
注意:
1、with 表达式左边操作数必须为 record 类型;
2、record 的引用类型成员,在拷贝的时候,只是将所指实例的引用进行了拷贝。
二、仅限 Init 的资源库
从 C# 9.0 开始,可为属性和索引器创建 init
访问器,而不是 set
访问器。 调用方可使用属性初始化表达式语法在创建表达式中设置这些值,但构造完成后,这些属性将变为只读。
仅限 init 的资源库提供了一个窗口用来更改状态。 构造阶段结束时,该窗口关闭。 在完成所有初始化(包括属性初始化表达式和 with 表达式)之后,构造阶段实际上就结束了。
属性初始值设定项可明确哪个值正在设置哪个属性。 缺点是这些属性必须是可设置的。
可在编写的任何类型中声明仅限 init
的资源库。
例如,以下结构定义了天气观察结构:
// 以下结构定义了天气观察结构
public struct WeatherObservation
{
public DateTime RecordedAt { get; init; }
public decimal TemperatureInCelsius { get; init; }
public decimal PressureInMillibars { get; init; }
public override string ToString() =>
$"At {RecordedAt:h:mm tt} on {RecordedAt:M/d/yyyy}: " +
$"Temp = {TemperatureInCelsius}, with {PressureInMillibars} pressure";
}
// 调用方可使用属性初始化表达式语法来设置值,同时仍保留不变性
var now = new WeatherObservation
{
RecordedAt = DateTime.Now,
TemperatureInCelsius = 20,
PressureInMillibars = 998.0m
};
//初始化后尝试更改观察值会导致编译器错误
// Error! CS8852.
now.TemperatureInCelsius = 18;
对于从派生类设置基类属性,仅限 init 的资源库很有用。这些设置器可在 with 表达式中使用。 可为定义的任何 class
、struct
或 record
声明仅限 init 的资源库。
三、顶级语句
顶级语句,就是从应用程序中删除了不必要的流程。例如最基本的“HelloWorld!”:
using System;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
// 只有一行代码执行所有操作,借助顶级语句
// 可使用 using 指令和执行操作的一行替换所有样本
using System;
Console.WriteLine("Hello World!");
// 如果需要单行程序,可删除 using 指令,并使用完全限定的类型名称
System.Console.WriteLine("Hello World!");
应用程序中只有一个文件可使用顶级语句。
如果编译器在多个源文件中找到顶级语句,则是错误的。
如果将顶级语句与声明的程序入口点方法(通常为 Main
方法)结合使用,也会出现错误。
从某种意义上讲,可认为一个文件包含通常位于 Program
类的 Main
方法中的语句。
顶级语句可提供类似脚本的试验体验,这与 Jupy