Unions discriminatoires
Les unions discriminées prennent en charge les valeurs qui peuvent faire partie d'un certain nombre de cas nommés, chacun pouvant avoir des valeurs et des types différents. Les unions discriminatoires sont utiles pour les données hétérogènes ; données qui peuvent avoir des cas spéciaux, y compris des cas d’erreur et valides ; données qui varient en type d’une instance à une autre ; et comme alternative pour les hiérarchies de petits objets. En outre, les unions discriminatoires récursives sont utilisées pour représenter des structures de données d’arborescence.
Syntaxe
[ attributes ]
type [accessibility-modifier] type-name =
| case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]
| case-identifier2 [of [fieldname3 : ]type3 [ * [ fieldname4 : ]type4 ...]
[ member-list ]
Remarques
Les syndicats discriminatoires sont similaires aux types d’union dans d’autres langues, mais il existe des différences. Comme avec un type union en C++ ou un type de variante en Visual Basic, les données stockées dans la valeur ne sont pas fixes ; il peut s’agir de l’une des options distinctes. Contrairement aux unions dans ces autres langues, chaque option possible, toutefois, se voit attribuer un identificateur de cas . Les identificateurs de cas sont des noms pour les différents types de valeurs possibles que les objets de ce type peuvent être ; les valeurs sont facultatives. Si les valeurs ne sont pas présentes, le cas équivaut à un cas d’énumération. Si des valeurs sont présentes, chaque valeur peut être une valeur unique d’un type spécifié ou un tuple qui agrège plusieurs champs des mêmes types ou différents. Vous pouvez attribuer un nom à un champ individuel, mais le nom est facultatif, même si d’autres champs dans le même cas sont nommés.
L'accès par défaut pour les unions faisant l'objet de discrimination est public
.
Par exemple, considérez la déclaration suivante d’un type Shape.
type Shape =
| Rectangle of width : float * length : float
| Circle of radius : float
| Prism of width : float * float * height : float
Le code précédent déclare une union discriminée Shape, dont les valeurs peuvent correspondre à l'un des trois cas suivants : Rectangle, Cercle et Prisme : Rectangle, Cercle et Prisme. Chaque cas a un ensemble de champs différent. La classe Rectangle a deux champs nommés, tous deux de type float
, qui ont pour noms 'largeur' et 'longueur'. Le cas du cercle n’a qu’un seul champ nommé, rayon. Le cas Prism a trois champs, dont deux (largeur et hauteur) sont nommés champs. Les champs non nommés sont appelés champs anonymes.
Vous construisez des objets en fournissant des valeurs pour les champs nommés et anonymes en fonction des exemples suivants.
let rect = Rectangle(length = 1.3, width = 10.0)
let circ = Circle (1.0)
let prism = Prism(5., 2.0, height = 3.0)
Ce code montre que vous pouvez utiliser les champs nommés dans l’initialisation, ou vous pouvez vous appuyer sur l’ordre des champs dans la déclaration et fournir simplement les valeurs de chaque champ à son tour. L'appel au constructeur de rect
dans le code précédent utilise les champs nommés, mais l'appel au constructeur de circ
utilise l'ordre. Vous pouvez combiner les champs ordonnés et les champs nommés, comme dans la construction de prism
.
Le type option
est une union discriminatoire simple dans la bibliothèque principale F#. Le type option
est déclaré comme suit.
// The option type is a discriminated union.
type Option<'a> =
| Some of 'a
| None
Le code précédent spécifie que le type Option
est une union discriminée qui a deux cas, Some
et None
. Le cas Some
a une valeur associée qui se compose d’un champ anonyme dont le type est représenté par le paramètre de type 'a
. Le cas None
n’a aucune valeur associée. Par conséquent, le type option
spécifie un type générique qui a une valeur d’un type donné ou pas de valeur. Le type Option
a également un alias de type minuscule, option
, qui est plus couramment utilisé.
Les identificateurs de cas peuvent être utilisés comme constructeurs pour le type union discriminée. Par exemple, le code suivant est utilisé pour créer des valeurs du type option
.
let myOption1 = Some(10.0)
let myOption2 = Some("string")
let myOption3 = None
Les identificateurs de cas sont également utilisés dans les expressions de correspondance de modèle. Dans une expression de correspondance de modèle, les identificateurs sont fournis pour les valeurs associées aux cas individuels. Par exemple, dans le code suivant, x
est l'identifiant de la valeur associée au cas Some
du type option
.
let printValue opt =
match opt with
| Some x -> printfn "%A" x
| None -> printfn "No value."
Dans les expressions de correspondance de modèle, vous pouvez utiliser des champs nommés pour spécifier des correspondances d’union discriminatoires. Pour le type de forme déclaré précédemment, vous pouvez utiliser les champs nommés comme le montre le code suivant pour extraire les valeurs des champs.
let getShapeWidth shape =
match shape with
| Rectangle(width = w) -> w
| Circle(radius = r) -> 2. * r
| Prism(width = w) -> w
Normalement, les identificateurs de cas peuvent être utilisés sans les qualifier avec le nom de l’union. Si vous souhaitez que le nom soit toujours qualifié avec le nom de l’union, vous pouvez appliquer l’attribut RequireQualifiedAccess à la définition du type d'union.
Désencapsuler les syndicats discriminatoires
Dans F# Les unions discriminées sont souvent utilisées dans la modélisation de domaine pour encapsuler un seul type. Il est facile d’extraire la valeur sous-jacente via la mise en correspondance des modèles. Vous n’avez pas besoin d’utiliser une expression de correspondance pour un cas unique :
let ([UnionCaseIdentifier] [values]) = [UnionValue]
L’exemple suivant illustre ceci :
type ShaderProgram = | ShaderProgram of id:int
let someFunctionUsingShaderProgram shaderProgram =
let (ShaderProgram id) = shaderProgram
// Use the unwrapped value
...
La correspondance des modèles est également autorisée directement dans les paramètres de fonction, ce qui vous permet de décompresser un cas unique :
let someFunctionUsingShaderProgram (ShaderProgram id) =
// Use the unwrapped value
...
Unions structurées discriminées
Vous pouvez également représenter des unions discriminantes comme des structures. Pour ce faire, utilisez l’attribut [<Struct>]
.
[<Struct>]
type SingleCase = Case of string
[<Struct>]
type Multicase =
| Case1 of string
| Case2 of int
| Case3 of double
Comme il s'agit de types de valeur et non de types de référence, il y a des considérations supplémentaires par rapport aux unions discriminées :
- Ils sont copiés en tant que types de valeur et ont une sémantique de types de valeur.
- Vous ne pouvez pas utiliser une définition de type récursive avec une union discriminée struct multicas.
Avant F# 9, il était nécessaire que chaque cas spécifie un nom de cas unique (au sein de l’union). À compter de F# 9, la limitation est levée.
Utilisation d’unions discriminantes au lieu de hiérarchies d’objets
Vous pouvez souvent utiliser une union discriminée comme alternative plus simple à une hiérarchie d’objets de petite taille. Par exemple, l’union discriminatoire suivante peut être utilisée au lieu d’une classe de base Shape
qui a des types dérivés pour le cercle, le carré, et ainsi de suite.
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
Au lieu d'utiliser une méthode virtuelle pour calculer une aire ou un périmètre, comme vous le feriez dans une implémentation orientée objet, vous pouvez utiliser la correspondance de motifs pour sélectionner les formules appropriées afin de calculer ces quantités. Dans l’exemple suivant, différentes formules sont utilisées pour calculer la zone, en fonction de la forme.
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)
La sortie est la suivante :
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
Utilisation d’unions discriminées pour les structures de données arborescentes
Les syndicats discriminatoires peuvent être récursifs, ce qui signifie que l’union elle-même peut être incluse dans le type d’un ou plusieurs cas. Les unions discriminées récursives peuvent être utilisées pour créer des structures d’arborescence, qui sont utilisées pour modéliser des expressions dans des langages de programmation. Dans le code suivant, une union discriminée récursive est utilisée pour créer une structure de données d’arborescence binaire. L’union se compose de deux cas : Node
, qui est un nœud avec une valeur entière, et des sous-arborescences gauche et droite, et Tip
, qui met fin à l’arborescence.
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
Dans le code précédent, resultSumTree
a la valeur 10. L’illustration suivante montre la structure d’arborescence pour myTree
.
Les unions discriminatoires fonctionnent bien si les nœuds de l’arborescence sont hétérogènes. Dans le code suivant, le type Expression
représente l’arborescence de syntaxe abstraite d’une expression dans un langage de programmation simple qui prend en charge l’ajout et la multiplication de nombres et de variables. Certains cas d’union ne sont pas récursifs et représentent des nombres (Number
) ou des variables (Variable
). D’autres cas sont récursifs et représentent des opérations (Add
et Multiply
), où les opérandes sont également des expressions. La fonction Evaluate
utilise une expression de correspondance pour traiter de manière récursive l’arborescence de syntaxe.
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
Lorsque ce code est exécuté, la valeur de result
est 5.
Membres
Il est possible de définir des membres dans des unions discriminantes. L’exemple suivant montre comment définir une propriété et implémenter une 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}"
Propriétés .Is*
sur les cas
Depuis F# 9, les unions discriminées exposent des propriétés .Is*
générées automatiquement pour chaque cas, ce qui vous permet de vérifier si une valeur appartient à un cas particulier.
Il s’agit de la façon dont elle peut être utilisée :
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
Attributs courants
Les attributs suivants sont couramment observés dans les unions discriminatoires :
[<RequireQualifiedAccess>]
[<NoEquality>]
[<NoComparison>]
[<Struct>]