Restrictions de type
Les restrictions de type sont des annotations de type sur des arguments de méthode pour limiter les types acceptés par cette méthode.
def add(x : Number, y : Number)
x + y
end
# Ok
add 1, 2 # Ok
# Error: no overload matches 'add' with types Bool, Bool
add true, false
Remarquez que si nous avions défini add
sans restriction de type,
nous aurions également eu une erreur de compilation:
def add(x, y)
x + y
end
add true, false
Le code précédent renvoie une erreur à la compilation:
Error in foo.cr:6: instantiating 'add(Bool, Bool)'
add true, false
^~~
in foo.cr:2: undefined method '+' for Bool
x + y
^
Tout ça parce-que lorsque vous appelez add
, elle est instanciée avec les types des arguments:
chaque appel de méthode avec une combinaison différente de type résulte en une instantiation différente de méthode.
La seule différence est que le premier message est légérement plus clair, mais chaque définition est valide dans le sens où vous aurez une erreur à la compilation
quand même. Donc, en général, il est préférable de ne pas spécifier les restrictions de type et de les utiliser principalement pour définir les surcharges de méthodes.
Par exemple, si on définit une classe qui a une méthode +
mais qui n'est pas un Number
, on peut utiliser la méthode add
qui n'a pas de restriction de type,
mais on ne peut utiliser la méthode add
qui a des restrictions.
# Une classe qui a une méthode + mais qui n'est pas un Number
class Six
def +(other)
6 + other
end
end
# méthode add sans restriction de type
def add(x, y)
x + y
end
# OK
add Six.new, 10
# méthode add avec restriction de type
def restricted_add(x : Number, y : Number)
x + y
end
# Error: no overload matches 'restricted_add' with types Six, Int32
restricted_add Six.new, 10
Reportez-vous à la grammaire de type pour la notation utilisée dans les restrictions de type.
Restriction self
self
est une restriction de type spéciale:
class Person
def ==(other : self)
other.name == name
end
def ==(other)
false
end
end
john = Person.new "John"
another_john = Person.new "John"
peter = Person.new "Peter"
john == another_john #=> true
john == peter #=> false (names differ)
john == 1 #=> false (because 1 is not a Person)
Dans l'exemple précédent self
revient à écrire Person
. Mais, en general, self
est équivalent à écrire le type qui possédera au final la méthode,
ce qui, lorsque des modules sont impliqués, devient plus utile.
Notez au passage que étant donné que Person
hérite de Reference
la seconde définition de ==
est inutile, car déjà définie dans Reference
.
Notez que self
représente toujours une correspondance avec un type d'instance, même dans les méthodes de classe:
class Person
def self.compare(p1 : self, p2 : self)
p1.name == p2.name
end
end
john = Person.new "John"
peter = Person.new "Peter"
Person.compare(john, peter) # OK
Vous pouvez utiliser self.class
pour vous restreindre au type Person.
La section suivante présente le suffixe .class
dans les restrictions de type.
Classes comme restrictions
En utilisant, par exemple, Int32
comme restriction de type fait que la méthode n'accepte que des instances de Int32
:
def foo(x : Int32)
end
foo 1 # OK
foo "hello" # Error
Si vous voulez qu'une méthode n'accepte que le type Int32 (et pas des instances), utilisez .class
:
def foo(x : Int32.class)
end
foo Int32 # OK
foo String # Error
Ce qui précéde est utile pour fournir des surcharges basées sur les types, et non sur les instances:
def foo(x : Int32.class)
puts "Got Int32"
end
def foo(x : String.class)
puts "Got String"
end
foo Int32 # affiche "Got Int32"
foo String # affiche "Got String"
Restrictions de type dans des splats
Vous pouvez spécifier des restrictions de type dans des splats:
def foo(*args : Int32)
end
def foo(*args : String)
end
foo 1, 2, 3 # OK, invoque la première surcharge
foo "a", "b", "c" # OK, invoque la seconde surcharge
foo 1, 2, "hello" # Erreur
foo() # Erreur
Quand vous spécifiez un type, tous les éléments d'un tuple doivent correspondre à ce type. De plus, le tuple vide ne correspond à aucun des cas précédents. Si vous voulez supporter le cas du tuple vide, ajoutez une surcharge supplémentaire:
def foo
# Voici le cas du tuple vide
end
Un moyen simple de faire la correspondance avec un ou plusieurs éléments de tout type est d'utiliser Object
comme restriction:
def foo(*args : Object)
end
foo() # Error
foo(1) # OK
foo(1, "x") # OK
Variables libres
Si vous utilisez une seule lettre majuscule comme restriction de type, l'identifiant devient une variable libre:
def foo(x : T)
T
end
foo(1) #=> Int32
foo("hello") #=> String
Ainsi, T
devient le type qui a été utilisé concrétement pour instancier la méthode.
Une variable libre peut être utilisée pour extraire le paramètre du type d'un type générique dans une restriction de type:
def foo(x : Array(T))
T
end
foo([1, 2]) #=> Int32
foo([1, "a"]) #=> (Int32 | String)
Pour créer une méthode qui accepte un nom de type, plutôt qu'une instance de type, ajoutez .class
à une variable libre dans la restriction de type:
def foo(x : T.class)
Array(T)
end
foo(Int32) #=> Array(Int32)
foo(String) #=> Array(String)
Variables dans les constructeurs
Les variables libres permettent d'utiliser l'inférence(voir définition en fin de page), de type lors de la création des types génériques. Reportez-vous à la section sur les Génériques.
inférence: Opération par laquelle on passe d'une assertion considérée comme vraie a une autre assertion au moyen d'un système de règles qui rend cette deuxième assertion également vraie.