密碼鎖

密碼鎖

學習練習

簡介

Crystal 允許一個變數由多種型別組成。 這就叫做聯合型別。 在 Crystal 中,聯合型別經常由編譯器推斷出來。

Note

聯合型別在執行階段仍然是單一型別,即使它由多種型別組成也一樣。 這表示如果一個聯合型別是由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?的引數不限定只能放單一型別,也可以放聯合型別。 它也能搭配&&來處理多種型別。

Note

is_a?方法用在控制運算式時,它的對象不能是實例變數或類別變數。 必須先把它們指定給區域變數才行。

as

把聯合型別變成單一型別的方法之一,是讓某個分支只有在型別符合特定型別時才能進入。 另一種做法是使用as方法。 它會透過執行階段的檢查,把聯合型別變成單一型別。 如果型別不是預期的型別,就會引發例外。

foo : String | Int32 = "Hello"
foo.as(String).downcase # => "hello"

foo.as(Int32) # Error: can't cast String to Int32
Caution

這種做法只適用於你確定型別就是預期的型別,或是你希望在型別不符時引發例外的情況。

在不當的設定下使用這種做法,可能會導致非預期的行為。

as?

as?的運作方式和as非常類似,但當型別不是預期的型別時,它會回傳 nil 而不是引發例外。 這表示它會回傳預期型別與Nil組成的聯合型別。

foo : (String | Int32) = "Hello"
foo.as?(String).downcase # => "hello"

foo.as?(Int32) # => nil

可為 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的全新作業系統。

這顆新處理器有能力處理比以往更安全的密碼,公司決定要好好善用這點。 他們請你為這支手機實作一套密碼鎖系統,讓使用者可以設定密碼,之後再檢查某個密碼是否正確。

Caution

這個練習中實作的密碼系統並不安全,僅供教學用途。 它不該用於任何真實世界的應用程式。

1. 設定密碼

這支手機允許使用者使用多種不同類型的密碼,從簡單的數字密碼,到更安全的英數字密碼,甚至還能使用儲存指紋的密碼。

這些不同類型的密碼各自使用不同的資料型態:數字密碼使用Int32,英數字密碼使用String,指紋密碼則使用Float64。

請實作PasswordLock類別,並加上一個初始化方法,它接受這三種密碼中的任一種作為引數,並將其存到名為@password的實例變數中。

password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>

2. 加密密碼

公司決定要將密碼加密,讓它不會以明文儲存,因此請你實作一個加密密碼的方法。

每種密碼類型都有自己獨特的加密方式:

  • Int32密碼:將密碼除以二,並四捨五入到最接近的整數。
  • String密碼:將密碼反轉。
  • Float64密碼:將密碼乘以四。

請實作一個名為encrypt的實例方法,它不接受任何引數,並會修改@password實例變數,將它加密。

password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>

password_lock.encrypt
# => #<PasswordLock:0x7f8e1b8c0b80 @password=617>

3. 檢查密碼是否正確

公司也請你實作一個方法,用來檢查某個密碼是否正確。

他們希望這個方法在密碼錯誤時回傳nil,在密碼正確時回傳"Unlocked"。

請實作一個名為unlock?的實例方法,接受要檢查的密碼作為引數。 密碼錯誤時,這個方法應回傳nil;密碼正確時則回傳"Unlocked"。

password_lock = PasswordLock.new(1234)
password_lock.encrypt
password_lock.unlock?(1234)
# => "Unlocked"
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Crystal Exercism

準備好開始 密碼鎖 了嗎?

註冊 Exercism,透過 26 個概念142 個練習 和真人引導來學習並精通 Crystal,全部免費。