跳到內容

結構體

除了使用 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副本,其中包含陣列的參考
  • array 參考的陣列會被修改(在其內部加入元素),透過 object.array << "foo"
  • 這也會反映在原始的 strukt 中,因為它持有同一個陣列的參考
  • object.array = ["new"] 會用新陣列的參考取代 strukt副本中的參考
  • object.array << "bar" 會附加到這個新建立的陣列
  • modify 會回傳對這個新陣列的參考,並印出其內容
  • 對這個新陣列的參考僅保留在 strukt副本中,而不是原始的 strukt 中,因此原始的 strukt 只保留了第一個敘述的結果,而不是其他兩個敘述的結果

Klass 是一個類別,因此它會以傳參考方式傳遞給 modify,而 object.array = ["new"] 會將新建立的陣列的參考儲存在原始的 klass 物件中,而不是像 strukt 那樣儲存在副本中。

繼承

  • 結構體會隱式繼承自 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