Condividi tramite


Unioni discriminanti

Le unioni discriminate forniscono supporto per valori che possono essere uno dei diversi casi nominati, ciascuno possibilmente con valori e tipi diversi. Le unioni discriminate sono utili per dati eterogenei; dati che possono avere casi speciali, inclusi casi validi e di errore; dati che variano in base al tipo da un'istanza a un'altra; e come alternativa alle gerarchie di oggetti di piccole dimensioni. Inoltre, le unioni discriminate ricorsive vengono usate per rappresentare strutture di dati ad albero.

Sintassi

[ attributes ]
type [accessibility-modifier] type-name =
    | case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]
    | case-identifier2 [of [fieldname3 : ]type3 [ * [ fieldname4 : ]type4 ...]

    [ member-list ]

Osservazioni

Le unioni discriminate sono simili ai tipi di unione in altre lingue, ma esistono differenze. Come per un tipo union in C++ o un tipo variant in Visual Basic, i dati memorizzati nel valore non sono fissi; può essere una tra diverse opzioni distinte. A differenza delle unioni in queste altre lingue, tuttavia, a ognuna delle possibili opzioni viene assegnato un identificatore di case . Gli identificatori di caso sono nomi per i vari possibili tipi di valori che gli oggetti di questo tipo potrebbero avere; i valori sono facoltativi. Se i valori non sono presenti, il caso è equivalente a un caso di enumerazione. Se sono presenti valori, ogni valore può essere un singolo valore di un tipo specificato o una tupla che aggrega più campi dello stesso tipo o di tipi diversi. È possibile assegnare un nome a un singolo campo, ma il nome è facoltativo, anche se vengono denominati altri campi nello stesso caso.

Per impostazione predefinita, l'accessibilità per le unioni discriminate è public.

Ad esempio, consideriamo la seguente dichiarazione di un tipo Shape.

type Shape =
    | Rectangle of width : float * length : float
    | Circle of radius : float
    | Prism of width : float * float * height : float

Il codice precedente dichiara una forma di unione discriminata, che può avere valori di uno qualsiasi dei tre casi: Rectangle, Circle e Prism. Ogni caso ha un set di campi diverso. Il caso Rectangle ha due campi denominati, entrambi di tipo float, che si chiamano larghezza e lunghezza. La classe Circle ha un solo campo denominato raggio. Il caso Prism include tre campi, due dei quali (larghezza e altezza) sono campi denominati. I campi senza nome vengono definiti campi anonimi.

Per creare oggetti, specificare i valori per i campi denominati e anonimi in base agli esempi seguenti.

let rect = Rectangle(length = 1.3, width = 10.0)
let circ = Circle (1.0)
let prism = Prism(5., 2.0, height = 3.0)

Questo codice mostra che è possibile usare i campi denominati nell'inizializzazione oppure fare affidamento sull'ordinamento dei campi nella dichiarazione e specificare solo i valori per ogni campo a sua volta. La chiamata del costruttore per rect nel codice precedente usa i campi denominati, ma la chiamata del costruttore per circ usa l'ordinamento. È possibile combinare i campi ordinati e i campi denominati, come nella costruzione di prism.

Il tipo option è una semplice unione discriminata nella libreria principale F#. Il tipo option viene dichiarato come segue.

// The option type is a discriminated union.
type Option<'a> =
    | Some of 'a
    | None

Il codice precedente specifica che il tipo Option è un'unione discriminata che ha due casi, Some e None. Il caso Some ha un valore associato costituito da un campo anonimo il cui tipo è rappresentato dal parametro di tipo 'a. Il case None non ha alcun valore associato. Di conseguenza, il tipo option specifica un tipo generico che può avere un valore di un tipo o non avere alcun valore. Il tipo Option ha anche un alias di tipo minuscolo, option, più comunemente usato.

Gli identificatori di caso possono essere usati come costruttori per il tipo di unione discriminata. Ad esempio, il codice seguente viene usato per creare valori del tipo di option.

let myOption1 = Some(10.0)
let myOption2 = Some("string")
let myOption3 = None

Gli identificatori di casi vengono utilizzati anche nelle espressioni di confronto di modelli. In un'espressione di corrispondenza di modelli vengono forniti identificatori per i valori associati ai singoli casi. Nel codice seguente, ad esempio, x è l'identificatore assegnato al valore associato al caso Some del tipo option.

let printValue opt =
    match opt with
    | Some x -> printfn "%A" x
    | None -> printfn "No value."

Nelle espressioni di pattern matching, è possibile usare campi denominati per specificare corrispondenze di unione discriminata. Per il tipo shape dichiarato in precedenza, è possibile utilizzare i campi denominati come illustrato nel codice seguente per estrarre i valori dei campi.

let getShapeWidth shape =
    match shape with
    | Rectangle(width = w) -> w
    | Circle(radius = r) -> 2. * r
    | Prism(width = w) -> w

In genere, gli identificatori di maiuscole e minuscole possono essere usati senza qualificarli con il nome dell'unione. Se si desidera che il nome sia sempre qualificato con il nome dell'unione, è possibile applicare l'attributo RequireQualifiedAccess alla definizione del tipo di unione.

Rimozione del wrapping delle unioni discriminate

Nelle Unioni Discriminate in F# vengono spesso usate nella modellazione dei domini per incapsulare un singolo tipo. È facile estrarre il valore sottostante anche tramite corrispondenza di pattern. Non è necessario usare un'espressione di corrispondenza per un singolo caso:

let ([UnionCaseIdentifier] [values]) = [UnionValue]

Nell'esempio seguente viene illustrato quanto segue:

type ShaderProgram = | ShaderProgram of id:int

let someFunctionUsingShaderProgram shaderProgram =
    let (ShaderProgram id) = shaderProgram
    // Use the unwrapped value
    ...

La corrispondenza di modelli è consentita anche direttamente nei parametri della funzione, quindi è possibile decomprimere un singolo caso:

let someFunctionUsingShaderProgram (ShaderProgram id) =
    // Use the unwrapped value
    ...

Strutture a unioni discriminate

È anche possibile rappresentare unioni discriminate come struct. Questa operazione viene eseguita con l'attributo [<Struct>].

[<Struct>]
type SingleCase = Case of string

[<Struct>]
type Multicase =
    | Case1 of string
    | Case2 of int
    | Case3 of double

Poiché si tratta di tipi valore e non di tipi riferimento, esistono considerazioni aggiuntive rispetto alle unioni discriminate di riferimento:

  1. Vengono copiati come tipi valore e hanno semantica del tipo di valore.
  2. Non puoi utilizzare una definizione di tipo ricorsivo con un'unione discriminante di struct multicase.

Prima di F# 9, per ogni caso era necessario specificare un nome caso univoco (all'interno dell'unione). A partire da F# 9, la limitazione viene revocata.

Uso di unioni discriminate anziché gerarchie di oggetti

È spesso possibile usare un'unione discriminata come alternativa più semplice a una piccola gerarchia di oggetti. Ad esempio, l'unione discriminata seguente può essere usata invece di una classe base Shape con tipi derivati per cerchio, quadrato e così via.

type Shape =
    // The value here is the radius.
    | Circle of float
    // The value here is the side length.
    | EquilateralTriangle of double
    // The value here is the side length.
    | Square of double
    // The values here are the height and width.
    | Rectangle of double * double

Anziché un metodo virtuale per calcolare un'area o un perimetro, come si farebbe in un'implementazione orientata agli oggetti, è possibile usare criteri di ricerca per diramare le formule appropriate per calcolare queste quantità. Nell'esempio seguente vengono usate formule diverse per calcolare l'area, a seconda della forma.

let pi = 3.141592654

let area myShape =
    match myShape with
    | Circle radius -> pi * radius * radius
    | EquilateralTriangle s -> (sqrt 3.0) / 4.0 * s * s
    | Square s -> s * s
    | Rectangle(h, w) -> h * w

let radius = 15.0
let myCircle = Circle(radius)
printfn "Area of circle that has radius %f: %f" radius (area myCircle)

let squareSide = 10.0
let mySquare = Square(squareSide)
printfn "Area of square that has side %f: %f" squareSide (area mySquare)

let height, width = 5.0, 10.0
let myRectangle = Rectangle(height, width)
printfn "Area of rectangle that has height %f and width %f is %f" height width (area myRectangle)

L'output è il seguente:

Area of circle that has radius 15.000000: 706.858347
Area of square that has side 10.000000: 100.000000
Area of rectangle that has height 5.000000 and width 10.000000 is 50.000000

Uso di unioni discriminate per strutture di dati ad albero

Le unioni discriminate possono essere ricorsive, vale a dire che l'unione stessa può essere inclusa nel tipo di uno o più casi. Le unioni discriminate ricorsive possono essere usate per creare strutture ad albero usate per modellare le espressioni nei linguaggi di programmazione. Nel codice seguente viene usata un'unione discriminante ricorsiva per creare una struttura di dati di albero binario. L'unione è costituita da due casi: Node, ovvero un nodo con un valore intero e i sottoalberi sinistro e destro, e Tip, che termina l'albero.

type Tree =
    | Tip
    | Node of int * Tree * Tree

let rec sumTree tree =
    match tree with
    | Tip -> 0
    | Node(value, left, right) -> value + sumTree (left) + sumTree (right)

let myTree =
    Node(0, Node(1, Node(2, Tip, Tip), Node(3, Tip, Tip)), Node(4, Tip, Tip))

let resultSumTree = sumTree myTree

Nel codice precedente resultSumTree ha il valore 10. Nella figura seguente viene illustrata la struttura ad albero per myTree.

Diagramma che mostra la struttura ad albero per myTree.

Le unioni discriminate funzionano bene se i nodi nell'albero sono eterogenei. Nel codice seguente il tipo Expression rappresenta l'albero della sintassi astratta di un'espressione in un semplice linguaggio di programmazione che supporta l'aggiunta e la moltiplicazione di numeri e variabili. Alcuni casi di unione non sono ricorsivi e rappresentano numeri (Number) o variabili (Variable). Altri casi sono ricorsivi e rappresentano operazioni (Add e Multiply), dove gli operandi sono anche espressioni. La funzione Evaluate usa un'espressione di corrispondenza per elaborare in modo ricorsivo l'albero della sintassi.

type Expression =
    | Number of int
    | Add of Expression * Expression
    | Multiply of Expression * Expression
    | Variable of string

let rec Evaluate (env: Map<string, int>) exp =
    match exp with
    | Number n -> n
    | Add(x, y) -> Evaluate env x + Evaluate env y
    | Multiply(x, y) -> Evaluate env x * Evaluate env y
    | Variable id -> env[id]

let environment = Map [ "a", 1; "b", 2; "c", 3 ]

// Create an expression tree that represents
// the expression: a + 2 * b.
let expressionTree1 = Add(Variable "a", Multiply(Number 2, Variable "b"))

// Evaluate the expression a + 2 * b, given the
// table of values for the variables.
let result = Evaluate environment expressionTree1

Quando questo codice viene eseguito, il valore di result è 5.

Membri

È possibile definire i membri delle unioni discriminate. L'esempio seguente illustra come definire una proprietà e implementare un'interfaccia:

open System

type IPrintable =
    abstract Print: unit -> unit

type Shape =
    | Circle of float
    | EquilateralTriangle of float
    | Square of float
    | Rectangle of float * float

    member this.Area =
        match this with
        | Circle r -> Math.PI * (r ** 2.0)
        | EquilateralTriangle s -> s * s * sqrt 3.0 / 4.0
        | Square s -> s * s
        | Rectangle(l, w) -> l * w

    interface IPrintable with
        member this.Print () =
            match this with
            | Circle r -> printfn $"Circle with radius %f{r}"
            | EquilateralTriangle s -> printfn $"Equilateral Triangle of side %f{s}"
            | Square s -> printfn $"Square with side %f{s}"
            | Rectangle(l, w) -> printfn $"Rectangle with length %f{l} and width %f{w}"

.Is* proprietà nei casi

A partire da F# 9, le unioni discriminate esporranno proprietà .Is* generate automaticamente per ogni caso, consentendo di verificare se un valore è di un determinato caso.

Questo è il modo in cui può essere usato:

type Contact =
    | Email of address: string
    | Phone of countryCode: int * number: string

type Person = { name: string; contact: Contact }

let canSendEmailTo person =
    person.contact.IsEmail      // .IsEmail is auto-generated

Attributi comuni

Gli attributi seguenti sono comunemente visualizzati nelle unioni discriminate:

  • [<RequireQualifiedAccess>]
  • [<NoEquality>]
  • [<NoComparison>]
  • [<Struct>]

Vedere anche