Struct 클래스

Struct 클래스

값을 저장하고 꺼내는 간단한 클래스를 만들 때 아주 편한 게 Struct예요. "이름 붙은 필드 몇 개 가진 값 객체"를 한 줄로 만들어 주죠.

출처: Ruby 4.0 API

본문

클래스 Struct는 값을 저장하고 가져올 수 있는 간단한 클래스를 만드는 편리한 방법을 제공해요.

이 예시는 Struct의 하위 클래스 Struct::Customer를 만들어요. 첫 인자인 문자열은 하위 클래스의 이름이고, 나머지 심볼 인자들이 새 하위 클래스의 멤버(member) 를 결정해요.

Customer = Struct.new('Customer', :name, :address, :zip)
Customer.name       # => "Struct::Customer"
Customer.class      # => Class
Customer.superclass # => Struct

각 멤버에는 값 저장·조회를 위한 writer, reader 두 메서드가 대응돼요:

methods = Customer.instance_methods false
methods # => [:zip, :address=, :zip=, :address, :name, :name=]

하위 클래스의 인스턴스는 ::new 메서드로 만들고 멤버에 값을 할당할 수 있어요:

joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe # => #<struct Struct::Customer name="Joe Smith", address="123 Maple, Anytown NC", zip=12345>

멤버 값은 이렇게 다룰 수 있어요:

joe.name    # => "Joe Smith"
joe.name = 'Joseph Smith'
joe.name    # => "Joseph Smith"

이렇게도 되는데, 멤버 이름을 문자열로 쓰든 심볼로 쓰든 상관없는 걸 볼 수 있어요:

joe[:name]  # => "Joseph Smith"
joe[:name] = 'Joseph Smith, Jr.'
joe['name'] # => "Joseph Smith, Jr."

Struct::new를 참고하세요.

여기 뭐가 있나 (What's Here)

먼저 다른 데 있는 것부터요. 클래스 Struct는:

  • 클래스 Object에서 상속받아요.
  • 수십 개의 추가 메서드를 제공하는 Enumerable 모듈을 포함해요.

불변(immutable) 값 객체를 정의하는 데 쓰이는 Data도 함께 보세요. Data는 비슷하지만 더 엄격한 개념이에요.

여기서 Struct 클래스가 제공하는 메서드는 이렇게 나뉘어요:

  • Struct 하위 클래스 생성
  • 조회
  • 비교
  • 가져오기
  • 할당
  • 반복
  • 변환

Struct 하위 클래스 생성 메서드

  • ::new: Struct의 새 하위 클래스를 반환해요.

조회 메서드

  • hash: 정수 해시 코드를 반환해요.
  • size (length의 별칭): 멤버의 개수를 반환해요.

비교 메서드

  • ==: 주어진 객체가 self와 같은지 여부를 반환해요. 멤버 값 비교는 ==를 써요.
  • eql?: 주어진 객체가 self와 같은지 여부를 반환해요. 멤버 값 비교는 eql?을 써요.

가져오기 메서드

  • []: 주어진 멤버 이름에 연결된 값을 반환해요.
  • to_a (values, deconstruct의 별칭): self의 멤버 값을 배열로 반환해요.
  • deconstruct_keys: 주어진 멤버 이름들에 대한 이름/값 쌍의 해시를 반환해요.
  • dig: 중첩 객체 안에서 주어진 멤버 이름과 추가 인자로 지정된 객체를 반환해요.
  • members: 멤버 이름의 배열을 반환해요.
  • select (filter의 별칭): 주어진 블록이 선택한 self의 멤버 값 배열을 반환해요.
  • values_at: 주어진 멤버 이름에 대한 값들을 담은 배열을 반환해요.

할당 메서드

  • []=: 주어진 멤버 이름에 주어진 값을 할당해요.

반복 메서드

  • each: 주어진 블록을 각 멤버 이름과 함께 호출해요.
  • each_pair: 주어진 블록을 각 멤버 이름/값 쌍과 함께 호출해요.

변환 메서드

  • inspect (to_s의 별칭): self의 문자열 표현을 반환해요.
  • to_h: self의 멤버 이름/값 쌍의 해시를 반환해요.

Public Class Methods

StructClass::keyword_init? → true or falsy value

클래스가 keyword_init: true로 초기화됐다면 true를 반환해요. 그렇지 않으면 nil 또는 false를 반환해요.

예시:

Foo = Struct.new(:a)
Foo.keyword_init? # => nil
Bar = Struct.new(:a, keyword_init: true)
Bar.keyword_init? # => true
Baz = Struct.new(:a, keyword_init: false)
Baz.keyword_init? # => false

StructClass::members → array_of_symbols

Struct 자손의 멤버 이름을 배열로 반환해요:

Customer = Struct.new(:name, :address, :zip)
Customer.members # => [:name, :address, :zip]

new(*member_names, keyword_init: nil) { |Struct_subclass| ... } → Struct_subclass

Struct.newStruct의 새 하위 클래스를 반환해요. 새 하위 클래스는:

  • 익명일 수도 있고, class_name으로 주어진 이름을 가질 수도 있어요.
  • member_names로 주어진 멤버들을 가질 수 있어요.
  • 일반 인자 또는 키워드 인자로 초기화할 수 있어요.

새 하위 클래스는 자기 자신만의 ::new 메서드를 가져요:

Foo = Struct.new('Foo', :foo, :bar) # => Struct::Foo
f = Foo.new(0, 1)                   # => #<struct Struct::Foo foo=0, bar=1>

클래스 이름 (Class Name)

문자열 인자 class_name을 주면 Struct::class_name이라는 이름의 새 하위 클래스를 반환해요:

Foo = Struct.new('Foo', :foo, :bar) # => Struct::Foo
Foo.name                            # => "Struct::Foo"
Foo.superclass                      # => Struct

문자열 인자 class_name이 없으면 익명 하위 클래스를 반환해요:

Struct.new(:foo, :bar).name # => nil

블록 (Block)

블록을 주면 만들어진 하위 클래스를 블록에 넘겨줘요:

Customer = Struct.new('Customer', :name, :address) do |new_class|
  p "The new subclass is #{new_class}"
  def greeting
    "Hello #{name} at #{address}"
  end
end           # => Struct::Customer
dave = Customer.new('Dave', '123 Main')
dave # =>     #<struct Struct::Customer name="Dave", address="123 Main">
dave.greeting # => "Hello Dave at 123 Main"

Struct.new에서 나온 출력:

"The new subclass is Struct::Customer"

멤버 이름 (Member Names)

심볼 인자 member_names가 새 하위 클래스의 멤버를 결정해요:

Struct.new(:foo, :bar).members        # => [:foo, :bar]
Struct.new('Foo', :foo, :bar).members # => [:foo, :bar]

새 하위 클래스에는 member_names에 대응하는 인스턴스 메서드가 있어요:

Foo = Struct.new('Foo', :foo, :bar)
Foo.instance_methods(false) # => [:foo, :bar, :foo=, :bar=]
f = Foo.new                 # => #<struct Struct::Foo foo=nil, bar=nil>
f.foo                       # => nil
f.foo = 0                   # => 0
f.bar                       # => nil
f.bar = 1                   # => 1
f                           # => #<struct Struct::Foo foo=0, bar=1>

싱글턴 메서드 (Singleton Methods)

Struct.new가 반환한 하위 클래스에는 이런 싱글턴 메서드가 있어요:

  • ::new가 하위 클래스의 인스턴스를 만들어요:
Foo.new          # => #<struct Struct::Foo foo=nil, bar=nil>
Foo.new(0)       # => #<struct Struct::Foo foo=0, bar=nil>
Foo.new(0, 1)    # => #<struct Struct::Foo foo=0, bar=1>
Foo.new(0, 1, 2) # Raises ArgumentError: struct size differs

키워드 인자로 초기화:

Foo.new(foo: 0)         # => #<struct Struct::Foo foo=0, bar=nil>
Foo.new(foo: 0, bar: 1) # => #<struct Struct::Foo foo=0, bar=1>
Foo.new(foo: 0, bar: 1, baz: 2)
# Raises ArgumentError: unknown keywords: baz
  • :inspect가 하위 클래스의 문자열 표현을 반환해요:
Foo.inspect
# => "Struct::Foo"
  • ::members가 멤버 이름의 배열을 반환해요:
Foo.members # => [:foo, :bar]

키워드 인자 (Keyword Argument)

기본적으로 새 하위 클래스의 인스턴스 초기화 인자는 위치 인자와 키워드 인자 모두 쓸 수 있어요.

선택적 키워드 인자 keyword_init:로 한 종류의 인자만 받도록 강제할 수 있어요:

KeywordsOnly = Struct.new(:foo, :bar, keyword_init: true)
KeywordsOnly.new(bar: 1, foo: 0)
# => #<struct KeywordsOnly foo=0, bar=1>
KeywordsOnly.new(0, 1)
# Raises ArgumentError: wrong number of arguments

PositionalOnly = Struct.new(:foo, :bar, keyword_init: false)
PositionalOnly.new(0, 1)
# => #<struct PositionalOnly foo=0, bar=1>
PositionalOnly.new(bar: 1, foo: 0)
# => #<struct PositionalOnly foo={:foo=>1, :bar=>2}, bar=nil>
# Note that no error is raised, but arguments treated as one hash value

# Same as not providing keyword_init:
Any = Struct.new(:foo, :bar, keyword_init: nil)
Any.new(foo: 1, bar: 2)
# => #<struct Any foo=1, bar=2>
Any.new(1, 2)
# => #<struct Any foo=1, bar=2>

Public Instance Methods

self == other → true or false

다음이 모두 참일 때만 true를, 그렇지 않으면 false를 반환해요:

  • other.class == self.class.
  • 각 멤버 이름 name에 대해 other.name == self.name.

예시:

Customer = Struct.new(:name, :address, :zip)
joe    = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe_jr = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe_jr == joe # => true
joe_jr[:name] = 'Joe Smith, Jr.'
# => "Joe Smith, Jr."
joe_jr == joe # => false

struct[name] → object

self에서 값을 반환해요.

심볼 또는 문자열 인자 name을 주면 명명된 멤버의 값을 반환해요:

Customer = Struct.new(:name, :address, :zip)
joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe[:zip] # => 12345

name이 멤버 이름이 아니면 NameError를 발생시켜요.

정수 인자 n을 주면, n이 범위 안일 때 self.values[n]을 반환해요. Array의 Array Indexes를 참고하세요:

joe[2]  # => 12345
joe[-2] # => "123 Maple, Anytown NC"

n이 범위를 벗어나면 IndexError를 발생시켜요.

struct[name] = value → value

멤버에 값을 할당해요.

심볼 또는 문자열 인자 name을 주면 명명된 멤버에 주어진 value를 할당하고 value를 반환해요:

Customer = Struct.new(:name, :address, :zip)
joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe[:zip] = 54321 # => 54321
joe # => #<struct Customer name="Joe Smith", address="123 Maple, Anytown NC", zip=54321>

name이 멤버 이름이 아니면 NameError를 발생시켜요.

정수 인자 n을 주면, n이 범위 안일 때 n 번째 멤버에 주어진 value를 할당해요. Array의 Array Indexes를 참고하세요:

joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe[2] = 54321           # => 54321
joe[-3] = 'Joseph Smith' # => "Joseph Smith"
joe # => #<struct Customer name="Joseph Smith", address="123 Maple, Anytown NC", zip=54321>

n이 범위를 벗어나면 IndexError를 발생시켜요.

deconstruct_keys(array_of_names) → hash

주어진 멤버 이름들에 대한 이름/값 쌍의 해시를 반환해요.

Customer = Struct.new(:name, :address, :zip)
joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
h = joe.deconstruct_keys([:zip, :address])
h # => {:zip=>12345, :address=>"123 Maple, Anytown NC"}

array_of_namesnil이면 모든 이름과 값을 반환해요:

h = joe.deconstruct_keys(nil)
h # => {:name=>"Joseph Smith, Jr.", :address=>"123 Maple, Anytown NC", :zip=>12345}

dig(name, *identifiers) → object

중첩 객체들 사이에서 객체를 찾아 반환해요. 중첩 객체는 여러 클래스의 인스턴스일 수 있어요. Dig Methods를 참고하세요.

심볼 또는 문자열 인자 name을 주면 nameidentifiers로 지정된 객체를 반환해요:

Foo = Struct.new(:a)
f = Foo.new(Foo.new({b: [1, 2, 3]}))
f.dig(:a) # => #<struct Foo a={:b=>[1, 2, 3]}>
f.dig(:a, :a) # => {:b=>[1, 2, 3]}
f.dig(:a, :a, :b) # => [1, 2, 3]
f.dig(:a, :a, :b, 0) # => 1
f.dig(:b, 0) # => nil

정수 인자 n을 주면 nidentifiers로 지정된 객체를 반환해요:

f.dig(0) # => #<struct Foo a={:b=>[1, 2, 3]}>
f.dig(0, 0) # => {:b=>[1, 2, 3]}
f.dig(0, 0, :b) # => [1, 2, 3]
f.dig(0, 0, :b, 0) # => 1
f.dig(:b, 0) # => nil

each { |value| ... } → self

주어진 블록을 각 멤버의 값과 함께 호출하고 self를 반환해요:

Customer = Struct.new(:name, :address, :zip)
joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe.each {|value| p value }

Output:

"Joe Smith"
"123 Maple, Anytown NC"
12345

블록이 없으면 Enumerator를 반환해요. 관련: each_pair.

each_pair { |(name, value)| ... } → self

주어진 블록을 각 멤버 이름/값 쌍과 함께 호출하고 self를 반환해요:

Customer = Struct.new(:name, :address, :zip) # => Customer
joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe.each_pair {|(name, value)| p "#{name} => #{value}" }

Output:

"name => Joe Smith"
"address => 123 Maple, Anytown NC"
"zip => 12345"

블록이 없으면 Enumerator를 반환해요. 관련: each.

eql?(other) → true or false

다음이 모두 참일 때만 true를, 그렇지 않으면 false를 반환해요:

  • other.class == self.class.
  • 각 멤버 이름 name에 대해 other.name.eql?(self.name).
Customer = Struct.new(:name, :address, :zip)
joe    = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe_jr = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe_jr.eql?(joe) # => true
joe_jr[:name] = 'Joe Smith, Jr.'
joe_jr.eql?(joe) # => false

관련: Object#==.

hash → integer

self의 정수 해시 값을 반환해요.

같은 클래스의 같은 내용을 가진 두 struct는 같은 해시 코드를 가져요(Struct#eql?로도 비교돼요):

Customer = Struct.new(:name, :address, :zip)
joe    = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe_jr = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe.hash == joe_jr.hash # => true
joe_jr[:name] = 'Joe Smith, Jr.'
joe.hash == joe_jr.hash # => false

관련: Object#hash.

inspect → string

self의 문자열 표현을 반환해요:

Customer = Struct.new(:name, :address, :zip) # => Customer
joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe.inspect # => "#<struct Customer name=\"Joe Smith\", address=\"123 Maple, Anytown NC\", zip=12345>"

members → array_of_symbols

self에서 멤버 이름을 배열로 반환해요:

Customer = Struct.new(:name, :address, :zip)
Customer.new.members # => [:name, :address, :zip]

관련: to_a.

select { |value| ... } → array

블록을 주면 블록이 truthy 값을 반환하는 self의 값 배열을 반환해요:

Customer = Struct.new(:name, :address, :zip)
joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
a = joe.select {|value| value.is_a?(String) }
a # => ["Joe Smith", "123 Maple, Anytown NC"]
a = joe.select {|value| value.is_a?(Integer) }
a # => [12345]

블록이 없으면 Enumerator를 반환해요.

size → integer

멤버의 개수를 반환해요.

Customer = Struct.new(:name, :address, :zip)
joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe.size #=> 3

to_a → array

self의 값을 배열로 반환해요:

Customer = Struct.new(:name, :address, :zip)
joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe.to_a # => ["Joe Smith", "123 Maple, Anytown NC", 12345]

관련: members.

to_h → hash

각 멤버의 이름과 값을 담은 해시를 반환해요:

Customer = Struct.new(:name, :address, :zip)
joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
h = joe.to_h
h # => {:name=>"Joe Smith", :address=>"123 Maple, Anytown NC", :zip=>12345}

블록을 주면 각 이름/값 쌍마다 호출돼요. 블록은 반환 해시에서 키/값 쌍이 되는 2-요소 배열을 반환해야 해요:

h = joe.to_h{|name, value| [name.upcase, value.to_s.upcase]}
h # => {:NAME=>"JOE SMITH", :ADDRESS=>"123 MAPLE, ANYTOWN NC", :ZIP=>"12345"}

블록이 적절하지 않은 값을 반환하면 ArgumentError를 발생시켜요.

values_at(*integers) → array

self에서 값들의 배열을 반환해요.

정수 인자 integers를 주면 integers 중 하나로 주어지는 각 값을 담은 배열을 반환해요:

Customer = Struct.new(:name, :address, :zip)
joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345)
joe.values_at(0, 2)    # => ["Joe Smith", 12345]
joe.values_at(2, 0)    # => [12345, "Joe Smith"]
joe.values_at(2, 1, 0) # => [12345, "123 Maple, Anytown NC", "Joe Smith"]
joe.values_at(0, -3)   # => ["Joe Smith", "Joe Smith"]

integers 중 하나라도 범위를 벗어나면 IndexError를 발생시켜요. Array의 Array Indexes를 참고하세요.

정수 범위 인자 integer_range를 주면 범위의 각 원소가 주는 각 값을 담은 배열을 반환해요. 구조체보다 큰 범위 원소는 nil로 채워요:

joe.values_at(0..2)
# => ["Joe Smith", "123 Maple, Anytown NC", 12345]
joe.values_at(-3..-1)
# => ["Joe Smith", "123 Maple, Anytown NC", 12345]
joe.values_at(1..4) # => ["123 Maple, Anytown NC", 12345, nil, nil]

범위 원소 중 음수이면서 범위를 벗어난 것이 있으면 RangeError를 발생시켜요. Array의 Array Indexes를 참고하세요.