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

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

Синтаксис

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

    [ member-list ]

Замечания

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

Доступность для дискриминированных профсоюзов publicпо умолчанию.

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

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

Предыдущий код объявляет дискриминированную фигуру объединения, которая может иметь значения любого из трех вариантов: прямоугольник, круг и prism. В каждом случае имеется другой набор полей. Прямоугольник имеет два именованных поля, оба типа float, имеющие ширину и длину имен. В случае "Круг" есть только одно именованное поле, радиус. В регистре 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 является дискриминированным объединением, в котором есть два случая, Some и None. В 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>]

См. также