ユニオン型¶
変数または式の型は、複数の型で構成される場合があります。これをユニオン型と呼びます。たとえば、異なるifブランチ内で同じ変数に代入する場合
if 1 + 2 == 3
a = 1
else
a = "hello"
end
a # : Int32 | String
ifの終わりでは、a
はInt32 | String
型(「Int32とStringのユニオン」と読みます)になります。このユニオン型はコンパイラによって自動的に作成されます。実行時には、a
はもちろん1つの型のみになります。これはclass
メソッドを呼び出すことで確認できます。
# The runtime type
a.class # => Int32
コンパイル時の型は、typeofを使用することで確認できます。
# The compile-time type
typeof(a) # => Int32 | String
ユニオンは任意に多数の型で構成できます。型がユニオン型である式に対してメソッドを呼び出す場合、ユニオン内のすべての型がそのメソッドに応答する必要があります。そうでない場合は、コンパイル時エラーが発生します。メソッド呼び出しの型は、それらのメソッドの戻り値の型のユニオン型になります。
# to_s is defined for Int32 and String, it returns String
a.to_s # => String
a + 1 # Error, because String#+(Int32) isn't defined
必要に応じて、変数をコンパイル時にユニオン型として定義できます。
# set the compile-time type
a = 0.as(Int32 | Nil | String)
typeof(a) # => Int32 | Nil | String
ユニオン型のルール¶
一般に、2つの型T1
とT2
が結合されると、結果はユニオンT1 | T2
になります。ただし、結果の型が異なる型になるケースがいくつかあります。
同じ階層下にあるクラスと構造体のユニオン¶
T1
とT2
が同じ階層下にあり、それらの最近傍共通祖先Parent
がReference
、Struct
、Int
、Float
、Value
のいずれでもない場合、結果の型はParent+
になります。これは仮想型と呼ばれ、基本的にはコンパイラが型をParent
またはそのサブタイプのいずれかとして認識することを意味します。
例:
class Foo
end
class Bar < Foo
end
class Baz < Foo
end
bar = Bar.new
baz = Baz.new
# Here foo's type will be Bar | Baz,
# but because both Bar and Baz inherit from Foo,
# the resulting type is Foo+
foo = rand < 0.5 ? bar : baz
typeof(foo) # => Foo+
同じサイズのタプルのユニオン¶
同じサイズの2つのタプルのユニオンは、各位置の型のユニオンを持つタプル型になります。
例:
t1 = {1, "hi"} # Tuple(Int32, String)
t2 = {true, nil} # Tuple(Bool, Nil)
t3 = rand < 0.5 ? t1 : t2
typeof(t3) # Tuple(Int32 | Bool, String | Nil)
同じキーを持つ名前付きタプルのユニオン¶
同じキー(順序は関係ありません)を持つ2つの名前付きタプルのユニオンは、各キーの型のユニオンを持つ名前付きタプル型になります。キーの順序は、左側のタプルからの順序になります。
例:
t1 = {x: 1, y: "hi"} # Tuple(x: Int32, y: String)
t2 = {y: true, x: nil} # Tuple(y: Bool, x: Nil)
t3 = rand < 0.5 ? t1 : t2
typeof(t3) # NamedTuple(x: Int32 | Nil, y: String | Bool)