Crystalでは、変数が複数の型から成ることができます。 これはユニオン型と呼ばれます。 Crystalでは、コンパイラーがユニオン型を推論するのが一般的です。
ユニオン型は、複数の型から成っていても、実行時には単一の型です。
つまり、ユニオン型がStringとInt32で構成されている場合、同時に両方になることはありません。
代わりに、StringかInt32のどちらかになります。
ユニオン型は、型をパイプ(|)で区切って宣言します。
多くの場合、括弧で囲みますが、必須ではありません。
最も一般的なユニオン型は(T | Nil)で、ここでTは型であり、Nilを返す可能性のあるメソッドで現れます。
これは、変数がnil許容であるとも言われます。
foo : String = "Hello"
foo = nil # Error: type must be String, not (String | Nil)
foo : (String | Nil) = "Hello"
foo = nil
型は2つに限らず、いくつでも指定できます。
foo : (String | Int32 | Nil | Float64) = "Hello"
foo = 1
foo = nil
foo = 1.0
typeof vs Object#class
変数の型を取得する方法は2つあります。
typeofを使うか、Object#classを使うかです。
違いは、typeofはコンパイル時に変数の型を返すのに対し、Object#classは実行時に型を返すことです。
つまり、例えば変数がユニオン型かどうかを確認したい場合、Object#classではそれができません。
実行時の型、つまり単一の型しか返さないからです。
foo = 0 == 0 ? "a" : 1
typeof(foo) # => (String | Int32)
foo.class # => String
ユニオン型は実行時には単一の型なので、標準的な操作はすべて使えます。 しかし、コードをコンパイルするとき、コンパイラーはそれがどの型であるかを知る必要があります。 そのため、型固有の操作を使いたいときは、コードがその時点でいずれか1つの型に定まるように書かれている必要があります。
foo : (String | Int32) = "Hello"
foo.downcase # Error: undefined method 'downcase' for (String | Int32)
Crystalには、ユニオン型のための特別なメソッドがあります。
is_a?メソッドです。
これは型を引数に取り、真偽値を返します。
nil?メソッドはis_a?(Nil)のショートカットです。
is_a?メソッドを制御式に書くと、コンパイラーにどの型であるかを伝え、それによってその型であることを保証します。
そしてelse分岐では、その型でないことが保証されます。
foo : (String | Int32) = "Hello"
if foo.is_a?(String)
typeof(foo) # => String
foo.downcase # => "hello"
end
このis_a?は、引数に単一の型だけでなく、ユニオン型を取ることもできます。
また、複数の型に対して&&と組み合わせることもできます。
is_a?メソッドは、制御式と組み合わせて使う場合、インスタンス変数やクラス変数にすることはできません。
代わりに、まずローカル変数に代入する必要があります。
ユニオン型を単一の型にする方法の1つは、特定の型である場合にのみ分岐に入るようにすることです。
もう1つの方法は、asメソッドを使うことです。
これは実行時チェックを行うことで、ユニオン型を単一の型にします。
型が期待したものでない場合、例外が発生します。
foo : String | Int32 = "Hello"
foo.as(String).downcase # => "hello"
foo.as(Int32) # Error: can't cast String to Int32
この方法は、型が期待したものであると確信している場合、またはそうでない場合に例外を発生させたい場合にのみ使うことを意図しています。
不適切な設定でこの方法を使うと、予期しない動作を引き起こす可能性があります。
as?はasと非常に似ていますが、型が期待したものでない場合、例外を発生させる代わりにnilを返します。
つまり、期待した型とNilのユニオン型を返します。
foo : (String | Int32) = "Hello"
foo.as?(String).downcase # => "hello"
foo.as?(Int32) # => nil
nil許容とは、変数が型かNilのどちらかになりうることを意味します。
これは(T | Nil)と書けます。
しかし、nil許容型は比較的一般的なので、省略記法があります。
T?です。
# This:
foo : (String | Nil) = "Hello"
foo = nil
# Is the same as:
foo : String? = "Hello"
foo = nil
コンパイラーは、どの型か確信が持てない場合、ユニオン型を推論します。
例えば、次のコードでは、fooはStringかInt32のどちらかになりうるため、コンパイラーはfooがどの型かわかりません。
if true
foo = 1
else
foo = "Hello"
end
typeof(foo) # => (Int32 | String)
この推論は自動的に行われ、他にも、例えばコンパイラーがメソッドがユニオン型を返すと推論するシナリオがあります。
character = "Hello world"[10]?
typeof(character) # => (Char | Nil)