Uniões discriminadas
As uniões discriminadas fornecem suporte para valores que podem ser um dos vários casos nomeados, possivelmente cada um com diferentes valores e tipos. Elas são úteis para dados heterogêneos: dados que podem ter casos especiais, incluindo casos válidos e de erro, dados que variam no tipo de uma instância para outra e como alternativa para hierarquias de objetos pequenos. Além disso, as 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
As uniões discriminadas são semelhantes aos tipos de união em outras linguagens, mas há diferenças. Assim como acontece 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. Ele pode ser uma das várias opções distintas. No entanto, ao contrário das uniões nessas outras linguagens, 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 os objetos desse tipo podem 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 poderá ser um só valor de um tipo especificado ou uma tupla que agrega vários campos dos mesmos tipos ou de tipos diferentes. Você pode dar um nome a um campo individual, mas o nome é opcional, mesmo que outros campos do mesmo caso sejam nomeados.
A acessibilidade para uniões discriminadas usa public
como padrão.
Por exemplo, considere a declaração a seguir 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 Shape de união discriminada, que pode ter valores de um destes três casos: Rectangle, Circle e Prism. Cada caso tem um conjunto diferente de campos. O caso Rectangle tem dois campos nomeados, ambos do tipo float
, que têm os nomes largura e comprimento. O caso Circle tem apenas um campo nomeado, raio. O caso Prism tem três campos, dois dos quais (largura e altura) são campos nomeados. Os campos sem nome são chamados de 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 contar com a ordenação dos campos na declaração e apenas fornecer os valores para cada campo por vez. A chamada do construtor a rect
no código anterior usa os campos nomeados, mas a chamada do construtor a circ
usa a ordenação. Você pode combinar os campos ordenados e os campos nomeados, como na construção de prism
.
O tipo option
é uma união discriminada simples na biblioteca principal F#. O tipo option
é declarado desta maneira.
// 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 de tipo 'a
. O caso None
não tem nenhum valor associado. Portanto, 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 de letra minúscula, 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 caso também são usados em expressões de padrões correspondentes. Em uma expressão de padrões correspondentes, os identificadores são fornecidos para os valores associados aos casos individuais. Por exemplo, no código a seguir, x
é o identificador dado o 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 padrões correspondentes, você pode usar campos nomeados para especificar correspondências de união discriminada. Para o tipo Shape declarado anteriormente, use 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 da união. Caso deseje que o nome seja sempre qualificado com o nome da união, aplique o atributo RequireQualifiedAccess à definição de tipo de união.
Como desencapsular uniões discriminadas
Em F#, as uniões discriminadas costumam ser usadas na modelagem de domínio para encapsular um só tipo. É fácil extrair o valor subjacente por meio de padrões correspondentes também. Você não precisa usar uma expressão de correspondência para um só caso:
let ([UnionCaseIdentifier] [values]) = [UnionValue]
O exemplo a seguir demonstra este:
type ShaderProgram = | ShaderProgram of id:int
let someFunctionUsingShaderProgram shaderProgram =
let (ShaderProgram id) = shaderProgram
// Use the unwrapped value
...
Os padrões correspondentes também são permitidos diretamente em parâmetros de função, para que você possa desencapsular um só caso neles:
let someFunctionUsingShaderProgram (ShaderProgram id) =
// Use the unwrapped value
...
Uniões discriminadas como structs
Você também pode representar uniões discriminadas como structs. Isso é feito com o atributo [<Struct>]
.
[<Struct>]
type SingleCase = Case of string
[<Struct>]
type Multicase =
| Case1 of Case1 : string
| Case2 of Case2 : int
| Case3 of Case3 : 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:
- Eles são copiados como tipos de valor e têm uma semântica de tipo de valor.
- Não é possível usar uma definição de tipo recursivo com uma união discriminada como struct de multicaso.
- Você precisa fornecer nomes de casos exclusivos para uma união discriminada como struct de multicaso.
Como usar uniões discriminadas em vez de hierarquias de objetos
Muitas vezes, você pode usar uma união discriminada como uma alternativa mais simples a uma hierarquia de objetos pequenos. Por exemplo, a união discriminada a seguir pode ser usada em vez de uma classe base Shape
que tenha tipos derivados para círculo, quadrado etc.
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 um perímetro, como você usará em uma implementação orientada a objeto, você poderá usar padrões correspondentes para criar ramificações para as fórmulas apropriadas a fim de 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 é da seguinte maneira:
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
Como usar uniões discriminadas para estruturas de dados de árvore
As uniões discriminadas podem ser recursivas, o que significa que a própria união pode ser incluída no tipo de um ou mais casos. As uniões discriminadas recursivas podem ser usadas para criar estruturas de á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 subárvores esquerda e direita, e Tip
, que encerra 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
.
As uniões discriminadas funcionam bem se os nós na árvore são 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 dá suporte à 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
), em que 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.
Membros
É possível definir membros em uniões discriminadas. O seguinte exemplo 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}"
Atributos comuns
Os seguintes atributos são comumente vistos em uniões discriminadas:
[<RequireQualifiedAccess>]
[<NoEquality>]
[<NoComparison>]
[<Struct>]