跳至内容

结构体

您无需使用 class 来定义类型,可以使用 struct 来定义。

struct Point
  property x, y

  def initialize(@x : Int32, @y : Int32)
  end
end

结构体继承自 Value,因此它们在栈上分配并按值传递:当传递给方法、从方法返回或赋值给变量时,实际上传递的是值的副本(而类继承自 Reference,在堆上分配并按引用传递)。

因此,结构体主要用于不可变数据类型和/或其他类型的无状态包装器,通常出于性能原因,以避免在传递小的副本可能更高效的情况下进行大量的小内存分配(有关更多详细信息,请参见 性能指南)。

仍然允许可变结构体,但在编写涉及可变性的代码时,您应该小心,以避免以下描述的意外情况。

按值传递

结构体始终按值传递,即使您从该结构体的方法返回 self

struct Counter
  def initialize(@count : Int32)
  end

  def plus
    @count += 1
    self
  end
end

counter = Counter.new(0)
counter.plus.plus # => Counter(@count=2)
puts counter      # => Counter(@count=1)

请注意,plus 的链式调用返回预期结果,但只有对 counter 的第一次调用修改了该变量,因为第二次调用是对从第一次调用传递给它的结构体的副本进行操作,并且该副本在表达式执行后被丢弃。

在处理结构体内可变类型时,您也应该小心。

class Klass
  property array = ["str"]
end

struct Strukt
  property array = ["str"]
end

def modify(object)
  object.array << "foo"
  object.array = ["new"]
  object.array << "bar"
end

klass = Klass.new
puts modify(klass) # => ["new", "bar"]
puts klass.array   # => ["new", "bar"]

strukt = Strukt.new
puts modify(strukt) # => ["new", "bar"]
puts strukt.array   # => ["str", "foo"]

这里 strukt 会发生什么?

  • Array 按引用传递,因此对 ["str"] 的引用存储在 strukt 的属性中。
  • strukt 传递给 modify 时,会传递一个 strukt副本,其中包含对数组的引用。
  • object.array << "foo" 修改了 array 引用的数组(在其中添加了元素)。
  • 这也反映在原始的 strukt 中,因为它持有对同一数组的引用。
  • object.array = ["new"] 用对新数组的引用替换了 strukt 副本中的引用。
  • object.array << "bar" 附加到这个新创建的数组。
  • modify 返回对这个新数组的引用,并打印其内容。
  • 对这个新数组的引用仅在 strukt副本中保存,而不是在原件中,因此这就是为什么原始的 strukt 仅保留第一个语句的结果,而没有保留其他两个语句的结果。

Klass 是一个类,因此它按引用传递给 modify,而 object.array = ["new"] 将对新创建数组的引用保存到原始的 klass 对象中,而不是像 strukt 那样保存到副本中。

继承

  • 结构体隐式地继承自 Struct,而 Struct 继承自 Value。类隐式地继承自 Reference
  • 结构体不能继承自非抽象结构体。

第二点是有原因的:结构体具有非常明确的内存布局。例如,上面的 Point 结构体占用 8 个字节。如果您有一个点数组,这些点会嵌入到数组的缓冲区中。

# The array's buffer will have 8 bytes dedicated to each Point
ary = [] of Point

如果继承了 Point,则该类型的数组还应考虑其他类型可以在其内部,因此每个元素的大小应增大以容纳这种情况。这肯定是不合预期的。因此,不能从非抽象结构体继承。另一方面,抽象结构体将有子类,因此,预期它们数组将考虑在其中可能有多种类型的可能性。

结构体还可以包含模块,并且可以是泛型,就像类一样。

记录

Crystal 标准库 提供了 record 宏。它简化了使用初始化程序和一些辅助方法的基本结构体类型的定义。

record Point, x : Int32, y : Int32

Point.new 1, 2 # => #<Point(@x=1, @y=2)>

record 宏扩展到以下结构体定义。

struct Point
  getter x : Int32

  getter y : Int32

  def initialize(@x : Int32, @y : Int32)
  end

  def copy_with(x _x = @x, y _y = @y)
    self.class.new(_x, _y)
  end

  def clone
    self.class.new(@x.clone, @y.clone)
  end
end