Nota
O acceso a esta páxina require autorización. Pode tentar iniciar sesión ou modificar os directorios.
O acceso a esta páxina require autorización. Pode tentar modificar os directorios.
Un tipo de unión representa un valor que puede ser uno de varios tipos de casos. Las uniones proporcionan conversiones implícitas de cada tipo de caso, coincidencia exhaustiva de patrones y seguimiento mejorado de nulabilidad. Use la union palabra clave para declarar un tipo de unión:
public union Pet(Cat, Dog, Bird);
Esta declaración crea una Pet unión con tres tipos de casos: Cat, Dogy Bird. Puede asignar cualquier valor de tipo de caso a una Pet variable. El compilador garantiza que switch las expresiones cubran todos los tipos de mayúsculas y minúsculas.
La documentación de referencia del lenguaje C# cubre la versión más reciente publicada del lenguaje C#. También contiene documentación inicial sobre las características de las versiones preliminares públicas de la próxima versión del lenguaje.
La documentación identifica cualquier característica introducida por primera vez en las últimas tres versiones del idioma o en las versiones preliminares públicas actuales.
Sugerencia
Para buscar cuándo se introdujo por primera vez una característica en C#, consulte el artículo sobre el historial de versiones del lenguaje C#.
Declare una unión cuando un valor debe ser exactamente uno de un conjunto fijo de tipos y desea que el compilador aplique que se controle cada posibilidad. Entre los escenarios habituales se incluyen los siguientes:
-
Resultado o error devuelve: un método devuelve un valor correcto o un valor de error, y el autor de la llamada debe controlar ambos. Una unión como
union Result(Success, Error)hace que el conjunto de resultados sea explícito. -
Envío de mensajes o comandos: un sistema procesa un conjunto cerrado de tipos de mensajes. Una unión garantiza que los nuevos tipos de mensajes generen advertencias en tiempo de compilación en cada
switchque aún no las controlen. - Reemplazar interfaces de marcador o clases base abstractas: si usa una interfaz o una clase abstracta únicamente para agrupar tipos para la coincidencia de patrones, una unión le proporciona una comprobación exhaustiva sin necesidad de herencia o miembros compartidos.
Una unión difiere de otras declaraciones de tipo de maneras importantes:
- A diferencia de o
classstruct, una unión no define nuevos miembros de datos. En su lugar, compone los tipos existentes en un conjunto cerrado de alternativas. -
interfaceA diferencia de , se cierra una unión: se define la lista completa de tipos de mayúsculas y minúsculas de la declaración y el compilador usa esa lista para las comprobaciones de exhaustivas. - A diferencia de ,
recorduna unión no agrega igualdad, clonación ni comportamiento de deconstrucción. Una unión se centra en "qué caso es así?" en lugar de "qué campos tiene?"
Los tipos de unión se basan en los UnionAttribute tipos y IUnion en el System.Runtime.CompilerServices espacio de nombres . El entorno de ejecución de .NET 11 incluye estos tipos.
Declaraciones de unión
Una declaración de unión especifica un nombre y una lista de tipos de mayúsculas y minúsculas:
public union Pet(Cat, Dog, Bird);
Los tipos de caso pueden ser cualquier tipo que se convierta en object, incluidas clases, estructuras, interfaces, parámetros de tipo, tipos que aceptan valores NULL y otras uniones. En los ejemplos siguientes se muestran diferentes posibilidades de tipo de caso:
public record class Cat(string Name);
public record class Dog(string Name, string Breed);
public record class Bird(string Name);
public record class None;
public record class Some<T>(T Value);
public union Option<T>(None, Some<T>);
public union IntOrString(int, string);
Cuando un tipo de caso es un tipo de valor (como int), una declaración de unión boxea el valor y la almacena como una sola object? referencia.
Los tipos de unión personalizados pueden usar una estrategia de almacenamiento diferente.
Una declaración de unión puede incluir un cuerpo con miembros adicionales, al igual que una estructura, sujeto a algunas restricciones. Las declaraciones de unión no pueden incluir campos de instancia, propiedades automáticas ni eventos similares a campos. Tampoco puede declarar constructores públicos con un único parámetro, ya que el compilador genera esos constructores como miembros de creación de unión. Cualquier constructor que declare debe usar un this(...) inicializador para delegar, directa o indirectamente, a un constructor generado. La unión siguiente Length agrega una propiedad que usa la TotalMeters coincidencia de patrones para controlar cada tipo de caso, junto con un Add método que combina dos longitudes:
public record class Meters(double Value);
public record class Feet(double Value);
public union Length(Meters, Feet)
{
public double TotalMeters => this switch
{
Meters m => m.Value,
Feet f => f.Value * 0.3048,
_ => throw new InvalidOperationException("The Length has no value."),
};
public Length Add(Length other) => new Meters(TotalMeters + other.TotalMeters);
}
Conversiones de unión
Existe una conversión de unión implícita de cada tipo de caso al tipo de unión:
static void BasicConversion()
{
Pet pet = new Dog("Rex", "Labrador");
Console.WriteLine(pet.Value); // output: Dog { Name = Rex, Breed = Labrador }
Pet pet2 = new Cat("Whiskers");
Console.WriteLine(pet2.Value); // output: Cat { Name = Whiskers }
}
Las conversiones de unión funcionan llamando al constructor generado correspondiente. Si existe un operador de conversión implícita definido por el usuario para el mismo tipo, el operador definido por el usuario tiene prioridad sobre la conversión de unión. Si más de un tipo de caso es igualmente aplicable al valor de origen, la conversión de unión es ambigua y el compilador notifica un error. Para más información sobre la prioridad de conversión, consulte la especificación de características.
Una conversión de unión a una estructura de unión que acepta valores NULL (T?) también funciona cuando T es un tipo de unión:
static void NullableUnionExample()
{
Pet? maybePet = new Dog("Buddy", "Beagle");
Pet? noPet = null;
Console.WriteLine(Describe(maybePet)); // output: Buddy
Console.WriteLine(Describe(noPet)); // output: no pet
static string Describe(Pet? pet) => pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
}
Coincidencia de patrones de unión
Cuando se usa un patrón de tipo o declaración en una expresión de unión, el patrón prueba el contenido de la unión, no la instancia de unión. Por ejemplo, un Dog patrón comprueba si Value es :Dog
static void PatternMatching()
{
Pet pet = new Dog("Rex", "Labrador");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
El formulario del patrón determina qué valor prueba:
| Pattern | Comportamiento en una expresión de unión |
|---|---|
Tipo o patrón de declaración, como Dog o Dog dog |
Comprueba el contenido con el tipo especificado. |
Propiedad o patrón posicional con un tipo, como Dog { Breed: "Labrador" } |
Comprueba el contenido con el tipo y, a continuación, aplica los patrones anidados a ese valor de caso. |
Propiedad o patrón posicional sin un tipo, como { Name: "Rex" } |
Usa las propiedades o Deconstruct el método de la instancia de unión. |
| Patrón de lista | Usa el recuento y los miembros de indexación de la instancia de unión, no los de su contenido. |
var o descartar _ el patrón |
Captura o coincide con la instancia de unión entrante sin desencapsularla. |
| Patrón constante o relacional | Comprueba el contenido. El null patrón también controla una referencia de unión nula o un contenedor que acepta valores NULL. |
Por ejemplo, defina una Pet unión con una propiedad de solo Name lectura que delegue al contenido Cat o Dog. Los patrones de tipo de la expresión de switch la propiedad prueban el contenido:
public union Pet(Cat, Dog)
{
public string Name => this switch
{
Cat cat => cat.Name,
Dog dog => dog.Name,
null => "",
};
}
El patrón Dog { Breed: "Labrador" } con tipo comprueba el contenido y lee el Breedelemento , una propiedad que la unión no tiene. En su lugar, el patrón { Name: "Rex" } sin tipo lee la propiedad de Name la unión, por lo que también coincide con un gato denominado Rex:
Pet pet = new Dog("Rex", "Labrador");
Console.WriteLine(pet is Dog { Breed: "Labrador" }); // output: True
Console.WriteLine(pet is { Name: "Rex" }); // output: True
pet = new Cat("Rex");
Console.WriteLine(pet is Dog { Breed: "Labrador" }); // output: False
Console.WriteLine(pet is { Name: "Rex" }); // output: True
El not patrón niega su patrón anidado. En pet is not null, el patrón anidado null comprueba el contenido, por lo que not null es una prueba útil de valor. Por el contrario, un patrón de propiedad sin tipo vacío, { }, comprueba la instancia de unión. Siempre coincide con una unión de estructura que no acepta valores NULL, incluso cuando Value es NULL:
Pet empty = default;
Console.WriteLine(empty is { }); // output: True
Console.WriteLine(empty is not null); // output: False
if (empty is var captured)
{
Console.WriteLine(captured.Value is null); // output: True
}
Los patrones lógicos conservan estas distinciones. En var pet and not null, var pet captura la unión entrante y not null comprueba si tiene contenido distinto de NULL. En not null and var pet, el var patrón sigue capturando la unión entrante: not no cambia el valor pasado al patrón de la derecha. Por el contrario, en Dog and { Breed: "Labrador" }, el patrón de tipo pasa la coincidencia Dog con el patrón de propiedad.
Nota:
Un patrón de tipo debe ser compatible con al menos un tipo de caso. Para las Pet declaraciones de este artículo, pet is Pet genera el error del compilador CS8121: ninguno de sus tipos de casos puede coincidir con Pet. No es una prueba en tiempo de ejecución de la instancia de unión. Para comprobar el contenido, use pet is not null o pet.Value is not null; para capturar la propia unión, use un var patrón.
En resumen, los patrones de tipo y declaración prueban el contenido. Los patrones recursivos con un tipo también prueban el contenido; Los patrones recursivos sin un tipo usan los propios miembros de la unión. Estas reglas son las mismas en una is expresión y una switch expresión.
Coincidencia nula
En el caso de las uniones de estructura, el null patrón comprueba si Value es null:
static void NullHandling()
{
Pet pet = default;
var description = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
Console.WriteLine(description); // output: no pet
Console.WriteLine(pet.Value is null); // output: True
}
En el caso de las uniones basadas en clases, null se realiza correctamente cuando la propia referencia de unión es null o su Value propiedad es null:
Result<string>? result = null;
if (result is null) { /* true — the reference is null */ }
Result<string> empty = new Result<string>((string?)null);
if (empty is null) { /* true — Value is null */ }
Para los tipos de estructura de unión que aceptan valores NULL (Pet?), null se ejecuta correctamente cuando el contenedor que acepta valores NULL no tiene ningún valor o cuando el valor de Value la unión subyacente es NULL.
Exhaustividad de la unión
Una switch expresión es exhaustiva sobre los tipos de caso de una unión cuando controla cada tipo de caso. Si el análisis de nulabilidad también tiene en cuenta el contenido que no es NULL, no necesita un descarte (_) ni var un brazo:
static void PatternMatching()
{
Pet pet = new Dog("Rex", "Labrador");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
Para controlar el contenido nulo, prefiera un null brazo sobre un var brazo o descartar (_). El null patrón indica la intención y conserva las advertencias al agregar un nuevo tipo de caso sin un brazo coincidente. Un var brazo o descarte también acepta cualquier caso nuevo, por lo que el compilador no puede advertirle sobre ese caso que falta.
Cuando el estado null con seguimiento de Value es "tal vez NULL", el compilador también advierte si omite un null brazo. Por ejemplo, NullablePet tiene un tipo de caso que acepta valores NULL, por lo que el contenido de su parámetro podría ser NULL:
public union NullablePet(Cat?, Dog, Bird);
static string DescribeNullableCase(NullablePet pet) => pet switch
{
Cat cat => cat.Name,
Dog dog => dog.Name,
Bird bird => bird.Name,
null => "no pet",
};
Al quitar el null brazo de este modificador, se genera la advertencia CS8655.
El análisis que acepta valores NULL no garantiza que una unión generada tenga contenido no NULL en tiempo de ejecución. Una unión predeterminada contiene null, incluido cuando se trata de un campo o elemento de matriz inicializado predeterminado. Si todos los tipos de caso no son null, el estado predeterminado del Value que se realiza el seguimiento es "no null", por lo que el compilador no advierte necesariamente sobre un brazo omitido null . Controle null la corrección siempre que el contenido predeterminado pueda llegar al modificador:
static void NullHandling()
{
Pet pet = default;
var description = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
Console.WriteLine(description); // output: no pet
Console.WriteLine(pet.Value is null); // output: True
}
En este Pet ejemplo, al quitar el null brazo no se produce ninguna advertencia missing-null-arm, a menos que el código anterior cambie el estado NULL con seguimiento, pero el modificador se produce SwitchExpressionException cuando recibe el valor predeterminado. Una prueba anterior is null puede cambiar el estado de seguimiento y provocar una advertencia. Este comportamiento es independiente de la asignación definitiva: el compilador rechaza una lectura de una variable local sin asignar.
Nulabilidad
El compilador realiza un seguimiento del estado NULL de la propiedad de Value una unión mediante las reglas siguientes:
- El estado null predeterminado de la propiedad de
Valueuna unión es "tal vez null" si el estado null predeterminado de cualquier tipo de caso es "tal vez null". De lo contrario, el estado null predeterminado es "not null". - Cuando se crea un valor de unión a partir de un tipo de caso (a través de un constructor o conversión de unión),
Valueobtiene el estado NULL del valor entrante. - Cuando los miembros o
HasValuedelTryGetValue(...)patrón de acceso que no son boxing consultan el contenido de la unión, el estado null deValuese convierte en "no null" en latruerama.
Tipos de unión personalizados
El compilador convierte una union declaración en una struct declaración. La estructura se marca con el [System.Runtime.CompilerServices.Union] atributo e implementa la IUnion interfaz . Incluye un constructor público para cada tipo de caso y una Value propiedad . El lenguaje proporciona conversiones de unión implícitas que llaman a esos constructores. Ese formulario generado se opina. Siempre es un struct, siempre se escriben los casos de tipo de valor y siempre se almacena el contenido como object?.
Es posible que necesite un comportamiento diferente si desea adaptar un tipo existente, crear una unión basada en clases o usar una estrategia de almacenamiento personalizada o si necesita compatibilidad con la interoperabilidad. Puede crear manualmente un tipo de unión.
Cualquier clase o estructura con un [Union] atributo es un tipo de unión si sigue el patrón de unión básico. El patrón de unión básico requiere:
- Atributo
[Union]del tipo. - Uno o varios constructores públicos, cada uno con un solo valor o
inparámetro. El tipo de parámetro de cada constructor define un tipo de caso. - Propiedad pública
Valuede tipoobject?(oobject) con ungetdescriptor de acceso.
Todos los miembros de la unión anteriores deben ser públicos. El compilador usa estos miembros para implementar conversiones de unión, coincidencia de patrones y comprobaciones de exhaustividad. También puede implementar el patrón de acceso no boxing o crear un tipo de unión basado en clases. El tipo de unión personalizada puede agregar más miembros.
El compilador supone que los tipos de unión personalizados cumplen estas reglas de comportamiento:
-
Sonido:
Valuesiempre devuelvenullo un valor de uno de los tipos de mayúsculas y minúsculas, incluido para un valor de estructura predeterminado. Una unión personalizada puede representar su valor predeterminado como un caso distinto de NULL, a diferencia del contenido null de una unión generada predeterminada. -
Estabilidad: si crea un valor de unión a partir de un tipo de caso,
Valuecoincide con ese tipo de caso (o esnullsi la entrada eranull). - Equivalencia de creación: si un valor se puede convertir implícitamente en dos tipos de casos diferentes, ambos miembros de creación producen el mismo comportamiento observable.
-
Coherencia del patrón de acceso: los
HasValuemiembros yTryGetValue, si están presentes, se comportan de forma equivalente a comprobarValuedirectamente.
En el ejemplo siguiente se muestra un tipo de unión personalizado:
[System.Runtime.CompilerServices.Union]
public struct Shape : System.Runtime.CompilerServices.IUnion
{
private readonly object? _value;
public Shape(Circle value) { _value = value; }
public Shape(Rectangle value) { _value = value; }
public object? Value => _value;
}
public record class Circle(double Radius);
public record class Rectangle(double Width, double Height);
static void ManualUnionExample()
{
Shape shape = new Shape(new Circle(5.0));
var area = shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
};
Console.WriteLine($"{area:F2}"); // output: 78.54
}
Patrón de acceso sin conversión boxing
Un tipo de unión personalizado puede implementar opcionalmente el patrón de acceso no boxing para permitir el acceso fuertemente tipado a casos de tipo valor sin conversión boxing durante la coincidencia de patrones. Este patrón requiere:
- Propiedad
HasValuede tipoboolque devuelvetruecuandoValuenonulles . - Método
TryGetValuepara cada tipo de caso que devuelvebooly entrega el valor a través de unoutparámetro .TryGetValuedevuelvetruesolo cuandoValuees un valor distinto de NULL de ese tipo de caso. Elouttipo del parámetro es identity-convertible al tipo de caso o al tipo de valor subyacente cuando el tipo de caso es un tipo de valor que acepta valores NULL.
[System.Runtime.CompilerServices.Union]
public struct IntOrBool : System.Runtime.CompilerServices.IUnion
{
private readonly int _intValue;
private readonly bool _boolValue;
private readonly byte _tag; // 0 = none, 1 = int, 2 = bool
public IntOrBool(int? value)
{
if (value.HasValue)
{
_intValue = value.Value;
_tag = 1;
}
}
public IntOrBool(bool? value)
{
if (value.HasValue)
{
_boolValue = value.Value;
_tag = 2;
}
}
public object? Value => _tag switch
{
1 => _intValue,
2 => _boolValue,
_ => null
};
public bool HasValue => _tag != 0;
public bool TryGetValue(out int value)
{
value = _intValue;
return _tag == 1;
}
public bool TryGetValue(out bool value)
{
value = _boolValue;
return _tag == 2;
}
}
static void NonBoxingExample()
{
IntOrBool val = new IntOrBool((int?)42);
var description = val switch
{
int i => $"int: {i}",
bool b => $"bool: {b}",
};
Console.WriteLine(description); // output: int: 42
}
Para la coincidencia de patrones, el compilador llama TryGetValue a comprobaciones de patrones de tipo (como union is T) y HasValue para comprobaciones de patrones NULL (como union is null), evitando la conversión boxing para casos de tipo de valor. Cada miembro se aplica a su propio tipo de patrón, ni es una reserva para el otro. Cuando falta un miembro, el compilador comprueba en su lugar la Value propiedad de ese patrón. Para obtener más información, consulte Cómo genera el compilador código para la coincidencia de patrones.
Proveedores de miembros de unión
Un tipo de unión puede delegar sus miembros de unión en una interfaz anidada IUnionMembers pública que contiene e implementa directamente. Esa interfaz es el proveedor de miembros de la unión. El compilador solo busca miembros de unión en la interfaz del proveedor, incluidos los miembros heredados. Los miembros declarados solo en el propio tipo de unión no se usan como miembros de la unión.
Al usar un proveedor de miembros de unión, la interfaz debe declarar estos miembros necesarios:
-
Métodos estáticos
Create: uno para cada tipo de caso, con un único parámetro y un tipo de valor devuelto que se puede convertir al tipo de unión. Estos métodos establecen los tipos de caso. -
Valueproperty: una propiedad oobject?públicaobjectcon ungetdescriptor de acceso que devuelve el valor contenido.
Opcionalmente, puede declarar estos miembros en la interfaz para que el compilador genere código de coincidencia de patrones más eficaz:
-
TryGetValuemétodos: uno para cada tipo de caso, devolviendoboolcon unoutparámetro del tipo de caso. -
HasValueproperty: una propiedad públicaboolcon ungetdescriptor de acceso que devuelvetruecuandoValuenonulles .
Cómo el compilador genera código para la coincidencia de patrones: TryGetValue y HasValue se aplica a diferentes tipos de patrones; tampoco es una reserva para la otra:
- Para un patrón de tipo, como
union is T, el compilador llamaTryGetValue(out T value)cuando se declara, extrayendo el valor y aplicando el patrón a él sin conversión boxing. De lo contrario, el compilador aplica el patrón a laValuepropiedad , que realiza una comprobación de tipo en tiempo de ejecución conTy puede marcar el valor cuandoTes un tipo de valor. - Para un patrón NULL, como
union is null, el compilador llamaHasValuecuando se declara para probar si la unión contiene un valor. De lo contrario, el compilador aplica el patrón NULL a laValuepropiedad directamente.
Cada miembro es opcional de forma independiente: sin TryGetValue, los patrones de tipo usan Value; sin HasValue, los patrones NULL usan Value. Declarar cualquiera de los miembros permite al compilador generar código más eficaz y fuertemente tipado para ese tipo de patrón:
[System.Runtime.CompilerServices.Union]
public struct Outcome<T> : Outcome<T>.IUnionMembers
{
private readonly object? _value;
private Outcome(object? value) => _value = value;
public interface IUnionMembers
{
static Outcome<T> Create(T? value) => new(value);
static Outcome<T> Create(Exception? value) => new(value);
object? Value { get; }
// Optional but recommended: TryGetValue enables efficient pattern matching
bool TryGetValue(out T value);
bool TryGetValue(out Exception value);
}
object? IUnionMembers.Value => _value;
public bool TryGetValue(out T value)
{
if (_value is T t)
{
value = t;
return true;
}
value = default!;
return false;
}
public bool TryGetValue(out Exception value)
{
if (_value is Exception e)
{
value = e;
return true;
}
value = default!;
return false;
}
}
Los proveedores de miembros de unión son útiles cuando el tipo de unión necesita un constructor privado o cuando la lógica de creación requiere un patrón de fábrica, como con record class tipos de unión.
Tipos de unión basados en clases
Una clase también puede ser un tipo de unión. Este tipo de unión es útil cuando se necesita la semántica de referencia o la herencia:
[System.Runtime.CompilerServices.Union]
public class Result<T> : System.Runtime.CompilerServices.IUnion
{
private readonly object? _value;
public Result(T? value) { _value = value; }
public Result(Exception? value) { _value = value; }
public object? Value => _value;
}
static void ClassUnionExample()
{
Result<string> ok = new Result<string>("success");
Result<string> err = new Result<string>(new InvalidOperationException("failed"));
Console.WriteLine(Describe(ok)); // output: OK: success
Console.WriteLine(Describe(err)); // output: Error: failed
static string Describe(Result<string> result) => result switch
{
string s => $"OK: {s}",
Exception e => $"Error: {e.Message}",
null => "null",
};
}
En el caso de las uniones basadas en clases, el null patrón coincide con una referencia nula y un valor NULL Value.
Especificación del lenguaje C#
Para obtener más información, consulte la especificación de características Uniones .