ZetCode

C# record

最后修改于 2023 年 7 月 5 日

C# record 教程展示了如何在 C# 中使用 record 类型。

Record 是一种引用类型,其主要目的是保存数据。 它对于数据分析非常有用。 record 类型简化了代码并提高了其可读性,并消除了不必要的样板代码。

record 类型提供以下功能:

编译器会创建 Object.Equals(Object)Object.GetHashCode 的重写。 它会创建用于 ==!= 运算符的方法,并实现 System.IEquatable<T>。 Records 还提供了 Object.ToString 的重写。

虽然 records 与标准类有很多相似之处,但它们的目的不同。 类用于定义对象的复杂层次结构及其职责,而 records 擅长存储数据以进行分析。

注意: record 类型是函数式编程理念的重要组成部分。 它是数据分析非常有用的工具。

重要的函数式语言,例如 F# 和 Clojure,自诞生以来就具有 record 类型。 record 类型出现在 C# 9.0 中。

C# record 位置语法

创建 record 的最简单方法是使用位置语法

record User(string FirstName, string LastName, string occupation);

使用此语法,编译器会自动创建以下内容:

使用位置语法创建的数据是不可变的。 数据不变性是函数式编程的重要基石。

C# record 简单示例

在以下示例中,我们创建一个简单的 record 类型。

Program.cs
var users = new List<User>
{
    new ("John", "Doe", 1230),
    new ("Lucy", "Novak", 670),
    new ("Ben", "Walter", 2050),
    new ("Robin", "Brown", 2300),
    new ("Amy", "Doe", 1250),
    new ("Joe", "Draker", 1190),
    new ("Janet", "Doe", 980),
    new ("Albert", "Novak", 1930),
};

users.ForEach(Console.WriteLine);

Console.WriteLine(users[0].FirstName);
Console.WriteLine(users[0].LastName);
Console.WriteLine(users[0].Salary);

record User(string FirstName, string LastName, int Salary);

我们在一行中创建了一个 User record; 我们已准备好使用它。

users.ForEach(Console.WriteLine);

我们遍历用户列表并将它们打印到控制台。 感谢 records 的内置格式化,我们拥有 records 的人类可读的输出。

Console.WriteLine(users[0].FirstName);
Console.WriteLine(users[0].LastName);
Console.WriteLine(users[0].Salary);

这些属性会自动创建。

$ dotner run
User { FirstName = John, LastName = Doe, Salary = 1230 }
User { FirstName = Lucy, LastName = Novak, Salary = 670 }
User { FirstName = Ben, LastName = Walter, Salary = 2050 }
User { FirstName = Robin, LastName = Brown, Salary = 2300 }
User { FirstName = Amy, LastName = Doe, Salary = 1250 }
User { FirstName = Joe, LastName = Draker, Salary = 1190 }
User { FirstName = Janet, LastName = Doe, Salary = 980 }
User { FirstName = Albert, LastName = Novak, Salary = 1930 }
John
Doe
1230

在第二个示例中,我们使用带有 LINQ 的 record 类型。

Program.cs
var cars = new List<Car>
{
    new ("Audi", "red", 52642),
    new ("Mercedes", "blue", 57127),
    new ("Skoda", "black", 9000),
    new ("Volvo", "red", 29000),
    new ("Bentley", "yellow", 350000),
    new ("Citroen", "white", 21000),
    new ("Hummer", "black", 41400),
    new ("Volkswagen", "white", 21600),
};

var groups = from car in cars
             group car by car.Colour;

foreach (var group in groups)
{
    Console.WriteLine(group.Key);

    foreach (var car in group)
    {
        Console.WriteLine($" {car.Name} {car.Price}");
    }
}

record Car(string Name, string Colour, int Price);

我们使用 LINQ 表达式按颜色对汽车进行分组。 同样,我们在一行中定义了 record 类型。 我们专注于数据分析,而不是复杂的 OOP 技术。

$ dotnet run
red
  Audi 52642
  Volvo 29000
blue
  Mercedes 57127
black
  Skoda 9000
  Hummer 41400
yellow
  Bentley 350000
white
  Citroen 21000
  Volkswagen 21600

C# record 相等性

Records 提供基于值的相等性。 当我们专注于数据分析时,这是预期的行为。 另一方面,类默认提供引用相等性。 要使用类实现基于值的相等性,我们必须向类定义添加额外的代码行,这通常通过 IDE 生成器完成,并被认为是样板代码。

Program.cs
var u1 = new User("John", "Doe", "gardener");
var u2 = new User("John", "Doe", "gardener");

Console.WriteLine(u1 == u2);

var p1 = new Person("Roger", "Roe", "driver");
var p2 = new Person("Roger", "Roe", "driver");

Console.WriteLine(p1 == p2);

record User(string FirstName, string LastName, string occupation);

class Person
{
    public Person(string firstName, string lastName, string occupation)
    {
        FirstName = firstName;
        LastName = lastName;
        Occupation = occupation;
    }

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Occupation { get; set; }
}

在此示例中,我们比较了具有相同数据的两个 records 和两个类对象。

$ dotnet run
True
False

Records 比较值,而类对象比较引用。 第二个输出为 False,因为 p1p2 指向内存中的两个不同对象。

C# record 解构

使用位置语法,我们自动实现了 Deconstruct 方法。

Program.cs
var u = new User("John", "Doe", 980);

(string fname, string lname, int sal) = u;

Console.WriteLine($"{fname} {lname} earns {sal} per month");

record User(string FirstName, string LastName, int Salary);

通过解构操作,可以将 record 的属性轻松地分离到变量中。

$ dotnet run
John Doe earns 980 per month

C# record 非破坏性突变

在函数式编程中,我们使用不可变数据。 当我们需要修改数据时,我们会创建原始数据的修改副本,该副本完好无损。 这个简单的规则为并发编程带来了巨大的好处。

我们可以使用 with 关键字来获取 record 的修改副本。

Program.cs
var users = new List<User>
{
    new ("John", "Doe", 1230),
    new ("Lucy", "Novak", 670),
    new ("Ben", "Walter", 2050),
    new ("Robin", "Brown", 2300),
    new ("Amy", "Doe", 1250),
    new ("Joe", "Draker", 1190),
    new ("Janet", "Doe", 980),
    new ("Albert", "Novak", 1930),
};

var users2 = new List<User>();
users.ForEach(u => users2.Add(u with { Salary = u.Salary + 200 }));

users.ForEach(Console.WriteLine);

Console.WriteLine("---------------");

users2.ForEach(Console.WriteLine);

record User(string FirstName, string LastName, int Salary);

在此示例中,我们有一个用户列表。 我们想给每个用户添加奖金。 我们没有修改原始列表,而是创建了一个新的列表,其中他们的工资已修改。

$ dotnet run
User { FirstName = John, LastName = Doe, Salary = 1230 }
User { FirstName = Lucy, LastName = Novak, Salary = 670 }
User { FirstName = Ben, LastName = Walter, Salary = 2050 }
User { FirstName = Robin, LastName = Brown, Salary = 2300 }
User { FirstName = Amy, LastName = Doe, Salary = 1250 }
User { FirstName = Joe, LastName = Draker, Salary = 1190 }
User { FirstName = Janet, LastName = Doe, Salary = 980 }
User { FirstName = Albert, LastName = Novak, Salary = 1930 }
---------------
User { FirstName = John, LastName = Doe, Salary = 1430 }
User { FirstName = Lucy, LastName = Novak, Salary = 870 }
User { FirstName = Ben, LastName = Walter, Salary = 2250 }
User { FirstName = Robin, LastName = Brown, Salary = 2500 }
User { FirstName = Amy, LastName = Doe, Salary = 1450 }
User { FirstName = Joe, LastName = Draker, Salary = 1390 }
User { FirstName = Janet, LastName = Doe, Salary = 1180 }
User { FirstName = Albert, LastName = Novak, Salary = 2130 }

C# 可变 record

可以创建一个可变 record。 但是,如果可能,最好使用不可变 record。

Program.cs
var u = new User("John", "Doe", "gardener");
Console.WriteLine(u);

u.Occupation = "driver";
Console.WriteLine(u);

record User
{
    public User(string firstName, string lastName, string occupation)
    {
        FirstName = firstName;
        LastName = lastName;
        Occupation = occupation;
    }
    public string FirstName { get; set; } = default!;
    public string LastName { get; set; } = default!;
    public string Occupation { get; set; } = default!;
};

通过实现我们自己的属性,我们得到一个可变 record。

$ dotnet run
User { FirstName = John, LastName = Doe, Occupation = gardener }
User { FirstName = John, LastName = Doe, Occupation = driver }

网络抓取示例

为了演示 records 的实用性,我们有一个更复杂的示例,它从网站上抓取数据。

$ dotnet add package AngleSharp
$ dotnet add package CsvHelper

我们需要将 AngleSharpCsvHelper 包添加到项目中。

Program.cs
using System.Collections.Generic;
using System.Globalization;
using AngleSharp;
using CsvHelper;

var config = Configuration.Default.WithDefaultLoader();
using var context = BrowsingContext.New(config);

var url = "https://nrf.com/resources/top-retailers/top-100-retailers/top-100-retailers-2019";
using var doc = await context.OpenAsync(url);

var htable = doc.GetElementById("stores-list--section-16266");
var trs = htable.QuerySelectorAll("tr").Skip(1);

var csvConfig = new CsvHelper.Configuration.CsvConfiguration(CultureInfo.CurrentCulture)
{
    ShouldQuote = args => false
};

using var fs = new StreamWriter("data.csv");
using var writer = new CsvWriter(fs, csvConfig);

var rows = new List<Row>();

foreach (var tr in trs)
{
    var tds = tr.QuerySelectorAll("td").Take(3);
    var fields = (from e in tds select e.TextContent).ToArray();
    var row = new Row(fields[0], fields[1], fields[2]);

    rows.Add(row);
}

writer.WriteRecords(rows);

record Row(string Rank, string Company, string Sales);

在此示例中,我们从网站上抓取数据。 在 HTML 表格中,有美国排名前 100 的零售商。 我们连接到该网站,解析 HTML 表格,并选择其中的三列。 解析后的数据保存到 CSV 文件中。 该示例创建了 Row record,用于存储解析后数据的一行。

来源

Records - 语言参考

在本文中,我们介绍了 C# record 类型。

作者

我的名字是 Jan Bodnar,我是一位充满热情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 迄今为止,我已经撰写了 1,400 多篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有 C# 教程