Partilhar via


Sindicatos discriminados

As uniões discriminadas fornecem suporte para valores que podem ser um de vários casos nomeados, possivelmente cada um com valores e tipos diferentes. Uniões discriminadas são úteis para dados heterogêneos; dados que podem ter casos especiais, incluindo casos válidos e de erro; dados que variam em tipo de uma instância para outra; e como alternativa para hierarquias de pequenos objetos. Além disso, uniões discriminadas recursivas são usadas para representar estruturas de dados de árvore.

Sintaxe

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

    [ member-list ]

Comentários

Os sindicatos discriminados são semelhantes aos tipos de sindicatos noutras línguas, mas existem diferenças. Como com um tipo de união em C++ ou um tipo de variante no Visual Basic, os dados armazenados no valor não são fixos; pode ser uma das várias opções distintas. Ao contrário das uniões nestas outras línguas, no entanto, cada uma das opções possíveis recebe um identificador de caso . Os identificadores de caso são nomes para os vários tipos possíveis de valores que objetos desse tipo poderiam ser; os valores são opcionais. Se os valores não estiverem presentes, o caso será equivalente a um caso de enumeração. Se os valores estiverem presentes, cada valor pode ser um único valor de um tipo especificado ou uma tupla que agrega vários campos do mesmo tipo ou de tipos diferentes. Você pode dar um nome a um campo individual, mas o nome é opcional, mesmo que outros campos no mesmo caso sejam nomeados.

Acessibilidade para sindicatos discriminados define-se para public.

Por exemplo, considere a seguinte declaração de um tipo Shape.

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

O código anterior declara uma Forma de união discriminada, que pode ter valores de qualquer um dos três casos: Retângulo, Círculo e Prisma. Cada caso tem um conjunto diferente de campos. A caixa do retângulo tem dois campos nomeados, ambos do tipo float, que têm os nomes largura e comprimento. O caso Circle tem apenas um campo nomeado, radius. O caso Prism tem três campos, dois dos quais (largura e altura) são campos nomeados. Os campos sem nome são referidos como campos anónimos.

Você constrói objetos fornecendo valores para os campos nomeados e anônimos de acordo com os exemplos a seguir.

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

Esse código mostra que você pode usar os campos nomeados na inicialização ou pode confiar na ordenação dos campos na declaração e apenas fornecer os valores para cada campo por vez. A chamada do construtor para rect no código anterior usa os campos nomeados, mas a chamada do construtor para circ usa a ordem dos campos. Você pode misturar os campos ordenados e campos nomeados, como na construção de prism.

O tipo option é uma união discriminada simples na biblioteca principal do F#. O tipo option é declarado da seguinte forma.

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

O código anterior especifica que o tipo Option é uma união discriminada que tem dois casos, Some e None. O caso Some tem um valor associado que consiste em um campo anônimo cujo tipo é representado pelo parâmetro type 'a. O caso None não tem valor associado. Assim, o tipo option especifica um tipo genérico que tem um valor de algum tipo ou nenhum valor. O tipo Option também tem um alias de tipo minúsculo, option, que é mais comumente usado.

Os identificadores de caso podem ser usados como construtores para o tipo de união discriminada. Por exemplo, o código a seguir é usado para criar valores do tipo option.

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

Os identificadores de casos também são usados em expressões de correspondência de padrões. Em uma expressão de correspondência de padrão, identificadores são fornecidos para os valores associados aos casos individuais. Por exemplo, no código a seguir, x é o identificador dado ao valor associado ao caso Some do tipo option.

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

Em expressões de correspondência de padrões, você pode usar campos nomeados para especificar correspondências de união discriminadas. Para o tipo de Forma declarado anteriormente, você pode usar os campos nomeados como mostra o código a seguir para extrair os valores dos campos.

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

Normalmente, os identificadores de caso podem ser usados sem qualificá-los com o nome do sindicato. Se desejar que o nome seja sempre qualificado com o nome da união, você pode aplicar o atributo RequireQualifiedAccess à definição de tipo de união.

Desembrulhar sindicatos discriminados

Em F#, as Uniões Discriminadas são frequentemente usadas na modelagem de domínio para encapsular um único tipo. Também é fácil extrair o valor subjacente por meio da correspondência de padrões. Não é necessário usar uma expressão de correspondência para um único caso:

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

O exemplo a seguir demonstra isso:

type ShaderProgram = | ShaderProgram of id:int

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

A correspondência de padrões também é permitida diretamente nos parâmetros de função, permitindo desembrulhar um único caso diretamente:

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

Estruturar sindicatos discriminados

Você também pode representar Uniões Discriminadas como estruturas. Isso é feito com o atributo [<Struct>].

[<Struct>]
type SingleCase = Case of string

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

Como esses são tipos de valor e não tipos de referência, há considerações extras em comparação com uniões discriminadas de referência:

  1. Eles são copiados como tipos de valor e possuem semântica de tipo de valor.
  2. Não é possível usar uma definição de tipo recursiva com uma união discriminada por estrutura de casos múltiplos.

Antes do F# 9, havia um requisito para cada caso especificar um nome de caso exclusivo (dentro da união). A partir do F# 9, a limitação é levantada.

Usando uniões discriminadas em vez de hierarquias de objetos

Muitas vezes, você pode usar uma união discriminada como uma alternativa mais simples para uma hierarquia de objetos pequenos. Por exemplo, a seguinte união discriminada poderia ser usada em vez de uma classe base Shape que tenha tipos derivados para círculo, quadrado e assim por diante.

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

Em vez de um método virtual para calcular uma área ou perímetro, como você usaria em uma implementação orientada a objetos, você pode usar a correspondência de padrões para ramificar fórmulas apropriadas para calcular essas quantidades. No exemplo a seguir, fórmulas diferentes são usadas para calcular a área, dependendo da 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)

A saída é a seguinte:

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

Usando uniões discriminadas para estruturas de dados de árvore

Os sindicatos discriminados podem ser recursivos, o que significa que o próprio sindicato pode ser incluído no tipo de um ou mais casos. Uniões discriminadas recursivas podem ser usadas para criar estruturas em árvore, que são usadas para modelar expressões em linguagens de programação. No código a seguir, uma união discriminada recursiva é usada para criar uma estrutura de dados de árvore binária. A união consiste em dois casos: Node, que é um nó com um valor inteiro e possui subárvores à esquerda e à direita, e Tip, que termina a árvore.

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

No código anterior, resultSumTree tem o valor 10. A ilustração a seguir mostra a estrutura da árvore para myTree.

Diagrama que mostra a estrutura da árvore para myTree.

Uniões discriminadas funcionam bem se os nós na árvore forem heterogêneos. No código a seguir, o tipo Expression representa a árvore de sintaxe abstrata de uma expressão em uma linguagem de programação simples que suporta a adição e multiplicação de números e variáveis. Alguns dos casos de união não são recursivos e representam números (Number) ou variáveis (Variable). Outros casos são recursivos e representam operações (Add e Multiply), onde os operandos também são expressões. A função Evaluate usa uma expressão de correspondência para processar recursivamente a árvore de sintaxe.

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 esse código é executado, o valor de result é 5.

Uniões Discriminadas Mutuamente Recursivas

As uniões discriminadas em F# podem ser mutuamente recursivas, o que significa que vários tipos de união podem fazer referência uns aos outros de forma recursiva. Isso é útil ao modelar estruturas hierárquicas ou interconectadas. Para definir uniões discriminadas mutuamente recursivas, use a palavra-chave and.

Por exemplo, considere uma representação de árvore de sintaxe abstrata (AST) onde as expressões podem incluir instruções e as instruções podem conter expressões:

type Expression =
    | Literal of int
    | Variable of string
    | Operation of string * Expression * Expression
and Statement =
    | Assign of string * Expression
    | Sequence of Statement list
    | IfElse of Expression * Statement * Statement

Membros

É possível definir membros em sindicatos discriminados. O exemplo a seguir mostra como definir uma propriedade e implementar uma interface:

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* propriedades sobre casos

Desde o F# 9, as uniões discriminadas expõem propriedades de .Is* geradas automaticamente para cada caso, permitindo que você verifique se um valor é de um caso específico.

É assim que pode ser utilizado:

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

Atributos comuns

Os seguintes atributos são comumente vistos em sindicatos discriminados:

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

Ver também