Spcnet.it Notizie geek e OSS/Linux. Su Telegram: InfoSec ITA Notizie
Home > Articolo > C# 15 introduce le union: basta OneOf e Results<> per modellare i tipi (.NET 11 Preview 6)
C# 15 introduce le union: basta OneOf e Results<> per modellare i tipi (.NET 11 Preview 6)

Da anni gli sviluppatori C# aspettano un modo nativo per dire “questo valore è esattamente uno tra questi tipi, e nient’altro”, con il compilatore in grado di garantirlo. In assenza di un supporto di linguaggio, la community si è arrangiata con pattern come Results<T> di ASP.NET Core Minimal API o con librerie come OneOf. Funzionano, ma con un limite strutturale: se aggiungi un quarto caso possibile a una funzione che prima ne restituiva tre, nessuno ti avvisa che uno switch a valle non gestisce il nuovo tipo. Manca l’esaustività, che è poi il punto centrale delle discriminated union.

Con .NET 11 Preview 6 (rilasciata il 14 luglio 2026) e C# 15, questa lacuna si chiude: arriva la parola chiave union, insieme a un intero pacchetto di funzionalità correlate (conversioni implicite, pattern matching esaustivo, gestione della nullabilità) che rendono i union type un cittadino di prima classe del linguaggio.

La sintassi: dichiarare un union type

La forma più semplice è la union declaration, una sintassi compatta che genera automaticamente uno struct:

public union Pet(Cat, Dog, Bird);

Questa singola riga dice al compilatore che un Pet è esattamente un Cat, un Dog o un Bird, e nient’altro. Sotto il cofano, il compilatore genera uno struct che implementa l’interfaccia System.Runtime.CompilerServices.IUnion e conserva il valore effettivo in una proprietà Value di tipo object?.

I union generici sono immediatamente utili per modellare risultati di operazioni che possono fallire, un caso d’uso ricorrentissimo in codice di produzione:

public union Result<TSuccess, TError>(TSuccess, TError);

Result<User, Error> Register(string username)
{
    if (string.IsNullOrEmpty(username))
        return new Error("Username cannot be empty");

    return new User(username);
}

Da notare l’ultima riga: nessun wrapping esplicito, nessuna chiamata a un factory method. Il valore User o Error viene convertito implicitamente in Result<User, Error> grazie alle union conversion generate automaticamente per ogni case type.

Pattern matching esaustivo, finalmente

Il vero salto di qualità è nel matching. Se dimentichi un caso in uno switch su un union type, il compilatore te lo dice:

string Describe(Pet pet) => pet switch
{
    Dog d => d.Name,
    Cat c => c.Name,
    // CS8509: switch expression does not handle all
    // possible values of its input type (Bird)
};

Aggiungendo il caso mancante, l’avviso scompare, e non serve più il classico _ => throw new InvalidOperationException("unreachable") per zittire il compilatore:

string Describe(Pet pet) => pet switch
{
    Dog d => d.Name,
    Cat c => c.Name,
    Bird b => b.Name,
};

Il punto interessante è che questa esaustività è dinamica: se in futuro aggiungi un nuovo case type al union (per esempio un quarto animale), ogni switch esistente che non lo gestisce si illumina di un warning. Per chi ha inseguito bug di produzione causati da un case dimenticato in un evento di dominio, è un cambiamento non da poco.

I pattern si applicano direttamente al union, senza bisogno di accedere manualmente a .Value: il compilatore effettua l’unwrapping in automatico, anche negli if pattern classici:

if (pet is Dog d)
{
    Console.WriteLine(d.Name);
}

Gestione del null

Il valore di default di un union type ha Value == null, ed è possibile intercettarlo esplicitamente con il pattern null:

string Describe(Pet pet) => pet switch
{
    null => "nessun animale",
    Dog d => d.Name,
    Cat c => c.Name,
    Bird b => b.Name,
};

La necessità o meno del ramo null dipende dal fatto che il parametro o il campo union-typed sia nullable: l’analisi di nullabilità del compilatore guida correttamente questi casi, in coerenza con le annotazioni #nullable enable già in uso nella maggior parte delle codebase moderne.

Un caso pratico: modellare eventi di dominio

I union type diventano davvero interessanti quando li si combina con i record per il domain modeling, per esempio in un sistema di elaborazione ordini con eventi distinti:

public record OrderPlaced(Guid OrderId, decimal Total);
public record OrderShipped(Guid OrderId, string TrackingNumber);
public record OrderCancelled(Guid OrderId, string Reason);
public union OrderEvent(OrderPlaced, OrderShipped, OrderCancelled);

string Summarize(OrderEvent evt) => evt switch
{
    OrderPlaced p    => $"Ordine {p.OrderId} piazzato per {p.Total:C}",
    OrderShipped s   => $"Ordine {s.OrderId} spedito, tracking: {s.TrackingNumber}",
    OrderCancelled c => $"Ordine {c.OrderId} annullato: {c.Reason}",
};

Quando tra tre mesi qualcuno aggiungerà OrderRefunded al union, ogni handler che non lo gestisce lancerà un warning in fase di compilazione, invece di fallire silenziosamente in produzione. È il compilatore che fa da revisore di esaustività al posto tuo.

I union possono anche esporre metodi e proprietà calcolate (ma non campi d’istanza), utile per incapsulare logica di conversione:

public union Length(Meters, Feet)
{
    public double TotalMeters => this switch
    {
        Meters m => m.Value,
        Feet f   => f.Value * 0.3048,
        _        => throw new InvalidOperationException(),
    };
}

Union personalizzati con l’attributo [Union]

La parola chiave union genera sempre uno struct. Se per motivi di identità, ereditarietà o layout di memoria serve un tipo reference, è possibile costruire un union personalizzato applicando direttamente l’attributo System.Runtime.CompilerServices.UnionAttribute e implementando l’interfaccia IUnion:

[System.Runtime.CompilerServices.Union]
public class 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;
}

Nella grande maggioranza dei casi, però, la sintassi union compatta copre già ogni esigenza pratica: l’approccio con attributo è pensato per scenari con requisiti specifici che lo struct generato non può soddisfare.

Come provarlo oggi

I union type sono attualmente in preview. Per sperimentarli serve l’SDK .NET 11 Preview e questa configurazione nel file di progetto:

<PropertyGroup>
    <LangVersion>preview</LangVersion>
    <TargetFramework>net11.0</TargetFramework>
</PropertyGroup>

Vale la pena ricordare che si tratta di una feature specification ancora in evoluzione: alcuni dettagli di comportamento (per esempio le regole esatte su nullabilità di default o sulle conversioni sollevate/”lifted”) sono ancora oggetto di discussione nel repository dotnet/csharplang, quindi è normale osservare piccoli cambiamenti tra una preview e l’altra prima del rilascio finale.

Conclusioni

F# ha le discriminated union fin dalle origini, e la richiesta di un equivalente in C# (csharplang issue #113) è aperta da anni. Con l’arrivo dei union type, non ogni problema di modellazione richiede più una gerarchia di classi: per rappresentare “uno tra un insieme chiuso di tipi”, ora esiste un costrutto di linguaggio dedicato, con conversioni implicite, pattern matching esaustivo e un’ottima integrazione con i record esistenti. Per chi lavora ogni giorno con API che possono restituire risultati eterogenei, o con event sourcing e domain modeling, è una delle novità più concrete della prossima versione di C#.

Fonte: .NET Blog, C# language reference proposal: Unions e Maarten Balliauw.

Condividi: Twitter  |  Facebook  |  LinkedIn
Unisciti alla discussione

Questo è un blog del Fediverso: puoi trovare questo articolo ovunque con @blog@insicurezzadigitale.com e ogni commento/risposta apparirà qui sotto.

Se vuoi commentare su C# 15 introduce le union: basta OneOf e Results<> per modellare i tipi (.NET 11 Preview 6), utilizza la discussione sul Forum.

>> forum community