diff --git a/docfx.json b/docfx.json index 81e6317284893..1b0777c2a43f6 100644 --- a/docfx.json +++ b/docfx.json @@ -55,7 +55,8 @@ "csharp-12.0/*.md", "csharp-13.0/*.md", "csharp-14.0/*.md", - "collection-expression-arguments.md" + "collection-expression-arguments.md", + "unions.md" ], "src": "_csharplang/proposals", "dest": "csharp/language-reference/proposals", @@ -698,6 +699,7 @@ "_csharplang/proposals/csharp-14.0/extension-operators.md": "Extension operators", "_csharplang/proposals/csharp-14.0/optional-and-named-parameters-in-expression-trees.md": "Optional and named parameters in expression trees", "_csharplang/proposals/collection-expression-arguments.md": "Collection expression arguments", + "_csharplang/proposals/unions.md": "Unions", "_roslyn/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 7.md": "C# compiler breaking changes since C# 10", "_roslyn/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 8.md": "C# compiler breaking changes since C# 11", "_roslyn/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 9.md": "C# compiler breaking changes since C# 12", @@ -826,6 +828,7 @@ "_csharplang/proposals/csharp-14.0/extension-operators.md": "This proposal extends the proposal for extensions to include *extension operators*, where an operator can be an extension member.", "_csharplang/proposals/csharp-14.0/optional-and-named-parameters-in-expression-trees.md": "This proposal allows an expression tree to include named and optional parameters. This enables expression trees to be more flexible in how they are constructed.", "_csharplang/proposals/collection-expression-arguments.md": "This proposal introduces collection expression arguments.", + "_csharplang/proposals/unions.md": "This proposal describes union types and union declarations. Unions allow expressing values from a closed set of types with exhaustive pattern matching.", "_roslyn/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 7.md": "Learn about any breaking changes since the initial release of C# 10 and included in C# 11", "_roslyn/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 8.md": "Learn about any breaking changes since the initial release of C# 11 and included in C# 12", "_roslyn/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 9.md": "Learn about any breaking changes since the initial release of C# 12 and included in C# 13", diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/BasicUnion.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/BasicUnion.cs new file mode 100644 index 0000000000000..8ed54f5ab38c7 --- /dev/null +++ b/docs/csharp/language-reference/builtin-types/snippets/unions/BasicUnion.cs @@ -0,0 +1,44 @@ +// +public record class Cat(string Name); +public record class Dog(string Name); +public record class Bird(string Name); +// + +// +public union Pet(Cat, Dog, Bird); +// + +public static class BasicUnionScenario +{ + public static void Run() + { + BasicConversion(); + PatternMatching(); + } + + // + 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 } + } + // + + // + 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 + } + // +} diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/BodyMembers.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/BodyMembers.cs new file mode 100644 index 0000000000000..8ce8467dec5e0 --- /dev/null +++ b/docs/csharp/language-reference/builtin-types/snippets/unions/BodyMembers.cs @@ -0,0 +1,30 @@ +// +public union OneOrMore(T, IEnumerable) +{ + public IEnumerable AsEnumerable() => Value switch + { + T single => [single], + IEnumerable multiple => multiple, + _ => [] + }; +} +// + +public static class BodyMembersScenario +{ + public static void Run() + { + BodyMembersExample(); + } + + // + static void BodyMembersExample() + { + OneOrMore single = "hello"; + OneOrMore multiple = new[] { "a", "b", "c" }.AsEnumerable(); + + Console.WriteLine(string.Join(", ", single.AsEnumerable())); // output: hello + Console.WriteLine(string.Join(", ", multiple.AsEnumerable())); // output: a, b, c + } + // +} diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/ClassUnion.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/ClassUnion.cs new file mode 100644 index 0000000000000..5ae6b433eeaf3 --- /dev/null +++ b/docs/csharp/language-reference/builtin-types/snippets/unions/ClassUnion.cs @@ -0,0 +1,38 @@ +// +[System.Runtime.CompilerServices.Union] +public class Result : System.Runtime.CompilerServices.IUnion +{ + private readonly object? _value; + + public Result(T? value) { _value = value; } + public Result(Exception? value) { _value = value; } + + public object? Value => _value; +} +// + +public static class ClassUnionScenario +{ + public static void Run() + { + ClassUnionExample(); + } + + // + static void ClassUnionExample() + { + Result ok = new Result("success"); + Result err = new Result(new InvalidOperationException("failed")); + + Console.WriteLine(Describe(ok)); // output: OK: success + Console.WriteLine(Describe(err)); // output: Error: failed + + static string Describe(Result result) => result switch + { + string s => $"OK: {s}", + Exception e => $"Error: {e.Message}", + null => "null", + }; + } + // +} diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/GenericUnion.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/GenericUnion.cs new file mode 100644 index 0000000000000..9593fd399e6c1 --- /dev/null +++ b/docs/csharp/language-reference/builtin-types/snippets/unions/GenericUnion.cs @@ -0,0 +1,35 @@ +// +public record class None; +public record class Some(T Value); +public union Option(None, Some); +// + +public static class GenericUnionScenario +{ + public static void Run() + { + GenericUnionExample(); + } + + // + static void GenericUnionExample() + { + Option some = new Some(42); + Option none = new None(); + + var result = some switch + { + Some s => $"Has value: {s.Value}", + None => "No value", + }; + Console.WriteLine(result); // output: Has value: 42 + + var result2 = none switch + { + Some s => $"Has value: {s.Value}", + None => "No value", + }; + Console.WriteLine(result2); // output: No value + } + // +} diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/ManualUnion.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/ManualUnion.cs new file mode 100644 index 0000000000000..b4055e06cf222 --- /dev/null +++ b/docs/csharp/language-reference/builtin-types/snippets/unions/ManualUnion.cs @@ -0,0 +1,37 @@ +// +[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); +// + +public static class ManualUnionScenario +{ + public static void Run() + { + ManualUnionExample(); + } + + // + 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 + } + // +} diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/MemberProvider.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/MemberProvider.cs new file mode 100644 index 0000000000000..5505875ebccaa --- /dev/null +++ b/docs/csharp/language-reference/builtin-types/snippets/unions/MemberProvider.cs @@ -0,0 +1,36 @@ +// Uncomment when union member providers are available in the compiler: + +// +// [System.Runtime.CompilerServices.Union] +// public record class Outcome : Outcome.IUnionMembers +// { +// private readonly object? _value; +// +// private Outcome(object? value) => _value = value; +// +// public interface IUnionMembers +// { +// static Outcome Create(T? value) => new(value); +// static Outcome Create(Exception? value) => new(value); +// object? Value { get; } +// } +// +// object? IUnionMembers.Value => _value; +// } +// + +// +// public static class MemberProviderScenario +// { +// public static void Run() +// { +// Outcome ok = "success"; +// var msg = ok switch +// { +// string s => $"OK: {s}", +// Exception e => $"Error: {e.Message}", +// }; +// Console.WriteLine(msg); +// } +// } +// diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/NonBoxingAccess.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/NonBoxingAccess.cs new file mode 100644 index 0000000000000..a0b3d60663a99 --- /dev/null +++ b/docs/csharp/language-reference/builtin-types/snippets/unions/NonBoxingAccess.cs @@ -0,0 +1,70 @@ +// +[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; + } +} +// + +public static class NonBoxingAccessScenario +{ + public static void Run() + { + NonBoxingExample(); + } + + // + 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 + } + // +} diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/NullHandling.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/NullHandling.cs new file mode 100644 index 0000000000000..67e1fb8950359 --- /dev/null +++ b/docs/csharp/language-reference/builtin-types/snippets/unions/NullHandling.cs @@ -0,0 +1,44 @@ +public static class NullHandlingScenario +{ + public static void Run() + { + NullHandling(); + NullableUnionExample(); + } + + // + 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 + } + // + + // + 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", + }; + } + // +} diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/Program.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/Program.cs new file mode 100644 index 0000000000000..eca889a8175e5 --- /dev/null +++ b/docs/csharp/language-reference/builtin-types/snippets/unions/Program.cs @@ -0,0 +1,10 @@ +BasicUnionScenario.Run(); +GenericUnionScenario.Run(); +ValueTypeCasesScenario.Run(); +BodyMembersScenario.Run(); +NullHandlingScenario.Run(); +ManualUnionScenario.Run(); +NonBoxingAccessScenario.Run(); +ClassUnionScenario.Run(); +// Uncomment when union member providers are available in the compiler: +// MemberProviderScenario.Run(); diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/RuntimePolyfill.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/RuntimePolyfill.cs new file mode 100644 index 0000000000000..52dcc5501d328 --- /dev/null +++ b/docs/csharp/language-reference/builtin-types/snippets/unions/RuntimePolyfill.cs @@ -0,0 +1,13 @@ +// Remove this file when UnionAttribute and IUnion are included in the .NET runtime. +// +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)] + public sealed class UnionAttribute : Attribute; + + public interface IUnion + { + object? Value { get; } + } +} +// diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/ValueTypeCases.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/ValueTypeCases.cs new file mode 100644 index 0000000000000..5c9349e1e608d --- /dev/null +++ b/docs/csharp/language-reference/builtin-types/snippets/unions/ValueTypeCases.cs @@ -0,0 +1,29 @@ +// +public union IntOrString(int, string); +// + +public static class ValueTypeCasesScenario +{ + public static void Run() + { + ValueTypeCasesExample(); + } + + // + static void ValueTypeCasesExample() + { + IntOrString val1 = 42; + IntOrString val2 = "hello"; + + Console.WriteLine(Describe(val1)); // output: int: 42 + Console.WriteLine(Describe(val2)); // output: string: hello + + static string Describe(IntOrString value) => value switch + { + int i => $"int: {i}", + string s => $"string: {s}", + null => "null", + }; + } + // +} diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/unions.csproj b/docs/csharp/language-reference/builtin-types/snippets/unions/unions.csproj new file mode 100644 index 0000000000000..c2cb17a624618 --- /dev/null +++ b/docs/csharp/language-reference/builtin-types/snippets/unions/unions.csproj @@ -0,0 +1,11 @@ + + + + Exe + net11.0 + preview + enable + enable + + + diff --git a/docs/csharp/language-reference/builtin-types/union.md b/docs/csharp/language-reference/builtin-types/union.md new file mode 100644 index 0000000000000..75b6a9d10c512 --- /dev/null +++ b/docs/csharp/language-reference/builtin-types/union.md @@ -0,0 +1,235 @@ +--- +title: "Union types" +description: Learn about union types in C#. Unions express values from a closed set of types with exhaustive pattern matching support. +ms.date: 03/20/2026 +f1_keywords: + - "union_CSharpKeyword" +helpviewer_keywords: + - "union keyword [C#]" + - "union type [C#]" + - "case type [C#]" +ai-usage: ai-assisted +--- +# Union types (C# reference) + +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: + +:::code language="csharp" source="snippets/unions/BasicUnion.cs" id="BasicDeclaration"::: + +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, and the compiler ensures that `switch` expressions cover all case types. + +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 `switch` that 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 `class` or `struct`, 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?" + +[!INCLUDE[csharp-version-note](../includes/initial-version.md)] + +> [!IMPORTANT] +> In .NET 10 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-implementation). + +## Union declarations + +A union declaration specifies a name and a list of case types: + +```csharp +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: + +:::code language="csharp" source="snippets/unions/BasicUnion.cs" id="CaseTypes"::: +:::code language="csharp" source="snippets/unions/GenericUnion.cs" id="GenericUnion"::: +:::code language="csharp" source="snippets/unions/ValueTypeCases.cs" id="ValueTypeCases"::: + +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: + +:::code language="csharp" source="snippets/unions/BodyMembers.cs" id="BodyMembers"::: + +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. + +## Union conversions + +An implicit *union conversion* exists from each case type to the union type. You don't need to call a constructor explicitly: + +:::code language="csharp" source="snippets/unions/BasicUnion.cs" id="BasicConversion"::: + +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. See the [language specification](~/_csharplang/proposals/unions.md) for details on conversion priority. + +A union conversion to a nullable union struct (`T?`) also works when `T` is a union type: + +:::code language="csharp" source="snippets/unions/NullHandling.cs" id="NullableUnionExample"::: + +## 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: + +:::code language="csharp" source="snippets/unions/BasicUnion.cs" id="PatternMatching"::: + +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`): + +```csharp +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: + +```csharp +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: + +:::code language="csharp" source="snippets/unions/NullHandling.cs" id="NullHandling"::: + +For class-based unions, `null` succeeds when either the union reference itself is null or its `Value` property is null: + +```csharp +Result? result = null; +if (result is null) { /* true — the reference is null */ } + +Result empty = new Result((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: + +:::code language="csharp" source="snippets/unions/BasicUnion.cs" id="PatternMatching"::: + +If the null state of the union's `Value` property is "maybe null," you must also handle `null` to avoid a warning: + +:::code language="csharp" source="snippets/unions/NullHandling.cs" id="NullHandling"::: + +## 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), `Value` gets the null state of the incoming value. +- When the non-boxing access pattern's `HasValue` or `TryGetValue(...)` members query the union's contents, the null state of `Value` becomes "not null" on the `true` branch. + +## Custom union types + +The compiler lowers a `union` declaration to a `struct` marked with the `[System.Runtime.CompilerServices.Union]` attribute, implements the `IUnion` interface, and generates 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 `[System.Runtime.CompilerServices.Union]` attribute on the type. +- One or more public constructors, each with a single by-value or `in` parameter. The parameter type of each constructor defines a *case type*. +- A public `Value` property of type `object?` (or `object`) with a `get` accessor. + +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](#non-boxing-access-pattern) or create a [class-based union type](#class-based-union-types). + +The compiler assumes that custom union types satisfy these behavioral rules: + +- **Soundness**: `Value` always returns `null` or a value of one of the case types - never a value of a different type. For struct unions, `default` produces a `Value` of `null`. +- **Stability**: If you create a union value from a case type, `Value` matches that case type (or is `null` if the input was `null`). +- **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 `HasValue` and `TryGetValue` members, if present, behave equivalently to checking `Value` directly. + +The following example shows a custom union type: + +:::code language="csharp" source="snippets/unions/ManualUnion.cs" id="ManualBasicPattern"::: + +:::code language="csharp" source="snippets/unions/ManualUnion.cs" id="ManualUnionExample"::: + +### 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 `HasValue` property of type `bool` that returns `true` when `Value` isn't `null`. +- A `TryGetValue` method for each case type that returns `bool` and delivers the value through an `out` parameter. + +:::code language="csharp" source="snippets/unions/NonBoxingAccess.cs" id="NonBoxingAccessPattern"::: + +:::code language="csharp" source="snippets/unions/NonBoxingAccess.cs" id="NonBoxingExample"::: + +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: + +:::code language="csharp" source="snippets/unions/ClassUnion.cs" id="ClassUnion"::: + +:::code language="csharp" source="snippets/unions/ClassUnion.cs" id="ClassUnionExample"::: + +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: + +:::code language="csharp" source="snippets/unions/RuntimePolyfill.cs" id="RuntimePolyfill"::: + +Union declarations generated by the compiler implement `IUnion`. You can check for any union value at runtime by using `IUnion`: + +```csharp +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: + +```csharp +public union Pet(Cat, Dog, Bird); +``` + +becomes equivalent to: + +```csharp +[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 10 Preview 2, these types aren't included in the runtime. You must declare them in your project to use union types. They'll be included in a future .NET preview. + +## C# language specification + +For more information, see the [Unions](~/_csharplang/proposals/unions.md) feature specification. + +## See also + +- [The C# type system](../../fundamentals/types/index.md) +- [Pattern matching](../operators/patterns.md) +- [Switch expression](../operators/switch-expression.md) +- [Value types](value-types.md) +- [Records](record.md) diff --git a/docs/csharp/language-reference/builtin-types/value-types.md b/docs/csharp/language-reference/builtin-types/value-types.md index e6d03c3df25ab..f7ca55c6794fd 100644 --- a/docs/csharp/language-reference/builtin-types/value-types.md +++ b/docs/csharp/language-reference/builtin-types/value-types.md @@ -1,7 +1,7 @@ --- description: Value types vs reference types, kinds of value types, and the built-in value types in C# title: "Value types" -ms.date: 01/14/2026 +ms.date: 03/20/2026 f1_keywords: - "cs.valuetypes" helpviewer_keywords: @@ -11,13 +11,13 @@ helpviewer_keywords: --- # Value types (C# reference) -*Value types* and [reference types](../keywords/reference-types.md) are the two main categories of C# types. A variable of a value type contains an instance of the type. This behavior differs from a variable of a reference type, which contains a reference to an instance of the type. By default, on [assignment](../operators/assignment-operator.md), passing an argument to a method, and returning a method result, variable values are copied. In the case of value-type variables, the corresponding type instances are copied. The following example demonstrates that behavior: +*Value types* and [reference types](../keywords/reference-types.md) are the two main categories of C# types. A variable of a value type contains an instance of the type. This behavior differs from a variable of a reference type, which contains a reference to an instance of the type. By default, on [assignment](../operators/assignment-operator.md), passing an argument to a method, and returning a method result, you copy variable values. In the case of value-type variables, you copy the corresponding type instances. The following example demonstrates that behavior: :::code language="csharp" source="snippets/shared/ValueTypes.cs" id="ValueTypeCopied"::: As the preceding example shows, operations on a value-type variable affect only that instance of the value type, stored in the variable. -If a value type contains a data member of a reference type, only the reference to the instance of the reference type is copied when a value-type instance is copied. Both the copy and original value-type instance have access to the same reference-type instance. The following example demonstrates that behavior: +If a value type contains a data member of a reference type, you copy only the reference to the instance of the reference type when you copy a value-type instance. Both the copy and original value-type instance have access to the same reference-type instance. The following example demonstrates that behavior: :::code language="csharp" source="snippets/shared/ValueTypes.cs" id="ShallowCopy"::: @@ -26,14 +26,15 @@ If a value type contains a data member of a reference type, only the reference t ## Kinds of value types and type constraints -A value type can be one of the two following kinds: +A value type can be one of the following kinds: - a [structure type](struct.md), which encapsulates data and related functionality - an [enumeration type](enum.md), which is defined by a set of named constants and represents a choice or a combination of choices +- a [union declaration](union.md), which defines a closed set of case types that a value can represent A [nullable value type](nullable-value-types.md) `T?` represents all values of its underlying value type `T` and an additional [null](../keywords/null.md) value. You can't assign `null` to a variable of a value type, unless it's a nullable value type. -You can use the [`struct` constraint](../../programming-guide/generics/constraints-on-type-parameters.md) to specify that a type parameter is a non-nullable value type. Both structure and enumeration types satisfy the `struct` constraint. You can use `System.Enum` in a base class constraint (that is known as the [enum constraint](../../programming-guide/generics/constraints-on-type-parameters.md#enum-constraints)) to specify that a type parameter is an enumeration type. +Use the [`struct` constraint](../../programming-guide/generics/constraints-on-type-parameters.md) to specify that a type parameter is a non-nullable value type. Both structure and enumeration types satisfy the `struct` constraint. Use `System.Enum` in a base class constraint (that is known as the [enum constraint](../../programming-guide/generics/constraints-on-type-parameters.md#enum-constraints)) to specify that a type parameter is an enumeration type. ## Built-in value types @@ -44,14 +45,14 @@ C# provides the following built-in value types, also known as *simple types*: - [bool](bool.md) that represents a Boolean value - [char](char.md) that represents a Unicode UTF-16 character -All simple types are structure types. They differ from other structure types in that they permit certain additional operations: +All simple types are struct types. They differ from other struct types in that they permit certain additional operations: - You can use literals to provide a value of a simple type.
For example, `'A'` is a literal of the type `char`, `2001` is a literal of the type `int`, and `12.34m` is a literal of the type `decimal`. - You can declare constants of the simple types by using the [const](../keywords/const.md) keyword.
For example, you can define `const decimal = 12.34m`. -
You can't declare constants of other structure types. +
You can't declare constants of other struct types. - Constant expressions, whose operands are all constants of the simple types, are evaluated at compile time. diff --git a/docs/csharp/language-reference/operators/patterns.md b/docs/csharp/language-reference/operators/patterns.md index b753c3f9d24f7..873b5872c12a9 100644 --- a/docs/csharp/language-reference/operators/patterns.md +++ b/docs/csharp/language-reference/operators/patterns.md @@ -1,7 +1,7 @@ --- title: "Patterns - Pattern matching using the is and switch expressions." description: "Learn about the patterns supported by the `is` and `switch` expressions. Combine multiple patterns using the `and`, `or`, and `not` operators." -ms.date: 01/20/2026 +ms.date: 03/20/2026 f1_keywords: - "and_CSharpKeyword" - "or_CSharpKeyword" @@ -43,7 +43,7 @@ For an example of how to use those patterns to build a data-driven algorithm, se ## Declaration and type patterns -Use declaration and type patterns to check if the run-time type of an expression is compatible with a given type. With a declaration pattern, you can also declare a new local variable. When a declaration pattern matches an expression, the variable is assigned to the converted expression result, as the following example shows: +Use declaration and type patterns to check if the run-time type of an expression is compatible with a given type. By using a declaration pattern, you can also declare a new local variable. When a declaration pattern matches an expression, it assigns the variable to the converted expression result, as the following example shows: :::code language="csharp" source="snippets/patterns/DeclarationAndTypePatterns.cs" id="BasicExample"::: @@ -101,7 +101,7 @@ Use a constant pattern to check for `null`, as the following example shows: :::code language="csharp" source="snippets/patterns/ConstantPattern.cs" id="NullCheck"::: -The compiler guarantees that no user-overloaded equality operator `==` is invoked when expression `x is null` is evaluated. +The compiler guarantees that it doesn't invoke a user-overloaded equality operator `==` when it evaluates expression `x is null`. You can use a [negated](#logical-patterns) `null` constant pattern to check for non-null, as the following example shows: @@ -115,13 +115,13 @@ Use a *relational pattern* to compare an expression result with a constant, as t :::code language="csharp" source="snippets/patterns/RelationalPatterns.cs" id="BasicExample"::: -In a relational pattern, you can use any of the [relational operators](comparison-operators.md) `<`, `>`, `<=`, or `>=`. The right-hand part of a relational pattern must be a constant expression. The constant expression can be of an [integer](../builtin-types/integral-numeric-types.md), [floating-point](../builtin-types/floating-point-numeric-types.md), [char](../builtin-types/char.md), or [enum](../builtin-types/enum.md) type. +In a relational pattern, use any of the [relational operators](comparison-operators.md) `<`, `>`, `<=`, or `>=`. The right-hand part of a relational pattern must be a constant expression. The constant expression can be of an [integer](../builtin-types/integral-numeric-types.md), [floating-point](../builtin-types/floating-point-numeric-types.md), [char](../builtin-types/char.md), or [enum](../builtin-types/enum.md) type. To check if an expression result is in a certain range, match it against a [conjunctive `and` pattern](#logical-patterns), as the following example shows: :::code language="csharp" source="snippets/patterns/RelationalPatterns.cs" id="WithCombinators"::: -If an expression result is `null` or fails to convert to the type of a constant by a nullable or unboxing conversion, a relational pattern doesn't match an expression. +If an expression result is `null` or fails to convert to the type of a constant by using a nullable or unboxing conversion, a relational pattern doesn't match the expression. For more information, see the [Relational patterns](~/_csharplang/proposals/csharp-9.0/patterns3.md#relational-patterns) section of the feature proposal note. @@ -141,17 +141,17 @@ Use the `not`, `and`, and `or` pattern combinators to create the following *logi :::code language="csharp" source="snippets/patterns/LogicalPatterns.cs" id="OrPattern"::: -As the preceding example shows, you can repeatedly use the pattern combinators in a pattern. +As the preceding example shows, you can use the pattern combinators repeatedly in a pattern. ### Precedence and order of checking -The pattern combinators follow this order, based on the binding order of expressions: +The pattern combinators check expressions in this order, based on the binding order of expressions: - `not` - `and` - `or` -The `not` pattern binds to its operand first. The `and` pattern binds after any `not` pattern expression binding. The `or` pattern binds after all `not` and `and` patterns bind to operands. The following example tries to match all characters that aren't lowercase letters, `a` - `z`. It has an error, because the `not` pattern binds before the `and` pattern: +The `not` pattern binds to its operand first. The `and` pattern binds after any `not` pattern expression binding. The `or` pattern binds after all `not` and `and` patterns bind to operands. The following example tries to match all characters that aren't lowercase letters, `a` through `z`. It has an error, because the `not` pattern binds before the `and` pattern: :::code language="csharp" source="snippets/patterns/LogicalPatterns.cs" id="NegationWithoutParens"::: @@ -168,7 +168,7 @@ Adding parentheses becomes more important as your patterns become more complicat :::code language="csharp" source="snippets/patterns/LogicalPatterns.cs" id="WithParentheses"::: > [!NOTE] -> The order in which patterns having the same binding order are checked is undefined. At run time, the right-hand nested patterns of multiple `or` patterns and multiple `and` patterns can be checked first. +> The order in which the compiler checks patterns that have the same binding order is undefined. At run time, the compiler can check the right-hand nested patterns of multiple `or` patterns and multiple `and` patterns first. For more information, see the [Pattern combinators](~/_csharplang/proposals/csharp-9.0/patterns3.md#pattern-combinators) section of the feature proposal note. @@ -236,7 +236,7 @@ You can also extend a positional pattern in any of the following ways: :::code language="csharp" source="snippets/patterns/PositionalPattern.cs" id="WithPropertyPattern"::: -- Combine two preceding usages, as the following example shows: +- Combine the two preceding usages, as the following example shows: :::code language="csharp" source="snippets/patterns/PositionalPattern.cs" id="CompletePositionalPattern"::: @@ -266,7 +266,7 @@ Use a *discard pattern* `_` to match any expression, including `null`, as the fo :::code language="csharp" source="snippets/patterns/DiscardPattern.cs" id="BasicExample"::: -In the preceding example, a discard pattern handles `null` and any integer value that doesn't have the corresponding member of the enumeration. That guarantee ensures that a `switch` expression in the example handles all possible input values. If you don't use a discard pattern in a `switch` expression and none of the expression's patterns matches an input, the runtime [throws an exception](switch-expression.md#non-exhaustive-switch-expressions). The compiler generates a warning if a `switch` expression doesn't handle all possible input values. +In the preceding example, a discard pattern handles `null` and any integer value that doesn't have the corresponding member of the enumeration. That guarantee ensures that a `switch` expression in the example handles all possible input values. If you don't use a discard pattern in a `switch` expression and none of the expression's patterns matches an input, the runtime [throws an exception](switch-expression.md#nonexhaustive-switch-expressions). The compiler generates a warning if a `switch` expression doesn't handle all possible input values. A discard pattern can't be a pattern in an `is` expression or a `switch` statement. In those cases, to match any expression, use a [`var` pattern](#var-pattern) with a discard: `var _`. A discard pattern can be a pattern in a `switch` expression. @@ -274,7 +274,7 @@ For more information, see the [Discard pattern](~/_csharpstandard/standard/patte ## Parenthesized pattern -You can put parentheses around any pattern. Typically, you do that to emphasize or change the precedence in [logical patterns](#logical-patterns), as the following example shows: +You can put parentheses around any pattern. Typically, you do this to emphasize or change the precedence in [logical patterns](#logical-patterns), as the following example shows: :::code language="csharp" source="snippets/patterns/LogicalPatterns.cs" id="ChangedPrecedence"::: @@ -288,7 +288,7 @@ As the preceding example shows, a list pattern matches when each nested pattern :::code language="csharp" source="snippets/patterns/ListPattern.cs" id="MatchAnyElement"::: -The preceding examples match a whole input sequence against a list pattern. To match elements only at the start or/and the end of an input sequence, use the *slice pattern* `..`, as the following example shows: +The preceding examples match a whole input sequence against a list pattern. To match elements only at the start or end - or both - of an input sequence, use the *slice pattern* `..`, as the following example shows: :::code language="csharp" source="snippets/patterns/ListPattern.cs" id="UseSlice"::: @@ -300,6 +300,30 @@ You can also nest a subpattern within a slice pattern, as the following example For more information, see the [List patterns](~/_csharplang/proposals/csharp-11.0/list-patterns.md) feature proposal note. +## Union patterns + +Starting with C# 15, when the incoming value of a pattern is a [union type](../builtin-types/union.md), patterns automatically *unwrap* the union. They apply to the union's `Value` property rather than the union value itself. This behavior makes the union transparent to pattern matching: + +```csharp +public record class Cat(string Name); +public record class Dog(string Name); +public union Pet(Cat, Dog); + +string Describe(Pet pet) => pet switch +{ + Dog d => d.Name, + Cat c => c.Name, +}; +``` + +Two patterns are exceptions: the `var` pattern and the discard `_` pattern apply to the union value itself, not its `Value` property. + +The `null` pattern checks whether the union's `Value` is null. For class-based unions, `null` also succeeds when the union reference itself is null. + +When a union type provides the *non-boxing access pattern* (`HasValue` and `TryGetValue` members), the compiler uses those members to avoid boxing value-type cases during pattern matching. + +For more information, see the [Union matching](../builtin-types/union.md#union-matching) section. For the specification, see [Unions](~/_csharplang/proposals/unions.md). + ## C# language specification For more information, see the [Patterns and pattern matching](~/_csharpstandard/standard/patterns.md) section of the [C# language specification](~/_csharpstandard/standard/README.md). diff --git a/docs/csharp/language-reference/operators/switch-expression.md b/docs/csharp/language-reference/operators/switch-expression.md index d86dc324090ae..cbd6d489773c8 100644 --- a/docs/csharp/language-reference/operators/switch-expression.md +++ b/docs/csharp/language-reference/operators/switch-expression.md @@ -1,7 +1,7 @@ --- title: "switch expression - Evaluate a pattern match expression using the `switch` expression" description: Learn about the C# `switch` expression that provides switch-like semantics based on pattern matching. You can compute a value based on which pattern an input variable matches. -ms.date: 01/20/2026 +ms.date: 03/20/2026 f1_keywords: - "switch-expression_CSharpKeyword" helpviewer_keywords: @@ -26,7 +26,7 @@ The preceding example shows the basic elements of a `switch` expression: In the preceding example, a `switch` expression uses the following patterns: - A [constant pattern](patterns.md#constant-pattern): to handle the defined values of the `Direction` enumeration. -- A [discard pattern](patterns.md#discard-pattern): to handle any integer value that doesn't have the corresponding member of the `Direction` enumeration (for example, `(Direction)10`). That pattern makes the `switch` expression [exhaustive](#non-exhaustive-switch-expressions). +- A [discard pattern](patterns.md#discard-pattern): to handle any integer value that doesn't have the corresponding member of the `Direction` enumeration (for example, `(Direction)10`). That pattern makes the `switch` expression [exhaustive](#nonexhaustive-switch-expressions). > [!IMPORTANT] > For information about the patterns supported by the `switch` expression and more examples, see [Patterns](patterns.md). @@ -43,10 +43,12 @@ A pattern might not be expressive enough to specify the condition for the evalua The preceding example uses [property patterns](patterns.md#property-pattern) with nested [var patterns](patterns.md#var-pattern). -## Non-exhaustive switch expressions +## Nonexhaustive switch expressions If none of a `switch` expression's patterns matches an input value, the runtime throws an exception. In .NET Core 3.0 and later versions, the exception is a . In .NET Framework, the exception is an . In most cases, the compiler generates a warning if a `switch` expression doesn't handle all possible input values. [List patterns](patterns.md#list-patterns) don't generate a warning when all possible inputs aren't handled. +For [union types](../builtin-types/union.md), a `switch` expression is exhaustive when it handles all case types. A catch-all arm isn't needed. If the null state of the union's `Value` property is "maybe null," you must also handle `null` to avoid a warning. For more information, see [Union exhaustiveness](../builtin-types/union.md#union-exhaustiveness). + > [!TIP] > To guarantee that a `switch` expression handles all possible input values, provide a `switch` expression arm with a [discard pattern](patterns.md#discard-pattern). diff --git a/docs/csharp/language-reference/toc.yml b/docs/csharp/language-reference/toc.yml index 8eeda1ce75b27..d73894371628a 100644 --- a/docs/csharp/language-reference/toc.yml +++ b/docs/csharp/language-reference/toc.yml @@ -37,6 +37,9 @@ items: - name: Struct types href: ./builtin-types/struct.md displayName: "struct type, readonly struct, ref struct, readonly ref struct" + - name: Union types + href: ./builtin-types/union.md + displayName: "union type, union declaration, case types" - name: Ref struct types href: ./builtin-types/ref-struct.md displayName: "ref struct, readonly ref struct" diff --git a/docs/csharp/specification/toc.yml b/docs/csharp/specification/toc.yml index 2286c56360af5..14f9da84c8270 100644 --- a/docs/csharp/specification/toc.yml +++ b/docs/csharp/specification/toc.yml @@ -233,6 +233,10 @@ items: href: ../../../_csharplang/proposals/csharp-11.0/low-level-struct-improvements.md - name: Inline arrays href: ../../../_csharplang/proposals/csharp-12.0/inline-arrays.md + - name: Unions + items: + - name: Union types + href: ../../../_csharplang/proposals/unions.md - name: Interfaces items: - name: Variance safety for static interface members diff --git a/docs/csharp/tour-of-csharp/tips-for-javascript-developers.md b/docs/csharp/tour-of-csharp/tips-for-javascript-developers.md index 929856c37e212..2abc303450c51 100644 --- a/docs/csharp/tour-of-csharp/tips-for-javascript-developers.md +++ b/docs/csharp/tour-of-csharp/tips-for-javascript-developers.md @@ -53,7 +53,7 @@ async Task FetchDataAsync() { } ``` -Learn more: [Asynchronous programming](../asynchronous-programming/index.md) +To learn more, see [Asynchronous programming](../asynchronous-programming/index.md). **Classes:** @@ -69,7 +69,7 @@ class Point { record Point(int X, int Y); ``` -Learn more: [Records](../fundamentals/types/records.md) +To learn more, see [Records](../fundamentals/types/records.md). **Pattern matching:** @@ -83,7 +83,7 @@ if (typeof value === "string") { /* ... */ } if (value is string s) { /* use s */ } ``` -Learn more: [Pattern matching](../fundamentals/functional/pattern-matching.md) +To learn more, see [Pattern matching](../fundamentals/functional/pattern-matching.md). ## What's new for you in C\# @@ -108,11 +108,11 @@ Some familiar features and idioms from JavaScript and TypeScript aren't availabl In addition, a few more TypeScript features aren't available in C#: -1. ***Union types***: C# doesn't support union types. However, design proposals are in progress. +1. ***Union types***: Beginning with C# 15, C# supports [union types](../language-reference/builtin-types/union.md). A union defines a closed set of named cases that a value can represent, and the compiler ensures exhaustive pattern matching over those cases. 1. ***Decorators***: C# doesn't have decorators. Some common decorators, such as `@sealed` are reserved keywords in C#. Other common decorators might have corresponding [Attributes](../language-reference/attributes/general.md). For other decorators, you can create your own attributes. 1. ***More forgiving syntax***: The C# compiler parses code more strictly than JavaScript requires. -If you're building a web application, consider using [Blazor](/aspnet/core/blazor/index) to build your application. Blazor is a full-stack web framework built for C#. Blazor components can run on the server, as .NET assemblies, or on the client using WebAssembly. Blazor supports interop with your favorite JavaScript or TypeScript libraries. +If you're building a web application, consider using [Blazor](/aspnet/core/blazor/index) to build your application. Blazor is a full-stack web framework built for C#. Blazor components can run on the server, as .NET assemblies, or on the client by using WebAssembly. Blazor supports interop with your favorite JavaScript or TypeScript libraries. ## Next steps diff --git a/docs/csharp/whats-new/csharp-15.md b/docs/csharp/whats-new/csharp-15.md index 3b0db85177798..ce0d6df36a172 100644 --- a/docs/csharp/whats-new/csharp-15.md +++ b/docs/csharp/whats-new/csharp-15.md @@ -1,22 +1,23 @@ --- title: What's new in C# 15 description: Get an overview of the new features in C# 15. C# 15 ships with .NET 11. -ms.date: 02/04/2026 +ms.date: 03/20/2026 ms.topic: whats-new ms.update-cycle: 365-days ai-usage: ai-assisted --- # What's new in C# 15 -C# 15 includes the following new features. You can try these features using the latest [Visual Studio 2026](https://visualstudio.microsoft.com/) version or the [.NET 11 preview SDK](https://dotnet.microsoft.com/download/dotnet): +C# 15 includes the following new features. Try these features by using the latest [Visual Studio 2026](https://visualstudio.microsoft.com/) insiders version or the [.NET 11 preview SDK](https://dotnet.microsoft.com/download/dotnet): - [Collection expression arguments](#collection-expression-arguments) +- [Union types](#union-types) -C# 15 is the latest C# release. C# 15 is supported on **.NET 11**. For more information, see [C# language versioning](../language-reference/configure-language-version.md). +C# 15 is the latest C# preview release. .NET 11 previews support C# 15. For more information, see [C# language versioning](../language-reference/configure-language-version.md). You can download the latest .NET 11 preview SDK from the [.NET downloads page](https://dotnet.microsoft.com/download). You can also download [Visual Studio 2026 insiders](https://visualstudio.microsoft.com/vs/), which includes the .NET 11 preview SDK. -New features are added to the "What's new in C#" page when they're available in public preview releases. The [working set](https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md#working-set) section of the [roslyn feature status page](https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md) tracks when upcoming features are merged into the main branch. +The "What's new in C#" page adds new features when they're available in public preview releases. The [working set](https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md#working-set) section of the [roslyn feature status page](https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md) tracks when upcoming features are merged into the main branch. You can find any breaking changes introduced in C# 15 in our article on [breaking changes](~/_roslyn/docs/compilers/CSharp/Compiler%20Breaking%20Changes%20-%20DotNet%2011.md). @@ -39,7 +40,36 @@ HashSet set = [with(StringComparer.OrdinalIgnoreCase), "Hello", "HELLO", // set contains only one element because all strings are equal with OrdinalIgnoreCase ``` -You can learn more about collection expression arguments in the [language reference article on collection expressions](../language-reference/operators/collection-expressions.md#collection-expression-arguments) or the [feature specification](~/_csharplang/proposals/collection-expression-arguments.md). For information on using collection expression arguments in collection initializers, see [Object and Collection Initializers](../programming-guide/classes-and-structs/object-and-collection-initializers.md#collection-expression-arguments). +To learn more about collection expression arguments, see the [language reference article on collection expressions](../language-reference/operators/collection-expressions.md#collection-expression-arguments) or the [feature specification](~/_csharplang/proposals/collection-expression-arguments.md). For information on using collection expression arguments in collection initializers, see [Object and Collection Initializers](../programming-guide/classes-and-structs/object-and-collection-initializers.md#collection-expression-arguments). + +## Union types + +C# 15 introduces *union types*, which represent a value that can be one of several *case types*. Declare a union with the `union` keyword: + +```csharp +public record class Cat(string Name); +public record class Dog(string Name); +public record class Bird(string Name); + +public union Pet(Cat, Dog, Bird); +``` + +Unions provide implicit conversions from each case type, and the compiler ensures `switch` expressions are exhaustive across all case types: + +```csharp +Pet pet = new Dog("Rex"); + +string name = pet switch +{ + Dog d => d.Name, + Cat c => c.Name, + Bird b => b.Name, +}; +``` + +Union types first appear in .NET 11 preview 2. In early .NET 11 previews, the `UnionAttribute` and `IUnion` interface aren't included in the runtime, so you must declare them in your project. A later .NET 11 preview will include these runtime types. Also, some features from the [proposal specification](~/_csharplang/proposals/unions.md) aren't yet implemented, including *union member providers*. Those features will be coming in future previews. + +For more information, see [Union types](../language-reference/builtin-types/union.md) in the language reference or the [feature specification](~/_csharplang/proposals/unions.md).