跳至内容

类型推断

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 | NilInt32 是因为 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`。