编写程序的时候,有时候需要比较两个对象是否相等,C#中的==运算符默认判断两个变量指向的是否是同一个对象。
如果两个对象是同一种类型,并且所有属性完全相等,但是它们是两个不同的对象,导致==运算符的比较结果是false,则可以通过重写Equals方法、重写==运算符等来解决这个问题,不过这要求编写非常多的额外代码。
在C#9.0中增加了记录(record)类型的语法,编译器会自动生成Equals、GetHashcode等方法。
定义
public record Person(string FirstName,string LastName);
可以看到,上面的Person类型的定义前面写的是record,而不是熟悉的class或者interface。
Person类型中定义了FirstName和LastName两个属性。
==比较
Person person1 = new("Ichi", "Shan");
Person person2 = new("Zack", "Yang");
Person person3 = new("Ichi", "Shan");
Console.WriteLine(person1);
Console.WriteLine(person1==person2);
Console.WriteLine(person1 ==person3);
Console.WriteLine(person1.FristName);
结果
编译器会根据Person类型中的属性定义,自动为Person类型生成包含全部属性的构造方法。
默认情况下,编译器会生成一个包含所有属性的构造方法,因此,new Person()、new Person(“Ichi”)这两种写法都是不可以的。
编译器同样会为record类型生成ToString方法和Equals方法等。
让我们来看一下反编译后的主干内容
public class Person : IEquatable<Person>
{
public Person(string firstName, string lastName)
{
this.FirstName = firstName;
this.LastName = lastName;
}
public string FirstName { get; set /*init*/; }
public string LastName { get; set /*init*/; }
public override string ToString()
{
throw new NotImplementedException();
}
public virtual bool Equals(Person? other)
{
throw new NotImplementedException();
}
}
可以看到,编译器确实把record类型的Person类型编译成一个Person类,并且提供了构造方法、属性、ToString方法、Equals方法等。
因此record类型编译后仍然只是一个普通的类,record是编译器提供的一个语法糖。
综上所述,record类型提供了为所有属性赋值的构造方法,所有属性都是只读的,对象之间可以进行值的相等性比较,并且编译器为类型提供了可读性强的ToString方法。
在需要编写不可变类并且需要进行对象值比较的时候,使用record可以把代码的编写难度大大降低。
当然,record类型的定义是比较灵活。
实现部分属性只读而部分属性可以读写的效果
//实现
public record Person(string LastName)
{
public string FirstName { get; set; }
public void SayHello()
{
Console.WriteLine($"Hello,我是{LastName}{FirstName}");
}
}
//调用
Person p1 = new("Shan");
Person p2 = new("Shan");
Console.WriteLine(p1);
Console.WriteLine(p2);
Console.WriteLine(p1==p2);
p1.FristName = "Ichi";
p1.SayHello();
Console.WriteLine(p1==p2);
Console.WriteLine(p1);
输出结果
可以看到,Person类型的LastName属性仍然是用record类型语法定义的只读属性,编译器为Person类型生成了包含为LastName属性赋值的构造方法。
而FirstName属性是用传统的语法定义的普通属性,这个属性是可读可写的。
在record类型中,也可以为类型提供多个构造方法,从而提供多种创造对象的途径。
public record User(string UserName,string? Email,int Age)
{
public User(string UserName,int Age) : this(UserName, null, Age)
{
}
}
上面的User类型中声明了3个属性:不可为空的string类型的UserName属性,可为空的string类型的Email属性,以及Age属性。
编译器自动生成一个包含这3个属性的构造方法,同时提供了一个为UserName的Age赋值的构造方法,这个构造方法通过this关键字调用编译器默认的构造方法完成对象的初始化。
调用不同的构造方法
User u1 = new("Ichi", 19);
User u2 = new("Ichi", "ichi@example.com", 23);
record类型的对象的属性默认都是只读的,而且我们也推荐使用属性都为只读的类型。
所有属性、成员变量都为只读的类型叫作”不可变类型”,不可变类型可以简化程序逻辑,并且可以减少并发访问、状态管理等麻烦。
With关键字简化创建
User u1 = new("Ichi", 19);
User u2 = new(u1.UserName, "ichi@example.com", u1.Age);
//使用with关键字
User u3 = u2 with { Email = "ichi@qq.com" };
Console.WriteLine(u3);
Console.WriteLine(u2);
Console.WriteLine(object.ReferenceEquals(u2,u3));
输出结果
可以看到,在第2行代码中,使用with关键字创建了u2对象的一个副本,其他属性都复制自u2对象,只有Email属性采用不同的值。