博客 / 詳情

返回

深入理解 C#.NET record:不可變對象與值語義的現代實踐

簡介

recordC# 9 引入的新引用類型(Reference Type),專門用於數據導向(Data-Oriented)的不可變對象。特別適合用於表示不可變的數據傳輸對象(DTO)、值對象和領域模型。

⚡ 主要特性:

  • 內置值相等性:兩個 record 實例如果屬性值相同,則被認為相等(值相等)。
  • 簡潔語法:通過“主構造函數”直接定義屬性。
  • 不可變設計:推薦使用 init 訪問器,實現只讀屬性。
  • 模式匹配友好:可以用 with、解構等簡化數據處理。

基本語法

簡單聲明

public record Person(string FirstName, string LastName);
  • record 關鍵字定義一個不可變引用類型。
  • (string FirstName, string LastName) 定義主構造函數和自動 init 屬性。

等價於:

public class Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }

    public Person(string firstName, string lastName)
        => (FirstName, LastName) = (firstName, lastName);

    // 自動生成值相等比較、Deconstruct 方法等
}

使用:

var p1 = new Person("Alice", "Smith");
Console.WriteLine(p1.FirstName); // Alice

不可變性(init-only)

Record 的屬性默認是隻讀的(init-only),這意味着它們只能在初始化時設置

public record Person(string FirstName, string LastName, int Age);

// 創建 record 實例
var person = new Person("John", "Doe", 30);

// 編譯錯誤:不能修改屬性
// person.Age = 31;

值相等(Value Equality)

引用類型 vs 值類型對比

類型 相等比較(默認)
class 引用相等(Reference Equality)
struct 值相等(字段逐一比較)
record 值相等(自動生成 Equals/==

示例:

var p1 = new Person("Alice", "Smith");
var p2 = new Person("Alice", "Smith");
Console.WriteLine(p1 == p2);      // True
Console.WriteLine(p1.Equals(p2)); // True

即使 p1p21 是不同實例,只要屬性值相同,就相等。

with 表達式(非破壞性複製)

record 可以使用 with 表達式複製並修改部分屬性:

var p1 = new Person("Alice", "Smith");
var p2 = p1 with { LastName = "Johnson" };

Console.WriteLine(p2.FirstName); // Alice
Console.WriteLine(p2.LastName);  // Johnson
  • p1 保持不變,p2 是新的實例。
  • 這是不可變對象的核心優勢。

解構(Deconstruction)

編譯器會自動生成 Deconstruct 方法:

var person = new Person("Alice", "Smith");
var (first, last) = person;
Console.WriteLine($"{first} {last}");

可直接用元組解構。

繼承與多態

record 支持繼承,且保留值相等語義:

public record Person(string Name);
public record Student(string Name, int Grade) : Person(Name);

Person p = new Student("Alice", 5);
Console.WriteLine(p is Student); // True

自動生成的 Equals 會包含類型檢查,子類與父類不同類型即使屬性相同也不相等。

顯式屬性定義

可以不用主構造函數,像 class 一樣定義:

public record Car
{
    public string Brand { get; init; }
    public string Model { get; init; }
}

class 的唯一區別是自動生成了值相等比較。

record struct(值類型記錄,C# 10+)

C# 10 引入 record struct,結合了 record 的值相等 和 struct 的值類型特性。

// Record struct(值類型)
public record struct Point(int X, int Y);

// 使用 record struct
var point1 = new Point(3, 4);
var point2 = new Point(3, 4);

Console.WriteLine(point1 == point2); // True
Console.WriteLine(point1); // Point { X = 3, Y = 4 }

// 修改 record struct(因為是值類型,所以可以修改)
point1.X = 5;
Console.WriteLine(point1); // Point { X = 5, Y = 4 }

// 只讀 record struct
public readonly record struct ImmutablePoint(int X, int Y);

var immutablePoint = new ImmutablePoint(3, 4);
// immutablePoint.X = 5; // 編譯錯誤:不能修改只讀字段

記錄的成員生成

編譯器為 record 自動生成以下內容:

  • Equals(object?)GetHashCode():基於屬性值。
  • ==!= 運算符。
  • Deconstruct 方法。
  • ToString():輸出形如 Person { FirstName = Alice, LastName = Smith }

這使得 record 非常適合作為 DTO、查詢結果、配置數據。

與 class / struct 對比

特性 class struct record(class) record struct
類型 引用類型 值類型 引用類型 值類型
相等性 引用相等 值相等 值相等 值相等
默認不可變 ❌(可變) ❌(可變) ✅(推薦 init ✅(推薦 readonly
內存分配 棧/堆 棧/堆
繼承 支持 僅接口 支持 僅接口
with 表達式

高級用法

與模式匹配

if (person is Person { FirstName: "Alice" })
    Console.WriteLine("Found Alice!");
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
public record Triangle(double Base, double Height) : Shape;

public static double CalculateArea(Shape shape)
{
    return shape switch
    {
        Circle c => Math.PI * c.Radius * c.Radius,
        Rectangle r => r.Width * r.Height,
        Triangle t => 0.5 * t.Base * t.Height,
        _ => throw new ArgumentException("Unknown shape")
    };
}

// 使用模式匹配
var circle = new Circle(5);
var rectangle = new Rectangle(4, 6);

Console.WriteLine(CalculateArea(circle)); // 78.53981633974483
Console.WriteLine(CalculateArea(rectangle)); // 24

位置記錄 + 解構

public record Order(int Id, decimal Price);
var order = new Order(1001, 99.99m);
var (id, price) = order; // 自動解構

實際應用場景

DTO(數據傳輸對象)

// API 響應 DTO
public record ApiResponse<T>(bool Success, string Message, T Data);

// API 請求 DTO
public record CreateUserRequest(string Username, string Email, string Password);

// 使用 record DTO
public class UserController : ControllerBase
{
    [HttpPost]
    public IActionResult CreateUser(CreateUserRequest request)
    {
        // 處理請求...
        var response = new ApiResponse<User>(true, "User created successfully", user);
        return Ok(response);
    }
}

配置對象

// 應用程序配置
public record AppSettings
{
    public string ConnectionString { get; init; }
    public int MaxRetryAttempts { get; init; } = 3;
    public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
    public LoggingSettings Logging { get; init; }
}

public record LoggingSettings
{
    public string Level { get; init; } = "Information";
    public string FilePath { get; init; } = "logs/app.log";
}

// 使用配置
var settings = new AppSettings
{
    ConnectionString = "Server=localhost;Database=MyDb",
    MaxRetryAttempts = 5,
    Logging = new LoggingSettings { Level = "Debug" }
};

適用場景

推薦使用 record 的場景:

  • 不可變的數據模型:DTOAPI 響應對象
  • 配置項、設置類
  • 數據庫查詢結果(EF Core 中很常見)
  • 事件/消息模型(Event SourcingCQRS

不推薦使用 record 的場景:

  • 對象需要頻繁修改。
  • 需要嚴格的引用語義(例如緩存中的唯一實例)。

性能與注意事項

  • record 是引用類型(除非是 record struct),所以在堆上分配,仍有 GC 開銷。
  • 值比較可能略微增加 Equals/GetHashCode 的計算成本。
  • with 表達式會創建新對象,不要在高頻場景頻繁使用。

總結

特性 説明
核心目標 簡化不可變數據類型的聲明和比較
類型 引用類型(默認)/值類型(record struct
相等性 自動實現基於值的相等性Equals==
不可變性 推薦使用 initreadonly
複製 with 表達式實現非破壞性複製
解構 自動生成 Deconstruct,支持元組解構
典型應用 DTO、API 數據模型、配置對象、事件/消息對象
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.