類別資料

類別資料提供一個方便的方式來定義簡單類別以供類值物件使用。

最簡單的使用範例

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 hash 轉換,以及 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)

資料 不提供任何成員寫入器或列舉器:它的目的是作為不可變原子值的儲存。但請注意,如果某些資料成員屬於可變類別,資料 沒有額外的不可變性強制執行

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"]>

另請參閱 結構,它是一個類似的概念,但具有更類似的容器 API,允許變更物件的內容並列舉它。

公開類別方法

定義 (*符號) → 類別 按一下以切換來源

定義一個新的資料類別。

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">

請注意,沒有成員的資料是可以接受的,並且可能是定義幾個同質資料類別的有用技術,例如

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
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_RAW(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;
}
DataClass::members → 符號陣列 按一下以切換來源

傳回資料類別的成員名稱陣列

Measure = Data.define(:amount, :unit)
Measure.members # => [:amount, :unit]
#define rb_data_s_members_m rb_struct_s_members_m
new(*args) → 執行個體 按一下以切換來源
new(**kwargs) → 執行個體
::[](*args) → 執行個體
::[](**kwargs) → 執行個體

使用 ::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">

所有引數都是強制性的(與 結構 不同),並轉換為關鍵字引數

Measure.new(amount: 1)
# in `initialize': missing keyword: :unit (ArgumentError)

Measure.new(1)
# in `initialize': missing keyword: :unit (ArgumentError)

請注意,Measure#initialize 總是接收關鍵字引數,並且在 initialize 中檢查強制性引數,而不是在 new 中。這對於重新定義初始化以轉換引數或提供預設值非常重要

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>>
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));
        }
        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_RAW(self);
    if (arg.unknown_keywords != Qnil) {
        rb_exc_raise(rb_keyword_error_new("unknown", arg.unknown_keywords));
    }
    return Qnil;
}

公開執行個體方法

self == other → true 或 false 按一下以切換來源

如果 otherself 是同一個類別,且所有成員都相等,則傳回 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

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_equal rb_struct_equal
deconstruct → 陣列 按一下以切換來源

傳回 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 rb_struct_to_a
deconstruct_keys(陣列_of_名稱_or_nil) → hash 按一下以切換來源

傳回名稱/值配對的 hash,用於模式比對。

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_deconstruct_keys rb_struct_deconstruct_keys
eql?(other) → true 或 false 按一下以切換來源

當兩個資料項目是 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_eql rb_struct_eql
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
#define rb_data_hash rb_struct_hash
inspect → 字串 按一下以切換來源

傳回 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_inspect(VALUE s)
{
    return rb_exec_recursive(inspect_struct, s, rb_str_new2("#<data "));
}
別名為: to_s
members → 符號陣列 按一下以切換來源

傳回 self 中的成員名稱作為陣列

Measure = Data.define(:amount, :unit)
distance = Measure[10, 'km']

distance.members #=> [:amount, :unit]
#define rb_data_members_m rb_struct_members_m
to_h → hash 按一下以切換來源
to_h {|名稱, 值| ... } → hash

傳回資料物件的 Hash 表示形式。

Measure = Data.define(:amount, :unit)
distance = Measure[10, 'km']

distance.to_h
#=> {:amount=>10, :unit=>"km"}

就像 Enumerable#to_h,如果提供區塊,則預期它會產生用於建構 hash 的金鑰值配對

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
#define rb_data_to_h rb_struct_to_h
to_s → 字串

傳回 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">
別名為: inspect
with(**kwargs) → 實例 按一下以切換來源

傳回 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
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);
}