共用方式為


工會

備註

本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。

功能規格與已完成實作之間可能有一些差異。 這些差異已記錄在相關的 語言設計會議(LDM)備忘錄中。

您可以在 規範的文章中深入瞭解將功能規範納入 C# 語言標準的過程。

Champion 期數:https://github.com/dotnet/csharplang/issues/9662

總結

Union 是一組相互連結的功能,這些功能結合起來為 C# 提供 union 類型的支援:

  • 聯集型別:具有 [Union] 屬性的結構體與類別被識別為 聯集型別,並支援 聯集行為
  • 案例類型:聯合型態擁有一組 案例類型,這些類型由建構子與工廠方法的參數所定義。
  • 聯合行為:聯合類型 支援以下聯合行為
    • 聯合轉換:每種案件類型之間都有隱含的 合併 轉換。
    • 聯集匹配:模式匹配與聯集值隱含地「展開」其內容,並將模式套用到底層值上。
    • 聯合集的窮盡性:當所有案件類型都已匹配,且無需備用案例時,對聯合值的切換式即為窮盡。
    • 聯合空檔:空檔分析強化了聯合內容空無狀態的追蹤。
  • 聯合模式:所有聯合類型都遵循基本 的聯合模式,但針對特定情境還有額外的可選模式。
  • 聯合聲明:簡寫語法允許直接宣告聯合類型。 實作是「有意見」的——一個遵循基本聯合模式並以單一參考欄位儲存內容的結構體宣告。
  • 聯集介面:語言已知並用於聯集宣告實作的介面。

動機

聯元是 C# 長期需求的功能,允許從封閉型別集合中表達值,且模式匹配能信賴其是窮全的。

聯集 型別 與聯集 宣告 的分離,讓 C# 能擁有簡潔的聯結宣告語法,且帶有主觀語意,同時允許現有型別或具有其他實作選項的型別選擇聯集行為。

C# 中提出的聯結是 型別 聯結,且非「歧視」或「標籤」聯結。 「區分聯集」可透過使用新類型宣告作為案例類型,來表達為「類型聯集」。 或者,它們也可以實作為 封閉階層結構,這是另一個相關且即將推出的 C# 功能,專注於窮盡性。

詳細設計

等位型別

任何帶有屬性的 System.Runtime.CompilerServices.UnionAttribute 類別或結構型別都被視為 聯合型別

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(Class | Struct, AllowMultiple = false)]
    public class UnionAttribute : Attribute;
}

工會類型必須遵循特定的公共 工會會員模式,這些模式必須在工會類型本身上宣告,或委派給「工會會員提供者」。

有些工會成員是強制性的,有些則是選擇性的。

工會類型包含一組案件類型,這些 案件 是根據特定工會成員的簽名來建立的。

聯合值的內容可以透過屬性 Value 存取。 該語言假設 永遠 Value 只包含其中一種案例類型的值,或為空(參見 良構性)。

工會會員供應商

預設情況下,工會成員會直接在工會類型上找到。 然而,如果 union 類型 直接包含 被呼叫 IUnionMembers 的介面宣告,則該介面會作為 union 成員提供者。 在這種情況下,工會成員 只會 在工會提供者上找到,而不是在工會類型本身。

union 成員的提供者介面必須是公開的,且 union 類型本身必須以介面形式實作。

我們用「 union-defining 類型 」來指集會成員所在的類型:如果存在,則稱為union成員提供者,否則則是union型別本身。

工會成員

工會成員會依姓名和簽名在工會定義的類型上查詢。 它們不必直接在定義聯合體的類型上宣告,但可以繼承。

任何工會成員不公開都是錯誤的。

創建成員與 Value 屬性為必填,統稱為 基本聯集模式

這些成員統稱為非拳擊聯盟的存取模式TryGetValueHasValue

不同的工會成員如下所述。

工會創立成員

聯會建立成員用於從案件類型值建立新的聯會值。

若聯集定義型別是聯集型別本身,則每個只有單一參數的建構子即為 聯集建構子。 聯集的案例類型被識別為由這些建構子的參數型態所建構的類型集合,方式如下:

  • 若參數型別為可空型(無論是值或參考),則 case 型別即為底層型別
  • 否則,案例類型即為參數類型。
// Union constructor making `Dog` a case type
public Pet(Dog value) { ... }
// Union constructor making `int` a case type
public Union(int? value) { ... }
// Union constructor making `string` a case type
public Union(string? value) { ... }

若 union 定義型別是 union 成員提供者,則每個具有單一參數且回傳型別可直接轉換為 union 型別的靜態 Create 方法,即為 union 工廠方法。 聯集的案例類型以以下方式識別為由這些工廠方法的參數型態所建構的類型集合:

  • 若參數型別為可空型(無論是值或參考),則 case 型別即為底層型別
  • 否則,案例類型即為參數類型。
// Union factory method making `Cat` a case type
public static Pet Create(Cat value) { ... }
// Union factory method making `int` a case type
public static Union Create(int? value) { ... }
// Union factory method making `string` a case type
public static Union Create(string? value) { ... }

工會建設者與工會工廠方法統稱為 工會創建會員

聯合創建成員的單一參數必須是副值或 in 參數。

一個聯合類型必須至少有一名聯合創建成員,因此至少有一個案件類型。

Value 屬性

Value 屬性允許存取合併中包含的值,無論其案件類型為何。

每個聯集定義型別必須宣告一個 Value 型別 object? 為 或 object的屬性。 該屬性必須有一個 get 存取器,且可選擇性地設置 init 一個 or set 附屬器,該存取器可為任何可及性,編譯器不會使用。

// Union 'Value' property
public object? Value { get; }

非拳擊入場會員

聯合型態可以選擇額外實作 非盒裝聯集存取模式,允許對每個案件類型進行強型別的條件存取,並提供檢查空值的方法。

這使得編譯器能更有效率地實作模式匹配,當案例類型為值型態並以值型態儲存在聯合中時。

非盒裝進入會員有:

  • 具有HasValue公共get附件的類型bool財產。 它可以選擇性地加入 init 一個或 accessor,該 accessor set 可以是任何可及性,編譯器不會使用。
  • 每種案件類型的方法 TryGetValue 。 該方法回傳 bool 並取一個可身份轉換為案例型別的單一出參數。
// Non-boxing access members
public bool HasValue { get { ... } }
public bool TryGetValue(out Dog value) { ... }

HasValue 當且僅當聯 Value 集的 不是空時,預期會返回真。

TryGetValue 當且僅當聯集的 屬於 Value 給定情況類型時,預期會回傳為真,若是,則在方法的 out 參數中交付該值。

結構完善性

語言與編譯器對聯集類型做出多項行為假設。 如果某型別符合聯集型別但不符合這些假設,則聯集行為可能無法如預期運作。

  • 合理性:屬性 Value 總是將 值為零值或某一案件類型值。 即使是 union 類型的預設值也是如此。
  • 穩定性:若由案例類型建立聯合值,該 Value 屬性將匹配該案例類型或空值。 若由一個 null 值創造聯合值,則該 Value 性質為 null
  • 創建等價性:若一個值可隱含轉換為兩種不同的案例類型,則任一案例類型的創建成員在以該值呼叫時具有相同的可觀察行為。
  • 存取模式一致性:若存在,與非盒控存取成員的行為HasValueTryGetValue可觀察等同於直接檢查該Value性質。

聯合類型的例子

Pet 在聯合類型本身實作基本聯集模式:

[Union] public record struct Pet
{
    // Creation members = case types are 'Dog' and 'Cat'
    public Pet(Dog value) => Value = value;
    public Pet(Cat value) => Value = value;

    // 'Value' property
    public object? Value { get; }
}

IntOrBool 在聯合類型本身實作非盒定存取模式:

public record 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 ? _value is 1 : _value;

    public bool HasValue => true;
    public bool TryGetValue(out int value)
    {
        value = _value;
        return !_isBool;
    }
    public bool TryGetValue(out bool value)
    {
        value = _isBool && _value is 1;
        return _isBool;
    }
}

註: 這只是非盒裝存取模式可能實作的一個範例。 使用者程式碼可以隨意儲存內容。 特別是,這並不妨礙拳擊的實施! 其名稱中的 the non-boxing 指的是允許編譯器的模式匹配實作以強型別方式存取每個案例類型,而非 object?-typed Value 性質。

Result<T> 透過一個聯合成員提供者實作基本模式:

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;
}

工會行為

聯集行為通常透過基本聯集模式實作。 若工會提供非盒裝存取模式,工會模式匹配會優先使用該模式。

工會轉型

聯集轉換隱含地從每個案例類型轉換為聯集類型。 具體來說,若有一個標準的隱式轉換從E某個型E別到某個型別C,且C是 的U聯集創建成員的參數型別,則會將 聯集轉換成聯集型別U。 如果 union 型別U是結構體,那麼如果有一個標準的隱式轉換,C從 類型E或表達E式轉換到 type 且C是 union 創建成員U的參數型別,則 有 union U? 轉換成 類型。

聯合轉換本身並非標準的隱含轉換。 因此,它可能不會參與使用者定義的隱性轉換或其他聯合轉換。

書中沒有明確的聯合轉換,僅有隱含的合併轉換。 因此,即使有明確的 E 從 轉換為 的 聯集 C類型,也不代表有明確的轉換 從 E 該聯集類型。

合併轉換是透過呼叫合併的創建成員來執行的:

Pet pet = dog;
// becomes
Pet pet = new Pet(dog);
// and
Result<string> result = "Hello"
//becomes
Result<string> result = Result<string>.IUnionMembers.Create("Hello");

若超載解析未找到單一最佳候選成員,或該成員非該聯盟類型的成員,則為錯誤。

聯合轉換只是隱含用戶定義轉換的另一種「形式」。 適用的使用者定義轉換運算子會「影子」合併轉換。

此決定背後的理由:

如果有人寫了使用者自訂運算子,應該會優先處理。 換句話說,如果使用者真的寫了自己的運算子,他們希望我們來呼叫它。 現有類型將轉換運算元轉換為聯合類型,至今仍以相同方式運作,適用於現有使用該運算元的程式碼。

以下範例中,隱含的使用者定義轉換優先於聯合轉換。

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => ...
    public S1(string x) => ...
    object System.Runtime.CompilerServices.IUnion.Value => ...
    public static implicit operator S1(int x) => ...
}

class Program
{
    static S1 Test1() => 10; // implicit operator S1(int x) is used
    static S1 Test2() => (S1)20; // implicit operator S1(int x) is used
}

以下範例中,當程式碼中使用明確的 cast 時,明確的使用者定義轉換優先於聯合轉換。 但當程式碼中沒有明確的鑄造時,會使用聯合轉換,因為明確的使用者定義轉換不適用。

struct S2 : System.Runtime.CompilerServices.IUnion
{
    public S2(int x) => ...
    public S2(string x) => ...
    object System.Runtime.CompilerServices.IUnion.Value => ...
    public static explicit operator S2(int x) => ...
}

class Program
{
    static S2 Test3() => 10; // Union conversion S2.S2(int) is used
    static S2 Test4() => (S2)20; // explicit operator S2(int x)
}

聯合配對

當模式的輸入值屬於聯集型別或聯集型別的可空值時,該可空值及其底層聯集值的內容可能會被「展開包裝」,視模式而定。

對於無條件 _var 模式,模式會套用到輸入值本身。 例如:

if (GetPet() is var pet) { ... } // 'pet' is the union value returned from `GetPet`

然而,所有其他模式都會隱含地套用到基礎聯集的 Value 性質上:

if (GetPet() is Dog dog) { ... }   // 'Dog dog' is applied to 'GetPet().Value'
if (GetPet() is null) { ... }      // 'null' is applied to 'GetPet().Value'
if (GetPet() is { } value) { ... } // '{ } value' is applied to 'GetPet().Value'

對於邏輯模式,此規則會分別應用於分支,但請記住模式的 and 左分支可能影響右分支的入門類型:

GetPet() switch
{
    var pet and not null   => ... // 'var pet' applies to the incoming 'Pet' and 'not null' to its 'Value'
    not null and var value => ... // 'not null' applies to the 'Value' as does 'var value' because of the 
                                  // left branch changing the incoming type to `object?`.
}

註: 這條規則意味著,這 GetPet() is Pet pet 很可能不會成功,這 Pet 適用於 內容,而非 Pet 工會本身。

註: 無條件 var 模式(以及 _,本質上是 的簡稱 var _)處理不同的原因,是假設其使用方式在質上與其他模式不同。 var 模式僅用來命名被匹配的值,常見於巢狀模式,例如 PetOwner{ Pet: var pet }。 此處的語意是 pet 保留聯集型別 Pet,而非 Value 將屬性退參用於無 object? 用型別。

若輸入值為類別型別,則 null 無論聯合值本身或 null 其包含值為 null,模式都會成功:

if (result is null) { ... } // if (result == null || result.Value == null)

其他聯集匹配模式僅在聯集值本身不 null為 時成功。

if (result is 1) { ... } // if (result != null && result.Value is 1)

同理,若輸入值為可空值型別(包裹結構聯集型別),則 null 無論輸入值本身為 null 還是其包含值為 null,模式都會成功:

if (result is null) { ... } // if (result.HasValue == false || result.GetValueOrDefault().Value == null)

其他聯集匹配模式僅在輸入值本身不 null為 時成功。

if (result is 1) { ... } // if (result.HasValue && result.GetValueOrDefault().Value is 1)

編譯器會偏好透過非盒定存取模式所規定的成員來實作模式行為。 雖然在良構性規則範圍內可以自由進行任何優化,但以下為保證應用的最小集合:

  • 對於一種包含檢查特定型態 T的模式,若 TryGetValue(S value) 有方法可用,且存在單位性,或隱含的參考/盒裝轉換從 TS,則該方法用於取得該值。 然後對該數值套用該模式。 若有多種此類方法,則若有,任何從 T 轉換為 S 的轉換方式都非拳擊轉換為首選。 若仍有多種方法,則依實作定義方式選擇一種。
  • 否則,對於包含檢查 的 null模式,若 HasValue 有屬性可用,則用該屬性檢查聯集值是否為空。
  • 否則,該模式會應用於存取該財產的結果 IUnion.Value

應用於聯集型別的 is 型態運算子,其意義與應用於聯集型別的型態模式相同。

聯合的窮盡性

聯集類型被假設已「耗盡」於其案例類型。 這表示若一個 switch 表達式能處理聯合體的所有案例類型,則稱為窮盡:

var name = pet switch
{
    Dog dog => ...,
    Cat cat => ...,
    // No warning about non-exhaustive switch
};

可空性

聯合 Value 屬性的零狀態會像其他屬性一樣被追蹤,但有以下修改:

  • 當呼叫一個 union 創建成員(無論是明確或透過 union 轉換)時,新的 union Value 會得到輸入值的 null 狀態。
  • 當非盒裝存取模式 HasValue 的 or TryGetValue(...) 被用來查詢聯合型別的內容(明確或透過模式匹配)時,它會 Value以類似直接檢查的方式影響 的 的空可空狀態 Value :的 Value 空狀態在分支上 true 變成「非空」。

即使聯合開關是窮盡的,如果接收 Value 聯合的屬性的 null 狀態是「可能是 null」,未處理的 null 也會發出警告。

Pet pet = GetNullableDog(); // 'pet.Value' is "maybe null"
var value = pet switch
{
    Dog dog => ...,
    Cat cat => ...,
    // Warning: 'null' not handled
}

聯合介面

以下介面是該語言在實作聯集功能時所使用的。

聯合接取介面

介面在 IUnion 編譯時將型別標記為聯合型別,並提供執行時存取聯合集內容的方式。

public interface IUnion
{
    // The value of the union or null
    object? Value { get; }
}

編譯器產生的 Union 實作此介面。

範例使用:

if (value is IUnion { Value: null }) { ... }

聯盟宣言

聯合聲明是 C# 中簡潔且有主見的聯合類型宣告方式。 它們宣告一個結構體,使用單一物件參考來儲存其 Value,這表示:

  • 盒裝:任何箱型中的價值類型都會在進入時被封箱。
  • 緊緻性:聯集值僅包含單一欄位。

其目的是讓工會聲明能很好地涵蓋絕大多數的使用情境。 手動編碼特定聯合體類型而非使用聯合聲明的主要原因有兩個:

  • 將現有型別調整至聯集模式以獲得聯集行為。
  • 例如為了效率或互通性,實施不同的儲存策略。

語法

聯集宣告有一個名稱和一個 聯合建構子 類型的清單。

union_declaration
    : attributes? struct_modifier* 'partial'? 'union' identifier type_parameter_list?
      '(' type (',' type)* ')'  struct_interfaces? type_parameter_constraints_clause* 
      (`{` struct_member_declaration* `}` | ';')
    ;

除了對結構成員的限制(§16.3)外,以下規定也適用於工會成員:

  • 不允許實例欄位、自動屬性或類欄位事件。
  • 不允許明確宣告且僅有單一參數的公開建構子。
  • 明確宣告的建構子必須使用 this(...) 初始化器(直接或間接)委派給其中一個產生的建構子。

聯集建構子類型可以是任何能轉換成 object的類型,例如介面、型別參數、可空型態及其他聯集。 結果案例重疊、聯位或為空都可以。

範例:

// Union of existing types
public union Pet(Cat, Dog, Bird);

// Union with function member
public union OneOrMore<T>(T, IEnumerable<T>)
{
    public IEnumerable<T> AsEnumerable() => Value switch
    {
        IEnumerable<T> list => list,
        T value => [value],
    }
}

// "Discriminated" union with freshly declared case types
public record class None();
public record class Some<T>(T value);
public union Option<T>(None, Some<T>);

#### Lowering

A union declaration is lowered to a struct declaration with

* the same attributes, modifiers, name, type parameters and constraints,
* implicit implementations of `IUnion`,
* a `public object? Value { get; }` auto-property,
* a public constructor for each *union constructor* type,
* any members in the union declaration's body.

It is an error for user-declared members to conflict with generated members.

Example:

``` c#
public union Pet(Cat, Dog){ ... }

降為:

[Union] public struct Pet : IUnion
{
    public Pet(Cat value) => Value = value;
    public Pet(Dog value) => Value = value;
    
    public object? Value { get; }
    
    ... // original body
}

未解決問題

[已解決]工會聲明算是紀錄嗎?

聯合聲明會被降為記錄結構

我認為這種預設行為是不必要的,且因為它無法設定,會大幅限制使用情境。 紀錄會產生大量未使用或不符合特定需求的程式碼。 例如,因為程式碼膨脹,在編譯器的程式碼庫中幾乎禁止記錄。 我覺得改掉預設會比較好:

  • 預設情況下,union 宣告會宣告一個只有 union 特定成員的一般結構體。
  • 使用者可以宣告記錄聯合: record union U(E1, ...) ...

解決: 聯合聲明是純結構,不是記錄結構。 record union ...這並不支援

[已解決]聯合宣言語法

看起來這個提議的語法不完整或是過於限制。 例如,看起來不允許使用基礎條款。 不過,我很容易想像需要像是實作介面這樣的情況。 我認為除了元素類型列表之外,語法應該會符合一般 struct/record struct 宣告,關鍵字 struct 會被替換成 union 關鍵字。

解決: 限制被解除。

[已解決]聯盟宣言成員

不允許實例欄位、自動屬性或類欄位事件。

這感覺很武斷,完全沒必要。

解決: 限制是保留的。

[已解決]可空值類型作為聯合案件類型

聯集的案例類型即為這些建構子所組成的參數類型集合。 聯集的案例類型被識別為這些工廠方法中的參數類型集合。

同時:

每種案件類型的方法 TryGetValue 。 該方法回傳 bool 並取一個對應給定情況類型的單一出參數,方式如下:

  • 若案例類型為可空值型別,該參數的型別應可身份轉換為底層型別
  • 否則,該類型應該是可身份轉換成案件類型。

在案件類型中有一個可空值型別,特別是型別模式不能將可空值型別作為目標型別,這是否有優勢? 感覺我們可以簡單地說,如果建構子/工廠的參數類型是可空值的,那麼對應的案例類型就是底層類型。 這樣我們就不需要那個額外的子句來做方法 TryGetValue ,所有的 out 參數都是案例類型。

解決: 建議獲得批准

[已解決]屬性的預設可空狀態Value

對於所有案例類型都無法為空的聯集型別,預設 Value 狀態為「not null」而非「可能是 null」。

在新設計中, Value property 不是在某個通用介面中定義,而是明確屬於宣告型別的 API,上述規則感覺像是過度工程化。 此外,該規則很可能會強制消費者在原本無法使用可空的型別的情況下使用可空的型別。

例如,考慮以下聯合聲明:

union U1(int, bool, DateTime);

根據引用規則,預設 Value 狀態為「非空」。 但這與該類型的行為不符, default(U1).Valuenull。 為了重新調整行為,消費者被迫至少讓一種案例類型成為可空。 類似這樣的情況:

union U1(int?, bool, DateTime);

但這很可能是不理想的,消費者可能不願意允許明確的有 int? 價值創造。

建議:移除引用規則,可空分析應利用屬性 Value 的註解來推斷其預設的空性。

解決: 提案獲准

[已解決]聯集值型別的 Nullable 聯集匹配

當模式的輸入值屬於聯集型態時,根據模式的不同,聯集值的內容可能會被「展開包裝」。

我們是否應該將此規則擴展到當模式的輸入值為 ?Nullable<union type>

試想以下情況:

    static bool Test1(StructUnion? u)
    {
        return u is 1;
    }   

    static bool Test2(ClassUnion? u)
    {
        return u is 1;
    }   

Test1 和 Test2 的意義 u is 1 非常不同。 在測試一中,這不是聯合匹配,在測試二中則是。 或許「聯合匹配」應該像其他情況中模式匹配一樣「挖掘」 Nullable<T> 一下。

如果我們採用這個,那麼 union Nullable<union type> 匹配null模式應該和 classs 一樣適用。 也就是說,當 (!nullableValue.HasValue || nullableValue.Value.Value is null)時,該模式為真。

解決: 提案獲得批准。

該如何處理「不良」的 API?

編譯器對於看起來像是匹配但實際上「不好」的 union 匹配 API 應該怎麼處理? 例如,編譯器找到 TryGetValue/HasValue 與簽名相符,但因為需要自訂修飾符或需要未知功能等,這被視為「壞」的。編譯器應該默默忽略 API 或回報錯誤嗎? 類似地,API 也可能被標記為過時/實驗性。 編譯器應該報告任何診斷結果、默默使用 API 還是默默不使用 API 嗎?

如果工會申報的類型缺失了怎麼辦

如果 UnionAttributeIUnionIUnion<TUnion> 缺少,會發生什麼事? 錯誤? 合成? 還是其他什麼?

[已解決]通用 IUnion 介面設計

已有論證指出 IUnion<TUnion> 不應繼承 IUnion 或限制其型態參數為 IUnion<TUnion>。 我們應該再談談。

解決:IUnion<TUnion>介面暫時被移除。

[已解決]可空的值類型作為案例類型及其與 TryGetValue

上述規則指出,若某案例類型是可空值型態,則對應 TryGetValue 方法中使用的參數型態應為 底層 型別。 這是因為這種方法永遠不會產生一個 null 數值。 在消費端,無法將可空值型態作為型態模式,而與底層型別的匹配應能映射到此方法的呼叫。

我們應該確認我們同意這種拆解。

解決: 同意/確認

非拳擊工會的存取模式

需要明確規定尋找合適HasValueTryGetValue與 API 的精確規則。 有遺產問題嗎? 讀寫 HasValue 匹配是否可接受? 等等。

[已解決] TryGetValue 匹配轉換

聯合配對部分寫道:

對於一個包含檢查特定型別 T的模式,若 TryGetValue(S value) 有方法可用,且隱含從 T 轉換為 S,則該方法用於取得該值。

隱含轉換的集合有受到限制嗎? 例如,允許使用者自訂轉換嗎? 那元組轉換和其他不那麼簡單的轉換呢? 其中一些甚至是標準的轉換。

這些方法有 TryGetValue 其他限制嗎? 例如,聯合模式章節暗示只有參數型別與案例類型相符的方法才會被考慮:

每種情況類型的T方法 public bool TryGetValue(out T value)

最好能有一個明確的答案。

解決: 僅考慮隱含的身份認同、參考或拳擊轉換

TryGetValue 以及可歸零分析

當非盒裝存取模式 HasValue 的 or TryGetValue(...) 被用來查詢聯合型別的內容(明確或透過模式匹配)時,它會 Value以類似直接檢查的方式影響 的 的空可空狀態 Value :的 Value 空狀態在分支上 true 變成「非空」。

這些方法有 TryGetValue 受到限制嗎? 例如,聯合模式章節暗示只有參數型別與案例類型相符的方法才會被考慮:

每種情況類型的T方法 public bool TryGetValue(out T value)

最好能有一個明確的答案。

釐清結構 default 聯集類型值的規則

注意:以下提到的預設可作廢規則已被移除。

注意:以下提及的「預設」良構規則已被移除。 我們應該確認這是我們想要的。

可撤銷性」部分說:

對於所有案例類型都無法為空的聯集型別,預設 Value 狀態為「not null」而非「可能是 null」。

假設以下範例中,目前實作將 視為Values2「非零」:

S2 s2 = default;

struct S2 : System.Runtime.CompilerServices.IUnion
{
    public S2(int x) => throw null!;
    public S2(bool x) => throw null!;
    object? System.Runtime.CompilerServices.IUnion.Value => throw null!;
}

同時, 良好形成 部分說:

  • 預設值:若聯集型別是值型別,其預設值為 nullValue
  • 預設建構子:若聯集型態有空標(無參數)建構子,則該聯集為 nullValue

這樣的實作會與上述範例中可空的分析行為相矛盾。

規則應該調整,還是狀態應該Valuedefault是「可能為零」? 如果是後者,初始化 S2 s2 = default; 是否應該產生可作廢性警告?

確認型別參數永遠不會是聯集型別,即使被限制為聯集型別。

class C1 : System.Runtime.CompilerServices.IUnion
{
    private readonly object _value;
    public C1(int x) { _value = x; }
    public C1(string x) { _value = x; }
    object System.Runtime.CompilerServices.IUnion.Value => _value;
}

class Program
{
    static bool Test1<T>(T u) where T : C1
    {
        return u is int; // Not a union matching
    }   

    static bool Test2<T>(T u) where T : C1
    {
        return u is string; // Not a union matching
    }   
}

後置條件屬性是否應該影響 Union 實例的預設空化性?

注意:以下提到的預設可作廢規則已被移除。 我們也不再從聯集建立方法推斷屬性的預設空化性 Value 。 因此,這個問題已經過時或不再適用於目前的設計。

對於所有案例類型都無法為空的聯集型別,預設 Value 狀態為「not null」而非「可能是 null」。

警告是否預期會在以下情境出現?

#nullable enable

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => throw null!;
    public S1([System.Diagnostics.CodeAnalysis.NotNull] bool? x) => throw null!;
    object? System.Runtime.CompilerServices.IUnion.Value => throw null!;
}
class Program
{
    static void Test2(S1 s)
    {
       // warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive).
       //                 For example, the pattern 'null' is not covered.
        _ = s switch { int => 1, bool => 3 }; // 
    } 
}

工會轉型

[已解決]在其他轉換中,它們在優先順序上屬於哪裡?

工會轉換感覺像是另一種用戶定義轉換。 因此,目前的實作會在嘗試分類隱含的使用者定義轉換失敗後立即將其分類,若存在,則視為另一種使用者定義轉換。 這會帶來以下後果:

  • 隱含的使用者定義轉換優先於聯合轉換
  • 當程式碼中使用明確 cast 時,明確的使用者定義轉換優先於聯合轉換
  • 當程式碼中沒有明確的鑄造時,聯合轉換會優先於明確的使用者定義轉換
struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => ...
    public S1(string x) => ...
    object System.Runtime.CompilerServices.IUnion.Value => ...
    public static implicit operator S1(int x) => ...
}

struct S2 : System.Runtime.CompilerServices.IUnion
{
    public S2(int x) => ...
    public S2(string x) => ...
    object System.Runtime.CompilerServices.IUnion.Value => ...
    public static explicit operator S2(int x) => ...
}

class Program
{
    static S1 Test1() => 10; // implicit operator S1(int x) is used
    static S1 Test2() => (S1)20; // implicit operator S1(int x) is used
    static S2 Test3() => 10; // Union conversion S2.S2(int) is used
    static S2 Test4() => (S2)20; // explicit operator S2(int x)
}

需要確認這是我們喜歡的行為。 否則轉換規則應該要釐清。

解決方法:

已獲工作小組批准。

[已解決]建構子參數的參考性

目前語言僅允許按數值與 in 參數來設定使用者自訂的轉換運算子。 感覺這種限制的理由同樣適用於適合合併轉換的建材機。

提案:

請調整上述章節中 a case type constructorUnion types 的定義:

-For each public constructor with exactly one parameter, the type of that parameter is considered a *case type* of the union type.
+For each public constructor with exactly one **by-value or `in`** parameter, the type of that parameter is considered a *case type* of the union type.

解決方法:

目前已獲得工作小組批准。 不過,我們可以考慮「拆分」case 類型建構子集合與適合聯集型別轉換的建構子集合。

[已解決]可取消轉換

可撤銷轉換(Nullable Conversion )章節明確列出可作為基礎的轉換。 現行規範並未建議對該清單做任何調整。 這會導致以下情境的錯誤:

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => throw null;
    public S1(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class Program
{
    static S1? Test1(int x)
    {
        return x; // error CS0029: Cannot implicitly convert type 'int' to 'S1?'
    }   
}

提案:

調整規範以支援隱含的可空檔轉換,ST?並由聯合轉換支持。 具體來說,假設 T 是聯集型別,則若從 聯集E轉換到 某型態C,則隱含地從某型別或表達E式轉換成型別T?,且C是 的T情境型別。 注意,並不要求 的 E 型別必須是不可空的值型別。 轉換以從 到 的T基礎聯集轉換S計算,接著從 到 的包裝TT?

解決方法:

已批准。

[已解決]提升改裝

我們是否想調整 「提升 轉換」部分以支援提升工會轉換? 目前不允許:

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(int x) => throw null;
    public S1(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class Program
{
    static S1 Test1(int? x)
    {
        return x; // error CS0029: Cannot implicitly convert type 'int?' to 'S1'
    }   

    static S1? Test2(int? y)
    {
        return y; // error CS0029: Cannot implicitly convert type 'int?' to 'S1?'
    }   
}

解決方法:

目前沒有解除工會轉化。 討論中的一些筆記:

這裡對使用者定義轉換的類比有點不成立。 一般來說,聯合體能夠包含一個空值。 目前尚不清楚提拔是否應該建立一個包含值的聯集型實例,還是應該產生null一個值nullNullable<Union>

[已解決]從基底型實例轉換區塊聯合?

目前的行為可能會讓人感到困惑:

struct S1 : System.Runtime.CompilerServices.IUnion
{
    public S1(System.ValueType x)
    {
    }
    public S1(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class Program
{
    static S1 Test1(System.ValueType x)
    {
        return x; // Union conversion
    }   

    static S1 Test2(System.ValueType y)
    {
        return (S1)y; // Unboxing conversion
    }   
}

請注意,該語言明確禁止從基底類型宣告使用者定義的轉換。 因此,不允許工會轉換這類情況或許合理。

解決方法:

暫時不要做什麼特別的事。 一般情境本來就無法完全保護。

[已解決]從介面類型的實例轉換區塊聯合?

目前的行為可能會讓人感到困惑:

struct S1 : I1, System.Runtime.CompilerServices.IUnion
{
    public S1(I1 x) => throw null;
    public S1(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

interface I1 { }

struct S2 : System.Runtime.CompilerServices.IUnion
{
    public S2(I1 x) => throw null;
    public S2(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class C3 : System.Runtime.CompilerServices.IUnion
{
    public C3(I1 x) => throw null;
    public C3(string x) => throw null;
    object System.Runtime.CompilerServices.IUnion.Value => throw null;
}

class Program
{
    static S1 Test1(I1 x)
    {
        return x; // Union conversion
    }   

    static S1 Test2(I1 x)
    {
        return (S1)x; // Unboxing
    }   

    static S2 Test3(I1 x)
    {
        return x; // Union conversion
    }   

    static S2 Test4(I1 x)
    {
        return (S2)x; // Union conversion
    }   

    static C3 Test3(I1 x)
    {
        return x; // Union conversion
    }   

    static C3 Test4(I1 x)
    {
        return (C3)x; // Reference conversion
    }   
}

請注意,該語言明確禁止從基底類型宣告使用者定義的轉換。 因此,不允許工會轉換這類情況或許合理。

解決方法:

暫時不要做什麼特別的事。 一般情境本來就無法完全保護。

IUnion 介面的命名空間

介面的命名空間 IUnion 尚未明確說明。 如果意圖是讓它保持在命名 global 空間中,那我們就明確說明這點。

提案:如果這是被忽略的部分,我們可以使用 System.Runtime.CompilerServices 命名空間。

類別作為 Union 類型

[已解決]檢查實例本身 null

如果一個聯集型別是類別型別,它的值本身可能是空值。 那空判定呢? 這個 null 模式被用來檢查 Value 屬性,那你要怎麼檢查這個聯合本身不是無效的呢?

例如:

  • SUnion結構體時,s is null對於 的S?值,只有當 s 本身為 null時 才是 true。 當 C 是類別時,c is null當 的值C?為 時,falsec 本身為 null,但當 c 本身不是c.Valuenull且 時 nulltrueUnion

另一個範例:

class C1 : IUnion
{
    private readonly object? _value;

    public C1(){}
    public C1(int x) { _value = x; }
    public C1(string x) { _value = x; }
    object? IUnion.Value => _value;
}

class Program
{
    static int Test1(C1? u)
    {
        // warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive).
        //                 For example, the pattern 'null' is not covered.
        // This is very confusing, the switch expression is indeed not exhaustive (u itself is not
        // checked for null), but there is a case 'null => 3' in the switch expression. 
        // It looks like the only way to shut off the warning is to use 'case _'. Adding it removes
        // all benefits of exhaustiveness checking, any union case could be missing and there would
        // be no diagnostic about that.  
        return u switch { int => 1, string => 2, null => 3 };
    }
}

這部分設計明顯是圍繞著聯集型別是結構體來優化的。 以下是一些選項:

  • 真可惜。 用 == 來做空檢定,而不是模式匹配。
  • 設模式 null (以及其他模式中的隱含空檢查)同時適用於聯集值及其 Value 屬性: u is null ==> u == null || u.Value == null
  • 禁止班級成為工會類型!

[已解決]從一個 Union 階級衍生出來

當一個類別使用一個 Union類別作為其基底類別時,根據現行規範,該類別本身就成為 Union一個類別。 這是因為它自動「繼承」了介面的實作 IUnion ,不需要重新實作。 同時,導出型態的建構子定義了這個新型 Union態中的型別集合。 很容易在這兩個類別周圍出現非常奇怪的語言行為:

class C1 : IUnion
{
    private readonly object _value;
    public C1(long x) { _value = x; }
    public C1(string x) { _value = x; }
    object IUnion.Value => _value;
}

class C2(int x) : C1(x);

class Program
{
    static int Test1(C1 u)
    {
        // Good
        return u switch { long => 1, string => 2, null => 3 };
    } 

    static int Test2(C2 u)
    {
        // error CS8121: An expression of type 'C2' cannot be handled by a pattern of type 'long'.
        // error CS8121: An expression of type 'C2' cannot be handled by a pattern of type 'string'.
        return u switch { long => 1, string => 2, null => 3 };
    } 
}

以下是一些選項:

  • 當類別類型成為 Union 類型時,請更改。 例如,當所有為真時,類別為 Union 型態:

    • 因為 sealed 導出型別不會被視為 Union型別,這點會讓人混淆。
    • 其基地沒有任何實作 IUnion

    這仍然不完美。 規則太隱晦了。 犯錯很容易。 聲明上沒有診斷功能,但 Union 匹配功能不行。

  • 禁止類別成為聯合類型。

[已解決]is 型運算子

is 型態運算子 被指定為執行時型別檢查。 從語法上看,它看起來很像類型模式,但其實不是。 因此,特殊 Union配對不會被使用,這可能會造成使用者混淆。

struct S1 : IUnion
{
    private readonly object _value;
    public S1(int x) { _value = x; }
    public S1(string x) { _value = x; }
    object IUnion.Value => _value;
}

class Program
{
    static bool Test1(S1 u)
    {
        return u is int; // warning CS0184: The given expression is never of the provided ('int') type
    }   

    static bool Test2(S1 u)
    {
        return u is string and ['1', .., '2']; // Good
    }   
}

如果是遞迴聯集,型別模式可能不會警告,但它仍然不會像使用者預期的那樣運作。

解決: 應該可以當作一種類型模式使用。

列表樣式

列表模式總是在匹配時 Union 失敗:

struct S1 : IUnion
{
    private readonly object _value;
    public S1(int[] x) { _value = x; }
    public S1(string[] x) { _value = x; }
    object IUnion.Value => _value;
}

class Program
{
    static bool Test1(S1 u)
    {
        // error CS8985: List patterns may not be used for a value of type 'object'. No suitable 'Length' or 'Count' property was found.
        // error CS0021: Cannot apply indexing with [] to an expression of type 'object'
        return u is [10];
    }   
}

static class Extensions
{
    extension(object o)
    {
        public int Length => 0;
    }
}

其他問題

  • 在聯合轉換中使用建構子和 TryGetValue(...) 在聯合模式匹配中使用的使用,都被規定在多個 1 應用時會寬鬆:他們會直接選擇一個。 根據良好形成規則,這不應該有影響,但我們對此感到舒適嗎?
  • 規範微妙地依賴於屬性 IUnion.Value 的實作,而非聯合元型別本身的屬性 Value 。 此設計旨在為現有類型(可能有其他 Value 用途特性)提供更大彈性來實作該模式。 但這很尷尬,也和其他會員直接用在聯合類型上的方式不一致。 我們應該改變嗎? 還有其他選項:
    • 要求工會類型揭露公共 Value 財產。
    • 如果有公共 Value 財產,建議使用;如果沒有,則退回到實 IUnion.Value 作(類似 GetEnumerator 規則)。
  • 所提議的合併聲明語法並非人人喜愛,尤其是在表達案例類型的方面。 目前的替代方案也常遭批評,但我們有可能最終會做出改變。 以下是對現行政府的一些主要擔憂:
    • 逗號作為案例類型之間的分隔符,似乎暗示順序很重要。
    • 括號內的列表看起來太像主要建構子(儘管沒有參數名稱)。
    • 這和列舉(enum)太不一樣了,列舉的「格」用捲曲的大括號。
  • 雖然聯合宣告產生的結構體只有一個參考欄位,但在並行情境中使用時仍可能出現意外行為。 例如,如果一個使用者定義的函式成員多次 this 被取消參照,包含變數可能已被兩個存取之間的另一執行緒作為整體重新指派。 編譯器可產生程式碼,必要時複製 this 到本地。 應該嗎? 一般來說,多大程度的並行韌性是理想且合理可達成的?