構造体¶
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
のプロパティに格納されます。strukt
がmodify
に渡されると、その内部に配列への参照を持つ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
オブジェクトに新しく作成された配列への参照を保存します。
継承¶
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