聯合型別代表的值可以是多種案件類型之一。 聯合會提供每種案例類型的隱式轉換、窮盡模式匹配,以及增強的空可追溯性追蹤。 使用 union 關鍵字宣告聯合類型:
public union Pet(Cat, Dog, Bird);
此宣告產生 Pet 包含三種情況類型的聯集: Cat、、 Dog和 Bird。 你可以將任何案件類型的值指派給變 Pet 數。 編譯器確保 switch 表達式涵蓋所有案例類型。
C# 語言參考資料記錄了 C# 語言最新版本。 同時也包含即將推出語言版本公開預覽功能的初步文件。
文件中標示了語言最近三個版本或目前公開預覽版中首次引入的任何功能。
小提示
欲查詢某功能何時首次在 C# 中引入,請參閱 C# 語言版本歷史的條目。
當值必須是固定型別集合中的一個,且你希望編譯器強制處理所有可能性時,宣告聯集。 常見情況包括:
-
結果或錯誤回傳:方法回傳成功值或錯誤值,呼叫者必須同時處理兩者。 像
union Result(Success, Error)這樣的聯合會明確呈現結果的集合。 -
訊息或指令派遣:系統處理一組封閉的訊息類型。 聯合體確保新訊息類型會在尚未處理的每個
switch訊息類型產生編譯時警告。 - 取代標記介面或抽象基底類別:如果你只用介面或抽象類別來群組型態以進行模式匹配,union 可以讓你在不需要繼承或共享成員的情況下,提供徹底性檢查。
聯集在重要方面與其他類型宣告不同:
- 與 或
class不同struct,聯合體不會定義新的資料成員。 相反地,它將現有類型組合成一組封閉的替代方案。 - 與 不同
interface,聯集是封閉的——你在宣告中定義完整的案例類型清單,編譯器會用該清單進行窮盡性檢查。 - 與 不同
record,聯合不會增加等式、克隆或解構行為。 一個工會關注的是「是哪個案件?」而不是「它有哪些領域?」
聯合型別依賴UnionAttribute命名空間中的 IUnion 和 System.Runtime.CompilerServices 型別。 .NET 11 執行環境包含這些類型。
聯盟宣言
聯合聲明指定名稱及案件類型清單:
public union Pet(Cat, Dog, Bird);
案例類型 可以是任何能轉換成 object的類型,包括類別、結構體、介面、型別參數、可空型態及其他聯集。 以下範例展示了不同的案例類型可能性:
public record class Cat(string Name);
public record class Dog(string Name, string Breed);
public record class Bird(string Name);
public record class None;
public record class Some<T>(T Value);
public union Option<T>(None, Some<T>);
public union IntOrString(int, string);
當一個案件類型是值型別(類似 int),聯合聲明會將該值框框並儲存為單一 object? 參考。
自訂聯盟類型 可以使用不同的儲存策略。
工會聲明可以包含一個包含額外成員的機構,就像結構組織一樣,但會受到一些限制。 聯合聲明不能包含實例欄位、自動屬性或類欄位事件。 你也不能用單一參數宣告公開建構子,因為編譯器會以聯合創建成員的形式產生這些建構子。 你宣告的任何建構子都必須使用 this(...) 初始化器,直接或間接地委派給產生的建構子。 以下 Length 聯集新增了一個 TotalMeters 利用模式匹配處理所有案例類型的特性,以及一個 Add 結合兩個長度的方法:
public record class Meters(double Value);
public record class Feet(double Value);
public union Length(Meters, Feet)
{
public double TotalMeters => this switch
{
Meters m => m.Value,
Feet f => f.Value * 0.3048,
_ => throw new InvalidOperationException("The Length has no value."),
};
public Length Add(Length other) => new Meters(TotalMeters + other.TotalMeters);
}
工會轉型
每個案例類型之間存在隱含 的聯集轉換 :
static void BasicConversion()
{
Pet pet = new Dog("Rex", "Labrador");
Console.WriteLine(pet.Value); // output: Dog { Name = Rex, Breed = Labrador }
Pet pet2 = new Cat("Whiskers");
Console.WriteLine(pet2.Value); // output: Cat { Name = Whiskers }
}
聯集轉換是透過呼叫相應產生的建構子來運作。 若存在相同類型的使用者定義隱含轉換運算子,該使用者定義運算子優先於聯集轉換。 若多個案例類型同時適用於原始值,則聯集轉換會不明確,編譯器會回報錯誤。 關於轉換優先權的詳細資訊,請參閱 功能規範。
當 是聯集型態時,也可T?將 Union 轉換成可空的 union 結構T體()。
static void NullableUnionExample()
{
Pet? maybePet = new Dog("Buddy", "Beagle");
Pet? noPet = null;
Console.WriteLine(Describe(maybePet)); // output: Buddy
Console.WriteLine(Describe(noPet)); // output: no pet
static string Describe(Pet? pet) => pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
}
聯合模式匹配
當你對聯集表達式使用型別或宣告模式時,該模式測試的是聯集的內容,而非聯集實例。 例如,一個Dog模式會測試是否Value為:Dog
static void PatternMatching()
{
Pet pet = new Dog("Rex", "Labrador");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
圖案的形式決定它測試的值:
| Pattern | 聯集表達式上的行為 |
|---|---|
類型或宣告模式,例如DogDog dog |
將內容物與指定類型進行測試。 |
具有型態的性質或位置模式,例如 Dog { Breed: "Labrador" } |
將內容與類型對照測試,然後將巢狀模式套用到該案例值。 |
沒有型別的屬性或位置模式,例如 { Name: "Rex" } |
使用聯合實例的屬性或 Deconstruct 方法。 |
| 列表樣式 | 使用聯合實例的成員數量與索引,而非其內容。 |
var 或棄牌 _ 模式 |
擷取或匹配接收的聯合實例,且不拆包。 |
| 恆定模式或關係模式 | 測試內容物。 該 null 模式同時處理空聯集參考或可空包。 |
例如,定義一個 Pet 聯集,具有唯讀 Name 屬性,該屬性委派給包含 Cat 的或 Dog。 屬性 switch 表達式中的型態模式測試內容:
public union Pet(Cat, Dog)
{
public string Name => this switch
{
Cat cat => cat.Name,
Dog dog => dog.Name,
null => "",
};
}
該型態 Dog { Breed: "Labrador" } 會測試內容並讀取 dog Breed的 ,這是 union 沒有的屬性。 未型別的模式 { Name: "Rex" } 讀取的是 union 的 Name 屬性,因此它也與一隻名為 Rex 的貓相符:
Pet pet = new Dog("Rex", "Labrador");
Console.WriteLine(pet is Dog { Breed: "Labrador" }); // output: True
Console.WriteLine(pet is { Name: "Rex" }); // output: True
pet = new Cat("Rex");
Console.WriteLine(pet is Dog { Breed: "Labrador" }); // output: False
Console.WriteLine(pet is { Name: "Rex" }); // output: True
該 not 模式否定了其巢狀模式。 在 pet is not null中,巢狀 null 模式檢查內容,因此 not null 是一個有用的有值測試。 相較之下,空的無型態屬性模式 { },則檢查 union 實例。 它總是匹配一個不可空的結構聯集,即使 Value 為 空:
Pet empty = default;
Console.WriteLine(empty is { }); // output: True
Console.WriteLine(empty is not null); // output: False
if (empty is var captured)
{
Console.WriteLine(captured.Value is null); // output: True
}
邏輯模式保留了這些區別。 在 var pet and not null中, var pet 捕捉到的聯集,並 not null 測試其內容是否非零。 在 not null and var pet中, var 該圖案仍捕捉到輸入的聯集: not 不會改變傳遞給右側圖案的值。 相較之下,在 中 Dog and { Breed: "Labrador" },型態模式會將匹配 Dog 的映射傳遞給屬性模式。
備註
型別模式必須至少與一種案例類型相容。 本文 Pet 中的宣告會 pet is Pet 產生編譯器錯誤 CS8121:它們的案例類型無法匹配 Pet。 它不是對 union 實例的執行時測試。 要檢查內容,請使用 pet is not null 或 pet.Value is not null;若要擷取聯集本身,則使用圖案 var 。
簡言之,類型與宣告模式測試內容。 遞迴模式與型別也會測試內容;沒有型別的遞迴模式則使用聯合體自身的成員。 這些規則在表達 is 式和 switch 表達式中是相同的。
空匹配
對於結構聯集,該 null 模式會檢查是否 Value 為空:
static void NullHandling()
{
Pet pet = default;
var description = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
Console.WriteLine(description); // output: no pet
Console.WriteLine(pet.Value is null); // output: True
}
對於基於類別的聯集, null 當聯集參考本身為空或其 Value 性質為空時,成功:
Result<string>? result = null;
if (result is null) { /* true — the reference is null */ }
Result<string> empty = new Result<string>((string?)null);
if (empty is null) { /* true — Value is null */ }
對於可空聯集的結構型別(Pet?), null 當可空封裝器沒有值或底層聯集 Value 為空值時,該類型會成功。
聯合的窮盡性
switch當一個表達式處理每個案件類型時,即為對聯合體案件類型的窮盡。 如果可空性分析也認為內容非空,則不需要 discard(_)或 var arm:
static void PatternMatching()
{
Pet pet = new Dog("Rex", "Labrador");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
處理空內容時,偏好使用 null 臂而非 var 棄置_臂。
null這個模式會說明你的意圖,並在你新增沒有對應臂的案件類型時保留警告。 A var 或 discard arm 也接受任何新的案例,因此編譯器無法警告你缺少的案例。
當追蹤 Value 的空狀態為「可能是空」時,編譯器也會警告你省略 null 臂。 例如, NullablePet 具有可空的案例類型,因此其參數內容可能是空:
public union NullablePet(Cat?, Dog, Bird);
static string DescribeNullableCase(NullablePet pet) => pet switch
{
Cat cat => cat.Name,
Dog dog => dog.Name,
Bird bird => bird.Name,
null => "no pet",
};
將臂從此開關移除 null 會產生警告 CS8655。
可空分析並不保證產生的聯集在執行時內容不是空。 預設聯集包含 null,包括預設初始化欄位或陣列元素。 如果所有案例類型皆非 null,預設 Value 追蹤狀態仍是「非 null」,因此編譯器不一定會警告缺少 null 臂。 當預設內容能到達你的交換機時,請處理 null 正確的處理:
static void NullHandling()
{
Pet pet = default;
var description = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
Console.WriteLine(description); // output: no pet
Console.WriteLine(pet.Value is null); // output: True
}
在這個例子中 Pet ,移除 null 臂不會產生缺失空臂警告,除非早期程式碼改變了追蹤的空狀態,但當交換器接收到預設值時會拋 SwitchExpressionException 棄。 較早 is null 的測試可能會改變追蹤狀態並引發警告。 此行為與確定指派不同:編譯器會拒絕未指派的本地變數讀取。
可空性
編譯器透過以下規則追蹤聯合屬性 Value 的空狀態:
- 如果任何案例類型的預設空無狀態都是「可能為空」,則該屬性的預設空無狀態
Value為「可能為空」。否則,預設的空狀態為「非空」。 - 當你從案例類型(透過建構子或合併轉換)建立聯合值時,
Value會得到該輸入值的空狀態。 - 當非盒定存取模式
HasValue或TryGetValue(...)成員查詢聯合集內容時,的Value空狀態在分支true上變成「非空」。
海關聯盟類型
編譯器會將宣 union 告轉換成宣 struct 告。 結構體標示屬性[System.Runtime.CompilerServices.Union]並實作介面。IUnion 它包含每種案件類型和一個 Value 屬性的公開建構器。 語言提供隱含的聯集轉換,呼叫這些建構子。 這種生成的形式很有主見。 它總是結構體,總是用值型箱子,並且總是以 object?。
如果你想調整現有型別、建立基於類別的聯集、使用自訂儲存策略,或是需要互操作支援,可能需要不同的行為。 你可以手動建立一個聯合體類型。
任何帶有屬性的[Union]類別或結構體,只要遵循基本聯集模式,則稱為聯合型別。 基本的合併模式要求:
- 屬性
[Union]在類型上。 - 一個或多個公共建構子,每個建構子有一個單一的參數或
in參數。 每個建構子的參數類型定義了一種 案例類型。 - 一個類型(或
Value)的公共object?財產object,帶有get一個附件。
所有先前的工會成員必須是公開會員。 編譯器利用這些成員來實作聯集轉換、模式匹配及徹底性檢查。 你也可以實作 非盒裝存取模式 ,或建立 基於類別的聯集型態。 你的自訂聯盟類型可以增加更多成員。
編譯器假設自訂聯集類型符合以下行為規則:
-
合理性:
Value總是回傳null或某一案例類型的值,包括預設結構值。 自訂聯集可以將其預設值表示為非空情況,這與預設產生的聯集的空內容不同。 -
穩定性:如果你從某個案例類型建立一個聯合值,該
Value值與該案件類型相符(或若輸入是null)。null - 創建等價性:若一個值隱含可轉換為兩種不同案例類型,兩個創建成員產生相同的可觀察行為。
-
存取模式一致性:
HasValue若 和TryGetValue成員存在,其行為等同於直接檢查Value。
以下範例展示了一種自訂聯合類型:
[System.Runtime.CompilerServices.Union]
public struct Shape : System.Runtime.CompilerServices.IUnion
{
private readonly object? _value;
public Shape(Circle value) { _value = value; }
public Shape(Rectangle value) { _value = value; }
public object? Value => _value;
}
public record class Circle(double Radius);
public record class Rectangle(double Width, double Height);
static void ManualUnionExample()
{
Shape shape = new Shape(new Circle(5.0));
var area = shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
};
Console.WriteLine($"{area:F2}"); // output: 78.54
}
非包箱存取模式
自訂聯集類型可選擇性實作 非盒裝存取模式 ,以強型別存取值型案件,且不會在模式匹配時進行盒裝。 此圖案要求:
- 一個 類型的
HasValue屬性,當bool不回傳true時 則不是Value。null - 每個案件類型都有
TryGetValue一套方法,透過參數bool回傳out並交付該值。TryGetValue只有當true是該案例類型的非空值時,才回傳Value。out參數的型別可身份轉換為案例類型,若案例類型為可空值型態,則可轉換為底層值類型。
[System.Runtime.CompilerServices.Union]
public struct IntOrBool : System.Runtime.CompilerServices.IUnion
{
private readonly int _intValue;
private readonly bool _boolValue;
private readonly byte _tag; // 0 = none, 1 = int, 2 = bool
public IntOrBool(int? value)
{
if (value.HasValue)
{
_intValue = value.Value;
_tag = 1;
}
}
public IntOrBool(bool? value)
{
if (value.HasValue)
{
_boolValue = value.Value;
_tag = 2;
}
}
public object? Value => _tag switch
{
1 => _intValue,
2 => _boolValue,
_ => null
};
public bool HasValue => _tag != 0;
public bool TryGetValue(out int value)
{
value = _intValue;
return _tag == 1;
}
public bool TryGetValue(out bool value)
{
value = _boolValue;
return _tag == 2;
}
}
static void NonBoxingExample()
{
IntOrBool val = new IntOrBool((int?)42);
var description = val switch
{
int i => $"int: {i}",
bool b => $"bool: {b}",
};
Console.WriteLine(description); // output: int: 42
}
對於模式匹配,編譯器會呼叫 TryGetValue 型別-模式檢查(例如 union is T)以及 HasValue 空模式檢查(例如 union is null),以避免值型態的情況出現盒裝。 每個成員都對應自己的模式類型——兩者都不是彼此的備胎。 當缺少成員時,編譯器 Value 會檢查該模式的屬性。 欲了解更多資訊,請參閱「 編譯器如何產生模式匹配程式碼」。
工會會員供應商
聯合體類型可以將其聯合體成員委派給其直接包含並實作的公開巢狀 IUnionMembers 介面。 這個介面就是 工會成員的提供者。 編譯器僅在提供者介面上找到聯合成員,包括繼承的成員。 僅在工會類型上申報的會員不會被視為工會會員。
使用聯合成員提供者時,介面必須宣告以下所需成員:
-
靜態
Create方法:每種案件類型各一個,包含單一參數及可身份轉換為聯合型別的回傳類型。 這些方法會決定你的案件類型。 -
Value屬性:具有get存取器回傳所含值的公共object或object?財產。
你可以選擇在介面上宣告這些成員,讓編譯器產生更有效率的模式匹配程式碼:
-
TryGetValue方法:每個案例類型一個,回傳boolout該案例類型的參數。 -
HasValue財產:bool公共財產,存取get者返回true時Value不null為。
編譯器如何產生模式匹配程式碼:TryGetValue以及HasValue適用於不同類型的模式——兩者都不是彼此的備用:
- 對於 型別模式,例如
union is T,編譯器在宣告時會呼叫TryGetValue(out T value),擷取該值並套用該模式,無需進行封框。 否則,編譯器會將該模式套用到Value屬性上,該屬性會在執行時T檢查,並在 為值型別時將該值T框定。 - 對於 空模式,例如
union is null,編譯器在宣告時呼叫HasValue以測試聯集是否包含某個值。 否則,編譯器會直接將空模式套用到屬性Value上。
每個成員都是獨立選擇性的:沒有 TryGetValue,類型模式使用 Value;沒有 HasValue,空模式使用 Value。 宣告任一成員都能讓編譯器產生更有效率且強型別的該模式類型程式碼:
[System.Runtime.CompilerServices.Union]
public struct Outcome<T> : Outcome<T>.IUnionMembers
{
private readonly object? _value;
private Outcome(object? value) => _value = value;
public interface IUnionMembers
{
static Outcome<T> Create(T? value) => new(value);
static Outcome<T> Create(Exception? value) => new(value);
object? Value { get; }
// Optional but recommended: TryGetValue enables efficient pattern matching
bool TryGetValue(out T value);
bool TryGetValue(out Exception value);
}
object? IUnionMembers.Value => _value;
public bool TryGetValue(out T value)
{
if (_value is T t)
{
value = t;
return true;
}
value = default!;
return false;
}
public bool TryGetValue(out Exception value)
{
if (_value is Exception e)
{
value = e;
return true;
}
value = default!;
return false;
}
}
當聯集型別需要私有建構子或建立邏輯需要工廠模式(如 record class 聯集型別)時,聯合成員提供者非常有用。
基於類別的聯合類型
類別也可以是聯集類型。 這種聯集在需要參考語意或繼承時非常有用:
[System.Runtime.CompilerServices.Union]
public class Result<T> : System.Runtime.CompilerServices.IUnion
{
private readonly object? _value;
public Result(T? value) { _value = value; }
public Result(Exception? value) { _value = value; }
public object? Value => _value;
}
static void ClassUnionExample()
{
Result<string> ok = new Result<string>("success");
Result<string> err = new Result<string>(new InvalidOperationException("failed"));
Console.WriteLine(Describe(ok)); // output: OK: success
Console.WriteLine(Describe(err)); // output: Error: failed
static string Describe(Result<string> result) => result switch
{
string s => $"OK: {s}",
Exception e => $"Error: {e.Message}",
null => "null",
};
}
對於類別聯集,該 null 模式同時匹配一個空參考與一個空 Value參考。
C# 語言規格
欲了解更多資訊,請參閱 Union 功能規範。