C#?15?類(lèi)型系統(tǒng)改進(jìn)Union?Types詳解
前言
Union 類(lèi)型(聯(lián)合類(lèi)型)一直是 C# 社區(qū)呼聲最高的特性之一。從最初的 discriminated unions 提案到今天,這個(gè)特性經(jīng)歷了多年的設(shè)計(jì)和討論,終于在 C# 15 中正式落地。
Union 類(lèi)型允許我們將一個(gè)值限定為一組封閉類(lèi)型中的某一種,并且在針對(duì) Union 值的 switch 表達(dá)式中獲得窮盡性檢查。編譯器能幫你確認(rèn)是否處理了所有 case 類(lèi)型,很多時(shí)候就不再需要那個(gè)煩人的 _ 兜底分支。
本文將介紹 C# 15 中 Union 類(lèi)型的設(shè)計(jì)和用法。
從一個(gè)實(shí)際問(wèn)題出發(fā)
假設(shè)我們要實(shí)現(xiàn)一個(gè)函數(shù),它可能返回一個(gè)正常的結(jié)果,也可能返回一個(gè)錯(cuò)誤。以前常見(jiàn)的做法是定義一個(gè)包裝類(lèi):
public class Result<T>
{
public T? Data { get; set; }
public Exception? Error { get; set; }
public bool IsSuccess => Error is null;
}這種寫(xiě)法有一個(gè)很明顯的問(wèn)題:Data 和 Error 在類(lèi)型上同時(shí)存在,編譯器沒(méi)法保證「成功時(shí)一定有 Data」或「失敗時(shí)一定有 Error」。正確性全靠人為約定,而不是類(lèi)型系統(tǒng)來(lái)保障。
有了 Union 類(lèi)型,這個(gè)問(wèn)題就迎刃而解了。
Union 聲明
C# 15 引入了全新的 union 關(guān)鍵字,可以用非常簡(jiǎn)潔的語(yǔ)法聲明一個(gè)聯(lián)合類(lèi)型:
public union Pet(Cat, Dog, Bird);
就是這么簡(jiǎn)單!這一行聲明了一個(gè)名為 Pet 的聯(lián)合類(lèi)型,它的值可以是 Cat、Dog 或 Bird 中的任何一種。
Union 聲明會(huì)被編譯器展開(kāi)為一個(gè)結(jié)構(gòu)體,內(nèi)部用單個(gè) object 引用來(lái)存儲(chǔ)值:
// 編譯器生成的等價(jià)代碼
[Union] public struct Pet : IUnion
{
public Pet(Cat value) => Value = value;
public Pet(Dog value) => Value = value;
public Pet(Bird value) => Value = value;
public object? Value { get; }
}也就是說(shuō),Union 聲明就是一種簡(jiǎn)潔的結(jié)構(gòu)體聲明方式,編譯器幫你生成了所有樣板代碼。
再來(lái)看一個(gè)更實(shí)用的例子,利用 Union 和已有類(lèi)型組合來(lái)實(shí)現(xiàn) Option<T>:
public record class None(); public record class Some<T>(T Value); public union Option<T>(None, Some<T>);
還可以在 Union 中添加自定義方法:
public union OneOrMore<T>(T, IEnumerable<T>)
{
public IEnumerable<T> AsEnumerable() => Value switch
{
IEnumerable<T> list => list,
T value => [value],
};
}這也很方便!
另外,case 類(lèi)型并不只限于這里展示的具體類(lèi)。按照提案,它們還可以是接口、類(lèi)型參數(shù)、可空類(lèi)型,甚至其他 Union,而且 case 之間允許重疊。
不過(guò),Union 聲明本身是一種有意“收緊”的聲明形式。你可以添加方法之類(lèi)的成員,但不能聲明實(shí)例字段、自動(dòng)屬性或類(lèi)字段事件;你也不能自己聲明 public 的單參數(shù)構(gòu)造函數(shù),而你顯式添加的構(gòu)造函數(shù)必須通過(guò) this(...) 委托到編譯器生成的 case 構(gòu)造函數(shù)之一。
Union 轉(zhuǎn)換
Union 類(lèi)型支持從每個(gè) case 類(lèi)型到聯(lián)合類(lèi)型的隱式轉(zhuǎn)換:
Cat cat = new Cat("小花");
Pet pet = cat; // 隱式 union 轉(zhuǎn)換,不需要顯式構(gòu)造
編譯器會(huì)將其轉(zhuǎn)換為對(duì)構(gòu)造函數(shù)的調(diào)用:
// 編譯器實(shí)際生成的代碼 Pet pet = new Pet(cat);
這意味著你不需要手動(dòng)去包裝值,直接賦值就行了。如果你之前有自定義的隱式轉(zhuǎn)換運(yùn)算符,那它的優(yōu)先級(jí)會(huì)高于 union 轉(zhuǎn)換,所以現(xiàn)有代碼不會(huì)受到影響。
這里還有一個(gè)容易忽略的點(diǎn):Union 轉(zhuǎn)換只有隱式形式。即使某個(gè) case 類(lèi)型存在顯式轉(zhuǎn)換,也不代表因此就自動(dòng)擁有到整個(gè) Union 類(lèi)型的顯式轉(zhuǎn)換。
Union 匹配
Union 類(lèi)型真正的威力在于和模式匹配的配合。
當(dāng)你對(duì)一個(gè) Union 值進(jìn)行模式匹配時(shí),編譯器會(huì)自動(dòng)拆包內(nèi)部的值:
Pet pet = GetPet();
if (pet is Dog dog)
{
// dog 已經(jīng)是 Dog 類(lèi)型,直接使用
dog.Bark();
}
// switch 表達(dá)式
string description = pet switch
{
Dog dog => $"這是一只狗:{dog.Name}",
Cat cat => $"這是一只貓:{cat.Name}",
Bird bird => $"這是一只鳥(niǎo):{bird.Name}",
};注意最后一個(gè)分支后面沒(méi)有 _ 兜底!因?yàn)檫@是一個(gè)針對(duì) Union 值的 switch 表達(dá)式,而編譯器也知道 Pet 的 case 類(lèi)型只有 Dog、Cat 和 Bird,所以它可以把這個(gè)表達(dá)式視為窮盡的。
這確實(shí)是一個(gè)非常實(shí)用的功能,不僅簡(jiǎn)化了代碼,還在編譯期幫你保證了安全性。假如以后你給 Pet 增加了一個(gè)新的 case 類(lèi)型 Fish,那么所有沒(méi)有處理 Fish 的 switch 表達(dá)式都會(huì)產(chǎn)生編譯警告,避免了遺漏。
對(duì)于無(wú)條件的 var 和 _ 模式,匹配的是 Union 值本身而不是內(nèi)部值:
if (pet is var p) { ... } // p 是 Pet 類(lèi)型,不是 object
這是有意為之。var 通常只是給當(dāng)前值起個(gè)名字,保留 Union 類(lèi)型比解出一個(gè) object? 更實(shí)用。
這也意味著,pet is Pet p 并不等同于 pet is var p。在 Pet p 這樣的類(lèi)型模式里,Pet 會(huì)作用在拆包后的內(nèi)部值上,而不是外層的 Union 值本身,所以這個(gè)模式通常不會(huì)成功。
null 模式還有一個(gè)值得專(zhuān)門(mén)提醒的細(xì)節(jié)。對(duì)于基于 class 的 Union,result is null 在兩種情況下都會(huì)成功:Union 對(duì)象本身是 null,或者它內(nèi)部的 Value 是 null。對(duì)于 U? 這種“nullable 包裹 struct Union”的情況也類(lèi)似:如果外層 nullable 沒(méi)有值,或者內(nèi)部 Union 的 Value 為 null,那么 u is null 都會(huì)成功。相對(duì)地,其他 Union 匹配模式只有在外層值本身存在時(shí)才會(huì)成功。
Union 窮盡性
剛才提到了窮盡性檢查,這是 Union 類(lèi)型最重要的能力之一。
union Result(int, string, Exception);
string Describe(Result r) => r switch
{
int n => $"數(shù)字:{n}",
string s => $"字符串:{s}",
Exception e => $"錯(cuò)誤:{e.Message}",
// 編譯器認(rèn)為已窮盡,無(wú)需 _ 分支
};但如果 Union 的值可能為 null(例如某個(gè) case 類(lèi)型是可空的),編譯器會(huì)要求你處理 null 的情況:
Pet pet = GetNullableDog(); // pet.Value 可能是 null
var result = pet switch
{
Dog dog => "汪",
Cat cat => "喵",
Bird bird => "啾",
// 警告:未處理 null
};
手動(dòng)實(shí)現(xiàn) Union 模式
Union 聲明雖然方便,但并不是獲得 Union 行為的唯一方式。你完全可以在已有類(lèi)型上手動(dòng)實(shí)現(xiàn) Union 模式,只需滿(mǎn)足以下條件:
- 類(lèi)型標(biāo)記
[Union]屬性 - 提供對(duì)應(yīng)每個(gè) case 類(lèi)型的單參數(shù)構(gòu)造函數(shù)
- 提供一個(gè)
object?類(lèi)型的Value屬性
[Union]
public struct IntOrString
{
private readonly object _value;
public IntOrString(int value) => _value = value;
public IntOrString(string value) => _value = value;
public object? Value => _value;
}這在需要適配已有類(lèi)型,或者需要自定義存儲(chǔ)策略時(shí)非常有用。
Union 成員提供者(IUnionMembers)
默認(rèn)情況下,編譯器通過(guò) Union 類(lèi)型自身的構(gòu)造函數(shù)來(lái)識(shí)別 case 類(lèi)型。但有些場(chǎng)景下你可能不想暴露公開(kāi)構(gòu)造函數(shù),或者想用工廠(chǎng)方法來(lái)創(chuàng)建 Union 值。這時(shí)可以在 Union 類(lèi)型內(nèi)部聲明一個(gè)名為 IUnionMembers 的接口,讓它充當(dāng)成員提供者。
[Union]
public record class Result<T> : Result<T>.IUnionMembers
{
object? _value;
public interface IUnionMembers
{
public static Result<T> Create(T value) => new() { _value = value };
public static Result<T> Create(Exception value) => new() { _value = value };
public object? Value { get; }
}
object? IUnionMembers.Value => _value;
}當(dāng) Union 類(lèi)型內(nèi)部包含 IUnionMembers 接口聲明時(shí),編譯器就不再?gòu)?Union 類(lèi)型本身查找構(gòu)造函數(shù)了,而是從這個(gè)接口上的 Create 工廠(chǎng)方法來(lái)確定 case 類(lèi)型。Value 屬性也改為在接口上聲明。
這種模式有幾個(gè)好處:
- 可以對(duì)外隱藏構(gòu)造函數(shù),只通過(guò)工廠(chǎng)方法創(chuàng)建實(shí)例
- 適合
class類(lèi)型的 Union(Union 聲明默認(rèn)生成的是struct) - 可以靈活控制內(nèi)部存儲(chǔ)和初始化邏輯
使用時(shí),隱式轉(zhuǎn)換會(huì)自動(dòng)走工廠(chǎng)方法:
Result<string> result = "Hello";
// 等價(jià)于
Result<string> result = Result<string>.IUnionMembers.Create("Hello");
Non-boxing 訪(fǎng)問(wèn)模式
默認(rèn)的 Union 模式通過(guò) object? 類(lèi)型的 Value 屬性訪(fǎng)問(wèn)內(nèi)部值,這意味著值類(lèi)型會(huì)產(chǎn)生裝箱。如果你對(duì)性能有更高要求,可以額外實(shí)現(xiàn) HasValue 和 TryGetValue 方法,讓編譯器在模式匹配時(shí)使用強(qiáng)類(lèi)型的訪(fǎng)問(wèn)路徑:
[Union]
public struct IntOrBool
{
private bool _isBool;
private int _value;
public IntOrBool(int value) => (_isBool, _value) = (false, value);
public IntOrBool(bool value) => (_isBool, _value) = (true, value ? 1 : 0);
public object Value => _isBool ? (object)(_value == 1) : _value;
// Non-boxing 訪(fǎng)問(wèn)模式
public bool HasValue => true;
public bool TryGetValue(out int value)
{
value = _value;
return !_isBool;
}
public bool TryGetValue(out bool value)
{
value = _isBool && _value == 1;
return _isBool;
}
}這樣編譯器在進(jìn)行模式匹配時(shí),就不需要通過(guò) Value 屬性來(lái)裝箱獲取值了,而是直接調(diào)用對(duì)應(yīng)的 TryGetValue,從而避免了裝箱開(kāi)銷(xiāo)。
Result 模式的例子
讓我們回到文章開(kāi)頭的問(wèn)題,用 Union 來(lái)實(shí)現(xiàn)一個(gè)類(lèi)型安全的 Result<T>:
public union Result<T>(T, Exception);
一行搞定。用起來(lái)是這樣的:
Result<int> Divide(int a, int b)
{
if (b == 0) return new DivideByZeroException();
return a / b;
}
var result = Divide(10, 3);
var message = result switch
{
int value => $"結(jié)果是 {value}",
Exception ex => $"出錯(cuò)了:{ex.Message}",
};不需要額外的包裝類(lèi),不需要 IsSuccess 屬性,類(lèi)型系統(tǒng)保證了每種情況都被處理。比以前的做法優(yōu)雅得多。
Union 與類(lèi)型層次結(jié)構(gòu)
值得一提的是,C# 的 Union 類(lèi)型是類(lèi)型的聯(lián)合而不是帶標(biāo)簽的聯(lián)合。如果你需要更接近傳統(tǒng) discriminated unions 的效果(即每個(gè)分支有獨(dú)立的名稱(chēng)和數(shù)據(jù)),可以用 record 作為 case 類(lèi)型來(lái)組合:
public record class Circle(double Radius);
public record class Rectangle(double Width, double Height);
public record class Triangle(double Base, double Height);
public union Shape(Circle, Rectangle, Triangle);
double Area(Shape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
Triangle t => 0.5 * t.Base * t.Height,
};如果不需要命名的分支,用現(xiàn)有的類(lèi)型直接組合就好。兩種方式各有適用場(chǎng)景,C# 在設(shè)計(jì)上給了充分的靈活性。
另外,如果你需要更加嚴(yán)格的封閉類(lèi)型層次結(jié)構(gòu),還可以關(guān)注即將推出的 closed hierarchies 特性,它和 Union 類(lèi)型是互補(bǔ)的關(guān)系。
為什么不用類(lèi)型擦除?
看到這里你可能會(huì)想:Union 值在運(yùn)行時(shí)不就是一個(gè) object 引用嗎?那為什么不直接用 object 加上編譯期的元數(shù)據(jù)信息來(lái)表示 Union 類(lèi)型呢?也就是說(shuō),讓 union Pet(Cat, Dog) 直接擦除成 object,編譯器靠 attribute 之類(lèi)的元數(shù)據(jù)來(lái)記住這里其實(shí)是 Pet,跟 dynamic、元組名和可空注解的處理方式一樣。
這個(gè)思路在 C# 語(yǔ)言設(shè)計(jì)工作組中被認(rèn)真討論過(guò),而且在類(lèi)型聯(lián)合提案中也確實(shí)有 Ad Hoc Union 這一類(lèi)設(shè)計(jì)是基于擦除的。但最終它沒(méi)有成為 C# 15 的實(shí)現(xiàn)方案,原因有幾個(gè):
泛型場(chǎng)景下會(huì)破壞類(lèi)型安全。 考慮這樣一段代碼:
public class MyCollection<T>
{
public bool TryAdd(object o)
{
if (o is T t)
{
// 添加 t
return true;
}
return false;
}
}
如果用 MyCollection<(int or string)> 實(shí)例化,而 (int or string) 被擦除成了 object,那么 o is T 就變成了 o is object,永遠(yuǎn)成功。任何類(lèi)型的值都能繞過(guò)檢查被塞進(jìn)集合,類(lèi)型安全就徹底崩了。
不擦除也有問(wèn)題。 如果換成包裝類(lèi)型 ValueUnion<T1, T2> 來(lái)避免擦除,那 (string or bool) 和 (bool or string) 在運(yùn)行時(shí)就是不同的類(lèi)型。這對(duì) ad hoc union 來(lái)說(shuō)是無(wú)法接受的,因?yàn)橛脩?hù)自然會(huì)認(rèn)為這兩個(gè)同一組類(lèi)型的聯(lián)合應(yīng)該可以互換使用。工作組曾調(diào)研過(guò)運(yùn)行時(shí)層面的解決方案,但結(jié)論是不完美且代價(jià)巨大。
包裝類(lèi)型方案在實(shí)用性上勝出。 語(yǔ)言設(shè)計(jì)工作組整理過(guò)一份 Trade Off Matrix,對(duì)比了三種可行路線(xiàn):類(lèi)層次結(jié)構(gòu)、object 引用(擦除)和包裝類(lèi)型。最終選擇的包裝類(lèi)型方案(即現(xiàn)在的 Nominal Type Unions)在向后兼容性、非 ABI 破壞性、可定制實(shí)現(xiàn)以及交付周期等維度上都有明顯優(yōu)勢(shì)。擦除方案雖然在匿名語(yǔ)法和動(dòng)態(tài)模式匹配方面更出色,但需要較大的運(yùn)行時(shí)改造才能安全工作,短期內(nèi)無(wú)法落地。
所以最終的設(shè)計(jì)是:Union 聲明生成一個(gè)結(jié)構(gòu)體包裝,內(nèi)部用 object? 引用存值。你可以把它理解為一個(gè)編譯器幫你維護(hù)的包裝類(lèi)型,但它不是單純的元數(shù)據(jù)注解,運(yùn)行時(shí)確實(shí)存在這個(gè)結(jié)構(gòu)體,Value 屬性也是真實(shí)可訪(fǎng)問(wèn)的。這讓 Union 在反射、序列化、跨程序集調(diào)用等場(chǎng)景下都能正確工作,而不只是一個(gè)編譯器的錯(cuò)覺(jué)。
結(jié)語(yǔ)
Union 類(lèi)型的加入,是 C# 類(lèi)型系統(tǒng)一次質(zhì)的飛躍。它解決了長(zhǎng)期以來(lái)用 C# 表達(dá)多選一類(lèi)型時(shí)的尷尬:不再需要靠約定、靠運(yùn)行時(shí)檢查,而是讓編譯器從類(lèi)型層面幫你把關(guān)。
簡(jiǎn)潔的 union 聲明語(yǔ)法讓大多數(shù)場(chǎng)景幾行代碼就搞定,而靈活的 Union 模式又允許在需要時(shí)完全自定義底層實(shí)現(xiàn)。這種簡(jiǎn)單場(chǎng)景簡(jiǎn)單做,復(fù)雜場(chǎng)景有出路的設(shè)計(jì)理念,非常符合 C# 一貫的風(fēng)格。
期待 C# 15 和 .NET 11 的正式發(fā)布~
到此這篇關(guān)于C# 15 類(lèi)型系統(tǒng)改進(jìn):Union Types的文章就介紹到這了,更多相關(guān)C# 15 類(lèi)型系統(tǒng)改進(jìn):Union Types內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#實(shí)現(xiàn)繪制隨機(jī)噪點(diǎn)和直線(xiàn)
這篇文章主要為大家詳細(xì)介紹了C#如何實(shí)現(xiàn)繪制隨機(jī)噪點(diǎn)和直線(xiàn),文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)C#有一定的幫助,感興趣的小伙伴可以跟隨小編一起了解一下2023-01-01
C#實(shí)現(xiàn)HTTP訪(fǎng)問(wèn)類(lèi)HttpHelper的示例詳解
在項(xiàng)目開(kāi)發(fā)過(guò)程中,我們經(jīng)常會(huì)訪(fǎng)問(wèn)第三方接口,如我們需要接入的第三方接口是Web API,這時(shí)候我們就需要使用HttpHelper調(diào)用遠(yuǎn)程接口了。本文為大家介紹了C#實(shí)現(xiàn)HTTP訪(fǎng)問(wèn)類(lèi)HttpHelper的示例代碼,需要的可以參考一下2022-09-09
使用C#定時(shí)刷新已經(jīng)登錄過(guò)的網(wǎng)頁(yè)
日常工作中,登錄內(nèi)部系統(tǒng)往往需要輸入用戶(hù)名、密碼、短信驗(yàn)證碼,但是如果30分鐘不操作,會(huì)自動(dòng)退出,為避免頻繁登錄,可以使用c#寫(xiě)一個(gè)自動(dòng)刷新指定網(wǎng)址頁(yè)面的程序,所以本文給大家介紹了如何使用C#定時(shí)刷新已經(jīng)登錄過(guò)的網(wǎng)頁(yè),需要的朋友可以參考下2025-12-12
利用WPF實(shí)現(xiàn)系統(tǒng)資源監(jiān)控的完整代碼
在現(xiàn)代軟件開(kāi)發(fā)中,系統(tǒng)資源監(jiān)控是系統(tǒng)管理、性能分析和故障診斷的重要工具,WPF是構(gòu)建現(xiàn)代化、美觀實(shí)用的系統(tǒng)監(jiān)控應(yīng)用的理想選擇,本文將詳細(xì)介紹如何使用WPF創(chuàng)建一個(gè)功能全面的系統(tǒng)資源監(jiān)控儀表盤(pán),需要的朋友可以參考下2026-01-01
C#連接SQLite數(shù)據(jù)庫(kù)并實(shí)現(xiàn)基本操作
本文介紹了SQLite,一個(gè)輕量級(jí)的跨平臺(tái)數(shù)據(jù)庫(kù)管理系統(tǒng),以及如何在C#中使用System.Data.SQLite庫(kù)進(jìn)行操作,包括創(chuàng)建、修改和查詢(xún)數(shù)據(jù)庫(kù),以及使用SQLiteHelper類(lèi)簡(jiǎn)化SQL使用,此外,還提到了DB文件查看工具SQLiteSpy的應(yīng)用,需要的朋友可以參考下2024-12-12
HttpWebRequest出錯(cuò).Section=ResponseHeader Detail=CR
HttpWebRequest出錯(cuò).Section=ResponseHeader Detail=CR...2007-03-03
C#循環(huán)與循環(huán)控制的表達(dá)式樹(shù)實(shí)現(xiàn)
這篇文章介紹了C#循環(huán)與循環(huán)控制的表達(dá)式樹(shù)實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-01-01
C#實(shí)現(xiàn)將類(lèi)的內(nèi)容寫(xiě)成JSON格式字符串的方法
這篇文章主要介紹了C#實(shí)現(xiàn)將類(lèi)的內(nèi)容寫(xiě)成JSON格式字符串的方法,涉及C#針對(duì)json格式數(shù)據(jù)轉(zhuǎn)換的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-08-08
C#WinForm實(shí)現(xiàn)多語(yǔ)言切換的示例
本文主要介紹了C#WinForm實(shí)現(xiàn)多語(yǔ)言切換的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01

