Ограничения типов
Ограничения типов применяются к параметрам методов, чтобы ограничить типы, принимаемые этим методом.
def add(x : Number, y : Number)
x + y
end
# Ok
add 1, 2
# Error: no overload matches 'add' with types Bool, Bool
add true, false
Обратите внимание: если бы мы определили add без ограничений типов, мы также получили бы ошибку времени компиляции:
def add(x, y)
x + y
end
add true, false
Приведённый выше код вызывает следующую ошибку компиляции:
Error in foo.cr:6: instantiating 'add(Bool, Bool)'
add true, false
^~~
in foo.cr:2: undefined method '+' for Bool
x + y
^
Это происходит потому, что при вызове add метод инстанцируется с типами аргументов: каждый вызов метода с другой комбинацией типов приводит к созданию отдельного экземпляра метода.
Единственное различие в том, что первое сообщение об ошибке немного понятнее, но оба определения безопасны, поскольку в любом случае вы получите ошибку времени компиляции. Поэтому, как правило, предпочтительно не указывать ограничения типов и использовать их почти исключительно для определения разных перегрузок методов. Это позволяет писать более универсальный и многократно используемый код. Например, если мы определим класс с методом +, но он не является Number, мы сможем использовать метод add без ограничений типов, но не сможем использовать метод add с ограничениями.
# A class that has a + method but isn't a Number
class Six
def +(other)
6 + other
end
end
# add method without type restrictions
def add(x, y)
x + y
end
# OK
add Six.new, 10
# add method with type restrictions
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
Обозначения, используемые в ограничениях типов, описаны в грамматике типов.
Обратите внимание, что ограничения типов не применяются к переменным внутри самих методов.
def handle_path(path : String)
path = Path.new(path) # *path* is now of the type Path
# Do something with *path*
end
Ограничение self
Особое ограничение типа — это self:
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)
В предыдущем примере self — это то же самое, что написать Person. Но в общем случае self означает то же, что и указание типа, которому в итоге будет принадлежать этот метод; при использовании модулей это становится полезнее.
К слову, поскольку Person наследует Reference, второе определение == не требуется, так как оно уже определено в Reference.
Обратите внимание, что self всегда означает соответствие типу экземпляра, даже в методах класса:
class Person
getter name : String
def initialize(@name)
end
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
Можно использовать self.class, чтобы ограничить тип значением Person. В следующем разделе рассматривается суффикс .class в ограничениях типов.
Классы в качестве ограничений
Использование, например, Int32 в качестве ограничения типа означает, что метод принимает только экземпляры Int32:
def foo(x : Int32)
end
foo 1 # OK
foo "hello" # Error
Если нужно, чтобы метод принимал только тип Int32 (а не его экземпляры), используйте .class:
def foo(x : Int32.class)
end
foo Int32 # OK
foo String # Error
Это полезно для создания перегрузок в зависимости от типов, а не от экземпляров:
def foo(x : Int32.class)
puts "Got Int32"
end
def foo(x : String.class)
puts "Got String"
end
foo Int32 # prints "Got Int32"
foo String # prints "Got String"
Ограничения типов в splat-аргументах
В splat-аргументах можно указывать ограничения типов:
def foo(*args : Int32)
end
def foo(*args : String)
end
foo 1, 2, 3 # OK, invokes first overload
foo "a", "b", "c" # OK, invokes second overload
foo 1, 2, "hello" # Error
foo() # Error
При указании типа все элементы кортежа должны соответствовать этому типу. Кроме того, пустой кортеж не соответствует ни одному из приведённых выше вариантов. Чтобы поддержать случай с пустым кортежем, добавьте ещё одну перегрузку:
def foo
# This is the empty-tuple case
end
Простой способ сопоставить один или несколько элементов любого типа — использовать Object в качестве ограничения:
def foo(*args : Object)
end
foo() # Error
foo(1) # OK
foo(1, "x") # OK
Свободные переменные
С помощью forall можно задать ограничение типа, используя тип аргумента или часть типа аргумента:
def foo(x : T) forall T
T
end
foo(1) # => Int32
foo("hello") # => String
То есть T становится типом, который фактически использовался для инстанцирования метода.
Свободную переменную можно использовать для извлечения аргумента типа из обобщённого типа в ограничении типа:
def foo(x : Array(T)) forall T
T
end
foo([1, 2]) # => Int32
foo([1, "a"]) # => (Int32 | String)
Чтобы создать метод, принимающий имя типа, а не экземпляр типа, добавьте .class к свободной переменной в ограничении типа:
def foo(x : T.class) forall T
Array(T)
end
foo(Int32) # => Array(Int32)
foo(String) # => Array(String)
Можно также указать несколько свободных переменных для сопоставления типов нескольких аргументов:
def push(element : T, array : Array(T)) forall T
array << element
end
push(4, [1, 2, 3]) # OK
push("oops", [1, 2, 3]) # Error
To the extent possible under law, the persons who contributed to this workhave waived
all copyright and related or neighboring rights to this workby associating CC0 with it.
https://crystal-lang.org/reference/1.2/syntax_and_semantics/type_restrictions.html