類型推斷¶
Crystal 的理念是盡可能減少類型限制。但是,有些限制是必要的。
考慮像這樣的類別定義
class Person
def initialize(@name)
@age = 0
end
end
我們可以很快看到 @age
是一個整數,但我們不知道 @name
的類型。編譯器可以從 Person
類別的所有用法推斷其類型。但是,這樣做會有一些問題
- 對於閱讀程式碼的人來說,類型並不明顯:他們也必須檢查
Person
的所有用法才能找出答案。 - 一些編譯器最佳化,例如只需分析一次方法,以及增量編譯,幾乎不可能實現。
隨著程式碼庫的增長,這些問題變得更加重要:理解專案變得更加困難,並且編譯時間變得難以忍受。
因此,Crystal 需要以明顯的方式(對人類來說盡可能明顯)知道實例和類別變數的類型。
有幾種方法可以讓 Crystal 知道這一點。
具有類型限制¶
最簡單但可能最繁瑣的方法是使用明確的類型限制。
class Person
@name : String
@age : Int32
def initialize(@name)
@age = 0
end
end
不具類型限制¶
如果省略明確的類型限制,編譯器將嘗試使用一組語法規則來推斷實例和類別變數的類型。
對於給定的實例/類別變數,當可以應用規則並可以猜測類型時,該類型會被添加到一個集合中。當無法應用更多規則時,推斷的類型將是這些類型的聯合。此外,如果編譯器推斷實例變數並非總是初始化,它也會包含Nil類型。
規則很多,但通常最常用的是前三個。無需記住所有規則。如果編譯器產生錯誤,指出無法推斷實例變數的類型,您可以隨時添加明確的類型限制。
以下規則僅提及實例變數,但它們也適用於類別變數。它們是
1. 賦值字面值¶
當字面值被賦值給實例變數時,字面值的類型會被添加到集合中。所有字面值都有相關聯的類型。
在以下範例中,@name
被推斷為 String
,而 @age
被推斷為 Int32
。
class Person
def initialize
@name = "John Doe"
@age = 0
end
end
此規則以及後續每個規則也將應用於 initialize
以外的方法。例如
class SomeObject
def lucky_number
@lucky_number = 42
end
end
在上述情況下,@lucky_number
將被推斷為 Int32 | Nil
:Int32
是因為 42 被賦值給它,而 Nil
是因為它沒有在所有類別的初始化方法中被賦值。
2. 賦值呼叫類別方法 new
的結果¶
當像 Type.new(...)
這樣的表達式被賦值給實例變數時,類型 Type
會被添加到集合中。
在以下範例中,@address
被推斷為 Address
。
class Person
def initialize
@address = Address.new("somewhere")
end
end
這也適用於泛型類型。這裡 @values
被推斷為 Array(Int32)
。
class Something
def initialize
@values = Array(Int32).new
end
end
注意:new
方法可能會被類型重新定義。在這種情況下,推斷的類型將是 new
返回的類型,如果可以使用一些後續規則推斷出該類型。
3. 賦值一個具有類型限制的方法參數的變數¶
在以下範例中,@name
被推斷為 String
,因為方法參數 name
具有 String
類型的類型限制,並且該參數被賦值給 @name
。
class Person
def initialize(name : String)
@name = name
end
end
請注意,方法參數的名稱並不重要;這也有效
class Person
def initialize(obj : String)
@name = obj
end
end
使用較短的語法從方法參數賦值實例變數具有相同的效果
class Person
def initialize(@name : String)
end
end
另請注意,編譯器不會檢查方法參數是否被重新賦值為不同的值
class Person
def initialize(name : String)
name = 1
@name = name
end
end
在上述情況下,編譯器仍然會推斷 @name
為 String
,稍後在完全輸入該方法時會產生編譯時錯誤,指出 Int32
無法賦值給類型為 String
的變數。如果 @name
不應該是 String
,請使用明確的類型限制。
4. 賦值一個具有回傳類型限制的類別方法結果¶
在以下範例中,@address
被推斷為 Address
,因為類別方法 Address.unknown
具有 Address
的回傳類型限制。
class Person
def initialize
@address = Address.unknown
end
end
class Address
def self.unknown : Address
new("unknown")
end
def initialize(@name : String)
end
end
事實上,上述程式碼在 self.unknown
中並不需要回傳型別的限制。原因是編譯器也會查看類別方法的主體,如果它可以應用先前的規則之一(它是 new
方法,或是字面值等等),它將從該表達式推斷型別。因此,上面的程式碼可以簡單地寫成這樣
class Person
def initialize
@address = Address.unknown
end
end
class Address
# No need for a return type restriction here
def self.unknown
new("unknown")
end
def initialize(@name : String)
end
end
這個額外規則非常方便,因為除了 new
之外,擁有「類似建構函式」的類別方法是很常見的。
5. 將帶有預設值的方法參數賦值給變數¶
在以下範例中,因為 name
的預設值是字串字面值,並且稍後將其賦值給 @name
,因此 String
將被添加到推斷型別的集合中。
class Person
def initialize(name = "John Doe")
@name = name
end
end
當然,這也適用於簡短語法
class Person
def initialize(@name = "John Doe")
end
end
預設參數值也可以是 Type.new(...)
方法或具有回傳型別限制的類別方法。
6. 賦值調用 lib
函數的結果¶
因為 lib 函數 必須具有明確的型別,所以編譯器可以在將其賦值給實例變數時使用回傳型別。
在以下範例中,@age
會被推斷為 Int32
。
class Person
def initialize
@age = LibPerson.compute_default_age
end
end
lib LibPerson
fun compute_default_age : Int32
end
7. 使用 out
lib 表達式¶
因為 lib 函數 必須具有明確的型別,所以編譯器可以使用 out
引數的型別,該型別應為指標型別,並使用解參考後的型別作為猜測。
在以下範例中,@age
會被推斷為 Int32
。
class Person
def initialize
LibPerson.compute_default_age(out @age)
end
end
lib LibPerson
fun compute_default_age(age_ptr : Int32*)
end
其他規則¶
編譯器會盡可能地聰明,以減少明確的型別限制。例如,如果賦值一個 if
表達式,則將從 then
和 else
分支推斷型別
class Person
def initialize
@age = some_condition ? 1 : 2
end
end
因為上面的 if
(嚴格來說是三元運算子,但它類似於 if
)具有整數字面值,所以 @age
成功地被推斷為 Int32
,而不需要多餘的型別限制。
另一種情況是 ||
和 ||=
class SomeObject
def lucky_number
@lucky_number ||= 42
end
end
在上面的範例中,@lucky_number
將被推斷為 Int32 | Nil
。這對於延遲初始化的變數非常有用。
常量也會被追蹤,因為對於編譯器(和人類)來說,這樣做非常簡單。
class SomeObject
DEFAULT_LUCKY_NUMBER = 42
def initialize(@lucky_number = DEFAULT_LUCKY_NUMBER)
end
end
這裡使用了規則 5(預設參數值),並且因為常量解析為整數字面值,所以 @lucky_number
被推斷為 Int32
。