通过


工会

注释

本文是功能规格说明。 此规范是功能的设计文档。 它包括建议的规范变更,以及功能设计和开发过程中所需的信息。 这些文章将持续发布,直至建议的规范变更最终确定并纳入当前的 ECMA 规范。

功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的 语言设计会议(LDM)记录中。

可以在有关 规范的文章中详细了解将功能规范采用 C# 语言标准的过程。

支持者问题:https://github.com/dotnet/csharplang/issues/9662

总结

联合 是一组相互链接的功能,结合为联合类型提供 C# 支持:

  • 联合类型:具有 [Union] 属性的结构和类被识别为 联合类型,并支持 联合行为
  • 事例类型:联合类型具有一组 事例类型,由构造函数和工厂方法的参数提供。
  • 联合行为:联合类型支持以下 联合行为
    • 联合转换:从每个事例类型到联合类型都有隐式 联合转换
    • 联合匹配:模式匹配与联合值隐式“解包”其内容,而是将模式应用于基础值。
    • 联合详尽性:当匹配所有事例类型时,切换联合值的表达式是详尽的,无需回退事例。
    • 联合可为 null 性:可为 Null 性分析增强了对联合内容的 null 状态的跟踪。
  • 联合模式:所有联合类型都遵循基本 联合模式,但特定方案还有其他可选模式。
  • 联合声明:简写语法允许直接声明联合类型。 实现是“有意见的”-一个遵循基本联合模式的结构声明,并将内容存储为单个引用字段。
  • 联合接口:一些接口由语言已知,并用于联合声明的实现。

动机

联合是一项长期请求的 C# 功能,它允许以模式匹配可以信任的详尽方式表达一组封闭类型的值。

联合 类型和 联合 声明 之间的分离允许 C# 具有带有有意见语义的简洁联合声明语法,同时允许具有其他实现选择的现有类型或类型选择加入联合行为。

C# 中建议的联合是 类型的 联合,而不是“区分”或“标记”。 可以使用新类型声明作为事例类型,以“类型联合”来表示“歧视联合”。 或者,它们可以实现为 封闭层次结构,这是另一个相关的即将推出的 C# 功能,侧重于详尽性。

详细设计

联合类型

具有 System.Runtime.CompilerServices.UnionAttribute 属性的任何类或结构类型都被视为 联合类型

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

联合类型必须遵循公共 联合成员的特定模式,该模式必须在联合类型本身上声明或委托给“联合成员提供程序”。

某些工会成员是强制性的,其他成员是可选的。

联合类型具有一组基于某些联合成员的签名建立 的事例类型

可以通过属性访问 Value 联合值的内容。 语言假定 Value 仅包含事例类型之一的值或 null(请参阅 格式正确)。

联合成员提供程序

默认情况下,联合成员在联合类型本身上找到。 但是,如果联合类型 直接包含 调用 IUnionMembers 的接口的声明,则该接口充当 联合成员提供程序。 在这种情况下, 联合成员仅在 联合成员提供程序上找到,而未在联合类型本身上找到。

联合成员提供程序接口必须是公共的,并且联合类型本身必须将其实现为接口。

对于找到联合成员的类型,我们使用术语 联合定义类型 :如果存在联合成员提供程序,则使用联合类型本身。

联合成员

联合成员按联合定义类型的名称和签名进行查找。 它们不必直接在联合定义类型上声明,但可以继承。

这是任何联合成员不公开的错误。

创建成员和 Value 属性是必需的,并统称为 基本联合模式

成员HasValueTryGetValue统称为非装箱联合访问模式

下面介绍了不同的联合成员。

联合创建成员

联合创建成员用于从事例类型值创建新的联合值。

如果联合定义类型是联合类型本身,则具有单个参数的每个 构造函数都是联合构造函数。 联合的事例类型按以下方式标识为从这些构造函数的参数类型生成的类型集:

  • 如果参数类型是可为 null 的类型(无论是值还是引用),则大小写类型是基础类型
  • 否则,大小写类型为参数类型。
// 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) { ... }

如果联合定义类型是联合成员提供程序,则每个静态 Create 方法都有一个参数,并且返回类型可转换为联合类型本身是 联合工厂方法。 联合事例类型以下列方式标识为从这些工厂方法的参数类型生成的类型集:

  • 如果参数类型是可为 null 的类型(无论是值还是引用),则大小写类型是基础类型
  • 否则,大小写类型为参数类型。
// 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 属性允许访问联合中包含的值,而不考虑其大小写类型。

每个联合定义类型都必须声明类型Valueobject?object. 该属性必须具有访问器,并且可以有一个getinitset访问器,该访问器可以是任何辅助功能,并且编译器不使用。

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

非装箱访问成员

联合类型可以选择实现 非装箱联合访问模式,该模式允许对每种事例类型进行强类型条件访问,以及检查 null 的方法。

这样,当事例类型是值类型并在联合中存储时,编译器可以更有效地实现模式匹配。

非装箱访问成员包括:

  • 具有HasValue公共get访问器的类型的bool属性。 它可以有一个 initset 访问器,它可以是任何辅助功能,编译器不使用。
  • TryGetValue每种事例类型的方法。 该方法返回 bool 并采用可转换为大小写类型的类型的单个 out 参数。
// Non-boxing access members
public bool HasValue { get { ... } }
public bool TryGetValue(out Dog value) { ... }

HasValue 如果并且仅当联合的 Value 值为 null 时,应返回 true。

TryGetValue 如果并且仅当联合 Value 的类型为给定大小写类型,如果是,则返回 true;如果是,请在方法的 out 参数中传递该值。

格式正确

语言和编译器对联合类型做出许多行为假设。 如果类型限定为联合类型,但不满足这些假设,则联合行为可能无法按预期工作。

  • 声音:Value 属性的计算结果始终为 null 或大小写类型的值。 即使对于联合类型的默认值也是如此。
  • 稳定性:如果从事例类型创建联合值,则该 Value 属性将匹配该事例类型或 null。 如果从 null 值创建联合值,则 Value 属性将为 null
  • 创建等效性:如果某个值隐式转换为两种不同的事例类型,则当使用该值调用时,任一事例类型的创建成员具有相同的可观察行为。
  • 访问模式一致性:如果存在,则TryGetValue表示非装箱访问成员的行为与直接检查Value属性的行为HasValue等效。

联合类型示例

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

注意: 这只是如何实现非装箱访问模式的示例。 用户代码可以采用任何喜欢的方式存储内容。 具体而言,它不会阻止实现装箱! 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;
}

联合行为

联合行为通常通过基本联合模式来实现。 如果联合提供非装箱访问模式,联合模式匹配将优先使用它。

联合转换

联合转换从其每个事例类型隐式转换为联合类型。 具体而言,如果存在从类型或表达式进行标准隐式转换UC并且是联合创建成员U的参数类型,则存在从E类型C或表达式E到联合类型的联合转换。 如果联合类型U是结构,则如果存在从类型或表达式进行标准隐式转换,C并且是联合创建成员U的参数类型,则从类型C或表达式E进行联合转换U?E

联合转换本身不是标准隐式转换。 因此,它可能不参与用户定义的隐式转换或其他联合转换。

隐式联合转换之外没有显式联合转换。 因此,即使有从 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");

如果重载解析找不到单个最佳候选成员,或者该成员不是联合类型的联合成员之一,则这是一个错误。

联合转换只是隐式用户定义转换的另一种“形式”。 适用的用户定义的转换运算符“shadows”联合转换。

此决定背后的理由:

如果某人编写了用户定义的运算符,则应获得优先级。 换句话说,如果用户实际上写了自己的操作员,他们希望我们调用它。 转换运算符转换为联合类型的现有类型在当前使用运算符的现有代码方面继续以相同的方式工作。

在以下示例中,隐式用户定义转换优先于联合转换。

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
}

在以下示例中,在代码中使用显式强制转换时,显式用户定义转换优先于联合转换。 但是,当代码中没有显式强制转换时,将使用联合转换,因为显式用户定义的转换不适用。

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

联合匹配

当模式的传入值为联合类型或联合类型的可为 null 时,可为 null 的值和基础联合值的内容可能会“解包”,具体取决于模式。

对于无条件 _ 模式和 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 或其包含值是否为 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则该方法用于获取值。 然后,该模式将应用于该值。 如果有多个此类方法,则如果可用,则首选从 TS 的转换不是装箱转换的任何方法。 如果仍有多个方法,则以实现定义的方式选择一个方法。
  • 否则,对于表示检查 null的模式,如果 HasValue 属性可用,则使用该属性来检查联合值是否为 null。
  • 否则,该模式将应用于访问 IUnion.Value 传入联合上的属性的结果。

应用于联合类型的 is-type 运算符与应用于联合类型的类型模式的含义相同。

联合详尽性

假定联合类型按大小写类型“已用尽”。 这意味着, switch 如果表达式处理所有联合事例类型,则表达式是详尽的:

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

可空性

与任何其他属性一样跟踪联合属性的 Value null 状态,并进行了以下修改:

  • 当调用联合创建成员(显式或通过联合转换)时,新联合将 Value 获取传入值的 null 状态。
  • 当非装箱访问模式HasValueTryGetValue(...)用于查询联合类型(显式或通过模式匹配)的内容时,它以与直接检查的方式影响Value'可 null 性状态Value:分支上的 true null 状态Value变为“非 null”。

即使联合开关是详尽的,如果传入联合的属性的 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; }
}

编译器生成的联合实现此接口。

示例用法:

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

联合声明

联合声明是一种在 C# 中声明联合类型的简洁和有意见的方式。 它们声明一个结构,该结构使用单个对象引用来存储结构 Value,这意味着:

  • 装箱:其事例类型中的任何值类型都将按条目装箱。
  • 紧凑性:联合值仅包含单个字段。

目的是让工会声明很好地涵盖绝大多数用例。 手动编码特定联合类型而不是使用联合声明的两个主要原因应该是:

  • 将现有类型适应联合模式以获取联合行为。
  • 实施不同的存储策略,例如效率或互操作原因。

Syntax

联合声明具有名称和 联合构造函数 类型列表。

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的任何类型,例如接口、类型参数、可为 null 的类型和其他联合。 结果案例重叠以及联合嵌套或为 null 时,情况很好。

示例:

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

开放性问题

[已解决]联合声明是否为记录?

联合声明将降低到记录结构

我认为这种默认行为是不必要的,并且鉴于此行为不可配置,因此会显著限制使用方案。 记录会生成大量未使用或不符合特定要求的代码。 例如,由于该代码膨胀,编译器的代码库中几乎禁止记录。 我认为最好更改默认值:

  • 默认情况下,联合声明声明声明仅包含特定于联合的成员的常规结构。
  • 用户可以声明记录联合: record union U(E1, ...) ...

分辨率: 联合声明是一个纯结构,而不是记录结构。 record union ...不支持

[已解决]Union 声明语法

建议的语法似乎不完整或不必要的限制。 例如,不允许使用 base 子句。 但是,我可以轻松地想象需要实现接口,例如。 我认为,除了元素类型列表外,语法应与关键字替换union关键字的struct常规struct/record struct声明匹配。

分辨率: 将删除该限制。

[已解决]联合声明成员

不允许实例字段、自动属性或类似字段的事件。

这感觉是任意的,绝对不必要的。

分辨率: 保留限制。

[已解决]联合大小写类型的可为 Null 值类型

联合事例类型标识为这些构造函数中的参数类型集。 联合的事例类型标识为这些工厂方法中的参数类型的集。

同时:

TryGetValue每种事例类型的方法。 该方法以下列方式返回 bool 并采用与给定事例类型对应的类型的单个 out 参数:

  • 如果事例类型是可以为 null 的值类型,则参数的类型应可转换为基础类型
  • 否则,该类型应可转换为大小写类型。

事例类型中是否有可为 null 值类型的优势,尤其是类型模式不能将可以为 null 的值类型用作目标类型? 假设我们可以简单地说,如果构造函数/工厂的参数类型是可为 null 的值类型,则相应的大小写类型是基础类型。 然后,我们不需要该方法的额外子句 TryGetValue ,所有 out 参数都是大小写类型。

分辨率: 建议已获得批准

[已解决]属性的默认可为 null 状态Value

对于没有可以为 null 的事例类型的联合类型,默认状态 Value 为“不为 null”,而不是“可能为 null”。

使用新设计时, Value 属性未在一些常规接口中定义,而是专门属于声明类型的 API,上面引用的规则感觉类似于过度工程。 此外,规则可能会强制使用者在不使用可以为 null 的类型的情况下使用可以为 null 的类型。

例如,请考虑以下联合声明:

union U1(int, bool, DateTime);

根据带引号的规则,默认状态 Value 为“非 null”。 但与类型的行为不匹配的是 default(U1).Valuenull。 为了重新调整行为,使用者被迫使至少一种大小写类型可为 null。 如下所示:

union U1(int?, bool, DateTime);

但这可能是不可取的,使用者可能不希望允许使用 int? 值显式创建。

建议:删除带引号的规则,可为 null 的分析应使用属性中的 Value 注释来推断其默认可为 null 性。

分辨率: 该提案获得批准

[已解决]联合值类型的可为 Null 的联合匹配

当模式的传入值是联合类型时,联合值的内容可能会“解包”,具体取决于模式。

当模式的传入值为 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 大相径庭。 在 Test1 中,它不是联合匹配,在 Test2 中是。 也许“联合匹配”应该“挖掘” Nullable<T> ,因为模式匹配通常在其他情况下执行。

如果我们使用该模式,则联合匹配 null 模式 Nullable<union type> 应与类一样工作。 即当模式为 true 时 (!nullableValue.HasValue || nullableValue.Value.Value is null)

分辨率: 该提案获得批准。

“坏”API 该怎么办?

编译器应该对类似于匹配的联合匹配 API 执行哪些操作,否则为“错误”? 例如,编译器查找具有匹配签名的 TryGetValue/HasValue,但它是“错误的”,因为需要自定义修饰符或需要未知功能等。编译器是否应以无提示方式忽略 API 或报告错误? 类似地,API 可能标记为已过时/实验性。 编译器是否应报告任何诊断,以无提示方式使用 API 还是以无提示方式不使用 API?

如果缺少联合声明的类型,该怎么办

如果 UnionAttribute缺少, IUnion 会发生什么 IUnion<TUnion> 情况? 错误? 合成? 别的?

[已解决]泛型 IUnion 接口的设计

已将不应继承IUnion自或约束其类型参数IUnion<TUnion>的参数设置为 IUnion<TUnion> 。 我们应该重新审视。

分辨率:IUnion<TUnion> 接口现已删除。

[已解决]可为 Null 的值类型(大小写类型及其交互) TryGetValue

上述规则指出,如果事例类型是可为 null 的值类型,则相应 TryGetValue 方法中使用的参数类型应为 基础 类型。 这是出于这样一个 null 事实,即永远不会通过此方法生成值。 在消耗端,不允许将可以为 null 的值类型用作类型模式,而与基础类型的匹配应能够映射到此方法的调用。

我们应该确认我们同意这一解包。

分辨率: 已同意/已确认

非装箱联合访问模式

需要指定用于查找合适 HasValue API 和 TryGetValue API 的精确规则。 是否涉及继承? 读/写 HasValue 是否是可接受的匹配项? 等。

[已解决] TryGetValue 匹配转换

“联合匹配”部分显示:

对于表示检查特定类型的T模式,如果TryGetValue(S value)方法可用,并且有从中S隐式转换T,则该方法用于获取值。

隐式转换集是否以任何方式受到限制? 例如,是否允许用户定义转换? 元组转换和其他不太普通的转换呢? 其中一些甚至是标准转换。

方法集 TryGetValue 是否以任何其他方式受到限制? 例如,Union Patterns 节意味着仅考虑具有与事例类型匹配的参数类型的方法:

public bool TryGetValue(out T value)每种事例类型的T方法。

有一个明确的答案是好的。

分辨率: 仅考虑隐式标识或引用或装箱转换

TryGetValue 和可为 null 的分析

当非装箱访问模式HasValueTryGetValue(...)用于查询联合类型(显式或通过模式匹配)的内容时,它以与直接检查的方式影响Value'可 null 性状态Value:分支上的 true null 状态Value变为“非 null”。

方法集 TryGetValue 是否以任何方式受到限制? 例如,Union Patterns 节意味着仅考虑具有与事例类型匹配的参数类型的方法:

public bool TryGetValue(out T value)每种事例类型的T方法。

有一个明确的答案是好的。

阐明有关 default 结构联合类型的值的规则

注意:已删除下面提到的默认可为空性规则。

注意:已删除下面提到的“默认”格式规则。 我们应该确认这是我们想要的。

Nullability 部分显示:

对于没有可以为 null 的事例类型的联合类型,默认状态 Value 为“不为 null”,而不是“可能为 null”。

因此,对于以下示例,当前实现将Values2被视为“非 null”:

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

同时, 格式正确的 部分说:

  • 默认值:如果联合类型是值类型,则默认值为 null 默认值 Value
  • 默认构造函数:如果联合类型具有 nullary (no-argument) 构造函数,则生成的联合将 null 作为其 Value

此类实现将与上述示例的可为 null 分析行为相矛盾。

是否应调整格式规则,或者是否应为“可能为 null”的状态Valuedefault? 如果后者,初始化 S2 s2 = default; 是否会产生可为 null 性警告?

确认类型参数绝不是联合类型,即使约束为一个。

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

后条件属性是否会影响联合实例的默认可为 null 性?

注意:已删除下面提到的默认可为空性规则。 我们不再从联合创建方法推断属性的默认可为 null 性 Value 。 因此,问题已过时/不再适用于当前设计。

对于没有可以为 null 的事例类型的联合类型,默认状态 Value 为“不为 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 }; // 
    } 
}

联合转换

[已解决]它们属于其他转换优先级的位置?

联合转换感觉类似于用户定义的转换的另一种形式。 因此,当前实现在尝试对隐式用户定义转换进行分类失败后将其分类,如果存在,则将其视为用户定义转换的另一种形式。 这会产生以下后果:

  • 隐式用户定义转换优先于联合转换
  • 在代码中使用显式强制转换时,显式用户定义转换优先于联合转换
  • 当代码中没有显式强制转换时,联合转换优先于显式用户定义转换
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)
}

需要确认这是我们喜欢的行为。 否则,应阐明转换规则。

解决方法:

工作组批准。

[已解决]构造函数参数的 Ref-ness

当前语言仅允许用户定义转换运算符的按值和 in 参数。 这种限制的原因也适用于适用于联合转换的构造函数。

建议:

调整上述部分中的定义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.

解决方法:

现已获得工作组批准。 但是,我们可以考虑“拆分”事例类型构造函数集和适合联合类型转换的构造函数集。

[已解决]可以为 Null 的转换

可以为 null 的转换 部分显式列出可用作基础的转换。 当前规范不建议对该列表进行任何调整。 这会导致以下方案出错:

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?'
    }   
}

建议:

调整规范以支持从联合转换到T?支持的隐式可为 null 转换S。 具体而言,假设T联合类型存在从类型或表达式的隐式转换为类型或表达式E(如果从E类型C进行联合转换T?并且C是大小写类型T)。 请注意,不要求类型 E 为不可为 null 的值类型。 转换计算为基础联合转换,ST后跟从换T行到T?

解决方法:

批准。

[已解决]提升的转换

是否调整 提升转换 部分以支持提升的联合转换? 目前不允许使用它们:

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 值。 目前还不清楚解除是否应创建一个联合类型的实例,其中包含 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。 那么,null 检查会怎么样? 模式 null 已被共同选择检查 Value 属性,那么如何检查联合本身是否不为 null?

例如:

  • S为结构时,s is null对于值trueS?仅当自身为nullsUnionC是类时,c is null对于一个UnionC?cfalse是当自身是null,但它是truec本身不是null并且c.Value是。null

另一个示例:

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 检查,而不是模式匹配。
  • null让模式(在其他模式中隐式 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 类型时发生更改。 例如,当所有 true 时,类是一种 Union 类型:

    • 这是因为 sealed 派生类型不会被视为 Union类型,因此会让人感到困惑。
    • 其任何基都未实现 IUnion

    这仍然不完美。 规则太微妙了。 很容易犯错。 声明没有诊断,但 Union 匹配不起作用。

  • 禁止类为联合类型。

[已解决]is-type 运算符

is-type 运算符 指定为运行时类型检查。 从语法上讲,它看起来非常类似于类型模式,但它不是。 因此,不会使用特殊 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(...) 都指定为宽大,当应用多个构造函数时,它们将只选取一个。 这不应该按照格式正确的规则很重要,但我们是否对此感到满意?
  • 该规范巧妙地依赖于属性的 IUnion.Value 实现,而不是在联合类型本身上找到的任何 Value 属性。 这意味着,为现有类型(可能具有自己的 Value 其他用途属性)实现模式提供了更大的灵活性。 但这是尴尬的,与其他成员如何找到和使用直接在联合类型上不一致。 我们应该做出改变吗? 其他一些选项:
    • 要求联合类型公开公共 Value 属性。
    • 如果存在公共属性,则首选公共 Value 属性,但回退到 IUnion.Value 实现(类似于 GetEnumerator 规则)。
  • 建议的联合声明语法并不普遍受到喜爱,尤其是在表达事例类型方面。 到目前为止,替代方案也受到批评,但有可能最终做出改变。 关于当前问题,有一些最关心的问题:
    • 将逗号作为事例类型之间的分隔符似乎意味着顺序很重要。
    • 带括号的列表看起来与主要构造函数(尽管没有参数名称)类似。
    • 与大括号中具有“事例”的枚举大相径庭。
  • 虽然联合声明使用单个引用字段生成结构,但在并发上下文中使用时,它们仍然容易出现意外行为。 例如,如果用户定义函数成员多次取消引用 this ,则包含变量可能已由两个访问之间的另一个线程重新分配为整体。 编译器可以根据需要生成要复制到 this 本地的代码。 应该吗? 一般情况下,可取且合理地实现哪种程度的并发复原能力?