跳至内容

类型限制

类型限制应用于方法参数,以限制该方法接受的类型。

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

来自实例变量的限制

在某些情况下,可以根据方法参数的使用情况来限制方法参数的类型。例如,考虑以下示例

class Foo
  @x : Int64

  def initialize(x)
    @x = x
  end
end

在这种情况下,我们知道初始化函数的参数 x 必须是一个 Int64,并且没有必要将其保持为无限制的。

当编译器找到从方法参数到实例变量的赋值时,它就会插入这样的限制。在上面的示例中,调用 Foo.new "hi" 会失败(注意类型限制)

Error: no overload matches 'Foo.new' with type String

Overloads are:
 - Foo.new(x : ::Int64)

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"

散点中的类型限制

可以在散点中指定类型限制

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

匹配任何类型的一个或多个元素的简单方法是使用 _ 作为限制

def foo(*args : _)
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

散点类型限制

如果散点参数的限制也包含一个散点,则该限制必须命名一个 Tuple 类型,并且与该参数相对应的参数必须与散点限制中的元素匹配

def foo(*x : *{Int32, String})
end

foo(1, "") # OK
foo("", 1) # Error
foo(1)     # Error

直接在散点限制中指定元组类型的情况非常罕见,因为上面的情况可以通过简单地不使用散点来表示(即 def foo(x : Int32, y : String))。但是,如果限制是一个自由变量,那么它会被推断为一个包含所有对应参数类型的 Tuple

def foo(*x : *T) forall T
  T
end

foo(1, 2)  # => Tuple(Int32, Int32)
foo(1, "") # => Tuple(Int32, String)
foo(1)     # => Tuple(Int32)
foo()      # => Tuple()

在最后一行,T 被推断为空元组,这对于具有非散点限制的散点参数是不可能的。

双散点参数类似地支持双散点类型限制

def foo(**x : **T) forall T
  T
end

foo(x: 1, y: 2)  # => NamedTuple(x: Int32, y: Int32)
foo(x: 1, y: "") # => NamedTuple(x: Int32, y: String)
foo(x: 1)        # => NamedTuple(x: Int32)
foo()            # => NamedTuple()

此外,也可以在泛型类型中使用单散点限制,一次提取多个类型参数

def foo(x : Proc(*T, Int32)) forall T
  T
end

foo(->(x : Int32, y : Int32) { x + y }) # => Tuple(Int32, Int32)
foo(->(x : Bool) { x ? 1 : 0 })         # => Tuple(Bool)
foo(->{ 1 })                            # => Tuple()