跳至內容

類型限制

類型限制會應用於方法參數,以限制該方法接受的類型。

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"

Splats 中的類型限制

您可以在 splats 中指定類型限制

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

Splat 類型限制

如果 splat 參數的限制也有 splat,則該限制必須命名為Tuple類型,並且對應於參數的引數必須符合 splat 限制的元素

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

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

直接在 splat 限制中指定元組類型極為罕見,因為上述情況可以簡單地通過不使用 splat 來表達(即 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 被推斷為空元組,這對於具有非 splat 限制的 splat 參數來說是不可能的。

雙 splat 參數類似地支援雙 splat 類型限制

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()

此外,單 splat 限制也可以在泛型類型內使用,以一次提取多個類型引數

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()