Ty

Type union en Crystal

1 exercice

À propos de Type union

Crystal permet à une variable d'être constituée de plusieurs types. On appelle cela un type union. En Crystal, il est courant qu'un type union soit inféré par le compilateur.

Note

Un type union reste un seul type à l'exécution, même s'il est constitué de plusieurs types. Cela signifie que si un type union est composé de String et de Int32, il ne sera pas les deux à la fois. Il sera soit un String, soit un Int32.

Un type union se déclare en séparant les types par une barre verticale (|). Ils sont souvent placés entre parenthèses, mais ce n'est pas obligatoire. Le type union le plus courant est (T | Nil) où T est un type, qui peut apparaître dans les méthodes susceptibles de renvoyer Nil. On dit alors que la variable est nilable.

foo : String = "Hello"
foo = nil # Error: type must be String, not (String | Nil)

foo : (String | Nil) = "Hello"
foo = nil

Il ne se limite pas à deux types, mais peut en compter autant que tu veux.

foo : (String | Int32 | Nil | Float64) = "Hello"
foo = 1
foo = nil
foo = 1.0

typeof vs Object#class

Il existe deux façons d'obtenir le type d'une variable. Soit en utilisant typeof, soit en utilisant Object#class. La différence est que typeof renvoie le type d'une variable à la compilation, tandis que Object#class renvoie le type à l'exécution. Cela signifie que si tu veux savoir si une variable est un type union, par exemple, Object#class ne pourra pas te le dire, car il ne renvoie que le type à l'exécution, qui est un type unique.

foo = 0 == 0 ? "a" : 1
typeof(foo) # => (String | Int32)
foo.class # => String

Opérations sur les types union

Comme un type union est un type unique à l'exécution, toutes les opérations standard fonctionnent dessus. Mais lors de la compilation, le compilateur a besoin de savoir de quel type il s'agit. Le code doit donc être agencé de telle façon qu'il ne puisse être que l'un des types lorsqu'on veut utiliser les opérations propres à un type.

foo : (String | Int32) = "Hello"
foo.downcase # Error: undefined method 'downcase' for (String | Int32)

Crystal possède cependant une méthode particulière pour les types union : la méthode is_a?, qui prend un type en argument et renvoie un booléen. La méthode nil? est un raccourci pour is_a?(Nil). Placer la méthode is_a? dans une expression de contrôle indique au compilateur de quel type il s'agit, et garantit donc qu'il s'agit bien de ce type. Et pour une branche else, cela garantit qu'il ne s'agit pas de ce type.

foo : (String | Int32) = "Hello"
if foo.is_a?(String)
  typeof(foo)  # => String
  foo.downcase # => "hello"
end

Ce is_a? n'est pas limité à un seul type en argument : il peut aussi prendre un type union. Il peut également être combiné avec && pour plusieurs types.

Note

La méthode is_a?, lorsqu'elle est utilisée conjointement avec une expression de contrôle, ne peut pas porter sur une variable d'instance ou une variable de classe. Il faut d'abord affecter celles-ci à une variable locale.

as

Une façon de transformer un type union en un type unique consiste à faire en sorte qu'une branche ne puisse être exécutée que si le type est un type précis. Une autre approche consiste à utiliser la méthode as. Celle-ci transforme un type union en un type unique en effectuant une vérification à l'exécution. Une exception est levée si le type n'est pas celui attendu.

foo : String | Int32 = "Hello"
foo.as(String).downcase # => "hello"

foo.as(Int32) # Error: can't cast String to Int32
Caution

Cette approche n'est prévue que pour les cas où tu es sûr que le type est bien celui attendu, ou bien si tu veux lever une exception lorsqu'il ne l'est pas.

L'utiliser dans un contexte inadapté peut entraîner un comportement inattendu.

as?

as? fonctionne de manière très similaire à as, mais renvoie nil au lieu de lever une exception si le type n'est pas celui attendu. Cela signifie qu'elle renvoie un type union composé du type attendu et de Nil.

foo : (String | Int32) = "Hello"
foo.as?(String).downcase # => "hello"

foo.as?(Int32) # => nil

Raccourci pour les types nilables

Nilable signifie qu'une variable peut être soit d'un type, soit Nil. On peut l'écrire (T | Nil). Mais comme les types nilables sont relativement courants, il existe un raccourci : T?.

# This:
foo : (String | Nil) = "Hello"
foo = nil

# Is the same as:
foo : String? = "Hello"
foo = nil

Infère les types union

Le compilateur infère un type union s'il n'est pas sûr du type dont il s'agit. Par exemple, dans le code suivant, le compilateur ne saura pas de quel type est foo, puisque celui-ci peut être soit un String, soit un Int32.

if true
  foo = 1
else
  foo = "Hello"
end

typeof(foo) # => (Int32 | String)

Cette inférence se produit automatiquement, et il existe d'autres scénarios où, par exemple, le compilateur infère qu'une méthode renvoie un type union.

character = "Hello world"[10]?
typeof(character) # => (Char | Nil)
Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet

Apprends Type union

L'entraînement est verrouillé

Déverrouille 1 exercice de plus pour t'entraîner sur Type union