Partilhar via


Sindicatos

Observação

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

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

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 .

Edição campeã: https://github.com/dotnet/csharplang/issues/9662

Resumo

Unions é um conjunto de funcionalidades interligadas, que se combinam para fornecer suporte a C# para tipos de uniões:

  • Tipos de união: Estruturas e classes que têm um [Union] atributo são reconhecidas como tipos de união e suportam os comportamentos de união.
  • Tipos de casos: Os tipos de união têm um conjunto de tipos de casos, que é dado por parâmetros aos construtores e métodos de fábrica.
  • Comportamentos sindicais: Os tipos de união suportam os seguintes comportamentos sindicatos:
    • Conversões de união: Existem conversões implícitas de união de cada tipo de caso para um tipo de união.
    • Correspondência de uniões: A correspondência de padrões com valores de união "desembrulha" implicitamente o seu conteúdo, aplicando o padrão ao valor subjacente em vez disso.
    • Exaustividade da união: Expressões de comutação sobre valores de união são exaustivas quando todos os tipos de casos foram emparelhados, sem necessidade de um caso de reserva.
    • Anulabilidade sindical: A análise de nulidade melhorou 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 básico de união, mas existem padrões opcionais adicionais para cenários específicos.
  • Declarações de sindicato: Uma sintaxe abreviada permite a declaração direta dos tipos de união. A implementação é "opinativa" – uma declaração de struct que segue o padrão básico de união e armazena o conteúdo como um único campo de referência.
  • Interfaces de união: Algumas interfaces são conhecidas pela linguagem e utilizadas na implementação das declarações de união.

Motivação

As uniões são uma funcionalidade C# muito solicitada, que permite expressar valores de um conjunto fechado de tipos de uma forma que a correspondência de padrões pode confiar que é exaustiva.

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

Os sindicatos propostos em C# são sindicatos de tipos e não são "discriminados" ou "etiquetados". "Sindicatos discriminados" podem ser expressos em termos de "sindicatos de tipo" usando declarações de tipo novas como tipos de caso. Alternativamente, podem ser implementados como uma hierarquia fechada, que é outra funcionalidade relacionada e futura do C# focada na exaustividade.

Design Detalhado

Tipos de União

Qualquer classe ou tipo de 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 sindicato deve seguir um certo padrão de membros sindicais públicos, que deve ser declarado no próprio tipo de sindicato ou delegado a um "fornecedor membro do sindicato".

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

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

O conteúdo de um valor de união pode ser acedido através de uma Value propriedade. A linguagem assume que Value só contém um valor de um dos tipos de caso, ou nulo (ver Bem-formada).

Fornecedores membros do sindicato

Por defeito, os membros do sindicato encontram-se no próprio tipo de sindicato. No entanto, se o tipo de união contiver diretamente uma declaração de uma interface chamada IUnionMembers , então a interface atua como fornecedor membro da união. Nesse caso, os membros do sindicato se encontram no fornecedor do membro do sindicato, não no próprio tipo de sindicato.

Uma interface de fornecedor membro de sindicato deve ser pública, e o próprio tipo de sindicato deve implementá-la como interface.

Usamos o termo tipo definidor de sindicato para o tipo onde se encontram os membros do sindicato: o fornecedor membro sindical, se existir, e o próprio tipo sindical caso contrário.

Membros da União

Os membros do sindicato são consultados pelo nome e assinatura no tipo que define o sindicato. Não têm de ser declarados diretamente no tipo que define a união, mas podem ser herdados.

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

Os membros da criação e a Value propriedade são obrigatórios e são coletivamente referidos como o padrão básico de sindicato.

Os HasValue membros e TryGetValue são coletivamente referidos como o padrão de acesso sindical sem boxe.

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

Membros da criação sindical

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

Se o tipo que define a união for o próprio tipo de união, cada construtor com um único parâmetro é um construtor de união. Os tipos de caso da união são identificados como o conjunto de tipos construídos a partir dos tipos de parâmetros destes construtores da seguinte forma:

  • Se o tipo de parâmetro for um tipo anulável (seja um valor ou uma referência), o tipo caso é 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 que define a união for um fornecedor membro da união, cada método estático Create com um único parâmetro e um tipo de retorno que seja convertível em identidade para o próprio tipo de união é um método union factory. Os tipos de casos da união são identificados como o conjunto de tipos construídos a partir dos tipos de parâmetros destes métodos de fábrica da seguinte forma:

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

Os construtores sindicalizados e os métodos sindicais das fábricas são designados coletivamente como membros de criação sindical.

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

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

Propriedade de valor

A Value propriedade permite o acesso ao valor contido numa união, independentemente do tipo de caso.

Todo tipo definidor de união deve declarar uma Value propriedade do tipo object? ou object. A propriedade deve ter um get acessório e pode opcionalmente ter um init acessório ou set , que pode ter qualquer acessibilidade e não é utilizado pelo compilador.

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

Membros não-boxing access

Um tipo de união pode optar por implementar adicionalmente o padrão de acesso de união não-boxing, que permite acesso condicional fortemente tipado a cada tipo de caso, bem como uma forma de verificar a existência nula.

Isto permite ao compilador implementar a correspondência de padrões de forma mais eficiente quando os tipos de caso são tipos de valor e armazenados como tal dentro da união.

Os membros que não fazem boxing access são:

  • Uma HasValue propriedade de tipo bool com um acessório público get . Pode opcionalmente ter um init acessório ou set , que pode ser de qualquer acessibilidade e não é utilizado pelo compilador.
  • Um método para cada tipo de TryGetValue caso. O método retorna bool e toma um único parâmetro de saída de um tipo que é convertível em 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 verdadeiro se e só se o da Value união não for nulo.

TryGetValue espera-se que retorne verdadeiro se e só se a união Value for do tipo de caso dado e, em caso afirmativo, entregue esse valor no parâmetro out do método.

Bem-formado;

A linguagem e o compilador fazem várias suposições comportamentais sobre os tipos de união. Se um tipo se qualificar como tipo de união mas não satisfazer essas suposições, então os comportamentos sindicais podem não funcionar como esperado.

  • Solidez: A Value propriedade avalia sempre como nula ou para um valor de um tipo de caso. Isto é verdade 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 Value propriedade irá corresponder a 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 convertível para dois tipos de casos diferentes, então o membro de criação para qualquer um desses tipos de casos tem o mesmo comportamento observável quando chamado com esse valor.
  • Consistência do padrão de acesso: O comportamento dos HasValue membros de acesso sem TryGetValue caixa, se presente, é observavelmente equivalente ao de verificar diretamente contra a Value propriedade.

Exemplos de tipos de união

Pet implementa o padrão básico de união 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: Isto é apenas um exemplo de como o padrão de acesso sem boxing pode ser implementado. O código do utilizador pode armazenar o conteúdo da forma que quiser. Em particular, não impede a implementação do boxe! O non-boxing "no seu nome" refere-se a permitir que a implementação de correspondência de padrões do compilador aceda a cada tipo de caso de forma fortemente tipada, em oposição à object?propriedade -typed Value .

Result<T> Implementa o padrão básico através de um fornecedor membro 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 sindicais

Os comportamentos de união são geralmente implementados através do padrão básico de união. Se o sindicato oferecer o padrão de acesso não boxeado, a correspondência de padrão da união fará uso preferencialmente dele.

Conversões da União

Uma conversão de união converte implicitamente para um tipo de união a partir de cada um dos seus tipos de caso. Especificamente, existe uma conversão de união para um tipo U de união a partir de um tipo ou expressão E se existir uma conversão implícita padrão de E para um tipo C e C for um tipo de parâmetro de um membro de criação de união de U. Se o tipo U union for um struct, há uma conversão union para type U? a partir de um tipo ou expressão E se houver uma conversão implícita padrão de E para um tipo C e C for um tipo de parâmetro de um membro de criação de union .U

Uma conversão de união não é, em si, uma conversão implícita padrão. Por isso, não pode participar numa conversão implícita definida pelo utilizador ou noutra conversão por união.

Não existem conversões de união explícitas para além das conversões implícitas de união. Assim, mesmo que haja uma conversão explícita de E para o tipo Cde caso de uma união , isso não significa que haja uma conversão explícita de E para esse tipo de união.

Uma conversão sindical é executada chamando o membro fundador 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 do melhor candidato, ou se esse membro não for um dos membros do sindicato do tipo de sindicato.

A conversão por união é apenas mais uma "forma" de conversão implícita definida pelo utilizador. Um operador de conversão definido pelo utilizador aplicável "shadows" unia a conversão.

A razão por detrás desta decisão:

Se alguém escreveu um operador definido pelo utilizador, deveria ter prioridade. Ou seja, se o utilizador escreveu o seu próprio operador, quer que o chamemos. Os tipos existentes com operadores de conversão transformados em tipos de união continuam a funcionar da mesma forma relativamente ao código existente que utiliza os operadores atualmente.

No exemplo seguinte, uma conversão definida pelo utilizador implícita 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 seguinte, quando o cast explícito é usado no código, uma conversão definida explícitamente pelo utilizador tem prioridade sobre uma conversão unionista. Mas, quando não há cast explícito no código, é usada uma conversão por união porque a conversão definida explícitamente pelo utilizador 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 sindical

Quando o valor recebido de um padrão é do tipo união ou nulo de um tipo união, o valor nulo e o conteúdo do valor da união subjacente podem ser "desembrulhados", dependendo do padrão.

Para os padrões incondicionais _ e var padrão, o padrão é aplicado ao próprio valor recebido. 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, esta regra é aplicada individualmente aos ramos, tendo em conta que o ramo esquerdo de um and padrão pode afetar o tipo de entrada do ramo 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: Esta regra significa que GetPet() is Pet pet provavelmente não terá sucesso, como Pet se aplica ao conteúdo, não à Pet união em si.

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

Se o valor recebido for um tipo de classe, então o null padrão terá sucesso independentemente de o valor de união em si ser null ou o seu valor contido ser null:

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

Outros padrões de correspondência de união só terão sucesso quando o valor da união em si não nullfor .

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

De forma semelhante, se o valor recebido for um tipo de valores anuláveis (envolvendo um tipo de união struct), então o null padrão terá sucesso independentemente de o valor recebido ser null ou o seu valor contido ser null:

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

Outros padrões de correspondência de uniões só terão sucesso quando o valor recebido em si não nullfor .

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

O compilador preferirá implementar o comportamento do 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-formação, os seguintes são os conjuntos mínimos garantidos a ser aplicados:

  • Para um padrão que implica verificar um tipo Tespecífico , se um TryGetValue(S value) método estiver disponível, e existir uma identidade, ou conversão implícita de referência/boxing de T para S, então esse método é usado para obter o valor. O padrão é então aplicado a esse valor. Se existirem mais do que um desses métodos, então qualquer conversão de T para S que não seja uma conversão boxing é preferida, se possível. Se ainda existirem mais do que um método, um é escolhido de forma definida pela implementação.
  • Caso contrário, para um padrão que implica verificar , nullse uma HasValue propriedade estiver disponível, essa propriedade é usada para verificar se o valor da união é nulo.
  • Caso contrário, o padrão é aplicado ao resultado de aceder à IUnion.Value propriedade na união de entrada.

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

Exaustividade sindical

Assume-se que um tipo de união está "esgotado" pelos seus tipos de casos. Isto significa que uma switch expressão é exaustiva se tratar de todos os tipos de casos de uma união:

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

Anulabilidade

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 sindical é chamado (explicitamente ou através de uma conversão sindical), o novo sindicato Value recebe o estado nulo do valor recebido.
  • Quando o padrão HasValue de acesso não-boxing é ou TryGetValue(...) é usado para consultar o conteúdo de um tipo de união (explicitamente ou via correspondência de padrões), isso afeta Valueo estado de anulabilidade da mesma forma que se Value tivesse sido verificado diretamente: O estado nulo de Value torna-se "não nulo" no true branch.

Mesmo quando um switch de união é exaustivo, se o estado nulo da propriedade da Value união de entrada for "talvez nulo", será dado um aviso em nulo não tratado.

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 seguintes interfaces são utilizadas pela linguagem na implementação das funcionalidades de união.

Interface de acesso sindical

A IUnion interface marca um tipo como tipo de união em tempo de compilação e fornece uma forma de aceder ao conteúdo da união em tempo de execução.

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

As uniões geradas pelo compilador implementam esta interface.

Exemplo de utilização:

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

Declarações sindicais

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

  • Boxe: Quaisquer tipos de valor entre os tipos de malas serão colocados na caixa.
  • Compacidade: Os valores de união contêm apenas um campo.

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

  • Adaptar tipos existentes aos padrões de união para obter comportamentos sindicais.
  • Implementar uma estratégia de armazenamento diferente, por exemplo, por razões 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* `}` | ';')
    ;

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

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

Os tipos de construtores union podem ser qualquer tipo que converta em object, por exemplo, interfaces, parâmetros de tipo, tipos anuláveis e outras uniões. É aceitável que os casos resultantes se sobreponham, e que as uniões se formem ou sejam nulas.

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 registo?

Uma declaração sindical é rebaixada a um registo

Acho que este comportamento padrão é desnecessário e, dado que não é configurável, vai limitar significativamente os cenários de uso. Os registos geram muito código que está por usar ou que não corresponde a requisitos específicos. Por exemplo, os registos são praticamente proibidos na base de código do compilador por causa desse excesso de código. Acho que seria melhor alterar o padrão:

  • Por defeito, uma declaração sindical declara uma estrutura regular apenas com membros específicos do sindicato.
  • Um utilizador pode declarar uma união de registos: record union U(E1, ...) ...

Resolução: Uma declaração sindical é uma declaração simples, não uma declaração de registo. Não record union ... é suportado

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

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

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

[Resolvido] Membros da declaração sindical

Campos de instância, auto-propriedades ou eventos semelhantes a campos não são permitidos.

Isto parece-me arbitrário e absolutamente desnecessário.

Resolução: A restrição mantém-se.

[Resolvido] Tipos de valor anulável como tipos de caso Union

Os tipos de caso da união são identificados como o conjunto de tipos de parâmetros a partir destes construtores. Os tipos de caso da união são identificados como o conjunto de tipos de parâmetros destes métodos de fábrica.

Ao mesmo tempo:

Um método para cada tipo de TryGetValue caso. O método devolve bool e toma um único parâmetro de saída de um tipo que corresponde ao tipo de caso dado da seguinte forma:

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

Há alguma vantagem em ter um tipo de valor nulo entre os tipos de casos, especialmente se um padrão de tipo não pode usar o tipo de valor nulo como tipo alvo? Parece que poderíamos simplesmente dizer que, se o tipo de parâmetro do constructor/factory for um tipo de valor nulo, então o tipo de caso correspondente é o tipo subjacente. Então não precisaríamos dessa cláusula extra para o TryGetValue método, todos os parâmetros de saída são tipos de caso.

Resolução: A sugestão é aprovada

[Resolvido] Estado Value padrão de propriedade nulo

Para tipos de união onde nenhum dos tipos de caso é nulo, o estado padrão para Value é "não nulo" em vez de "talvez nulo".

Com o novo design, onde Value a propriedade não está definida numa interface geral, mas é uma API que pertence especificamente ao tipo declarado, a regra citada acima parece uma engenharia excessiva. Além disso, a regra provavelmente obrigará os consumidores a usar tipos anuláveis em situações em que, de outra forma, 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 citada, 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 é obrigado 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 citada, a análise anulável deve usar anotações da Value propriedade para inferir a sua nulidade padrão.

Resolução: A proposta é aprovada

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

Quando o valor recebido de um padrão é do tipo union, o conteúdo do valor union pode ser "desembrulhado", dependendo do padrão.

Devemos expandir esta regra a cenários em que o valor recebido de um padrão é de um Nullable<union type>?

Considere o seguinte 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 são muito diferentes. No Test1 não é um match de sindicato, no Test2 é. Talvez a "correspondência de sindicatos" deva "cavar" como Nullable<T> a correspondência de padrões normalmente faz noutras situações.

Se seguirmos por isso, então o padrão de correspondência null sindical deverá Nullable<union type> funcionar como contra classes. Ou seja, o padrão é verdadeiro quando (!nullableValue.HasValue || nullableValue.Value.Value is null).

Resolução: A proposta é aprovada.

O que fazer em relação a APIs "más"?

O que deve o compilador fazer em relação a APIs de correspondência de uniões que parecem uma correspondência, mas que, de resto, são "más"? Por exemplo, o compilador encontra o TryGetValue/HasValue com a assinatura correspondente, mas é "mau" porque é necessário um modificador personalizado ou requer uma funcionalidade desconhecida, etc. O compilador deve ignorar silenciosamente a API ou reportar um erro? De forma semelhante, a API pode ser marcada como Obsoleta/Experimental. O compilador deve reportar algum diagnóstico, usar a API silenciosamente ou não usar a API silenciosamente?

E se os tipos para declaração sindical estiverem em falta

O que acontece se UnionAttribute, IUnion ou IUnion<TUnion> se estiverem em falta? Erro? Sintetizar? Outra coisa?

[Resolvido] Design da interface IUnion genérica

Foram feitos argumentos que IUnion<TUnion> não devem herdar de IUnion nem restringir o seu parâmetro de tipo a IUnion<TUnion>. Devíamos revisitar.

Resolução: A IUnion<TUnion> interface foi removida por agora.

[Resolvido] Tipos de valor nulo como tipos de caso e a sua interação com TryGetValue

As regras acima indicam que, se um tipo de caso for um tipo de valor nulo, o tipo de parâmetro usado num método correspondente TryGetValue deve ser o tipo subjacente . Isto é motivado pelo facto de nunca obter um null valor através deste método. No lado do consumo, um tipo de valor nulo não é permitido como padrão de tipo, enquanto uma correspondência com o tipo subjacente deveria poder mapear para uma chamada deste método.

Devemos confirmar que concordamos com este desembrulho.

Resolução: Concordo/confirmado

O padrão de acesso sindical não-boxe

É necessário especificar regras precisas para encontrar APIs adequadas HasValueTryGetValue . Há herança envolvida? Leitura/escrita HasValue é uma correspondência aceitável? Etc.

[Resolvido] TryGetValue Conversões correspondentes

A secção Union Matching diz:

Para um padrão que implica verificar um tipo Tespecífico , se um TryGetValue(S value) método estiver disponível, e houver uma conversão implícita de T para S, então esse método é usado para obter o valor.

O conjunto de conversões implícitas é restrito de alguma forma? Por exemplo, são permitidas conversões definidas pelo utilizador? E quanto às conversões de tuplas e outras conversões não tão triviais? Algumas dessas são até conversões padrão.

O conjunto de TryGetValue métodos está restrito de alguma outra forma? Por exemplo, a secção Union Patterns implica que apenas 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: Apenas são consideradas conversões implícitas de identidade, referência ou boxing

TryGetValue e análise anulável

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

O conjunto de TryGetValue métodos é restrito de alguma forma? Por exemplo, a secção Union Patterns implica que apenas 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 sobre default valores dos tipos de união struct

Nota: A regra de anulabilidade padrão mencionada abaixo foi removida.

Nota: As regras "padrão" de bem-formação mencionadas abaixo foram removidas. Devemos confirmar que é isto que queremos.

A secção de anulabilidade diz:

Para tipos de união onde nenhum dos tipos de caso é nulo, o estado padrão para Value é "não nulo" em vez de "talvez nulo".

Dado que, para o exemplo abaixo, a implementação atual considera Value de s2 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 secção Bem-formada diz:

  • Valor padrão: Se um tipo de união for um tipo de valor, o seu valor padrão tem null como seu Value.
  • Construtor padrão: Se um tipo de união tem um construtor nulo (sem argumento), a união resultante tem null como seu Value.

Uma implementação assim estará em contradição com o comportamento de análise anulável no exemplo acima.

As regras de Bem-formado devem ser ajustadas, ou o estado de Valuedefault deve ser "talvez nulo"? Se for o segundo, a inicialização S2 s2 = default; deve produzir um aviso de anulabilidade?

Confirme que um parâmetro de tipo nunca é um tipo de união, mesmo quando limitado 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 por defeito de uma instância Union?

Nota: A regra de anulabilidade padrão mencionada abaixo foi removida. E já não inferimos a nulidade padrão da Value propriedade a partir dos métodos de criação de uniões. Portanto, a questão é obsoleta/já não aplicável ao design atual.

Para tipos de união onde nenhum dos tipos de caso é nulo, o estado padrão para Value é "não nulo" em vez de "talvez nulo".

O aviso é esperado no seguinte cenário

#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 da União

[Resolvido] Onde pertencem elas em termos de prioridades entre outras conversões?

As conversões sindicalizadas parecem mais uma forma de conversão definida pelo utilizador. Portanto, a implementação atual classifica-os logo após uma tentativa falhada de classificar uma conversão implícita definida pelo utilizador e, em caso de existência, é tratada apenas como mais uma forma de conversão definida pelo utilizador. Isto tem as seguintes consequências:

  • Uma conversão implícita definida pelo utilizador tem prioridade sobre uma conversão por união
  • Quando o cast explícito é usado no código, uma conversão definida pelo utilizador explícita tem prioridade sobre uma conversão union
  • Quando não existe um cast explícito no código, uma conversão de união tem prioridade sobre uma conversão explícita definida pelo utilizador
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)
}

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

Resolution:

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 utilizador. Parece que as razões para esta restrição também se aplicam a construtores adequados para conversões de sindicatos.

Proposta:

Ajustar a definição de a case type constructor na Union types secçã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.

Resolution:

Aprovado pelo grupo de trabalho por agora. No entanto, poderíamos considerar "dividir" o conjunto de construtores de tipo caso e o conjunto de construtores adequados para conversões de tipos de união.

[Resolvido] Conversões anuláveis

A secção de Conversões Anuláveis lista explicitamente as conversões que podem ser usadas como subjacentes. A especificação atual não propõe quaisquer ajustes a essa lista. Isto resulta num 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:

Ajusta a especificação para suportar uma conversão implícita anulável de S para, T? apoiada por uma conversão de união. Especificamente, assumindo T que é um tipo union, há uma conversão implícita para um tipo T? a partir de um tipo ou expressão E , se houver uma conversão union de E para um tipo C e C for um tipo de caso de T. Note-se, não há necessidade de que o tipo de E seja um tipo de valor não anulável. A conversão é avaliada como a conversão da união subjacente de S para T seguida por um enrolamento de T para T?

Resolution:

Aprovado.

[Resolvido] Conversões elevadas

Queremos ajustar a secção de conversões elevadas para suportar conversões sindicalizadas elevadas? Atualmente, 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?'
    }   
}

Resolution:

Por agora, não há conversões de sindicato elevado. Algumas notas da discussão:

A analogia com as conversões definidas pelo utilizador é um pouco detalhada aqui. Em geral, as uniões conseguem conter um valor nulo que entra em . Não está claro se o levantamento deve criar uma instância de um tipo de união com null valor armazenado, ou se deve criar um null valor de Nullable<Union>.

[Resolvido] Conversão de união de blocos a partir 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
    }   
}

Note que a linguagem proíbe explicitamente declarar conversões definidas pelo utilizador a partir de um tipo base. Por isso, pode fazer sentido não permitir conversões sindicais desse tipo.

Resolution:

Não faças nada de especial por agora. Cenários genéricos não podem ser totalmente protegidos de qualquer forma.

[Resolvido] Conversão de união de blocos a partir 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
    }   
}

Note que a linguagem proíbe explicitamente declarar conversões definidas pelo utilizador a partir de um tipo base. Por isso, pode fazer sentido não permitir conversões sindicais desse tipo.

Resolution:

Não faças nada de especial por agora. Cenários genéricos não podem ser totalmente protegidos de qualquer forma.

Espaço de nomes da interface IUnion

O espaço de nomes contendo para IUnion a interface permanece não especificado. Se a intenção for mantê-la num global namespace, vamos afirmar isso explicitamente.

Proposta: Se isto for algo simplesmente ignorado, poderíamos usar System.Runtime.CompilerServices namespace.

Classes como Union tipos

[Resolvido] Verificação da instância em si para null

Se um tipo de união for um tipo de classe, o seu valor pode ser nulo. E quanto aos testes de nulo então? O null padrão foi cooptado para verificar a Value propriedade, então como verificar se a união em si não é nula?

Por exemplo:

  • Quando é um Union structo, s is null para um valor de S?é trueapenas quando s ele próprio é null.S Quando C é uma Union classe, c is null para um valor de C?é falsequando c ele próprio é null, mas é true quando c ele próprio 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 };
    }
}

Esta parte do desenho está claramente otimizada em torno da expectativa de que um tipo de união seja um struct. Algumas opções:

  • Que pena. Usa == para a tua verificação de nulo em vez de uma correspondência de padrão.
  • Seja o null padrão (e a verificação nula implícita noutros padrões) aplicam-se tanto ao valor de união como à sua Value propriedade: u is null ==> u == null || u.Value == null.
  • Proibir as aulas de serem do tipo sindicalizado!

[Resolvido] Derivando de uma Union classe

Quando uma classe usa uma Unionclasse como sua classe base, de acordo com a especificação atual, torna-se uma Unionclasse ela própria. Isto acontece porque automaticamente "herda" a implementação da IUnion interface, não é obrigado a reimplementá-la. Ao mesmo tempo, os construtores do tipo derivado definem o conjunto dos tipos neste novo Union. É muito fácil chegar a comportamentos linguísticos muito estranhos 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:

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

    • Isto deve-se sealed ao facto de os tipos derivados não serem considerados como Uniontipos, o que é confuso.
    • Nenhuma das suas bases implementa IUnion

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

  • Proibir que as classes sejam do tipo sindicalizado.

[Resolvido] O operador do tipo is

O operador do tipo is é especificado como uma verificação do tipo de execução. Sintaticamente parece muito um padrão de tipos, mas não é. Portanto, a correspondência especial Unionnão será utilizada, o que pode causar confusão no utilizador.

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 tipos pode não dar aviso, mas ainda assim não fará o que o utilizador pensaria.

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

Padrão de lista

O padrão de lista falha sempre 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 questões

  • Tanto o uso de construtores em conversões de união como o uso de TryGetValue(...) construtores em correspondência de padrões de união são especificados para serem mais flexíveis quando se aplicam vários: eles simplesmente escolhem um. Isto não deveria importar segundo as regras de bem-formação, mas estamos confortáveis com isso?
  • A especificação baseia-se subtilmente na implementação da IUnion.Value propriedade em vez de qualquer Value propriedade encontrada no próprio tipo de união. Isto pretende dar maior flexibilidade aos tipos existentes (que podem ter propriedades Value próprias 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 de sindicato. Devemos mudar alguma coisa? Algumas outras opções:
    • Exigir que os membros do sindicato exponham uma propriedade pública Value .
    • Prefiro uma propriedade pública Value se existir, mas recorre à IUnion.Value implementação se não existir (semelhante a GetEnumerator regras).
  • A sintaxe proposta da declaração sindical não é universalmente apreciada, especialmente quando se trata de expressar os tipos de casos. As alternativas até agora também são criticadas, mas é possível que acabemos por fazer uma mudança. Algumas das principais preocupações levantadas sobre o atual:
    • As vírgulas como separadores entre tipos de casos podem parecer implicar que a ordem importa.
    • Listas entre parênteses parecem demasiado construtores primários (apesar de não terem nomes de parâmetros).
    • Demasiado diferente dos enums, que têm as suas "caixas" em aparelhos enrolados.
  • Embora as declarações de união gerem structs com um único campo de referência, continuam a ser algo suscetíveis a comportamentos inesperados quando usadas num contexto concorrente. Por exemplo, se um membro de função definido pelo utilizador desreferenciar this mais do que uma vez, a variável contenda pode ter sido reatribuída como um todo por outro thread entre os dois acessos. O compilador podia gerar código para copiar this para um local quando necessário. Deveria? Em geral, que grau de resiliência em concorrência é desejável e razoavelmente alcançável?