Union Types in C# 15: insiemi chiusi di tipi con pattern matching esaustivo
Le union types sono finalmente arrivate in C#. Disponibili in anteprima a partire da .NET 11 Preview 2, C# 15 introduce la parola chiave union che consente di dichiarare un tipo che può contenere esattamente uno tra un insieme fisso di tipi, con conversioni implicite e pattern matching esaustivo garantito dal compilatore.
È una delle funzionalità più richieste dalla community da anni, e capire come funziona — e soprattutto perché è stata progettata in questo modo — fa la differenza tra usarla correttamente o incappare negli stessi antipattern che esistevano prima.
Il problema che le union types risolvono
Prima di C# 15, quando un metodo doveva restituire uno tra diversi tipi possibili, le opzioni disponibili erano tutte imperfette:
object: nessun vincolo sui tipi effettivamente memorizzati; il chiamante doveva gestire logica difensiva per valori inattesi.- Interfacce marcatore o classi base astratte: più sicure, ma non “chiuse” — chiunque poteva implementare l’interfaccia o derivare dalla classe base, quindi il compilatore non poteva mai considerare l’insieme completo dei tipi possibili.
- Librerie di terze parti come
OneOf: funzionali, ma senza supporto diretto del compilatore per l’esaustività.
Un caso tipico è il risultato di un’operazione che può restituire un valore di successo oppure un errore: si potrebbe usare object, un’eccezione, oppure un tipo result wrapper. Nessuna di queste opzioni è soddisfacente perché manca la garanzia statale che tutti i casi siano gestiti.
Le union types risolvono questi problemi dichiarando un insieme chiuso di “case types”: non devono essere correlati tra loro, nessun altro tipo può essere aggiunto, e il compilatore garantisce che le espressioni switch che gestiscono la union siano esaustive — senza aver bisogno di un ramo _ o default.
Sintassi di base
La dichiarazione è minimalista:
public record class Cat(string Name);
public record class Dog(string Name);
public record class Bird(string Name);
public union Pet(Cat, Dog, Bird);
Una sola riga dichiara Pet come un nuovo tipo le cui variabili possono contenere un Cat, un Dog o un Bird. Il compilatore fornisce conversioni implicite da ciascun case type:
Pet pet = new Dog("Rex");
Console.WriteLine(pet.Value); // Dog { Name = Rex }
Pet pet2 = new Cat("Whiskers");
Console.WriteLine(pet2.Value); // Cat { Name = Whiskers }
Il compilatore emette un errore se si cerca di assegnare un tipo che non fa parte dei case types. Questa è la garanzia fondamentale: l’insieme è veramente chiuso a livello di compilazione.
Pattern matching esaustivo
Quando si usa un’istanza di un tipo union non nulla, il compilatore conosce l’insieme completo dei case types, quindi un’espressione switch che li copre tutti è esaustiva — senza bisogno del _ finale:
string name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Questo è il vantaggio principale: se in futuro si aggiunge un quarto case type a Pet, ogni espressione switch che non lo gestisce produce un avviso del compilatore. I casi mancanti vengono rilevati in fase di compilazione, non a runtime.I pattern si applicano alla proprietà Value della union, non alla union struct stessa. Questo “unwrapping” è automatico — si scrive Dog d e il compilatore verifica Value internamente. Le eccezioni sono var e _, che si applicano al valore della union stessa.
Per gestire il valore di default (null):
Pet pet = default;
var description = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "nessun animale",
};
// description è "nessun animale"
Union con corpo e metodi helper
È possibile aggiungere membri helper alla union tramite un corpo, proprio come per qualsiasi altra dichiarazione di tipo. Un esempio pratico è OneOrMore<T>, utile per API che accettano sia un singolo elemento che una collezione:
public union OneOrMore<T>(T, IEnumerable<T>)
{
public IEnumerable<T> AsEnumerable() => Value switch
{
T single => [single],
IEnumerable<T> multiple => multiple,
null => []
};
}
I chiamanti passano la forma che preferiscono, e AsEnumerable() normalizza il risultato:
OneOrMore<string> tags = "dotnet";
OneOrMore<string> moreTags = new[] { "csharp", "unions", "preview" };
foreach (var tag in tags.AsEnumerable())
Console.Write($"[{tag}] ");
// [dotnet]
foreach (var tag in moreTags.AsEnumerable())
Console.Write($"[{tag}] ");
// [csharp] [unions] [preview]
Si noti che AsEnumerable gestisce esplicitamente il caso null: lo stato null predefinito della proprietà Value è maybe-null, quindi il compilatore richiede la gestione di questo caso per garantire la correttezza.
Compatibilità con librerie esistenti e scenari avanzati
Per le librerie che già forniscono tipi union-like con proprie strategie di storage (come quelle basate su OneOf), C# 15 prevede un meccanismo di compatibilità: qualsiasi classe o struct con l’attributo [System.Runtime.CompilerServices.Union] viene riconosciuta come tipo union dal compilatore, purché segua il pattern base — costruttori pubblici a parametro singolo e proprietà Value pubblica.
Per scenari ad alte prestazioni dove i case types includono tipi valore, le librerie possono implementare il pattern di accesso non-boxing aggiungendo una proprietà HasValue e metodi TryGetValue. Il tipo union generato dal compilatore usa object? internamente e quindi fa boxing dei tipi valore — per hot path critici conviene valutare i custom union types.
Come provare le union types oggi
Le union types sono disponibili a partire da .NET 11 Preview 2. I passaggi per iniziare sono:
- Installare il .NET 11 Preview SDK
- Creare o aggiornare un progetto che punta a
net11.0 - Impostare
<LangVersion>preview</LangVersion> nel file di progetto
Poiché UnionAttribute e IUnion non sono ancora inclusi nel runtime nel Preview 2, vanno dichiarati manualmente nel progetto:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,
AllowMultiple = false)]
public sealed class UnionAttribute : Attribute;
public interface IUnion
{
object? Value { get; }
}
}
Una volta aggiunti questi tipi, si possono dichiarare e usare normalmente le union types. Il supporto IDE in Visual Studio sarà disponibile nella prossima build Insiders; il C# DevKit Insiders lo include già.
Il quadro più ampio: la roadmap dell’esaustività
Le union types fanno parte di una strategia più ampia del team C# per portare la verifica dell’esaustività direttamente nel compilatore. Le proposte correlate attualmente in discussione sono:
- Closed hierarchies: il modificatore
closed su una classe impedisce la dichiarazione di classi derivate al di fuori dell’assembly di definizione, consentendo al compilatore di considerare esaustive le espressioni switch sulla gerarchia. - Closed enums: un
closed enum impedisce la creazione di valori diversi dai membri dichiarati, risolvendo il problema dei valori enum “numerici” inattesi.
Insieme, questi tre meccanismi danno a C# un percorso completo verso la verifica statica dell’esaustività: union types per insiemi chiusi di tipi, closed hierarchies per gerarchie sigillate, closed enums per insiemi fissi di valori.
Conclusione
Le union types in C# 15 non sono un semplice porting delle discriminated union di F#: sono state progettate come aggiunta nativa all’ecosistema C#, composte da tipi esistenti, integrate con il sistema di pattern matching già consolidato, e compatibili con le librerie union-like già diffuse. La garanzia di esaustività del compilatore è il beneficio più concreto: i casi mancanti diventano avvisi a tempo di compilazione, non bug a runtime.
La feature è in preview e il team accetta feedback attivamente su GitHub. Vale la pena esplorarla ora per contribuire alla forma definitiva della feature, prevista per la release di novembre 2026 con .NET 11.
Fonte: Explore union types in C# 15 – .NET Blog
C# 15 introduces union types — declare a closed set of case types with implicit conversions and exhaustive pattern matching. Try unions in preview today and see the broader exhaustiveness roadmap.
Bill Wagner (.NET Blog)
macfranc
in reply to Matthias • • •Matthias likes this.
Barbara Elers
in reply to Matthias • •Matthias
in reply to Barbara Elers • • •@Barbara Elers
Hier ein kleiner Blick auf die Mobile App.
Barbara Elers likes this.
Hamiller Friendica
in reply to Matthias • • •@Matthias Oh cool, die Idee horizontal durch die Kommentare zu wischen, ist mir noch gar nicht gekommen. Oder hab ich da was falsch interpretiert?
/cc @Barbara Elers
Barbara Elers
in reply to Hamiller Friendica • • •Matthias
in reply to Hamiller Friendica • • •@Hamiller Friendica
Genau so ist es. Das fühlt sich sehr angenehm an
@Barbara Elers
Hamiller Friendica likes this.
Barbara Elers
in reply to Matthias • •Dieter Fröhling likes this.
Matthias
in reply to Barbara Elers • — (52.2647687 10.5236133) • •@Barbara Elers
Du hast bereits einen Account. Gib die URL
loma.mlfür einen bestehenden Account an. Danach kannst du dich mit deinen Daten anmelden.Barbara Elers
in reply to Matthias • •Matthias
in reply to Barbara Elers • • •Wenn du schon angemeldet warst, dann hast du jetzt dein Konto mit Raccoon neu verbunden.
like this
Barbara Elers e Dieter Fröhling like this.
Barbara Elers
in reply to Matthias • • •Matthias
in reply to Barbara Elers • • •Ja, mach in Ruhe.
Barbara Elers likes this.
Matthias
in reply to Barbara Elers • • •@Barbara Elers
Jetzt verstehe ich erst ;) Wir haben die Registrierung geschlossen. Daher werden wir nicht mehr in der Liste der Instanzen angezeigt.
Um dich mit dem bestehenden Account zu verbinden, klickst du die Auswahl "Sonstiges" an. Jetzt hast du ein frei beschreibbares Feld. Dort gibst du
loma.mlein. Anschließend noch Login und Passwort eingeben und du bist mit der App verbunden.like this
Dieter Fröhling e Barbara Elers like this.
Barbara Elers
in reply to Matthias • • •Ich habe das nochmal überlegt und diese raccoon app wieder deinstalliert.
VegOS
in reply to Matthias • • •Quite strange experience! With Fedilab on my tablet I get exactly the same public network timeline I see with the desktop browser frontend. Raccoon shows me a different one. Other order and posts, I do not see on my desktop. I block two instances on my desktop but this cannot make such a difference.
weserfeuer likes this.