Crystal 允許一個變數由多種型別組成。 這就叫做聯合型別。 在 Crystal 中,聯合型別經常由編譯器推斷出來。
聯合型別在執行階段仍然是單一型別,即使它由多種型別組成也一樣。
這表示如果一個聯合型別是由String和Int32組成,它不會在同一時間同時是兩者。
它會是String,不然就是Int32。
宣告聯合型別的方式,是用直線符號(|)把型別隔開。
聯合型別通常會用括號包起來,但這並非必要。
最常見的聯合型別是(T | Nil),其中T是某種型別;當方法可能回傳Nil時,就會出現這種型別。
這也表示這個變數是nilable。
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
nilable 的意思是變數可以是某個型別或Nil。
它可以寫成(T | Nil)。
不過由於可為 nil 的型別相當常見,所以有個簡寫:T?。
# This:
foo : (String | Nil) = "Hello"
foo = nil
# Is the same as:
foo : String? = "Hello"
foo = nil
你任職的公司即將推出全新的智慧型手機,名叫Smarty 5,它具備全新的相機系統、全新的外型設計,以及一顆革命性的全新處理器,搭配一套名為smartyOS的全新作業系統。
這顆新處理器有能力處理比以往更安全的密碼,公司決定要好好善用這點。 他們請你為這支手機實作一套密碼鎖系統,讓使用者可以設定密碼,之後再檢查某個密碼是否正確。
這個練習中實作的密碼系統並不安全,僅供教學用途。 它不該用於任何真實世界的應用程式。
這支手機允許使用者使用多種不同類型的密碼,從簡單的數字密碼,到更安全的英數字密碼,甚至還能使用儲存指紋的密碼。
這些不同類型的密碼各自使用不同的資料型態:數字密碼使用Int32,英數字密碼使用String,指紋密碼則使用Float64。
請實作PasswordLock類別,並加上一個初始化方法,它接受這三種密碼中的任一種作為引數,並將其存到名為@password的實例變數中。
password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>
公司決定要將密碼加密,讓它不會以明文儲存,因此請你實作一個加密密碼的方法。
每種密碼類型都有自己獨特的加密方式:
Int32密碼:將密碼除以二,並四捨五入到最接近的整數。String密碼:將密碼反轉。Float64密碼:將密碼乘以四。請實作一個名為encrypt的實例方法,它不接受任何引數,並會修改@password實例變數,將它加密。
password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>
password_lock.encrypt
# => #<PasswordLock:0x7f8e1b8c0b80 @password=617>
公司也請你實作一個方法,用來檢查某個密碼是否正確。
他們希望這個方法在密碼錯誤時回傳nil,在密碼正確時回傳"Unlocked"。
請實作一個名為unlock?的實例方法,接受要檢查的密碼作為引數。
密碼錯誤時,這個方法應回傳nil;密碼正確時則回傳"Unlocked"。
password_lock = PasswordLock.new(1234)
password_lock.encrypt
password_lock.unlock?(1234)
# => "Unlocked"