1. 前言
record 類別的 positional 語法真的很方便,適合簡單情境:
public record User(string Name, int Age);
但有時你會很想幫 record 加一些自訂方法、不一樣的屬性、改變存取修飾詞,或是在建構子裡加點邏輯(像是驗證或自動轉換資料)。可惜 positional record 沒地方加這些!這時候就該用明確 body 的語法了,如果:
- 你想擴充 record 加方法、屬性或額外邏輯。
- 你需要控制屬性的行為(setter、getter、init、驗證)。
- 你想要多個不同的建構子,對應不同初始化方式。
- 你要加 interface 或實作特殊方法。
打造有 body 的 record
語法跟 class 很像。你應該很熟這種寫法:
public class User
{
/* ... */
}
現在只是換成 record:
public record User
{
// 明確定義的屬性
public string Name { get; init; }
public int Age { get; init; }
// 額外邏輯
public string GetGreeting()
{
return $"哈囉,我叫 {Name},我 {Age} 歲!";
}
// 自訂建構子
public User(string name, int age)
{
Name = name;
if (age < 0)
throw new ArgumentException("Age 不能是負數!");
Age = age;
}
}
有趣小知識: 如果你明確定義自己的屬性,編譯器不會自動幫你產生 positional 語法的屬性。一切都很明確、很可控。
混合型:hybrid 語法
C# 允許你兩種都用:你可以宣告 positional record,再加上 body:
public record User(string Name, int Age)
{
public string GetGreeting()
{
return $"我是 {Name},我 {Age} 歲!";
}
}
這種寫法 Name 跟 Age 屬性還是會自動產生,然後你自訂的方法就可以寫在 body 裡面。
2. 跟一般 record 的差異 — body 的細節
- 明確 record 可以完全控制建構子、屬性、方法。
- 你可以實作 interface 或加自訂比較邏輯,適合複雜的物件識別規則。
- 跟單純 positional record 不一樣,要加新屬性只要在 body 宣告就好。
// 新手錯誤!
public record User(string Name, int Age)
{
public string Name { get; init; } // ← 衝突! 屬性重複宣告
}
新手常犯錯: 有些同學會同時寫 public record User(string Name, int Age),又在 body 裡加 public string Name { get; init; },以為這是兩個不同變數。其實不是!這樣會衝突(重複宣告)。要嘛全用 positional,要嘛全部明確定義屬性——不要混著來。
實用範例
我們繼續開發一個 console 應用程式,讓使用者建立訂單。假設有個訂單類別 Order,之前長這樣:
public record Order(string Product, int Quantity, double Price);
現在我們想驗證數量(quantity 不能小於 1),還要加一個自動算總價的屬性:
public record Order
{
public string Product { get; init; }
public int Quantity { get; init; }
public double Price { get; init; }
public double TotalCost => Quantity * Price;
public Order(string product, int quantity, double price)
{
Product = product ?? throw new ArgumentNullException(nameof(product));
if (quantity < 1)
throw new ArgumentException("數量必須大於等於 1!");
Quantity = quantity;
Price = price;
}
public override string ToString()
=> $"商品: {Product}, 數量: {Quantity}, 總價: {TotalCost}";
}
注意我們明確定義屬性,設成 init setter(不可變物件),還加了自動計算總價——程式碼更彈性!
程式呼叫:
var order = new Order("腳踏車", 2, 15000);
Console.WriteLine(order); // 商品: 腳踏車, 數量: 2, 總價: 30000
3. record struct
struct 的進化
在 record struct 出現前,C# 的 struct 就像「簡單的工作馬」——複製快、存在 stack,很適合短小的資料包(像座標或顏色)。但它沒有 record 的好處:沒有 positional 語法、with 表達式、預設值比較、還有其他「甜甜的功能」。
現在 C# 可以用 record 風格宣告 struct:
public record struct Point(int X, int Y);
這到底有什麼好處?
- 自動實作 Equals、GetHashCode、ToString —— 你的 struct 也能漂亮比較、列印!
- with 複製語法:var p2 = p1 with { X = 10 };
- 可以用 positional 或明確 body 語法。
比較:傳統 struct VS record struct
| struct | record struct | |
|---|---|---|
| Positional 語法 | 沒有 | 有 |
| with 表達式 | 沒有 | 有 |
| 不可變性 | 沒有(預設) | 有(init) |
| 值比較 | 沒有(預設) | 有 |
| ToString | 預設 | 漂亮 |
record struct 的明確 body 語法
跟一般 record 一樣,只是 struct:
public record struct Rectangle
{
public int Width { get; init; }
public int Height { get; init; }
public int Area => Width * Height;
public Rectangle(int width, int height)
{
Width = width > 0 ? width : throw new ArgumentException("寬度 > 0");
Height = height > 0 ? height : throw new ArgumentException("高度 > 0");
}
public void Print()
{
Console.WriteLine($"尺寸: {Width} x {Height}, 面積: {Area}");
}
}
使用範例:
var rect = new Rectangle(10, 7);
rect.Print(); // 尺寸: 10 x 7, 面積: 70
// 複製並改寬度,原本的不變
var wideRect = rect with { Width = 20 };
wideRect.Print(); // 尺寸: 20 x 7, 面積: 140
record struct 的特點
- 它還是 struct —— value type。賦值時會複製!
- 有 record 的所有優點:值比較、with 複製、漂亮的 ToString。
- 推薦用在小型、精簡、不可變的資料集,想避免 heap 配置時。
- 可以用 positional 參數或明確 body。
4. 常見錯誤與地雷
太容易變動: record struct 不會自動讓欄位不可變,如果你像 public int Value; 這樣宣告,還是 mutable。要用 init setter 才是真的 immutable struct!
比較: 如果你手動加新欄位(沒放在 positional 語法裡),要注意:只有建構子或有 init setter 的欄位,才會參與自動值比較。
複製: 這是 struct,所以... 都是複製!不要跟 reference record 混淆。
with 表達式的誤會: 它們永遠只做 shallow copy,不會深層複製巢狀物件。
GO TO FULL VERSION