Compartilhar via


Uniões

Observação

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ela inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação propostas sejam finalizadas e incorporadas na especificação ECMA atual.

Pode haver algumas divergências entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas LDM (reunião de design de idioma) pertinentes.

Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações.

Problema do especialista: https://github.com/dotnet/csharplang/issues/9662

Resumo

Uniões é um conjunto de recursos interligados, que combinam para fornecer suporte em C# para tipos de união:

  • Tipos de união: structs e classes que têm um [Union] atributo são reconhecidos como tipos de união e dão suporte aos comportamentos da união.
  • Tipos de caso: os tipos de união têm um conjunto de tipos de casos, que é dado por parâmetros para construtores e métodos de fábrica.
  • Comportamentos sindicais: os tipos de união dão suporte aos seguintes comportamentos sindicais:
    • Conversões de união: há conversões de união implícitas de cada tipo de caso para um tipo de união.
    • Correspondência de união: a correspondência de padrões em relação aos valores da união implicitamente "desembrulha" seu conteúdo, aplicando o padrão ao valor subjacente.
    • Esgotamento da união: as expressões de alternância sobre valores de união são exaustivas quando todos os tipos de casos são correspondidos, sem a necessidade de um caso de fallback.
    • Nulidade da união: a análise de nulidade aprimorou o acompanhamento do estado nulo do conteúdo de uma união.
  • Padrões de união: todos os tipos de união seguem um padrão de união básico, mas há padrões opcionais adicionais para cenários específicos.
  • Declarações sindicais: uma sintaxe abreviada permite a declaração de tipos de união diretamente. A implementação é "opinativa" – uma declaração struct que segue o padrão de união básico e armazena o conteúdo como um único campo de referência.
  • Interfaces de união: algumas interfaces são conhecidas pelo idioma e usadas em sua implementação de declarações de união.

Motivação

As uniões são um recurso C# solicitado há muito tempo, que permite expressar valores de um conjunto fechado de tipos de forma que a correspondência de padrões possa confiar seja exaustiva.

A separação entre tipos de união e declarações sindicais permite que o C# tenha uma sintaxe sucinta de declaração sindical com semântica opinativa, ao mesmo tempo em que permite que tipos ou tipos existentes com outras opções de implementação optem por comportamentos de união.

Os sindicatos propostos em C# são uniões de tipos e não "discriminados" ou "marcados". "Uniões discriminadas" podem ser expressas em termos de "uniões de tipos" usando novas declarações de tipo como tipos de caso. Como alternativa, eles podem ser implementados como uma hierarquia fechada, que é outro recurso de C# relacionado e próximo focado na exaustão.

Design detalhado

Tipos de união

Qualquer tipo de classe ou struct com um System.Runtime.CompilerServices.UnionAttribute atributo é considerado um tipo de união:

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

Um tipo de união deve seguir um determinado padrão de membros do sindicato público, que deve ser declarado no próprio tipo sindical ou delegado a um "provedor de membros do sindicato".

Alguns membros do sindicato são obrigatórios, e outros são opcionais.

Um tipo de união tem um conjunto de tipos de casos que são estabelecidos com base nas assinaturas de determinados membros do sindicato.

O conteúdo de um valor de união pode ser acessado por meio de uma Value propriedade. A linguagem pressupõe que Value só contenha um valor de um dos tipos de maiúsculas e minúsculas ou nulo (consulte Bem-formado).

Provedores de membros do sindicato

Por padrão, os membros do sindicato são encontrados no próprio tipo sindical. No entanto, se o tipo de união contiver diretamente uma declaração de uma interface chamada IUnionMembers , a interface atuará como um provedor de membros do sindicato. Nesse caso, os membros do sindicato são encontrados no provedor de membros do sindicato, não no próprio tipo sindical.

Uma interface do provedor de membros do sindicato deve ser pública e o próprio tipo de união deve implementá-la como uma interface.

Usamos o tipo de definição de união do termo para o tipo em que os membros do sindicato são encontrados: o provedor de membros do sindicato, se existir, e o próprio tipo sindical.

Membros do sindicato

Os membros do sindicato são procurados pelo nome e assinatura no tipo de definição do sindicato. Eles não precisam ser declarados diretamente sobre o tipo de definição de união, mas podem ser herdados.

É um erro para qualquer membro do sindicato não ser público.

Os membros de criação e a Value propriedade são obrigatórios e são coletivamente chamados de padrão de união básico.

O e TryGetValue os HasValue membros são coletivamente referidos como o padrão de acesso da união não boxing.

Os diferentes membros do sindicato são descritos a seguir.

Membros da criação do sindicato

Os membros de criação do sindicato são usados para criar novos valores de união a partir de um valor de tipo de caso.

Se o tipo de definição de união for o próprio tipo de união, cada construtor com um único parâmetro será um construtor de união. Os tipos de caso da união são identificados como o conjunto de tipos criados a partir de tipos de parâmetro desses construtores da seguinte maneira:

  • Se o tipo de parâmetro for um tipo anulável (seja um valor ou uma referência), o tipo de caso será o tipo subjacente
  • Caso contrário, o tipo de caso é o tipo de parâmetro.
// 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) { ... }

Se o tipo de definição de união for um provedor de membros do sindicato, cada método estático Create com um único parâmetro e um tipo de retorno que seja conversível de identidade para o próprio tipo de união será um método union factory. Os tipos de caso da união são identificados como o conjunto de tipos criados a partir de tipos de parâmetro desses métodos de fábrica da seguinte maneira:

  • Se o tipo de parâmetro for um tipo anulável (seja um valor ou uma referência), o tipo de caso será o tipo subjacente
  • Caso contrário, o tipo de caso é o tipo de parâmetro.
// 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) { ... }

Construtores sindicais e métodos de fábrica sindical são referidos coletivamente como membros da criação sindical.

O único parâmetro de um membro de criação de união deve ser um parâmetro ou in por valor.

Um tipo de união deve ter pelo menos um membro de criação do sindicato e, portanto, pelo menos um tipo de caso.

Propriedade Value

A Value propriedade permite acesso ao valor contido em uma união, independentemente de seu tipo de caso.

Cada tipo de definição de união deve declarar uma Value propriedade do tipo object? ou object. A propriedade deve ter um get acessador e, opcionalmente, pode ter um init ou set acessador, que pode ser de qualquer acessibilidade e não é usado pelo compilador.

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

Membros de acesso não boxing

Um tipo de união pode optar por implementar adicionalmente o padrão de acesso união não boxing, que permite acesso condicional fortemente tipado a cada tipo de caso, bem como uma maneira de verificar se há nulo.

Isso permite que o compilador implemente a correspondência de padrões com mais eficiência quando os tipos de caso são tipos de valor e armazenados como tal dentro da união.

Os membros de acesso não boxing são:

  • Uma HasValue propriedade do tipo bool com um acessador público get . Opcionalmente, ele pode ter um init ou set acessador, que pode ser de qualquer acessibilidade e não é usado pelo compilador.
  • Um TryGetValue método para cada tipo de caso. O método retorna bool e usa um único parâmetro out de um tipo que é conversível de identidade para o tipo de caso.
// Non-boxing access members
public bool HasValue { get { ... } }
public bool TryGetValue(out Dog value) { ... }

HasValue espera-se que retorne true se e somente se a união Value não for nula.

TryGetValue espera-se que retorne true se e somente se o da Value união for do tipo de caso determinado e, em caso afirmativo, entregar esse valor no parâmetro de saída do método.

Boa formação

O idioma e o compilador fazem uma série de suposições comportamentais sobre tipos de união. Se um tipo se qualificar como um tipo de união, mas não atender a essas suposições, os comportamentos sindicais poderão não funcionar conforme o esperado.

  • Solidez: a Value propriedade sempre é avaliada como nula ou como um valor de um tipo de caso. Isso é verdadeiro mesmo para o valor padrão do tipo de união.
  • Estabilidade: se um valor de união for criado a partir de um tipo de caso, a propriedade corresponderá a Value esse tipo de caso ou nulo. Se um valor de união for criado a partir de um null valor, a Value propriedade será null.
  • Equivalência de criação: se um valor for implicitamente conversível para dois tipos de casos diferentes, o membro de criação para qualquer um desses tipos de caso terá o mesmo comportamento observável quando chamado com esse valor.
  • Consistência do padrão de acesso: o comportamento dos membros de HasValueTryGetValue acesso não boxing, se presentes, é observavelmente equivalente ao da verificação diretamente da Value propriedade.

Exemplos de tipos de união

Pet implementa o padrão de união básico no próprio tipo de união:

[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 implementa o padrão de acesso não boxing no próprio tipo de união:

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

Nota: Este é apenas um exemplo de como o padrão de acesso não boxing pode ser implementado. O código do usuário pode armazenar o conteúdo da maneira que quiser. Em particular, isso não impede que a implementação seja boxing! Em non-boxing seu nome, refere-se a permitir que a implementação de correspondência de padrões do compilador acesse cada tipo de caso de forma fortemente tipada, em oposição à object?propriedade tipada Value .

Result<T> implementa o padrão básico por meio de um provedor de membros do sindicato:

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

Comportamentos de união

Os comportamentos sindicais geralmente são implementados por meio do padrão de união básico. Se o sindicato oferecer o padrão de acesso não boxing, a correspondência de padrões de união fará uso preferencialmente dele.

Conversões de união

Uma conversão de união converte implicitamente em um tipo de união de cada um de seus tipos de maiúsculas e minúsculas. Especificamente, há uma conversão de união em um tipo U de união de um tipo ou expressão E se houver uma conversão implícita padrão de E um tipo C e C for um tipo de parâmetro de um membro de criação de união.U Se o tipo U de união for um struct, haverá uma conversão de união para digitar U? de um tipo ou expressão E se houver uma conversão implícita padrão de E um tipo C e C for um tipo de parâmetro de um membro de criação de união.U

Uma conversão de união não é em si uma conversão implícita padrão. Portanto, ele não pode participar de uma conversão implícita definida pelo usuário ou de outra conversão de união.

Não há conversões de união explícitas além das conversões de união implícitas. Portanto, mesmo que haja uma conversão explícita de E um tipo Cde caso de união, isso não significa que haja uma conversão explícita desse tipo de E união.

Uma conversão de união é executada chamando o membro de criação do sindicato:

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

É um erro se a resolução de sobrecarga não encontrar um único membro candidato melhor ou se esse membro não for um dos membros do sindicato do tipo sindical.

A conversão de união é apenas mais uma "forma" de uma conversão implícita definida pelo usuário. Uma conversão de união "sombras" do operador de conversão definida pelo usuário aplicável.

A lógica por trás desta decisão:

Se alguém escreveu um operador definido pelo usuário, ele deverá ter prioridade. Em outras palavras, se o usuário realmente escreveu seu próprio operador, ele quer que a chamemos. Os tipos existentes com operadores de conversão transformados em tipos de união continuam funcionando da mesma maneira em relação ao código existente que utiliza os operadores atualmente.

No exemplo a seguir, uma conversão implícita definida pelo usuário tem prioridade sobre uma conversão de união.

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
}

No exemplo a seguir, quando a conversão explícita é usada no código, uma conversão explícita definida pelo usuário tem prioridade sobre uma conversão de união. Mas, quando não há conversão explícita no código, uma conversão de união é usada porque a conversão explícita definida pelo usuário não é aplicável.

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

Correspondência de união

Quando o valor de entrada de um padrão é de um tipo de união ou de uma anulável de um tipo de união, o valor anulável e o conteúdo do valor da união subjacente podem ser "desembrulhados", dependendo do padrão.

Para os padrões e var incondicional_, o padrão é aplicado ao próprio valor de entrada. Por exemplo:

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

No entanto, todos os outros padrões são aplicados implicitamente à propriedade da Value união subjacente:

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'

Para padrões lógicos, essa regra é aplicada individualmente aos branches, tendo em mente que o branch esquerdo de um and padrão pode afetar o tipo de entrada do branch direito:

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?`.
}

Nota: Essa regra significa que GetPet() is Pet pet provavelmente não terá êxito, como Pet é aplicado ao conteúdo, não à Pet própria união.

Nota: A razão para o tratamento diferente do padrão incondicional var (bem como _, que é essencialmente uma abreviação para var _) é uma suposição de que seu uso é qualitativamente diferente de outros padrões. var os padrões são usados simplesmente para nomear o valor que está sendo correspondido, muitas vezes em padrões aninhados, como PetOwner{ Pet: var pet }. Aqui, a semântica útil é para pet manter o tipo Petde união, em vez de a Value propriedade ser desreferenciada para um tipo inútil object? .

Se o valor de entrada for um tipo de classe, o null padrão terá êxito independentemente de o valor da união em si null ser ou seu valor contido for null:

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

Outros padrões de correspondência sindical só serão bem-sucedidos quando o valor da união em si não nullfor .

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

Da mesma forma, se o valor de entrada for um tipo de valores anulável (encapsulando um tipo de união struct), o null padrão terá êxito independentemente de o valor de entrada em null si ser ou seu valor contido for null:

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

Outros padrões de correspondência de união só serão bem-sucedidos quando o valor de entrada em si não nullfor .

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

O compilador preferirá implementar o comportamento de padrão por meio de membros prescritos pelo padrão de acesso não boxing. Embora seja livre para fazer qualquer otimização dentro dos limites das regras de bem-formatação, o seguinte é o conjunto mínimo garantido a ser aplicado:

  • Para um padrão que implica a verificação de um tipo Tespecífico, se um TryGetValue(S value) método está disponível e há uma identidade ou referência implícita/conversão de boxing de T para S, em seguida, esse método é usado para obter o valor. Em seguida, o padrão é aplicado a esse valor. Se houver mais de um método desse tipo, qualquer em que a conversão de T para S não for uma conversão de boxe será preferencial se disponível. Se ainda houver mais de um método, um será escolhido de maneira definida pela implementação.
  • Caso contrário, para um padrão que implica em verificar null, se uma HasValue propriedade estiver disponível, essa propriedade será usada para verificar se o valor da união é nulo.
  • Caso contrário, o padrão é aplicado ao resultado do acesso à IUnion.Value propriedade na união de entrada.

O operador de tipo is aplicado a um tipo de união tem o mesmo significado que um padrão de tipo aplicado ao tipo de união.

Esgotamento da união

Supõe-se que um tipo de união esteja "esgotado" por seus tipos de caso. Isso significa que uma switch expressão será exaustiva se lidar com todos os tipos de maiúsculas e minúsculas de uma união:

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

Nulidade

O estado nulo da propriedade de Value uma união é acompanhado como qualquer outra propriedade, com estas modificações:

  • Quando um membro de criação do sindicato é chamado (explicitamente ou por meio de Value uma conversão união), o novo sindicato obtém o estado nulo do valor de entrada.
  • Quando o padrão de acesso não boxing ou HasValueTryGetValue(...) é usado para consultar o conteúdo de um tipo de união (explicitamente ou por meio de correspondência de padrões), ele afeta Valueo estado de nulidade da mesma forma que se Value tivesse sido verificado diretamente: o estado nulo se Value torna "não nulo" no true branch.

Mesmo quando uma opção de união for exaustiva, se o estado nulo da propriedade da união de Value entrada for "talvez nulo", um aviso será dado em nulo sem tratamento.

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

Interfaces de união

As interfaces a seguir são usadas pelo idioma em sua implementação de recursos de união.

Interface de acesso da União

A IUnion interface marca um tipo como um tipo de união em tempo de compilação e fornece uma maneira de acessar o conteúdo da união em runtime.

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

Uniões geradas pelo compilador implementam essa interface.

Uso de exemplo:

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

Declarações de união

As declarações sindicais são uma forma sucinta e opinativa de declarar tipos de união em C#. Eles declaram um struct que usa uma única referência de objeto para armazenar seu Value, o que significa:

  • Boxing: todos os tipos de valor entre seus tipos de maiúsculas e minúsculas serão encaixoados na entrada.
  • Compactação: os valores da união contêm apenas um único campo.

A intenção é que as declarações sindicais cubram muito bem a grande maioria dos casos de uso. Espera-se que as duas principais razões para codificar tipos de união específicos à mão em vez de usar declarações sindicais sejam:

  • Adaptando tipos existentes aos padrões de união para obter comportamentos de união.
  • Implementando uma estratégia de armazenamento diferente por motivos de eficiência ou interoperabilidade.

Sintaxe

Uma declaração de união tem um nome e uma lista de tipos de construtores de união .

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

Além das restrições aos membros do struct (§16.3), o seguinte se aplica aos membros do sindicato:

  • Campos de instância, propriedades automáticas ou eventos semelhantes a campos não são permitidos.
  • Construtores públicos declarados explicitamente com um único parâmetro não são permitidos.
  • Construtores declarados explicitamente devem usar um this(...) inicializador para (direta ou indiretamente) delegar a um dos construtores gerados.

Os tipos de construtores de união podem ser qualquer tipo convertido objectem, por exemplo, interfaces, parâmetros de tipo, tipos anuláveis e outras uniões. É bom que os casos resultantes se sobreponham e que os sindicatos aninham ou sejam nulos.

Exemplos:

// 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){ ... }

É reduzido para:

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

Perguntas abertas

[Resolvido] A declaração sindical é um registro?

Uma declaração de união é reduzida a um struct de registro

Acho que esse comportamento padrão é desnecessário e, considerando que não é configurável, limitará significativamente os cenários de uso. Os registros geram muitos códigos que não são utilizados ou não correspondem a requisitos específicos. Por exemplo, os registros são praticamente proibidos na base de código do compilador devido a esse bloat de código. Acho que seria melhor alterar o padrão:

  • Por padrão, uma declaração sindical declara um struct regular com apenas membros específicos do sindicato.
  • Um usuário pode declarar uma união de registros: record union U(E1, ...) ...

Resolução: Uma declaração de união é um struct simples, não um struct de registro. Não record union ... há suporte para o

[Resolvido] Sintaxe da declaração da união

Parece que a sintaxe proposta é incompleta ou desnecessariamente limitada. Por exemplo, parece que a cláusula base não é permitida. No entanto, posso facilmente imaginar a necessidade de implementar uma interface, por exemplo. Acho que, além da lista de tipos de elemento, a sintaxe deve corresponder à declaração regular struct/record struct em que a struct palavra-chave é substituída pela union palavra-chave.

Resolução: A restrição é removida.

[Resolvido] Membros da declaração do sindicato

Campos de instância, propriedades automáticas ou eventos semelhantes a campos não são permitidos.

Isso parece arbitrário e absolutamente desnecessário.

Resolução: A restrição é mantida.

[Resolvido] Tipos de valor anuláveis como tipos de caso union

Os tipos de maiúsculas e minúsculas da união são identificados como o conjunto de tipos de parâmetro desses construtores. Os tipos de maiúsculas e minúsculas da união são identificados como o conjunto de tipos de parâmetro desses métodos de fábrica.

Ao mesmo tempo:

Um TryGetValue método para cada tipo de caso. O método retorna bool e usa um único parâmetro out de um tipo que corresponde ao tipo de caso fornecido da seguinte maneira:

  • Se o tipo de caso for um tipo de valor anulável, o tipo do parâmetro deverá ser conversível de identidade para o tipo subjacente
  • Caso contrário, o tipo deve ser conversível de identidade para o tipo de caso.

Há uma vantagem de ter um tipo de valor anulável entre os tipos de caso, especialmente que um padrão de tipo não pode usar o tipo de valor anulável como o tipo de destino? Parece que podemos simplesmente dizer que, se o tipo de parâmetro do construtor/fábrica for um tipo de valor anulável, o tipo de caso correspondente será o tipo subjacente. Em seguida, não precisaríamos dessa cláusula extra para o TryGetValue método, todos os parâmetros externos são tipos de caso.

Resolução: A sugestão é aprovada

[Resolvido] Estado de Value propriedade anulável padrão

Para tipos de união em que nenhum dos tipos de caso são anuláveis, o estado Value padrão é "não nulo" em vez de "talvez nulo".

Com o novo design, em Value que a propriedade não é definida em alguma interface geral, mas é uma API que pertence especificamente ao tipo declarado, a regra citada acima parece excesso de engenharia. Além disso, a regra provavelmente forçará os consumidores a usar tipos anuláveis em situações em que tipos anuláveis não seriam usados.

Por exemplo, considere a seguinte declaração de união:

union U1(int, bool, DateTime);

De acordo com a regra entre aspas, o estado padrão para Value "não é nulo". Mas isso não corresponde ao comportamento do tipo, default(U1).Value é null. Para realinhar o comportamento, o consumidor é forçado a tornar pelo menos um tipo de caso anulável. Algo como:

union U1(int?, bool, DateTime);

Mas isso provavelmente é indesejável, o consumidor pode não querer permitir a criação explícita com int? valor.

Proposta: remover a regra entre aspas, a análise anulável deve usar anotações da Value propriedade para inferir sua nulidade padrão.

Resolução: A proposta foi aprovada

[Resolvido] Correspondência de união para Nullable de um tipo de valor união

Quando o valor de entrada de um padrão é de um tipo de união, o conteúdo do valor da união pode ser "desembrulhado", dependendo do padrão.

Devemos expandir essa regra para cenários em que o valor de entrada de um padrão é de um Nullable<union type>?

Considere este cenário:

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

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

O significado de u is 1 em Test1 e Test2 é muito diferente. No Teste1 não é uma correspondência de união, no Teste2 é. Talvez a "correspondência sindical" deva "cavar" Nullable<T> como a correspondência de padrões geralmente faz em outras situações.

Se formos com isso, então o padrão de correspondência null sindical em relação Nullable<union type> deve funcionar como contra as classes. Ou seja, o padrão é verdadeiro quando (!nullableValue.HasValue || nullableValue.Value.Value is null).

Resolução: A proposta foi aprovada.

O que fazer com apIs "ruins"?

O que o compilador deve fazer sobre APIs de união correspondentes que se parecem com uma correspondência, mas de outra forma "ruim"? Por exemplo, o compilador localiza TryGetValue/HasValue com assinatura correspondente, mas é "ruim" porque um modificador personalizado necessário ou requer um recurso desconhecido, etc. O compilador deve ignorar silenciosamente a API ou relatar um erro? Semelhante, a API pode ser marcada como Obsoleta/Experimental. O compilador deve relatar qualquer diagnóstico, usar silenciosamente a API ou silenciosamente não usar a API?

E se os tipos de declaração de união estiverem ausentes

O que acontece se UnionAttribute, IUnion ou IUnion<TUnion> estiver faltando? Erro? Sintetizar? Outra coisa?

[Resolvido] Design da interface IUnion genérica

Foram feitos argumentos que IUnion<TUnion> não devem herdar ou restringir seu parâmetro de IUnion tipo a IUnion<TUnion>. Devemos revisitar.

Resolução: A IUnion<TUnion> interface é removida por enquanto.

[Resolvido] Tipos de valor anuláveis como tipos de caso e sua interação com TryGetValue

As regras acima afirmam que, se um tipo de caso for um tipo de valor anulável, o tipo de parâmetro usado em um método correspondente TryGetValue deverá ser o tipo subjacente . Isso é motivado pelo fato de que um null valor nunca seria produzido por meio desse método. No lado do consumo, um tipo de valor anulável não é permitido como um padrão de tipo, enquanto uma correspondência com o tipo subjacente deve ser capaz de mapear para uma chamada desse método.

Devemos confirmar que concordamos com essa desembrulhamento.

Resolução: Acordado/confirmado

O padrão de acesso união não boxing

Precisa especificar regras precisas para localizar APIs e TryGetValue adequadasHasValue. A herança está envolvida? A leitura/gravação HasValue é uma correspondência aceitável? Etc.

[Resolvido] TryGetValue conversões correspondentes

A seção Correspondência da União diz:

Para um padrão que implica na verificação de um tipo Tespecífico, se um TryGetValue(S value) método está disponível e há uma conversão implícita de T para S, em seguida, esse método é usado para obter o valor.

O conjunto de conversões implícitas é restrito de alguma forma? Por exemplo, as conversões definidas pelo usuário são permitidas? E as conversões de tupla e outras conversões não tão triviais? Algumas delas são até conversões padrão.

O conjunto de métodos é restrito de TryGetValue outra forma? Por exemplo, a seção Padrões de União implica que somente métodos com um tipo de parâmetro correspondente a um tipo de caso são considerados:

um public bool TryGetValue(out T value) método para cada tipo Tde caso.

Seria bom ter uma resposta explícita.

Resolução: Somente a identidade implícita, ou referência ou conversões de boxe são consideradas

TryGetValue e análise anulável

Quando o padrão de acesso não boxing ou HasValueTryGetValue(...) é usado para consultar o conteúdo de um tipo de união (explicitamente ou por meio de correspondência de padrões), ele afeta Valueo estado de nulidade da mesma forma que se Value tivesse sido verificado diretamente: o estado nulo se Value torna "não nulo" no true branch.

O conjunto de métodos é restrito de TryGetValue alguma forma? Por exemplo, a seção Padrões de União implica que somente métodos com um tipo de parâmetro correspondente a um tipo de caso são considerados:

um public bool TryGetValue(out T value) método para cada tipo Tde caso.

Seria bom ter uma resposta explícita.

Esclarecer regras em torno default de valores de tipos de união struct

Observação: a regra de nulidade padrão mencionada abaixo foi removida.

Observação: as regras de bem-formatação "padrão" mencionadas abaixo foram removidas. Devemos confirmar que é isso que queremos.

A seção Nullability diz:

Para tipos de união em que nenhum dos tipos de caso são anuláveis, o estado Value padrão é "não nulo" em vez de "talvez nulo".

Considerando que, para o exemplo a seguir, a implementação atual considera Values2 como "não nula":

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

Ao mesmo tempo, a seção Bem formado diz:

  • Valor padrão: se um tipo de união for um tipo de valor, seu valor padrão terá null como seu Value.
  • Construtor padrão: se um tipo de união tiver um construtor nullary (sem argumento), a união resultante terá null como seu Value.

Uma implementação como essa estará em contradição com o comportamento de análise anulável para o exemplo acima.

As regras de bem-formatação devem ser ajustadas ou o estado deve Valuedefault ser "talvez nulo"? Se este último, a inicialização S2 s2 = default; deve produzir um aviso de nulidade?

Confirme se um parâmetro de tipo nunca é um tipo de união, mesmo quando restrito a um.

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

Os atributos pós-condição devem afetar a nulidade padrão de uma instância da União?

Observação: a regra de nulidade padrão mencionada abaixo foi removida. E não inferimos mais a nulidade padrão da propriedade dos Value métodos de criação da união. Portanto, a questão é obsoleta/não se aplica mais ao design atual.

Para tipos de união em que nenhum dos tipos de caso são anuláveis, o estado Value padrão é "não nulo" em vez de "talvez nulo".

É o aviso esperado no cenário a seguir

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

Conversões de união

[Resolvido] Onde eles pertencem entre outras conversões em termos de prioridade?

As conversões de união parecem outra forma de uma conversão definida pelo usuário. Portanto, a implementação atual os classifica logo após uma tentativa fracassada de classificar uma conversão implícita definida pelo usuário e, em caso de existência, é tratada como apenas outra forma de uma conversão definida pelo usuário. Isso tem as seguintes consequências:

  • Uma conversão implícita definida pelo usuário tem prioridade sobre uma conversão de união
  • Quando a conversão explícita é usada no código, uma conversão explícita definida pelo usuário tem prioridade sobre uma conversão de união
  • Quando não há conversão explícita no código, uma conversão de união tem prioridade sobre uma conversão explícita definida pelo usuário
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)
}

Precisa confirmar se esse é o comportamento que gostamos. Caso contrário, as regras de conversão devem ser esclarecidas.

Resolução :

Aprovado pelo grupo de trabalho.

[Resolvido] Ref-ness do parâmetro do construtor

Atualmente, a linguagem permite apenas por valor e in parâmetros para operadores de conversão definidos pelo usuário. Parece que as razões para essa restrição também são aplicáveis a construtores adequados para conversões de união.

Proposta:

Ajuste a definição de uma case type constructor seção na Union types seção acima:

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

Resolução :

Aprovado pelo grupo de trabalho por enquanto. No entanto, podemos considerar "dividir" o conjunto de construtores de tipo de caso e o conjunto de construtores adequados para conversões de tipo de união.

[Resolvido] Conversões anuláveis

A seção Conversões anuláveis lista explicitamente conversões que podem ser usadas como subjacentes. A especificação atual não propõe nenhum ajuste nessa lista. Isso resulta em um erro para o seguinte cenário:

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

Proposta:

Ajuste a especificação para dar suporte a uma conversão nula implícita de S uma T? conversão de união com suporte. Especificamente, supondo T que seja um tipo de união, haverá uma conversão implícita em um tipo T? de um tipo ou expressão E se houver uma conversão de união de E um tipo C e C for um tipo de caso.T Observe que não há nenhum requisito para o tipo de ser um tipo de E valor não anulável. A conversão é avaliada como a conversão de união subjacente de S para T seguida por um encapsulamento de T para T?

Resolução :

Aprovado.

[Resolvido] Conversões levantadas

Desejamos ajustar a seção conversões suspensas para dar suporte a conversões de união suspensas? Atualmente, eles não são permitidos:

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

Resolução :

Não há conversões de união suspensas por enquanto. Algumas anotações da discussão:

A analogia às conversões definidas pelo usuário divide um pouco aqui. Em geral, os sindicatos são capazes de conter um valor nulo que entra. Não está claro se o levantamento deve criar uma instância de um tipo de união com null o valor armazenado nele ou se deve criar um null valor de Nullable<Union>.

[Resolvido] Bloquear a conversão de união de uma instância de um tipo base?

Pode-se achar o comportamento atual confuso:

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

Observe que o idioma não permite explicitamente declarar conversões definidas pelo usuário de um tipo base. Portanto, pode fazer sentido não permitir conversões sindicais como essa.

Resolução :

Não faça nada de especial por enquanto. Cenários genéricos não podem ser totalmente protegidos de qualquer maneira.

[Resolvido] Bloquear a conversão de união de uma instância de um tipo de interface?

Pode-se achar o comportamento atual confuso:

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

Observe que o idioma não permite explicitamente declarar conversões definidas pelo usuário de um tipo base. Portanto, pode fazer sentido não permitir conversões sindicais como essa.

Resolução :

Não faça nada de especial por enquanto. Cenários genéricos não podem ser totalmente protegidos de qualquer maneira.

Namespace da interface IUnion

A contenção de namespace para IUnion interface permanece não especificada. Se a intenção for mantê-lo em um global namespace, vamos declarar isso explicitamente.

Proposta: se isso for algo simplesmente ignorado, poderemos usar System.Runtime.CompilerServices o namespace.

Classes como Union tipos

[Resolvido] Verificando a própria instância para null

Se um tipo de união for um tipo de classe, seu valor poderá ser nulo. E quanto a verificações nulas? O null padrão foi cooptada para verificar a Value propriedade, então como você verifica se a união em si não é nula?

Por exemplo:

  • Quando S é um Union struct, s is null para um valor de S?é trueapenas quando s ele mesmo é null. Quando C é uma Union classe, c is null para um valor de C?é falsequando c ele mesmo é null, mas é true quando c em si não nullé e c.Value é null.

Outro exemplo:

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

Essa parte do design é claramente otimizada em torno da expectativa de que um tipo de união seja um struct. Algumas opções:

  • Muito ruim. Use == para sua verificação nula em vez de uma correspondência de padrão.
  • Permitir que o null padrão (e a verificação nula implícita em outros padrões) se apliquem ao valor da união e à sua Value propriedade: u is null ==> u == null || u.Value == null.
  • Não permitir que as classes sejam tipos de união!

[Resolvido] Derivando de uma Union classe

Quando uma classe usa uma Unionclasse como sua classe base, de acordo com a especificação atual, ela se torna uma Unionclasse em si. Isso acontece porque ele "herda" automaticamente a implementação da IUnion interface, não é necessário implementá-la novamente. Ao mesmo tempo, construtores do tipo derivado definem o conjunto de tipos neste novo Union. É muito fácil chegar a um comportamento de linguagem muito estranho em torno das duas classes:

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

Algumas opções:

  • Altere quando um tipo de classe é um Union tipo. Por exemplo, uma classe é um Union tipo quando tudo é verdadeiro:

    • Isso ocorre sealed porque os tipos derivados não serão considerados como Uniontipos, permitindo o que é confuso.
    • Nenhuma de suas bases é implementada IUnion

    Isso ainda não é perfeito. As regras são muito sutis. É fácil cometer um erro. Não há nenhum diagnóstico na declaração, mas Union a correspondência não funciona.

  • Não permite que as classes sejam tipos de união.

[Resolvido] O operador is-type

O operador is-type é especificado como uma verificação de tipo de runtime. Sintaticamente parece muito com um padrão de tipo, mas não é. Portanto, a correspondência especial Unionnão será usada, o que pode levar a uma confusão do usuário.

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

No caso de uma união recursiva, o padrão de tipo pode não dar nenhum aviso, mas ainda não fará o que o usuário pode achar que faria.

Resolução: Deve funcionar como um padrão de tipo.

Padrão de lista

O padrão de lista sempre falha com Union a correspondência:

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

Outras perguntas

  • O uso de construtores em conversões de união e o uso da correspondência de TryGetValue(...) padrões de união são especificados para serem brandos quando vários se aplicam: eles simplesmente escolherão um. Isso não deve importar de acordo com as regras de bem-formação, mas estamos confortáveis com isso?
  • A especificação depende sutilmente da implementação da IUnion.Value propriedade em vez de qualquer Value propriedade encontrada no próprio tipo de união. Isso serve para dar maior flexibilidade aos tipos existentes (que podem ter sua própria Value propriedade para outros usos) para implementar o padrão. Mas é estranho, e inconsistente com a forma como outros membros são encontrados e usados diretamente no tipo sindical. Devemos fazer uma mudança? Algumas outras opções:
    • Exigir tipos de união para expor uma propriedade pública Value .
    • Prefira uma propriedade pública Value se ela existir, mas recue para a IUnion.Value implementação se não (semelhante às GetEnumerator regras).
  • A sintaxe da declaração sindical proposta não é universalmente amada, especialmente quando se trata de expressar os tipos de casos. Alternativas até agora também se reúnem com críticas, mas é possível que acabemos fazendo uma mudança. Algumas das principais preocupações expressas sobre a atual:
    • Vírgulas como separadores entre tipos de caso podem parecer implicar que a ordem importa.
    • As listas parênteses se parecem demais com construtores primários (apesar de não terem nomes de parâmetro).
    • Muito diferente das enumerações, que têm seus "casos" em chaves.
  • Embora as declarações de união gerem structs com um único campo de referência, elas ainda são um pouco suscetíveis a um comportamento inesperado quando usadas em um contexto simultâneo. Por exemplo, se um membro de função definido pelo usuário desreferenciar this mais de uma vez, a variável que contém poderá ter sido reatribuída como um todo por outro thread entre os dois acessos. O compilador pode gerar código para copiar this para um local quando necessário. Deveria? Em geral, que grau de resiliência de simultaneidade é desejável e razoavelmente alcançável?