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)