C# 梳理二 新增特性 record
1. 概念特性
1.1 什么是 Record 类型?
C# 中的 Record 类型是一种轻量级的、用于表示不可变数据的特殊类型。它结合了面向对象编程的优点,提供了简洁而强大的语法,使得创建和操作不可变数据更加容易。
1.2 Record 的几大特性
1.2.1 不可变性
Record 类型是不可变的,这意味着一旦创建了一个记录,其状态就不能被修改。任何修改都会返回一个新的记录,确保了数据的稳定性。
1.2.2 自动属性
Record 类型引入了自动属性的概念,通过简化的语法,我们可以直接在记录的声明中列出属性的名称和类型,减少了样板代码,使代码更加清晰。
1.2.3 With 表达式
with
表达式允许我们基于已有的记录创建新的记录,并在创建时修改其中的某些属性。这提高了记录的可变性,同时保持了不可变性。
1.2.4 Equality 比较
Record 类型自动处理值相等比较,无需显式重写Equals方法
1.2.5 ToString() 方法
Record 类型自动生成了 ToString()
方法,以人类可读的方式输出记录的属性,方便调试和日志记录。
2. 与普通类代码对比说明
通过一个具体的实例来说明record的优势以及与普通类的对比。我们来考虑一个简单的学生信息系统。
首先,使用record定义Student
:
public record Student
{
public string Name { get; init; }
public int Age { get; init; }
public string Major { get; init; }
}
这个record包含学生的姓名、年龄和专业。现在,我们创建两个学生对象:
var student1 = new Student("Alice", 20, "Computer Science");
var student2 = new Student("Bob", 22, "Mathematics");
现在,让我们看看record的优势和实际使用场景:
1. 不可变性:
// 试图修改学生的年龄,这将会产生一个新的学生对象
var modifiedStudent = student1 with { Age = 21 };
这里我们创建了一个新的Student
记录,而不是在原始记录上修改。这确保了记录的不可变性。
通过位置参数(record class 和 readonly record struct)创建的,还是通过指定 init 访问器创建的,都具有浅的不可变性。 初始化后,将不能更改值型属性的值或引用型属性的引用。 不过,引用型属性引用的数据是可以更改的。 下面的示例展示了引用型不可变属性的内容(本例中是数组)是可变的:
public record Person(string FirstName, string LastName, string[] PhoneNumbers);
public static void Main()
{
Person person = new("Nancy", "Davolio", new string[1] { "555-1234" });
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-1234
person.PhoneNumbers[0] = "555-6789";
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-6789
}
在 C# 中,record 类型中的属性默认使用 init 访问器,这使得它们在创建实例时可以被初始化,但之后无法被修改。如果你需要在创建后修改属性,可以使用 get; set; 来定义可变属性。
2. 自动属性和位置模式:
在上述代码中,我们使用get; init;来声明自动属性。这意味着这些属性可以被读取(get),但只能在构造函数中初始化(init)。这符合record的不可变性原则,确保了一旦创建了record对象,其属性就不能被修改。
还有另一种更为简单的声明方式
public record Student(string Name, int Age, string Major);
这种方式使用了 Record 类型的简化语法,直接在构造函数的参数中声明属性的名称和类型。在这种情况下,编译器会自动生成与构造函数参数相对应的属性,并自动添加 init 访问器,确保这些属性也是不可变的。
位置模式是c#8.0 新增的 可以和record结合一起使用
var student1 = new Student("Alice", 20, "Computer Science");
// 使用位置模式提取学生的信息
var (name, age, major) = student1;
这样,我们使用位置模式轻松地从学生记录中提取信息。
3. With 表达式:
// 基于已有的学生对象创建一个新的学生对象,同时修改专业
var updatedStudent = student1 with { Major = "Information Technology" };
with
表达式使我们能够在创建新对象的同时修改属性。
with
表达式可以设置位置属性或使用标准属性语法创建的属性。 显式声明属性必须有一个 init 或 set 访问器才能在 with 表达式中进行更改。
with
表达式的结果是一个浅的副本,这意味着对于引用属性,只复制对实例的引用。 原始记录和副本最终都具有对同一实例的引用。
4. Equality 比较:
// record提供了自动的相等比较
public record AllName(string FirstName, string LastName, string[] phoneNum);
AllName all = new("zhang", "xiao", new string[1]{"23254" });
var all2 = all with { phoneNum = all.phoneNum };
if (all2 == all) Console.WriteLine("值相等性");
//结果为 值相等性
Record自动处理值相等比较,无需显式重写Equals方法。
前提是类型相同 例:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Person student = new Student("Nancy", "Davolio", 3);
Console.WriteLine(teacher==student);// output: False
Person student2 = new Student("Nancy", "Davolio", 3);
Console.WriteLine(student2 == student);// output: True
Student student3 = new Student("Nancy", "Davolio", 3);
Console.WriteLine(student3 == student);// output: True
}
在代码比较派生类型的两个实例时,合成的相等性方法会检查基类型和派生类型的所有数据成员是否相等。
编译器合成了一个 EqualityContract 属性,该属性返回一个与记录类型匹配的 Type 对象,先比较类型是否相同
再比较值,如果类型不相同值相同比较结果还是不相同。
5.ToString() 方法
Console.WriteLine(person.ToString());
record 自动生成 ToString() 方法,当调用 ToString() 方法时,它将返回一个包含记录属性值的字符串,类似于 “Person { FirstName = John, LastName = Doe, Age = 30 }”。
当然你也可以重定义tostring()
public record Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
public int Age { get; init; }
public override string ToString()
{
return $"Person: {FirstName} {LastName}, Age {Age}";
}
}
普通类实现
现在,让我们看看如果我们使用普通的类来实现相同的学生信息系统会是什么样子:
public class Student
{
public string Name { get; }
public int Age { get; }
public string Major { get; }
public Student(string name, int age, string major)
{
Name = name;
Age = age;
Major = major;
}
// 省略 Equals 和其他方法...
}
然后我们创建两个学生对象:
var student1 = new Student("Alice", 20, "Computer Science");
var student2 = new Student("Bob", 22, "Mathematics");
对比上述record的例子,你可以看到:
- 普通类需要手动编写属性、构造函数以及可能的Equals方法。
- 修改属性时,需要创建新的对象,而不像record一样使用
with
表达式。 - 位置模式的提取可能需要显式的方法或属性。
总体而言,record的优势在于简化了不可变数据的创建、修改和比较,减少了样板代码的编写。这特别有助于处理大量的数据对象,并提高了代码的可读性和可维护性。
3. 实际适用场景
3.1 为什么选择 Record 类型?
Record 类型在许多实际开发场景中表现出色。例如,对于数据传输对象、不可变的配置信息、领域实体等,Record 类型提供了一种更简洁、更安全的表示方式。
3.2 具体应用场景
考虑一个在线图书商城的订单系统:
public record Order
{
public int OrderId { get; init; }
public string ProductName { get; init; }
public int Quantity { get; init; }
public decimal TotalPrice => Quantity * UnitPrice;
// 其他属性和方法...
}
// 在订单系统中创建订单
var order = new Order
{
OrderId = 1,
ProductName = "Book",
Quantity = 3,
// 其他属性...
};
在这个例子中,Order 类型的不可变性、自动属性以及计算属性(TotalPrice)非常适合表示订单信息。使用 Record 类型,我们能够以清晰而简洁的方式表达业务概念,同时确保订单的不可变性。 这里我们看到:
- 不可变性: 订单信息一旦创建就不可修改,确保订单状态的稳定性。
- 自动属性: 订单的属性声明变得更加简洁,不再需要显式的构造函数。
- With 表达式: 使用
with
表达式创建新订单时更加方便,可以轻松修改部分属性。 - 计算属性: TotalPrice 是一个计算属性,依赖于其他属性的值,方便而直观。
这个例子突显了 Record 类型在处理不可变数据和简单数据结构时的优势。