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
它并不局限于两种类型,你想要多少种都可以。
foo : (String | Int32 | Nil | Float64) = "Hello"
foo = 1
foo = nil
foo = 1.0
typeof 与 Object#class
有两种方法可以获取变量的类型。
一种是使用 typeof,另一种是使用 Object#class。
区别在于,typeof返回变量在编译时的类型,而Object#class返回运行时的类型。
这意味着,如果你想判断一个变量是否为联合类型,Object#class无法告诉你,因为它在运行时只返回单一的类型。
foo = 0 == 0 ? "a" : 1
typeof(foo) # => (String | Int32)
foo.class # => String
由于联合类型在运行时是单一类型,所有标准操作都可以作用于它。 但在编译代码时,编译器需要知道它究竟是哪种类型。 因此,当你想使用特定于某个类型的操作时,代码必须安排成只能取其中某一种类型。
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? 方法在与控制表达式结合使用时,不能作用于实例变量或类变量。
需要先把它们赋值给一个局部变量。
把联合类型变成单一类型的一种方法,是让某个分支只有在类型为特定类型时才能进入。
另一种做法是使用 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。
if true
foo = 1
else
foo = "Hello"
end
typeof(foo) # => (Int32 | String)
这种推断是自动进行的,还有其他一些场景,例如编译器会推断某个方法返回联合类型。
character = "Hello world"[10]?
typeof(character) # => (Char | Nil)