コンテンツへスキップ

構造体

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 を変更するのは最初の呼び出しのみです。2回目の呼び出しは、最初の呼び出しから渡された構造体のコピーに対して動作し、このコピーは式が実行された後に破棄されるためです。

構造体内で可変の型を操作する場合は、特に注意が必要です。

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 のプロパティに格納されます。
  • struktmodify に渡されると、その内部に配列への参照を持つ struktコピーが渡されます。
  • array が参照する配列は、 object.array << "foo" によって変更されます(要素が追加されます)。
  • これは、元の strukt でも同じ配列への参照を保持しているため、反映されます。
  • object.array = ["new"] は、struktコピーの参照を新しい配列への参照で置き換えます。
  • object.array << "bar" は、この新しく作成された配列に追加します。
  • modify は、この新しい配列への参照を返し、その内容が出力されます。
  • この新しい配列への参照は、struktコピーでのみ保持されていましたが、元の strukt には保持されていなかったため、元の strukt は最初のステートメントの結果のみを保持し、他の2つのステートメントの結果は保持されませんでした。

Klass はクラスであるため、modify に参照渡しされ、object.array = ["new"] は、strukt の場合のようにコピーではなく、元の klass オブジェクトに新しく作成された配列への参照を保存します。

継承

  • 構造体は暗黙的に Struct を継承し、StructValue を継承します。クラスは暗黙的に Reference を継承します。
  • 構造体は非抽象構造体を継承できません。

2番目の点には理由があります。構造体は非常に明確に定義されたメモリレイアウトを持っています。たとえば、上記の 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