Structs
Structs (구조체)
Crystal에서 타입을 정의하는 키워드는 class만 있는 게 아니에요. struct로도 타입을 만들 수 있는데, 이 둘은 값이 저장되고 전달되는 방식에서 큰 차이가 있어요. 간단한 값 타입을 만들 때 struct가 아주 유용해요.
본문
class 대신 struct로 타입을 정의할 수 있어요.
struct Point
property x, y
def initialize(@x : Int32, @y : Int32)
end
end
struct는 Value를 상속하므로 스택에 할당되고 값으로 전달돼요. 즉 메서드에 넘기거나 메서드에서 반환하거나 변수에 할당할 때 실제로 값의 복사본이 전달돼요. 반면 class는 Reference를 상속해서 힙에 할당되고 참조로 전달돼요.
따라서 struct는 주로 불변(immutable) 데이터 타입이나 다른 타입의 상태 없는 래퍼를 만드는 데 유용해요. 특히 작은 복사본을 전달하는 게 더 효율적일 때, 메모리 할당을 많이 피하려는 성능상 이유가 큰 경우가 많아요. 자세한 내용은 성능 가이드를 참고하세요.
변경 가능한(mutable) struct도 물론 허용돼요. 다만 아래에서 설명할 놀라움을 피하려면 가변성(mutability)을 다루는 코드를 작성할 때 조심해야 해요.
값으로 전달하기
struct는 항상 값으로 전달돼요. 심지어 그 구조체의 메서드에서 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)
counter.plus.plus가 기대한 결과(@count=2)를 반환하지만, 원래 변수 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에 넘기면, 안에 그 배열 참조를 가진 복사본이 전달돼요.object.array << "foo"가array가 참조하는 배열을 수정해요(요소 추가).- 이 배열은 원래
strukt와 같은 배열을 참조하므로 원본strukt에도 그대로 반영돼요. object.array = ["new"]는strukt의 복사본에 있는 참조를 새 배열로 교체해요.object.array << "bar"가 이 새 배열에 값을 추가해요.modify는 이 새 배열의 참조를 반환하고 그 내용이 출력돼요.- 이 새 배열의 참조는
strukt의 복사본에만 들고 있었지 원본에는 없으므로, 원본strukt는 첫 번째 문장의 결과만 남고 나머지 두 문장의 결과는 남지 않아요.
Klass는 클래스라서 modify에 참조로 전달돼요. 그래서 object.array = ["new"]가 새 배열의 참조를 복사본이 아니라 원래 klass 객체에 저장해요. strukt 때와 달리 원본에 그대로 반영되는 거죠.
상속
struct는 암묵적으로 Struct를 상속받고,Struct는 Value를 상속받아요. 클래스는 암묵적으로 Reference를 상속받아요.struct는 비추상(non-abstract) 구조체를 상속할 수 없어요.
두 번째 규칙에는 이유가 있어요. 구조체는 메모리 레이아웃이 아주 잘 정의되어 있어요. 예를 들어 위의 Point 구조체는 8바이트를 차지해요. 포인트 배열이 있으면 각 포인트는 배열 버퍼 안에 그대로 박혀 있어요.
# 배열의 버퍼는 각 Point마다 8바이트를 할당해요
ary = [] of Point
만약 Point가 상속될 수 있다면, 배열 안에 다른 타입이 들어올 가능성까지 고려해야 하므로 각 요소의 크기를 키워야 해요. 그것은 분명히 예상 밖의 일이에요. 그래서 비추상 구조체는 상속할 수 없어요. 반면 추상(abstract) 구조체는 하위 타입을 가질 것이므로, 그 배열이 여러 타입을 담을 가능성을 고려하는 게 당연해요.
구조체는 클래스처럼 모듈을 include할 수도 있고 제네릭일 수도 있어요.
레코드
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
더 알아보기
- 참조로 전달되는 타입은 Classes and methods 문서에서 다뤄요.
- 구조체를 쓰면 좋은 성능 상황은 성능 가이드에서 확인할 수 있어요.