ジェネリクス¶
ジェネリクスを使用すると、別の型に基づいて型をパラメーター化できます。ジェネリクスは型多相性を提供します。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
はまだ型にバインドされておらず、T
はMyBox
の型パラメータであるため、コンパイラはそれを与えられた引数の型にバインドします。- コンパイラが生成した
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)
によってU
がInt32
になり、それがジェネリックモジュールのインクルードを介してT
をInt32
にするため、T
がInt32
になることに注意してください。
ジェネリック型の継承¶
ジェネリッククラスと構造体は継承できます。継承するとき、ジェネリック型のインスタンスを指定するか、型変数を委譲できます。
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