类型推断¶
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`。