Размеченные объединения

Различаемые профсоюзы обеспечивают поддержку значений, которые могут быть одним из нескольких именованных случаев, возможно, с разными значениями и типами. Различаемые объединения полезны для разнородных данных; данные, которые могут иметь особые случаи, включая допустимые и ошибочные случаи; данные, которые различаются в типе от одного экземпляра к другому; и в качестве альтернативы для небольших иерархий объектов. Кроме того, рекурсивные различаемые объединения используются для представления структур данных дерева.

Синтаксис

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

    [ member-list ]

Remarks

Различаемые профсоюзы похожи на типы профсоюзов на других языках, но существуют различия. Как и в случае с типом объединения в C++ или типом варианта в Visual Basic, данные, хранящиеся в значении, не фиксируются; это может быть один из нескольких различных вариантов. В отличие от профсоюзов на этих других языках, однако каждому из возможных вариантов присваивается идентификатор дела. Идентификаторы регистра — это имена для различных возможных типов значений, которые могут быть объектами этого типа; значения являются необязательными. Если значения отсутствуют, регистр эквивалентен регистру перечисления. Если значения присутствуют, каждое значение может быть либо одним значением указанного типа, либо кортежем, который объединяет несколько полей одного или разных типов. Можно присвоить отдельному полю имя, но имя является необязательным, даже если другие поля в том же случае именуются.

Специальные возможности для различаемых профсоюзов по умолчанию.public

Например, рассмотрим следующее объявление типа фигуры.

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

В приведенном выше коде объявляется различающаяся фигура объединения, которая может иметь значения любого из трех вариантов: прямоугольник, круг и призма. Каждый вариант имеет разные наборы полей. В регистре прямоугольника есть два именованных поля( оба типа float, которые имеют ширину и длину имен). Вариант Circle имеет только одно именованное поле, радиус. В регистре Prism есть три поля, два из которых (ширина и высота) являются именованными полями. Неименованные поля называются анонимными полями.

Объекты создаются путем предоставления значений именованных и анонимных полей в соответствии со следующими примерами.

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

В этом коде показано, что можно использовать именованные поля в инициализации или полагаться на упорядочение полей в объявлении и просто указать значения для каждого поля в свою очередь. Вызов конструктора rect в предыдущем коде использует именованные поля, но вызов конструктора использует circ упорядочение. Вы можете смешивать упорядоченные поля и именованные поля, как при построении prism.

Тип option — это простое различаемое объединение в основной библиотеке F#. Тип option объявлен следующим образом.

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

Предыдущий код указывает, что тип Option является дискриминированным объединением, которое имеет два варианта и SomeNone. Регистр Some имеет связанное значение, которое состоит из одного анонимного поля, тип которого представлен параметром 'aтипа. Регистр None не имеет связанного значения. Таким образом, option этот тип задает универсальный тип, имеющий значение какого-либо типа или не имеющий значения. Option Тип также имеет псевдоним нижнего регистра, optionкоторый чаще всего используется.

Идентификаторы регистра можно использовать в качестве конструкторов для различаемого типа объединения. Например, следующий код используется для создания значений option типа.

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

Идентификаторы регистра также используются в выражениях сопоставления шаблонов. В выражении сопоставления шаблонов идентификаторы предоставляются для значений, связанных с отдельными вариантами. Например, в следующем коде указан идентификатор, x заданный значением, связанным с Some вариантом option типа.

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

В выражениях сопоставления шаблонов можно использовать именованные поля для указания совпадений с дискриминированным объединением. Для типа фигуры, объявленного ранее, можно использовать именованные поля, как показано в следующем коде, чтобы извлечь значения полей.

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

Как правило, идентификаторы регистра можно использовать без их квалификации с именем объединения. Если вы хотите, чтобы имя всегда было квалифицировано с именем объединения, можно применить атрибут RequireQualifiedAccess к определению типа объединения.

Распакуние дискриминированных профсоюзов

В F# дискриминированные объединения часто используются в моделировании предметной области для упаковки одного типа. Кроме того, можно легко извлечь базовое значение с помощью сопоставления шаблонов. Не нужно использовать выражение соответствия для одного случая:

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

Следующий пример демонстрирует это:

type ShaderProgram = | ShaderProgram of id:int

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

Сопоставление шаблонов также разрешено напрямую в параметрах функции, поэтому вы можете распаковывать один регистр там:

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

Структуры дискриминированных профсоюзов

Вы также можете представлять дискриминированные профсоюзы в виде структур. Это делается с помощью атрибута [<Struct>] .

[<Struct>]
type SingleCase = Case of string

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

Так как это типы значений, а не ссылочные типы, существуют дополнительные рекомендации по сравнению со ссылочными различаемых объединениями:

  1. Они копируются как типы значений и имеют семантику типа значения.
  2. Определение рекурсивного типа нельзя использовать с многофакторной структурой, дискриминируемой объединением.
  3. Необходимо указать уникальные имена вариантов для многофакторной структуры дискриминированного союза.

Использование дискриминированных союзов вместо иерархий объектов

Часто можно использовать различаемое объединение в качестве более простой альтернативы небольшой иерархии объектов. Например, вместо базового класса, имеющего производные типы для круга, квадрата и т. д., можно использовать следующее различающееся Shape объединение.

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

Вместо виртуального метода для вычисления области или периметра, как и в объектно-ориентированной реализации, можно использовать сопоставление шаблонов с ветвью для соответствующих формул для вычисления этих объемов. В следующем примере для вычисления области используются различные формулы в зависимости от фигуры.

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)

Вывод выглядит следующим образом.

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

Использование различаемых союзов для структур данных дерева

Дискриминированные профсоюзы могут быть рекурсивными, то есть само объединение может быть включено в тип одного или нескольких случаев. Рекурсивные различаемые объединения можно использовать для создания структур дерева, которые используются для моделирования выражений на языках программирования. В следующем коде рекурсивное объединение дискриминированных данных используется для создания структуры данных двоичного дерева. Объединение состоит из двух вариантов, Nodeкоторый является узлом с целочисленным значением и левым и правым поддеревами, а Tipтакже завершает дерево.

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

В предыдущем коде resultSumTree имеет значение 10. На следующем рисунке показана структура дерева для myTree.

Diagram that shows the tree structure for myTree.

Различаемые профсоюзы хорошо работают, если узлы в дереве разнородны. В следующем коде тип Expression представляет абстрактное дерево синтаксиса выражения на простом языке программирования, который поддерживает добавление и умножение чисел и переменных. Некоторые варианты объединения не рекурсивны и представляют числа (Number) или переменные (Variable). Другие случаи рекурсивны и представляют операции (Add и Multiply), где операнды также являются выражениями. Функция Evaluate использует выражение соответствия для рекурсивной обработки дерева синтаксиса.

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

При выполнении этого кода значение result равно 5.

Элементы

Можно определить членов по различаемой профсоюзам. В следующем примере показано, как определить свойство и реализовать интерфейс:

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}"

Общие атрибуты

В различаемых профсоюзах часто встречаются следующие атрибуты:

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

См. также