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 vs 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
你所在的公司即将发布一款全新的智能手机,名叫 Smarty 5。它拥有全新的相机系统、全新的设计,以及一颗革命性的新处理器,运行着名为 smartyOS 的全新操作系统。
这颗新处理器能处理比以往更安全的密码,公司决定好好利用这一点。他们请你为这款手机实现一套密码锁系统,让用户可以设置密码,然后检查给定的密码是否正确。
本练习中实践的密码系统并不安全,仅用于教学目的。 切勿将其用于任何真实的应用中。
这款手机允许用户使用多种不同类型的密码:从简单的数字密码,到更安全的字母数字密码,甚至是可以存储指纹的密码。
这些不同类型的密码使用不同的数据类型:数字密码使用 Int32,字母数字密码使用 String,指纹密码使用 Float64。
实现 PasswordLock 类,它的初始化方法接受这三种密码中的任意一种作为实参,并将其存储在名为 @password 的实例变量中。
password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>
公司决定加密密码,使它不以明文形式存储,并要求你实现一个用于加密密码的方法。
每种密码类型都有各自独特的加密方式:
Int32 密码:密码除以 2,并四舍五入到最接近的整数。String 密码:密码被反转。Float64 密码:密码乘以 4。实现一个名为 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"