Data 클래스
Data 클래스
Data 클래스는 값과 같은(value-alike) 객체를 위한 간단한 클래스를 정의하는 편리한 방법을 제공해요.
가장 간단한 사용 예시예요:
Measure = Data.define(:amount, :unit)
# 위치 인자 생성자가 제공돼요
distance = Measure.new(100, 'km')
#=> #<data Measure amount=100, unit="km">
# 키워드 인자 생성자도 제공돼요
weight = Measure.new(amount: 50, unit: 'kg')
#=> #<data Measure amount=50, unit="kg">
# 객체를 만드는 또 다른 형태:
speed = Measure[10, 'mPh']
#=> #<data Measure amount=10, unit="mPh">
# 키워드 인자로도 동작해요:
area = Measure[amount: 1.5, unit: 'm^2']
#=> #<data Measure amount=1.5, unit="m^2">
# 인자 접근자(accessor)가 제공돼요:
distance.amount #=> 100
distance.unit #=> "km"
만들어진 객체는 합리적인 == 연산자, to_h 해시 변환, 그리고 패턴 매칭에 쓰이는 deconstruct/deconstruct_keys 정의도 갖고 있어요.
::define 메서드는 선택적 블록을 받아서 새로 정의된 클래스의 컨텍스트에서 평가해요. 이걸로 추가 메서드를 정의할 수 있어요:
Measure = Data.define(:amount, :unit) do
def <=>(other)
return unless other.is_a?(self.class) && other.unit == unit
amount <=> other.amount
end
include Comparable
end
Measure[3, 'm'] < Measure[5, 'm'] #=> true
Measure[3, 'm'] < Measure[5, 'kg']
# comparison of Measure with Measure failed (ArgumentError)
Data는 멤버 writer나 열거자(enumerator)를 제공하지 않아요. 불변의 원자적인 값들을 저장하기 위한 것이거든요. 다만 데이터 멤버 중 일부가 가변 클래스라면 Data가 추가적인 불변성을 강제하지는 않아요:
Event = Data.define(:time, :weekdays)
event = Event.new('18:00', %w[Tue Wed Fri])
#=> #<data Event time="18:00", weekdays=["Tue", "Wed", "Fri"]>
# #time= 이나 #weekdays= 접근자는 없지만, 변경은 여전히 가능해요:
event.weekdays << 'Sat'
event
#=> #<data Event time="18:00", weekdays=["Tue", "Wed", "Fri", "Sat"]>
비슷한 개념으로 Struct도 있어요. 다만 Struct는 더 컨테이너같은 API를 제공해서 객체의 내용을 바꾸고 열거할 수 있어요.
출처: Ruby 3.3 API
본문
클래스 메서드
define(*symbols) → class— 새Data클래스를 정의해요.
measure = Data.define(:amount, :unit)
#=> #<Class:0x00007f70c6868498>
measure.new(1, 'km')
#=> #<data amount=1, unit="km">
# 새 클래스를 상수에 저장하면 #inspect에 영향이 있고 더 자연스러워요:
Measure = Data.define(:amount, :unit)
#=> Measure
Measure.new(1, 'km')
#=> #<data Measure amount=1, unit="km">
멤버가 없는 Data도 괜찮아요. 여러 동질의 데이터 클래스를 정의하는 유용한 기법이 될 수 있어요.
class HTTPFetcher
Response = Data.define(:body)
NotFound = Data.define
# ... implementation
end
이제 HTTPFetcher의 서로 다른 응답 종류들이 일관된 표현을 갖게 돼요. 그리고 패턴 매칭에 쓰기도 편리해요.
case fetcher.get(url)
in HTTPFetcher::Response(body)
# process body variable
in HTTPFetcher::NotFound
# handle not found case
end
DataClass::members → array_of_symbols— 데이터 클래스의 멤버 이름 배열을 돌려줘요:Measure.members # => [:amount, :unit]new(*args) → instance,new(**kwargs) → instance,::[](*args) → instance,::[](**kwargs) → instance—::define으로 정의된 클래스의 생성자들은 위치 인자와 키워드 인자 모두 받아요. 모든 인자는 필수이고(Struct와 달리) 키워드 인자로 변환돼요.
Measure = Data.define(:amount, :unit)
Measure.new(1, 'km') #=> #<data Measure amount=1, unit="km">
Measure.new(amount: 1, unit: 'km') #=> #<data Measure amount=1, unit="km">
Measure[1, 'km'] #=> #<data Measure amount=1, unit="km">
Measure[amount: 1, unit: 'km'] #=> #<data Measure amount=1, unit="km">
Measure#initialize는 항상 키워드 인자를 받고, 필수 인자 검사는 new가 아니라 initialize에서 일어나요. 이것은 인자를 변환하거나 기본값을 주려고 initialize를 재정의할 때 중요해요:
Measure = Data.define(:amount, :unit) do
NONE = Data.define
def initialize(amount:, unit: NONE.new)
super(amount: Float(amount), unit:)
end
end
Measure.new('10', 'km') # => #<data Measure amount=10.0, unit="km">
Measure.new(10_000) # => #<data Measure amount=10000.0, unit=#<data NONE>>
인스턴스 메서드
self == other → true or false—other가self와 같은 클래스이고 모든 멤버가 같으면 true를 돌려줘요.
Measure = Data.define(:amount, :unit)
Measure[1, 'km'] == Measure[1, 'km'] #=> true
Measure[1, 'km'] == Measure[2, 'km'] #=> false
Measure[1, 'km'] == Measure[1, 'm'] #=> false
deconstruct → array— 패턴 매칭에 쓰이도록self의 값들을 배열로 돌려줘요:Measure[10, 'km'].deconstruct #=> [10, "km"]deconstruct_keys(array_of_names_or_nil) → hash— 패턴 매칭에 쓰이도록 이름/값 쌍의 해시를 돌려줘요:Measure[10, 'km'].deconstruct_keys(nil) #=> {:amount=>10, :unit=>"km"}eql?(other) → true or false— 두 데이터 항목이Hash의 키일 때 사용되는 동등성 검사예요.==와의 미묘한 차이는 멤버도eql?메서드로 비교한다는 점이에요.
Measure[1, 'km'] == Measure[1.0, 'km'] #=> true, 값으로는 같아요
Measure[1, 'km'].eql? Measure[1.0, 'km'] #=> false, 다른 해시 키를 나타내요
hash → integer— 같은 클래스의 같은 내용을 가진 데이터 객체가 같은hash값을 갖도록Object#hash를 재정의해요.inspect → string(to_s의 별칭) —self의 문자열 표현을 돌려줘요.p distance #<data Measure amount=10, unit="km">members → array_of_symbols—self의 멤버 이름 배열을 돌려줘요:distance.members #=> [:amount, :unit]to_h → hash,to_h {|name, value| ... } → hash— 데이터 객체의Hash표현을 돌려줘요.
distance.to_h #=> {:amount=>10, :unit=>"km"}
distance.to_h { |name, val| [name.to_s, val.to_s] } #=> {"amount"=>"10", "unit"=>"km"}
to_h와 initialize 사이에는 유용한 대칭이 있어요: Measure.new(**distance.to_h)가 원래 객체와 같은 값을 만들어 줘요.
with(**kwargs) → instance—self의 얕은 복사본을 돌려줘요. 키워드 인자를 주면 해당 필드 값을 그 키워드 값으로 갱신한 복사본을 만들어요.Data클래스가 멤버로 갖지 않는 키워드를 주면 오류예요.
Point = Data.define(:x, :y)
origin = Point.new(x: 0, y: 0)
up = origin.with(x: 1) # #<data Point x=1, y=0>
out = origin.with(z: 1) # ArgumentError: unknown keyword: :z
some_point = origin.with(1, 2) # ArgumentError: expected keyword arguments, got positional arguments
더 알아보기
- Struct — 비슷하지만 더 컨테이너같은 API를 제공하는 개념