Piezīmes
Lai piekļūtu šai lapai, ir nepieciešama autorizācija. Varat mēģināt pierakstīties vai mainīt direktorijus.
Lai piekļūtu šai lapai, ir nepieciešama autorizācija. Varat mēģināt mainīt direktorijus.
A union type represents a value that can be one of several case types. Unions provide implicit conversions from each case type, exhaustive pattern matching, and enhanced nullability tracking. Use the union keyword to declare a union type:
public union Pet(Cat, Dog, Bird);
This declaration creates a Pet union with three case types: Cat, Dog, and Bird. You can assign any case type value to a Pet variable. The compiler ensures that switch expressions cover all case types.
The C# language reference documents the most recently released version of the C# language. It also contains initial documentation for features in public previews for the upcoming language release.
The documentation identifies any feature first introduced in the last three versions of the language or in current public previews.
Tip
To find when a feature was first introduced in C#, consult the article on the C# language version history.
Declare a union when a value must be exactly one of a fixed set of types and you want the compiler to enforce that every possibility is handled. Common scenarios include:
- Result-or-error returns: A method returns either a success value or an error value, and the caller must handle both. A union like
union Result(Success, Error)makes the set of outcomes explicit. - Message or command dispatching: A system processes a closed set of message types. A union ensures new message types produce compile-time warnings at every
switchthat doesn't handle them yet. - Replacing marker interfaces or abstract base classes: If you use an interface or abstract class solely to group types for pattern matching, a union gives you exhaustiveness checking without requiring inheritance or shared members.
A union differs from other type declarations in important ways:
- Unlike a
classorstruct, a union doesn't define new data members. Instead, it composes existing types into a closed set of alternatives. - Unlike an
interface, a union is closed—you define the complete list of case types in the declaration, and the compiler uses that list for exhaustiveness checks. - Unlike a
record, a union doesn't add equality, cloning, or deconstruction behavior. A union focuses on "which case is it?" rather than "what fields does it have?"
Important
In .NET 11 Preview 2, the runtime doesn't include the UnionAttribute and IUnion interface. To use union types, you must declare them yourself. To see the required declarations, see Union implementation.
Union declarations
A union declaration specifies a name and a list of case types:
public union Pet(Cat, Dog, Bird);
Case types can be any type that converts to object, including classes, structs, interfaces, type parameters, nullable types, and other unions. The following examples show different case type possibilities:
public record class Cat(string Name);
public record class Dog(string Name);
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);
When a case type is a value type (like int), the value is boxed when stored in the union's Value property. Unions store their contents as a single object? reference.
A union declaration can include a body with additional members, just like a struct, subject to some restrictions. Union declarations can't include instance fields, auto-properties, or field-like events. You also can't declare public constructors with a single parameter, because the compiler generates those constructors as union creation members:
public union OneOrMore<T>(T, IEnumerable<T>)
{
public IEnumerable<T> AsEnumerable() => Value switch
{
T single => [single],
IEnumerable<T> multiple => multiple,
_ => []
};
}
Union conversions
An implicit union conversion exists from each case type to the union type. You don't need to call a constructor explicitly:
static void BasicConversion()
{
Pet pet = new Dog("Rex");
Console.WriteLine(pet.Value); // output: Dog { Name = Rex }
Pet pet2 = new Cat("Whiskers");
Console.WriteLine(pet2.Value); // output: Cat { Name = Whiskers }
}
Union conversions work by calling the corresponding generated constructor. If a user-defined implicit conversion operator exists for the same type, the user-defined operator takes priority over the union conversion. For details on conversion priority, see the language specification.
A union conversion to a nullable union struct (T?) also works when T is a union type:
static void NullableUnionExample()
{
Pet? maybePet = new Dog("Buddy");
Pet? noPet = null;
Console.WriteLine(Describe(maybePet)); // output: Dog: 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",
};
}
Union matching
When you pattern match on a union type, patterns apply to the union's Value property, not the union value itself. This "unwrapping" behavior means the union is transparent to pattern matching:
static void PatternMatching()
{
Pet pet = new Dog("Rex");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
Two patterns are exceptions to this rule: the var pattern and the discard _ pattern apply to the union value itself, not its Value property. Use var to capture the union value when GetPet() returns a Pet? (Nullable<Pet>):
if (GetPet() is var pet) { /* pet is the Pet? value returned from GetPet */ }
In logical patterns, each branch follows the unwrapping rule individually. The following pattern tests that the Pet? isn't null and its Value isn't null:
GetPet() switch
{
var pet and not null => ..., // 'var pet' captures the Pet?; 'not null' checks Value
}
Note
Because patterns apply to Value, a pattern like pet is Pet typically doesn't match, since Pet is tested against the contents of the union, not the union itself.
Null matching
For struct unions, the null pattern checks whether Value is null:
static void NullHandling()
{
Pet pet = default;
Console.WriteLine(pet.Value is null); // output: True
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
}
For class-based unions, null succeeds when either the union reference itself is null or its Value property is 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 */ }
For nullable union struct types (Pet?), null succeeds when the nullable wrapper has no value or when the underlying union's Value is null.
Union exhaustiveness
A switch expression is exhaustive when it handles all case types of a union. The compiler warns only if a case type isn't handled. You don't need to include a discard pattern (_) or var pattern to match any type:
static void PatternMatching()
{
Pet pet = new Dog("Rex");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
If the null state of the union's Value property is "maybe null," you must also handle null to avoid a warning:
static void NullHandling()
{
Pet pet = default;
Console.WriteLine(pet.Value is null); // output: True
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
}
Nullability
The compiler tracks the null state of a union's Value property through the following rules:
- When you create a union value from a case type (through a constructor or union conversion),
Valuegets the null state of the incoming value. - When the non-boxing access pattern's
HasValueorTryGetValue(...)members query the union's contents, the null state ofValuebecomes "not null" on thetruebranch.
Custom union types
The compiler converts a union declaration to a struct declaration. The struct is marked with the [System.Runtime.CompilerServices.Union] attribute, implements the IUnion interface. It includes a public constructor and an implicit conversion for each case type along with a Value property. That generated form is opinionated. It's always a struct, always boxes value-type cases, and always stores contents as object?.
When you need different behavior - such as a class-based union, a custom storage strategy, interop support, or if you want to adapt an existing type - you can create a union type manually.
Any class or struct with a [Union] attribute is a union type if it follows the basic union pattern. The basic union pattern requires:
- A
[Union]attribute on the type. - One or more public constructors, each with a single by-value or
inparameter. The parameter type of each constructor defines a case type. - A public
Valueproperty of typeobject?(orobject) with agetaccessor.
All union members must be public. The compiler uses these members to implement union conversions, pattern matching, and exhaustiveness checks. You can also implement the non-boxing access pattern or create a class-based union type.
The compiler assumes that custom union types satisfy these behavioral rules:
- Soundness:
Valuealways returnsnullor a value of one of the case types - never a value of a different type. For struct unions,defaultproduces aValueofnull. - Stability: If you create a union value from a case type,
Valuematches that case type (or isnullif the input wasnull). - Creation equivalence: If a value is implicitly convertible to two different case types, both creation members produce the same observable behavior.
- Access pattern consistency: The
HasValueandTryGetValuemembers, if present, behave equivalently to checkingValuedirectly.
The following example shows a custom union type:
[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
}
Non-boxing access pattern
A custom union type can optionally implement the non-boxing access pattern to enable strongly typed access to value-type cases without boxing during pattern matching. This pattern requires:
- A
HasValueproperty of typeboolthat returnstruewhenValueisn'tnull. - A
TryGetValuemethod for each case type that returnsbooland delivers the value through anoutparameter.
[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
}
The compiler prefers TryGetValue over the Value property when implementing pattern matching, which avoids boxing value types.
Class-based union types
A class can also be a union type. This type of union is useful when you need reference semantics or inheritance:
[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",
};
}
For class-based unions, the null pattern matches both a null reference and a null Value.
Union implementation
The following attribute and interface support union types at compile time and runtime:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
public sealed class UnionAttribute : Attribute;
public interface IUnion
{
object? Value { get; }
}
}
Union declarations generated by the compiler implement IUnion . You can check for any union value at runtime by using IUnion:
if (value is IUnion { Value: null }) { /* the union's value is null */ }
When you declare a union type, the compiler generates a struct that implements IUnion. For example, the Pet declaration (public union Pet(Cat, Dog, Bird);) becomes equivalent to:
[Union] public struct Pet : IUnion
{
public Pet(Cat value) => Value = value;
public Pet(Dog value) => Value = value;
public Pet(Bird value) => Value = value;
public object? Value { get; }
}
Important
In .NET 11 Preview 2, these types aren't included in the runtime. To use union types, you must declare them in your project. They'll be included in a future .NET preview.
C# language specification
For more information, see the Unions feature specification.