跳至內容

泛型

泛型允許您基於另一個類型參數化一個類型。泛型提供類型多型。考慮一個 Box 類型

class MyBox(T)
  def initialize(@value : T)
  end

  def value
    @value
  end
end

int_box = MyBox(Int32).new(1)
int_box.value # => 1 (Int32)

string_box = MyBox(String).new("hello")
string_box.value # => "hello" (String)

another_box = MyBox(String).new(1) # Error, Int32 doesn't match String

泛型對於實作集合類型特別有用。ArrayHashSet 都是泛型類型,Pointer 也是。

允許多個類型參數

class MyDictionary(K, V)
end

任何名稱都可以用作類型參數

class MyDictionary(KeyType, ValueType)
end

泛型類別方法

當接收者的類型引數未指定時,泛型類型的類別方法中的類型限制會成為自由變數。這些自由變數隨後會從呼叫的引數中推斷出來。例如,也可以寫成

int_box = MyBox.new(1)          # : MyBox(Int32)
string_box = MyBox.new("hello") # : MyBox(String)

在上面的程式碼中,我們不必指定 MyBox 的類型引數,編譯器會按照以下流程推斷它們

  • 編譯器會從 MyBox#initialize(@value : T) 產生一個沒有明確定義的自由變數的 MyBox.new(value : T) 方法
  • MyBox.new(value : T) 中的 T 尚未繫結到一個類型,且 TMyBox 的類型參數,因此編譯器會將其繫結到給定引數的類型
  • 編譯器產生的 MyBox.new(value : T) 會呼叫 MyBox(T)#initialize(@value : T),其中 T 現在已繫結

透過這種方式,泛型類型使用起來就不那麼繁瑣。請注意,為了使其運作,#initialize 方法本身不需要指定任何自由變數。

相同的類型推斷也適用於 .new 以外的類別方法

class MyBox(T)
  def self.nilable(x : T)
    MyBox(T?).new(x)
  end
end

MyBox.nilable(1)     # : MyBox(Int32 | Nil)
MyBox.nilable("foo") # : MyBox(String | Nil)

在這些範例中,T 僅被推斷為自由變數,因此接收者本身的 T 仍然是未繫結的。因此,呼叫無法推斷 T 的其他類別方法會發生錯誤

module Foo(T)
  def self.foo
    T
  end

  def self.foo(x : T)
    foo
  end
end

Foo.foo(1)        # Error: can't infer the type parameter T for the generic module Foo(T). Please provide it explicitly
Foo(Int32).foo(1) # OK

泛型結構與模組

結構與模組也可以是泛型的。當模組是泛型時,您可以這樣引入它

module Moo(T)
  def t
    T
  end
end

class Foo(U)
  include Moo(U)

  def initialize(@value : U)
  end
end

foo = Foo.new(1)
foo.t # Int32

請注意,在上面的範例中,T 會變成 Int32,因為 Foo.new(1) 會使 U 變成 Int32,這反過來又會透過引入泛型模組使 T 變成 Int32

泛型類型繼承

泛型類別和結構可以被繼承。繼承時,您可以指定泛型類型的實例,或委派類型變數

class Parent(T)
end

class Int32Child < Parent(Int32)
end

class GenericChild(T) < Parent(T)
end

具有可變引數數量的泛型

我們可以使用 splat 運算子 定義具有可變引數數量的泛型類別。

讓我們看一個範例,我們定義一個名為 Foo 的泛型類別,然後我們將使用不同數量的類型變數來使用它

class Foo(*T)
  getter content

  def initialize(*@content : *T)
  end
end

# 2 type variables:
# (explicitly specifying type variables)
foo = Foo(Int32, String).new(42, "Life, the Universe, and Everything")

p typeof(foo) # => Foo(Int32, String)
p foo.content # => {42, "Life, the Universe, and Everything"}

# 3 type variables:
# (type variables inferred by the compiler)
bar = Foo.new("Hello", ["Crystal", "!"], 140)
p typeof(bar) # => Foo(String, Array(String), Int32)

在以下範例中,我們透過繼承來定義類別,為泛型類型指定實例

class Parent(*T)
end

# We define `StringChild` inheriting from `Parent` class
# using `String` for generic type argument:
class StringChild < Parent(String)
end

# We define `Int32StringChild` inheriting from `Parent` class
# using `Int32` and `String` for generic type arguments:
class Int32StringChild < Parent(Int32, String)
end

如果我們需要使用 0 個引數來實例化一個 class 呢?在這種情況下,我們可以這樣做

class Parent(*T)
end

foo = Parent().new
p typeof(foo) # => Parent()

但是我們不應將 0 個引數與未指定泛型類型變數混淆。以下範例會引發錯誤

class Parent(*T)
end

foo = Parent.new # Error: can't infer the type parameter T for the generic class Parent(*T). Please provide it explicitly

class Foo < Parent # Error: generic type arguments must be specified when inheriting Parent(*T)
end