コンテンツにスキップ

ジェネリクス

ジェネリクスを使用すると、別の型に基づいて型をパラメーター化できます。ジェネリクスは型多相性を提供します。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

ジェネリクスは特にコレクション型を実装するのに役立ちます。Array, Hash, Set はジェネリック型であり、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

上記の例では、Foo.new(1)によってUInt32になり、それがジェネリックモジュールのインクルードを介してTInt32にするため、TInt32になることに注意してください。

ジェネリック型の継承

ジェネリッククラスと構造体は継承できます。継承するとき、ジェネリック型のインスタンスを指定するか、型変数を委譲できます。

class Parent(T)
end

class Int32Child < Parent(Int32)
end

class GenericChild(T) < Parent(T)
end

可変個の引数を持つジェネリクス

スプラット演算子を使用して、可変個の引数を持つジェネリッククラスを定義できます。

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