Compartir a través de


Uniones

Nota:

Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos y se incorporan en la especificación ECMA actual.

Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de idioma (LDM) pertinentes.

Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones.

Problema planteado por el experto: https://github.com/dotnet/csharplang/issues/9662

Resumen

Uniones es un conjunto de características intervinculadas que se combinan para proporcionar compatibilidad con C# para los tipos de unión:

  • Tipos de unión: las estructuras y clases que tienen un [Union] atributo se reconocen como tipos de unión y admiten los comportamientos de unión.
  • Tipos de caso: los tipos de unión tienen un conjunto de tipos de casos, que se proporcionan mediante parámetros a constructores y métodos de fábrica.
  • Comportamientos de unión: los tipos de unión admiten los siguientes comportamientos de unión:
    • Conversiones de unión: hay conversiones de unión implícitas de cada tipo de caso a un tipo de unión.
    • Coincidencia de uniones: la coincidencia de patrones con los valores de unión "desencapsula" implícitamente su contenido, aplicando el patrón al valor subyacente en su lugar.
    • Exhaustividad de unión: las expresiones switch sobre los valores de unión son exhaustivas cuando se han coinciden todos los tipos de casos, sin necesidad de un caso de reserva.
    • Nulabilidad de unión: el análisis de nulabilidad ha mejorado el seguimiento del estado NULL del contenido de una unión.
  • Patrones de unión: todos los tipos de unión siguen un patrón de unión básico, pero hay patrones opcionales adicionales para escenarios específicos.
  • Declaraciones de unión: una sintaxis abreviada permite la declaración de tipos de unión directamente. La implementación está "fundamentada" : una declaración de estructura que sigue el patrón de unión básico y almacena el contenido como un único campo de referencia.
  • Interfaces de unión: el lenguaje conoce algunas interfaces y se usan en su implementación de declaraciones de unión.

Motivación

Las uniones son una característica de C# solicitada durante mucho tiempo, que permite expresar valores de un conjunto cerrado de tipos de forma que la coincidencia de patrones pueda ser exhaustiva.

La separación entre los tipos de unión y las declaraciones de unión permite a C# tener una sintaxis de declaración de unión con semántica fundamentada, al tiempo que permite que los tipos o tipos existentes con otras opciones de implementación opten por comportamientos de unión.

Las uniones propuestas en C# son uniones de tipos y no "discriminadas" o "etiquetadas". Las "uniones discriminadas" se pueden expresar en términos de "uniones de tipo" mediante declaraciones de tipo fresco como tipos de caso. Como alternativa, se pueden implementar como una jerarquía cerrada, que es otra característica relacionada, próxima de C# centrada en la exhaustiva.

Diseño detallado

Tipos de unión

Cualquier tipo de clase o estructura con un System.Runtime.CompilerServices.UnionAttribute atributo se considera un tipo de unión:

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

Un tipo de unión debe seguir un determinado patrón de miembros de la unión pública, que deben declararse en el propio tipo de unión o delegarse en un "proveedor de miembros de la unión".

Algunos miembros de la unión son obligatorios y otros son opcionales.

Un tipo de unión tiene un conjunto de tipos de casos que se establecen en función de las firmas de determinados miembros de la unión.

Se puede acceder al contenido de un valor de unión a través de una Value propiedad . El lenguaje supone que Value solo contiene un valor de uno de los tipos de caso o null (consulte El formato correcto).

Proveedores de miembros de unión

De forma predeterminada, los miembros de la unión se encuentran en el propio tipo de unión. Sin embargo, si el tipo de unión contiene directamente una declaración de una interfaz denominada IUnionMembers , la interfaz actúa como proveedor de miembros de unión. En ese caso, los miembros de la unión solo se encuentran en el proveedor de miembros de la unión, no en el propio tipo de unión.

Una interfaz de proveedor de miembro de unión debe ser pública y el propio tipo de unión debe implementarlo como una interfaz.

Usamos el término tipo de definición de unión para el tipo donde se encuentran los miembros de la unión: el proveedor de miembros de la unión si existe y el tipo de unión en sí mismo.

Miembros de la unión

Los miembros de la unión se buscan por nombre y firma en el tipo que define la unión. No tienen que declararse directamente en el tipo de definición de unión, pero se pueden heredar.

Se trata de un error para que cualquier miembro de la unión no sea público.

Los miembros de creación y la Value propiedad son obligatorios y se conocen colectivamente como el patrón de unión básico.

Los HasValue miembros y TryGetValue se conocen colectivamente como el patrón de acceso de unión no boxing.

Los diferentes miembros de la unión se describen en lo siguiente.

Miembros de creación de uniones

Los miembros de creación de uniones se usan para crear nuevos valores de unión a partir de un valor de tipo de caso.

Si el tipo que define la unión es el propio tipo de unión, cada constructor con un único parámetro es un constructor de unión. Los tipos de caso de la unión se identifican como el conjunto de tipos creados a partir de tipos de parámetros de estos constructores de la siguiente manera:

  • Si el tipo de parámetro es un tipo que acepta valores NULL (ya sea un valor o una referencia), el tipo de caso es el tipo subyacente.
  • De lo contrario, el tipo de caso es el 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) { ... }

Si el tipo de definición de unión es un proveedor de miembros de unión, cada método estático Create con un único parámetro y un tipo de valor devuelto que se puede convertir a la identidad en el propio tipo de unión es un método de factoría de unión. Los tipos de caso de la unión se identifican como el conjunto de tipos creados a partir de tipos de parámetros de estos métodos de fábrica de la siguiente manera:

  • Si el tipo de parámetro es un tipo que acepta valores NULL (ya sea un valor o una referencia), el tipo de caso es el tipo subyacente.
  • De lo contrario, el tipo de caso es el 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) { ... }

Los constructores de unión y los métodos de fábrica de uniones se conocen colectivamente como miembros de creación de la unión.

El único parámetro de un miembro de creación de uniones debe ser un parámetro por valor o in .

Un tipo de unión debe tener al menos un miembro de creación de unión y, por tanto, al menos un tipo de caso.

Value (propiedad)

La Value propiedad permite el acceso al valor contenido en una unión, independientemente de su tipo de caso.

Cada tipo de definición de unión debe declarar una Value propiedad de tipo object? o object. La propiedad debe tener un get descriptor de acceso y puede tener opcionalmente un init descriptor de acceso o set , que puede ser de cualquier accesibilidad y no lo usa el compilador.

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

Miembros de acceso no boxing

Un tipo de unión puede optar por implementar además el patrón de acceso de unión no boxing, que permite el acceso condicional fuertemente tipado a cada tipo de caso, así como una manera de comprobar si hay un valor NULL.

Esto permite al compilador implementar la coincidencia de patrones de forma más eficaz cuando los tipos de caso son tipos de valor y se almacenan como tales dentro de la unión.

Los miembros de acceso no boxing son:

  • Propiedad HasValue de tipo bool con un descriptor de acceso público get . Opcionalmente, puede tener un init descriptor de acceso o set , que puede ser de cualquier accesibilidad y no lo usa el compilador.
  • Método TryGetValue para cada tipo de caso. El método devuelve bool y toma un único parámetro out de un tipo que es identity-convertible al tipo de caso.
// Non-boxing access members
public bool HasValue { get { ... } }
public bool TryGetValue(out Dog value) { ... }

HasValue se espera que devuelva true si y solo si el de Value la unión no es null.

TryGetValue se espera que devuelva true si y solo si la unión Value es del tipo de caso especificado y, si es así, entrega ese valor en el parámetro out del método.

Formato correcto

El lenguaje y el compilador realizan una serie de suposiciones de comportamiento sobre los tipos de unión. Si un tipo se califica como un tipo de unión pero no satisface esas suposiciones, es posible que los comportamientos de unión no funcionen según lo previsto.

  • Sonido: la Value propiedad siempre se evalúa como null o con un valor de un tipo de caso. Esto es true incluso para el valor predeterminado del tipo de unión.
  • Estabilidad: si se crea un valor de unión a partir de un tipo de caso, la Value propiedad coincidirá con ese tipo de caso o null. Si se crea un valor de unión a partir de un null valor, la Value propiedad será null.
  • Equivalencia de creación: si un valor se puede convertir implícitamente en dos tipos de casos diferentes, el miembro de creación para cualquiera de esos tipos de casos tiene el mismo comportamiento observable cuando se llama a con ese valor.
  • Coherencia del patrón de acceso: el comportamiento de los HasValue miembros de acceso y TryGetValue no boxing, si está presente, es observablemente equivalente al de la comprobación en la Value propiedad directamente.

Ejemplos de tipos de unión

Pet implementa el patrón de unión básico en el propio tipo de unión:

[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 el patrón de acceso no boxing en el propio tipo de unión:

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 es solo un ejemplo de cómo se puede implementar el patrón de acceso no boxing. El código de usuario puede almacenar el contenido de la manera que quiera. En concreto, no impide que la implementación boxing. El non-boxing en su nombre hace referencia a permitir que la implementación de coincidencia de patrones del compilador acceda a cada tipo de caso de forma fuertemente tipada, en lugar de a la object?propiedad -typed Value .

Result<T> implementa el patrón básico a través de un proveedor de miembros de la unión:

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

Comportamientos de unión

Los comportamientos de unión se implementan generalmente mediante el patrón de unión básico. Si la unión ofrece el patrón de acceso no boxing, la coincidencia de patrones de unión hará uso preferentemente de ella.

Conversiones de unión

Una conversión de unión se convierte implícitamente en un tipo de unión de cada uno de sus tipos de mayúsculas y minúsculas. En concreto, hay una conversión de unión a un tipo U de unión de un tipo o expresión E si hay una conversión implícita estándar de E a un tipo C y C es un tipo de parámetro de un miembro de creación de unión de U. Si el tipo U de unión es un struct, hay una conversión de unión al tipo U? o expresión E si hay una conversión implícita estándar de E a un tipo C y C es un tipo de parámetro de un miembro de creación de unión de U.

Una conversión de unión no es una conversión implícita estándar. Por lo tanto, no puede participar en una conversión implícita definida por el usuario u otra conversión de unión.

No hay conversiones de unión explícitas más allá de las conversiones de unión implícitas. Por lo tanto, incluso si hay una conversión explícita del tipo Cde caso de E una unión, esto no significa que haya una conversión explícita de a ese tipo de E unión.

Se ejecuta una conversión de unión llamando al miembro de creación de la unión:

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

Se trata de un error si la resolución de sobrecarga no encuentra un único miembro mejor candidato o si ese miembro no es uno de los miembros de la unión del tipo de unión.

La conversión de unión es simplemente otra "forma" de una conversión implícita definida por el usuario. Conversión de operador de conversión "sombras" definida por el usuario aplicable.

La justificación de esta decisión:

Si alguien escribió un operador definido por el usuario, debería obtener prioridad. En otras palabras, si el usuario escribió realmente su propio operador, quiere que lo llamemos. Los tipos existentes con operadores de conversión transformados en tipos de unión siguen funcionando de la misma manera con respecto al código existente que usa los operadores en la actualidad.

En el ejemplo siguiente, una conversión implícita definida por el usuario tiene prioridad sobre una conversión de unión.

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
}

En el ejemplo siguiente, cuando se usa la conversión explícita en el código, una conversión explícita definida por el usuario tiene prioridad sobre una conversión de unión. Sin embargo, cuando no hay ninguna conversión explícita en el código, se usa una conversión de unión porque no se aplica la conversión explícita definida por el usuario.

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

Coincidencia de uniones

Cuando el valor entrante de un patrón es de un tipo de unión o de un tipo que acepta valores NULL de un tipo de unión, el valor que acepta valores NULL y el contenido del valor de unión subyacente pueden ser "desencapsulados", dependiendo del patrón.

Para los patrones y var incondicionales_, el patrón se aplica al propio valor entrante. Por ejemplo:

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

Sin embargo, todos los demás patrones se aplican implícitamente a la propiedad de Value la unión subyacente:

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 los patrones lógicos, esta regla se aplica individualmente a las ramas, teniendo en cuenta que la rama izquierda de un and patrón puede afectar al tipo entrante de la rama derecha:

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 regla significa que es probable que no se realice correctamente, ya Pet que GetPet() is Pet pet se aplica al contenido, no a la Pet propia unión.

Nota: La razón para el tratamiento diferente del patrón incondicional var (así como _, que es básicamente una abreviatura para var _) es una suposición de que su uso es cualitativamente diferente de otros patrones. var Los patrones se usan simplemente para asignar un nombre al valor que se va a comparar, a menudo en patrones anidados, como PetOwner{ Pet: var pet }. Aquí, la semántica útil es para pet conservar el tipo Petde unión , en lugar de la Value propiedad que se va a desreferenciar a un tipo inútil object? .

Si el valor entrante es un tipo de clase, el null patrón se realizará correctamente independientemente de si el propio valor de unión es null o su valor contenido es null:

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

Otros patrones de coincidencia de uniones solo se realizarán correctamente cuando el propio valor de unión no nullsea .

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

Del mismo modo, si el valor entrante es un tipo de valores que aceptan valores NULL (encapsulando un tipo de unión de estructura), el null patrón se realizará correctamente independientemente de si el propio valor entrante es null o su valor contenido es null:

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

Otros patrones de coincidencia de uniones solo se realizarán correctamente cuando el propio valor entrante no nullsea .

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

El compilador prefiere implementar el comportamiento del patrón por medio de los miembros prescritos por el patrón de acceso no boxing. Aunque es libre de realizar cualquier optimización dentro de los límites de las reglas de forma correcta, se garantiza que se aplique el siguiente conjunto mínimo:

  • Para un patrón que implica comprobar un tipo Tespecífico , si un TryGetValue(S value) método está disponible y hay una identidad, o una conversión implícita de referencia o conversión boxing implícita de T a S, ese método se usa para obtener el valor. A continuación, el patrón se aplica a ese valor. Si hay más de un método de este tipo, siempre que la conversión de a TS no sea una conversión boxing se prefiere si está disponible. Si todavía hay más de un método, se elige uno de manera definida por la implementación.
  • De lo contrario, para un patrón que implica comprobar nullsi hay una HasValue propiedad disponible, esa propiedad se usa para comprobar si el valor de unión es null.
  • De lo contrario, el patrón se aplica al resultado de tener acceso a la IUnion.Value propiedad en la unión entrante.

El operador is-type aplicado a un tipo de unión tiene el mismo significado que un patrón de tipo aplicado al tipo de unión.

Exhaustividad de la unión

Se supone que un tipo de unión se "agota" por sus tipos de mayúsculas y minúsculas. Esto significa que una switch expresión es exhaustiva si controla todos los tipos de casos de una unión:

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

Nulabilidad

Se realiza un seguimiento del estado NULL de la propiedad de Value una unión como cualquier otra propiedad, con estas modificaciones:

  • Cuando se llama a un miembro de creación de unión (explícitamente o a través de una conversión de unión), la nueva unión Value obtiene el estado NULL del valor entrante.
  • Cuando los patrones HasValue de acceso no boxing o TryGetValue(...) se usan para consultar el contenido de un tipo de unión (explícitamente o a través de la coincidencia de patrones), afecta al Valueestado de nulabilidad del mismo modo que si Value se hubiera comprobado directamente: el estado null de Value se convierte en "no null" en la true rama.

Incluso cuando un modificador de unión es exhaustivo, si el estado NULL de la propiedad de Value la unión entrante es "tal vez null", se proporcionará una advertencia en null no controlada.

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

Interfaces de unión

El lenguaje usa las interfaces siguientes en su implementación de características de unión.

Interfaz de acceso de unión

La IUnion interfaz marca un tipo como un tipo de unión en tiempo de compilación y proporciona una manera de acceder al contenido de la unión en tiempo de ejecución.

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

Las uniones generadas por el compilador implementan esta interfaz.

Ejemplo de uso:

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

Declaraciones de unión

Las declaraciones de unión son una forma concisa y fundamentada de declarar tipos de unión en C#. Declaran una estructura que usa una sola referencia de objeto para almacenar su Value, lo que significa:

  • Conversión boxing: los tipos de valor entre sus tipos de caso se muestran en la entrada.
  • Compactación: los valores de unión solo contienen un único campo.

La intención es que las declaraciones de unión cubran la gran mayoría de los casos de uso muy bien. Se espera que las dos razones principales para codificar a mano tipos de unión específicos en lugar de usar declaraciones de unión sean:

  • Adaptar los tipos existentes a los patrones de unión para obtener comportamientos de unión.
  • Implementar una estrategia de almacenamiento diferente por motivos de eficiencia o interoperabilidad.

Sintaxis

Una declaración de unión tiene un nombre y una lista de tipos de constructores de unión .

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

Además de las restricciones de los miembros struct (§16.3), se aplica lo siguiente a los miembros de la unión:

  • No se permiten campos de instancia, propiedades automáticas ni eventos similares a campos.
  • No se permiten constructores públicos declarados explícitamente con un único parámetro.
  • Los constructores declarados explícitamente deben usar un this(...) inicializador para delegar (directa o indirectamente) en uno de los constructores generados.

Los tipos de constructores de unión pueden ser cualquier tipo que se convierta en object, por ejemplo, interfaces, parámetros de tipo, tipos que aceptan valores NULL y otras uniones. Es adecuado para que los casos resultantes se superpongan y para que las uniones anidan o sean null.

Ejemplos:

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

Se reduce a:

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

Preguntas abiertas

[Resuelto] ¿Es una declaración de unión un registro?

Una declaración de unión se reduce a una estructura de registro.

Creo que este comportamiento predeterminado no es necesario y, dado que no es configurable, limitará significativamente los escenarios de uso. Los registros generan una gran cantidad de código que no se usa o no coinciden con requisitos específicos. Por ejemplo, los registros están prácticamente prohibidos en la base de código del compilador debido a ese sobredimensionamiento de código. Creo que sería mejor cambiar el valor predeterminado:

  • De forma predeterminada, una declaración de unión declara una estructura normal con solo miembros específicos de la unión.
  • Un usuario puede declarar una unión de registros: record union U(E1, ...) ...

Resolución: Una declaración de unión es una estructura sin formato, no una estructura de registro. record union ... no se admite

[Resuelto] Sintaxis de declaración de unión

Parece que la sintaxis propuesta está incompleta o limita innecesariamente. Por ejemplo, parece que no se permite la cláusula base. Sin embargo, puedo imaginar fácilmente una necesidad de implementar una interfaz, por ejemplo. Creo que, aparte de element-types-list, la sintaxis debe coincidir con la declaración regular struct/record struct donde la struct palabra clave se reemplaza por union la palabra clave .

Resolución: La restricción se quita.

[Resuelto] Miembros de declaración de unión

No se permiten campos de instancia, propiedades automáticas ni eventos similares a campos.

Esto se siente arbitrario y absolutamente innecesario.

Resolución: La restricción se mantiene.

[Resuelto] Tipos de valor que aceptan valores NULL como tipos de mayúsculas y minúsculas union

Los tipos de caso de la unión se identifican como el conjunto de tipos de parámetros de estos constructores. Los tipos de caso de la unión se identifican como el conjunto de tipos de parámetros de estos métodos de fábrica.

Al mismo tiempo:

Método TryGetValue para cada tipo de caso. El método devuelve bool y toma un único parámetro out de un tipo que corresponde al tipo de caso especificado de la siguiente manera:

  • Si el tipo de caso es un tipo de valor que acepta valores NULL, el tipo del parámetro debe ser identity-convertible al tipo subyacente.
  • De lo contrario, el tipo debe convertirse en identidad en el tipo de caso.

¿Existe una ventaja de tener un tipo de valor que acepta valores NULL entre los tipos de caso especialmente que un patrón de tipo no puede usar un tipo de valor que acepta valores NULL como tipo de destino? Parece que simplemente podríamos decir que, si el tipo de parámetro del constructor o generador es un tipo de valor que acepta valores NULL, el tipo de caso correspondiente es el tipo subyacente. A continuación, no necesitaríamos esa cláusula adicional para el TryGetValue método , todos los parámetros out son tipos de mayúsculas y minúsculas.

Resolución: Se aprueba la sugerencia.

[Resuelto] Estado que acepta valores NULL predeterminado de la Value propiedad

En el caso de los tipos de unión en los que ninguno de los tipos de caso admite valores NULL, el estado predeterminado de Value es "no null" en lugar de "tal vez null".

Con el nuevo diseño, donde Value la propiedad no está definida en alguna interfaz general, pero es una API que pertenece específicamente al tipo declarado, la regla entrecomillada anteriormente se parece a la ingeniería excesiva. Además, es probable que la regla obligue a los consumidores a usar tipos que aceptan valores NULL en situaciones en las que no se usarían tipos que aceptan valores NULL.

Por ejemplo, considere la siguiente declaración de unión:

union U1(int, bool, DateTime);

Según la regla entrecomillada, el estado predeterminado de Value es "not null". Pero eso no coincide con el comportamiento del tipo, default(U1).Value es null. Para realinear el comportamiento, el consumidor se ve obligado a convertir al menos un tipo de caso en nullable. Algo parecido a:

union U1(int?, bool, DateTime);

Pero es probable que no sea deseable, es posible que el consumidor no quiera permitir la creación explícita con int? valor.

Propuesta: quite la regla entrecomillada, el análisis que acepta valores NULL debe usar anotaciones de la Value propiedad para deducir su nulabilidad predeterminada.

Resolución: Se aprueba la propuesta

[Resuelto] Coincidencia de unión para que acepta valores NULL de un tipo de valor de unión

Cuando el valor entrante de un patrón es de un tipo de unión, el contenido del valor de unión puede ser "desencapsulado", dependiendo del patrón.

¿Deberíamos expandir esta regla a escenarios cuando el valor entrante de un patrón es de un Nullable<union type>objeto ?

Imagine la siguiente situación:

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

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

El significado de u is 1 en Test1 y Test2 son muy diferentes. En Test1 no es una unión coincidente, en Test2 es. Quizás la "coincidencia de uniones" debe "profundizar" como Nullable<T> la coincidencia de patrones normalmente lo hace en otras situaciones.

Si vamos con eso, el patrón de coincidencia null de unión con Nullable<union type> debe funcionar como en las clases. Es decir, el patrón es true cuando (!nullableValue.HasValue || nullableValue.Value.Value is null).

Resolución: Se aprueba la propuesta.

¿Qué hacer con las API "incorrectas"?

¿Qué debe hacer el compilador sobre las API de coincidencia de uniones que tienen un aspecto similar a una coincidencia, pero de lo contrario"? Por ejemplo, el compilador busca TryGetValue/HasValue con la firma coincidente, pero es "malo" porque un modificador personalizado necesario o requiere una característica desconocida, etc. ¿El compilador debe omitir silenciosamente la API o notificar un error? De forma similar, es posible que la API se marque como Obsoleta o Experimental. ¿Debe el compilador notificar algún diagnóstico, usar silenciosamente la API o no usar la API de forma silenciosa?

¿Qué ocurre si faltan tipos para la declaración de unión?

¿Qué ocurre si UnionAttributefalta o IUnionIUnion<TUnion> ? ¿Error? ¿Sintetizar? ¿Otra cosa?

[Resuelto] Diseño de la interfaz IUnion genérica

Se han realizado argumentos que IUnion<TUnion> no deben heredar de IUnion ni restringir su parámetro de tipo a IUnion<TUnion>. Deberíamos volver a visitar.

Resolución: La IUnion<TUnion> interfaz se quita por ahora.

[Resuelto] Tipos de valor que aceptan valores NULL como tipos de caso y su interacción con TryGetValue

Las reglas anteriores establecen que si un tipo de caso es un tipo de valor que acepta valores NULL, el tipo de parámetro usado en un método correspondiente TryGetValue debe ser el tipo subyacente . Esto está motivado por el hecho de que un null valor nunca se produciría a través de este método. En el lado del consumo, no se permite un tipo de valor que acepta valores NULL como un patrón de tipo, mientras que una coincidencia con el tipo subyacente debe poder asignarse a una llamada de este método.

Debemos confirmar que estamos de acuerdo con este desencapsulado.

Resolución: Acordado/confirmado

El patrón de acceso de unión no boxing

Debe especificar reglas precisas para buscar API y TryGetValue adecuadasHasValue. ¿Está implicada la herencia? ¿Es de lectura y escritura HasValue una coincidencia aceptable? Etcetera.

[Resuelto] TryGetValue conversiones coincidentes

La sección Coincidencia de uniones dice:

Para un patrón que implica comprobar un tipo Tespecífico , si un TryGetValue(S value) método está disponible y hay una conversión implícita de T a S, ese método se usa para obtener el valor.

¿El conjunto de conversiones implícitas está restringido de alguna manera? Por ejemplo, ¿se permiten conversiones definidas por el usuario? ¿Qué ocurre con las conversiones de tupla y otras conversiones no tan triviales? Algunas de ellas son incluso conversiones estándar.

¿El conjunto de TryGetValue métodos está restringido de otra manera? Por ejemplo, la sección Patrones de unión implica que solo se tienen en cuenta los métodos con un tipo de parámetro que coincida con un tipo de caso:

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

Sería bueno tener una respuesta explícita.

Resolución: Solo se consideran conversiones implícitas de identidad o referencia o conversión boxing.

TryGetValue y el análisis que acepta valores NULL

Cuando los patrones HasValue de acceso no boxing o TryGetValue(...) se usan para consultar el contenido de un tipo de unión (explícitamente o a través de la coincidencia de patrones), afecta al Valueestado de nulabilidad del mismo modo que si Value se hubiera comprobado directamente: el estado null de Value se convierte en "no null" en la true rama.

¿El conjunto de TryGetValue métodos está restringido de alguna manera? Por ejemplo, la sección Patrones de unión implica que solo se tienen en cuenta los métodos con un tipo de parámetro que coincida con un tipo de caso:

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

Sería bueno tener una respuesta explícita.

Aclaración de reglas en torno a default los valores de los tipos de unión de estructura

Nota: Se ha quitado la regla de nulabilidad predeterminada mencionada a continuación.

Nota: Se han quitado las reglas de forma correcta "predeterminadas" mencionadas a continuación. Debemos confirmar que esto es lo que queremos.

En la sección Nulabilidad se indica:

En el caso de los tipos de unión en los que ninguno de los tipos de caso admite valores NULL, el estado predeterminado de Value es "no null" en lugar de "tal vez null".

Dado que, para el ejemplo siguiente, la implementación actual tiene en cuenta Values2 como "not null":

S2 s2 = default;

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

Al mismo tiempo, la sección De forma correcta dice:

  • Valor predeterminado: si un tipo de unión es un tipo de valor, el valor predeterminado tiene null como su Value.
  • Constructor predeterminado: si un tipo de unión tiene un constructor nullary (sin argumento), la unión resultante tiene null como su Value.

Una implementación similar a la que va a estar en conflicto con el comportamiento de análisis que acepta valores NULL para el ejemplo anterior.

¿Se deben ajustar las reglas de forma correcta o debe Valuedefault ser "tal vez null"? Si es el segundo, ¿debe inicializarse S2 s2 = default; una advertencia de nulabilidad?

Confirme que un parámetro de tipo nunca es un tipo de unión, incluso cuando está restringido a uno.

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

¿Deben los atributos posteriores a la condición afectar a la nulabilidad predeterminada de una instancia de Union?

Nota: Se ha quitado la regla de nulabilidad predeterminada mencionada a continuación. Y ya no inferimos la nulabilidad predeterminada de la Value propiedad de los métodos de creación de unión. Por lo tanto, la pregunta está obsoleta o ya no es aplicable al diseño actual.

En el caso de los tipos de unión en los que ninguno de los tipos de caso admite valores NULL, el estado predeterminado de Value es "no null" en lugar de "tal vez null".

Es la advertencia esperada en el siguiente escenario.

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

Conversiones de unión

[Resuelto] ¿Dónde pertenecen entre otras conversiones con prioridad?

Las conversiones de unión se sienten como otra forma de una conversión definida por el usuario. Por lo tanto, la implementación actual las clasifica justo después de un intento erróneo de clasificar una conversión implícita definida por el usuario y, en caso de existencia, se trata como otra forma de conversión definida por el usuario. Esto tiene las siguientes consecuencias:

  • Una conversión implícita definida por el usuario tiene prioridad sobre una conversión de unión
  • Cuando se usa la conversión explícita en el código, una conversión explícita definida por el usuario tiene prioridad sobre una conversión de unión.
  • Cuando no hay ninguna conversión explícita en el código, una conversión de unión tiene prioridad sobre una conversión explícita definida por el usuario
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)
}

Es necesario confirmar que este es el comportamiento que nos gusta. De lo contrario, se deben aclarar las reglas de conversión.

Resolución:

Aprobado por el grupo de trabajo.

[Resuelto] Ref-ness of constructor's parameter (Ref-ness of constructor's parameter)

Actualmente, el lenguaje solo permite por valor y in parámetros para los operadores de conversión definidos por el usuario. Parece que las razones de esta restricción también son aplicables a los constructores adecuados para las conversiones de unión.

Propuesta:

Ajuste la definición de en la case type constructorUnion types sección anterior:

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

Resolución:

Aprobado por el grupo de trabajo por ahora. Sin embargo, podríamos considerar la posibilidad de "dividir" el conjunto de constructores de tipo de caso y el conjunto de constructores adecuados para las conversiones de tipos de unión.

[Resuelto] Conversiones que aceptan valores NULL

En la sección Conversiones que aceptan valores NULL se enumeran explícitamente las conversiones que se pueden usar como subyacentes. La especificación actual no propone ningún ajuste a esa lista. Esto produce un error para el escenario siguiente:

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

Propuesta:

Ajuste la especificación para admitir una conversión implícita que acepta valores NULL de S a respaldada T? por una conversión de unión. En concreto, suponiendo T que es un tipo de unión hay una conversión implícita a un tipo T? de un tipo o expresión E si hay una conversión de unión de E a un tipo C y C es un tipo de caso de T. Tenga en cuenta que no es necesario que el tipo de valor sea un tipo de E valor que no acepta valores NULL. La conversión se evalúa como la conversión de unión subyacente de S a T seguida de un ajuste de T a . T?

Resolución:

Aprobado.

[Resuelto] Conversiones levantadas

¿Deseamos ajustar la sección conversiones levantadas para admitir conversiones de unión levantadas? Actualmente no están 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?'
    }   
}

Resolución:

No se han elevado las conversiones de unión por ahora. Algunas notas de la discusión:

La analogía con las conversiones definidas por el usuario desglosa un poco aquí. En general, las uniones pueden contener un valor NULL que se incluye. No está claro si la elevación debe crear una instancia de un tipo de unión con null el valor almacenado en él o si debe crear un null valor de Nullable<Union>.

[Resuelto] ¿Bloquear la conversión de unión desde una instancia de un tipo base?

Uno podría encontrar el comportamiento actual 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
    }   
}

Tenga en cuenta que el idioma no permite explícitamente declarar conversiones definidas por el usuario a partir de un tipo base. Por lo tanto, podría hacer que sence no permita conversiones de unión como esa.

Resolución:

No haga nada especial por ahora. Los escenarios genéricos no se pueden proteger completamente de todos modos.

[Resuelto] ¿Bloquear la conversión de unión desde una instancia de un tipo de interfaz?

Uno podría encontrar el comportamiento actual 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
    }   
}

Tenga en cuenta que el idioma no permite explícitamente declarar conversiones definidas por el usuario a partir de un tipo base. Por lo tanto, podría hacer que sence no permita conversiones de unión como esa.

Resolución:

No haga nada especial por ahora. Los escenarios genéricos no se pueden proteger completamente de todos modos.

Espacio de nombres de la interfaz IUnion

El espacio de nombres que contiene la IUnion interfaz sigue sin especificarse. Si la intención es mantenerlo en un global espacio de nombres, vamos a indicarlo explícitamente.

Propuesta: Si esto es algo que simplemente se pasa por alto, podríamos usar System.Runtime.CompilerServices el espacio de nombres.

Clases como Union tipos

[Resuelto] Comprobación de la propia instancia de null

Si un tipo de unión es un tipo de clase, es posible que el valor sea NULL. ¿Qué ocurre con las comprobaciones nulas? El null patrón ha sido co-optado por comprobar la Value propiedad, así que ¿cómo se comprueba que la propia unión no es nula?

Por ejemplo:

  • Cuando S es una Union estructura, s is null para un valor de S?es truesolo cuando s es null. Cuando C es una Union clase, c is null para un valor de C?es falsecuando c es null, pero es true cuando c sí mismo no nulles y c.Value es null.

Otro ejemplo:

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 del diseño está claramente optimizada en torno a la expectativa de que un tipo de unión es una estructura. Algunas opciones:

  • Muy mal. Use == para la comprobación nula en lugar de una coincidencia de patrón.
  • Deje que el patrón (y la null comprobación implícita nula en otros patrones) se apliquen tanto al valor de unión como a su Value propiedad: u is null ==> u == null || u.Value == null.
  • ¡No permitir que las clases sean tipos de unión!

[Resuelto] Derivación de una Union clase

Cuando una clase usa una Unionclase como clase base, según la especificación actual, se convierte en una Unionpropia clase. Esto sucede porque "hereda" automáticamente la implementación de la IUnion interfaz, no es necesario volver a implementarla. Al mismo tiempo, los constructores del tipo derivado definen el conjunto de tipos de este nuevo Union. Es muy fácil llegar a un comportamiento de lenguaje muy extraño alrededor de las dos clases:

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

Algunas opciones:

  • Cambie cuando un tipo de clase es un Union tipo. Por ejemplo, una clase es un Union tipo cuando todo es true:

    • Se debe sealed a que los tipos derivados no se considerarán como Uniontipos, lo que permite lo que resulta confuso.
    • Ninguna de sus bases implementa IUnion

    Esto sigue sin ser perfecto. Las reglas son demasiado sutiles. Es fácil cometer un error. No hay ningún diagnóstico en la declaración, pero Union la coincidencia no funciona.

  • No permitir que las clases sean tipos de unión.

[Resuelto] Operador is-type

El operador is-type se especifica como una comprobación de tipo en tiempo de ejecución. Sintácticamente se parece mucho a un patrón de tipo, pero no lo es. Por lo tanto, no se usará la coincidencia especial Union, lo que podría provocar confusión del usuario.

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

En el caso de una unión recursiva, el patrón de tipo podría no dar ninguna advertencia, pero aún no hará lo que podría pensar que haría el usuario.

Resolución: Debe funcionar como un patrón de tipo.

Patrón de lista

El patrón de lista siempre produce un error con Union la coincidencia:

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

Otras preguntas

  • Tanto el uso de constructores en conversiones de unión como el uso de en la coincidencia de TryGetValue(...) patrones de unión se especifican como lenientos cuando se apliquen varios: solo elegirán uno. Esto no debe importar según las reglas de buena forma, pero ¿nos sentimos cómodos con ella?
  • La especificación se basa subproceso en la implementación de la IUnion.Value propiedad en lugar de en cualquier Value propiedad que se encuentre en el propio tipo de unión. Esto está diseñado para proporcionar mayor flexibilidad para los tipos existentes (que pueden tener su propia Value propiedad para otros usos) para implementar el patrón. Pero es incómodo e incoherente con la forma en que se encuentran y usan otros miembros directamente en el tipo de unión. ¿Deberíamos hacer un cambio? Otras opciones:
    • Requerir tipos de unión para exponer una propiedad pública Value .
    • Prefiere una propiedad pública Value si existe, pero retroceda a la IUnion.Value implementación si no es (similar a GetEnumerator las reglas).
  • A la sintaxis propuesta de declaración de unión no se le encanta universalmente, especialmente cuando se trata de expresar los tipos de caso. Las alternativas hasta ahora también se reúnen con críticas, pero es posible que terminemos haciendo un cambio. Algunas de las principales preocupaciones que se muestran sobre la actual:
    • Las comas como separadores entre tipos de caso pueden parecer implicar que el orden es importante.
    • Las listas entre paréntesis tienen un aspecto demasiado parecido a los constructores principales (a pesar de no tener nombres de parámetro).
    • Demasiado diferente de las enumeraciones, que tienen sus "casos" en llaves.
  • Aunque las declaraciones de unión generan estructuras con un único campo de referencia, siguen siendo algo susceptibles a un comportamiento inesperado cuando se usan en un contexto simultáneo. Por ejemplo, si un miembro de función definido por el usuario desreferencias this más de una vez, la variable contenedora puede haber sido reasignada como un todo por otro subproceso entre los dos accesos. El compilador podría generar código para copiarlo this en un entorno local cuando sea necesario. ¿Deberías hacerlo? En general, ¿qué grado de resistencia de simultaneidad es deseable y razonablemente alcanzable?