Класс Data
Класс Data предоставляет удобный способ определения простых классов для объектов, подобных значениям.
Простейший пример использования:
Measure = Data.define(:amount, :unit) # Positional arguments constructor is provided distance = Measure.new(100, 'km') #=> #<data Measure amount=100, unit="km"> # Keyword arguments constructor is provided weight = Measure.new(amount: 50, unit: 'kg') #=> #<data Measure amount=50, unit="kg"> # Alternative form to construct an object: speed = Measure[10, 'mPh'] #=> #<data Measure amount=10, unit="mPh"> # Works with keyword arguments, too: area = Measure[amount: 1.5, unit: 'm^2'] #=> #<data Measure amount=1.5, unit="m^2"> # Argument accessors are provided: 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 не предоставляет методов записи для элементов или перечислителей: он предназначен для хранения неизменяемых атомарных значений. Однако учтите, что если один из элементов данных относится к изменяемому классу, Data не обеспечивает дополнительную защиту от изменений:
Event = Data.define(:time, :weekdays)
event = Event.new('18:00', %w[Tue Wed Fri])
#=> #<data Event time="18:00", weekdays=["Tue", "Wed", "Fri"]>
# There is no #time= or #weekdays= accessors, but changes are
# still possible:
event.weekdays << 'Sat'
event
#=> #<data Event time="18:00", weekdays=["Tue", "Wed", "Fri", "Sat"]>
См. также Struct — похожую концепцию с более ориентированным на контейнер API, позволяющим изменять содержимое объекта и перебирать его.
Общедоступные методы класса
static VALUE
rb_data_s_def(int argc, VALUE *argv, VALUE klass)
{
VALUE rest;
long i;
VALUE data_class;
rest = rb_ident_hash_new();
RBASIC_CLEAR_CLASS(rest);
for (i=0; i<argc; i++) {
VALUE mem = rb_to_symbol(argv[i]);
if (rb_is_attrset_sym(mem)) {
rb_raise(rb_eArgError, "invalid data member: %"PRIsVALUE, mem);
}
if (RTEST(rb_hash_has_key(rest, mem))) {
rb_raise(rb_eArgError, "duplicate member: %"PRIsVALUE, mem);
}
rb_hash_aset(rest, mem, Qtrue);
}
rest = rb_hash_keys(rest);
RBASIC_CLEAR_CLASS(rest);
OBJ_FREEZE(rest);
data_class = anonymous_struct(klass);
setup_data(data_class, rest);
if (rb_block_given_p()) {
rb_mod_module_eval(0, 0, data_class);
}
return data_class;
} Определяет новый класс Data.
measure = Data.define(:amount, :unit) #=> #<Class:0x00007f70c6868498> measure.new(1, 'km') #=> #<data amount=1, unit="km"> # It you store the new class in the constant, it will # affect #inspect and will be more natural to use: 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 будут иметь единообразное представление:
#<data HTTPFetcher::Response body="<html..."> #<data HTTPFetcher::NotFound>
И будут удобны для использования при сопоставлении с образцом:
case fetcher.get(url) in HTTPFetcher::Response(body) # process body variable in HTTPFetcher::NotFound # handle not found case end
#define rb_data_s_members_m rb_struct_s_members_m
Возвращает массив имён элементов класса данных:
Measure = Data.define(:amount, :unit) Measure.members # => [:amount, :unit]
static VALUE
rb_data_initialize_m(int argc, const VALUE *argv, VALUE self)
{
VALUE klass = rb_obj_class(self);
rb_struct_modify(self);
VALUE members = struct_ivar_get(klass, id_members);
size_t num_members = RARRAY_LEN(members);
if (argc == 0) {
if (num_members > 0) {
rb_exc_raise(rb_keyword_error_new("missing", members));
}
OBJ_FREEZE(self);
return Qnil;
}
if (argc > 1 || !RB_TYPE_P(argv[0], T_HASH)) {
rb_error_arity(argc, 0, 0);
}
if (RHASH_SIZE(argv[0]) < num_members) {
VALUE missing = rb_ary_diff(members, rb_hash_keys(argv[0]));
rb_exc_raise(rb_keyword_error_new("missing", missing));
}
struct struct_hash_set_arg arg;
rb_mem_clear((VALUE *)RSTRUCT_CONST_PTR(self), num_members);
arg.self = self;
arg.unknown_keywords = Qnil;
rb_hash_foreach(argv[0], struct_hash_set_i, (VALUE)&arg);
// Freeze early before potentially raising, so that we don't leave an
// unfrozen copy on the heap, which could get exposed via ObjectSpace.
OBJ_FREEZE(self);
if (arg.unknown_keywords != Qnil) {
rb_exc_raise(rb_keyword_error_new("unknown", arg.unknown_keywords));
}
return Qnil;
} Конструкторы классов, определённых с помощью ::define, принимают как позиционные, так и именованные аргументы.
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"> # Alternative shorter initialization with [] Measure[1, 'km'] #=> #<data Measure amount=1, unit="km"> Measure[amount: 1, unit: 'km'] #=> #<data Measure amount=1, unit="km">
Все аргументы обязательны (в отличие от Struct) и преобразуются в именованные аргументы:
Measure.new(amount: 1) # in `initialize': missing keyword: :unit (ArgumentError) Measure.new(1) # in `initialize': missing keyword: :unit (ArgumentError)
Обратите внимание, что Measure#initialize всегда получает именованные аргументы, а обязательные аргументы проверяются в initialize, а не в new. Это может быть важно при переопределении initialize для преобразования аргументов или задания значений по умолчанию:
Measure = Data.define(:amount, :unit)
class Measure
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 Measure::NONE>>
Общедоступные методы экземпляра
#define rb_data_equal rb_struct_equal
Возвращает true, если other принадлежит тому же классу, что и self, и все элементы равны.
Примеры:
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 Measurement = Data.define(:amount, :unit) # Even though Measurement and Measure have the same "shape" # their instances are never equal Measure[1, 'km'] == Measurement[1, 'km'] #=> false
#define rb_data_deconstruct rb_struct_to_a
Возвращает значения в self в виде массива для использования при сопоставлении с образцом:
Measure = Data.define(:amount, :unit)
distance = Measure[10, 'km']
distance.deconstruct #=> [10, "km"]
# usage
case distance
in n, 'km' # calls #deconstruct underneath
puts "It is #{n} kilometers away"
else
puts "Don't know how to handle it"
end
# prints "It is 10 kilometers away"
Или с проверкой класса:
case distance
in Measure(n, 'km')
puts "It is #{n} kilometers away"
# ...
end
#define rb_data_deconstruct_keys rb_struct_deconstruct_keys
Возвращает хеш пар «имя/значение» для использования при сопоставлении с образцом.
Measure = Data.define(:amount, :unit)
distance = Measure[10, 'km']
distance.deconstruct_keys(nil) #=> {:amount=>10, :unit=>"km"}
distance.deconstruct_keys([:amount]) #=> {:amount=>10}
# usage
case distance
in amount:, unit: 'km' # calls #deconstruct_keys underneath
puts "It is #{amount} kilometers away"
else
puts "Don't know how to handle it"
end
# prints "It is 10 kilometers away"
Или с проверкой класса:
case distance
in Measure(amount:, unit: 'km')
puts "It is #{amount} kilometers away"
# ...
end
#define rb_data_eql rb_struct_eql
Проверка равенства, используемая, когда два элемента данных являются ключами Hash.
Небольшое отличие от == заключается в том, что элементы также сравниваются с помощью метода eql?, что может быть важно в некоторых случаях:
Measure = Data.define(:amount, :unit) Measure[1, 'km'] == Measure[1.0, 'km'] #=> true, they are equal as values # ...but... Measure[1, 'km'].eql? Measure[1.0, 'km'] #=> false, they represent different hash keys
Дополнительные объяснения использования метода см. также в описании Object#eql?.
#define rb_data_hash rb_struct_hash
Переопределяет Object#hash (используемый для различения объектов в качестве ключей Hash), чтобы объекты данных одного класса с одинаковым содержимым имели одинаковое значение hash и представляли один и тот же ключ Hash.
Measure = Data.define(:amount, :unit) Measure[1, 'km'].hash == Measure[1, 'km'].hash #=> true Measure[1, 'km'].hash == Measure[10, 'km'].hash #=> false Measure[1, 'km'].hash == Measure[1, 'm'].hash #=> false Measure[1, 'km'].hash == Measure[1.0, 'km'].hash #=> false # Structurally similar data class, but shouldn't be considered # the same hash key Measurement = Data.define(:amount, :unit) Measure[1, 'km'].hash == Measurement[1, 'km'].hash #=> false
static VALUE
rb_data_inspect(VALUE s)
{
return rb_exec_recursive(inspect_struct, s, rb_str_new2("#<data "));
} Возвращает строковое представление self:
Measure = Data.define(:amount, :unit) distance = Measure[10, 'km'] p distance # uses #inspect underneath #<data Measure amount=10, unit="km"> puts distance # uses #to_s underneath, same representation #<data Measure amount=10, unit="km">
#define rb_data_members_m rb_struct_members_m
Возвращает имена элементов из self в виде массива:
Measure = Data.define(:amount, :unit) distance = Measure[10, 'km'] distance.members #=> [:amount, :unit]
#define rb_data_to_h rb_struct_to_h
Возвращает представление объекта данных в виде Hash.
Measure = Data.define(:amount, :unit)
distance = Measure[10, 'km']
distance.to_h
#=> {:amount=>10, :unit=>"km"}
Как и в случае с Enumerable#to_h, если передан блок, он должен возвращать пары «ключ-значение» для создания хеша:
distance.to_h { |name, val| [name.to_s, val.to_s] }
#=> {"amount"=>"10", "unit"=>"km"}
Обратите внимание на полезную симметрию между to_h и initialize:
distance2 = Measure.new(**distance.to_h) #=> #<data Measure amount=10, unit="km"> distance2 == distance #=> true
Возвращает строковое представление self:
Measure = Data.define(:amount, :unit) distance = Measure[10, 'km'] p distance # uses #inspect underneath #<data Measure amount=10, unit="km"> puts distance # uses #to_s underneath, same representation #<data Measure amount=10, unit="km">
static VALUE
rb_data_with(int argc, const VALUE *argv, VALUE self)
{
VALUE kwargs;
rb_scan_args(argc, argv, "0:", &kwargs);
if (NIL_P(kwargs)) {
return self;
}
VALUE h = rb_struct_to_h(self);
rb_hash_update_by(h, kwargs, 0);
return rb_class_new_instance_kw(1, &h, rb_obj_class(self), TRUE);
} Возвращает поверхностную копию self: копируются переменные экземпляра self, но не объекты, на которые они ссылаются.
Если методу переданы именованные аргументы, копия будет создана с соответствующими обновлёнными значениями полей. Обратите внимание, что передавать именованный аргумент, которого нет среди элементов класса Data, нельзя.
Point = Data.define(:x, :y) origin = Point.new(x: 0, y: 0) up = origin.with(x: 1) right = origin.with(y: 1) up_and_right = up.with(y: 1) p origin # #<data Point x=0, y=0> p up # #<data Point x=1, y=0> p right # #<data Point x=0, y=1> p up_and_right # #<data Point x=1, y=1> out = origin.with(z: 1) # ArgumentError: unknown keyword: :z some_point = origin.with(1, 2) # ArgumentError: expected keyword arguments, got positional arguments
Ruby Core © 1993–2025 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.