класс Enumerator
Класс, позволяющий осуществлять итерацию как внутри, так и снаружи.
Перечислитель можно создать с помощью следующих методов.
Большинство методов имеют две формы: форму с блоком, где содержимое оценивается для каждого элемента перечисления, и форму без блока, которая возвращает новый перечислитель, оборачивающий итерацию.
enumerator = %w(one two three).each
puts enumerator.class # => Enumerator
enumerator.each_with_object("foo") do |item, obj|
puts "#{obj}: #{item}"
end
# foo: one
# foo: two
# foo: three
enum_with_obj = enumerator.each_with_object("foo")
puts enum_with_obj.class # => Enumerator
enum_with_obj.each do |item, obj|
puts "#{obj}: #{item}"
end
# foo: one
# foo: two
# foo: three
Это позволяет объединять перечислители в цепочки. Например, вы можете преобразовать элементы списка в строки, содержащие индекс и элемент как строку, следующим образом:
puts %w[foo bar baz].map.with_index { |w, i| "#{i}:#{w}" }
# => ["0:foo", "1:bar", "2:baz"]
Внешняя итерация
Перечислитель также может использоваться как внешний итератор. Например, Enumerator#next возвращает следующее значение итератора или возбуждает исключение StopIteration, если перечислитель достиг конца.
e = [1,2,3].each # returns an enumerator object. puts e.next # => 1 puts e.next # => 2 puts e.next # => 3 puts e.next # raises StopIteration
next, next_values, peek, и peek_values — единственные методы, использующие внешнюю итерацию (и Array#zip(Enumerable-not-Array), который использует next внутри).
Эти методы не влияют на другие методы внутренней итерации, если только сам метод основной итерации не имеет побочных эффектов, например, IO#each_line.
FrozenError будет возбуждено, если эти методы вызываются для замороженного перечислителя. Поскольку rewind и feed также изменяют состояние для внешней итерации, эти методы также могут возбуждать FrozenError.
Внешняя итерация существенно отличается от внутренней из-за использования волокна:
-
Волокно добавляет некоторую накладную стоимость по сравнению с внутренней итерацией.
-
Трассировка стека будет содержать только стек из перечислителя, а не из вышестоящих уровней.
-
Локальные переменные волокна не наследуются внутри перечислителя, который вместо этого начинает без локальных переменных волокна.
-
Переменные хранения волокна наследуются и предназначены для обработки волокон перечислителя. Присвоение переменной хранения волокна влияет только на текущее волокно, поэтому, если вы хотите изменить состояние вызывающего волокна перечислителя, вам нужна дополнительная косвенность (например, используйте некоторый объект в переменной хранения волокна и измените его ivar).
Конкретно:
Thread.current[:fiber_local] = 1
Fiber[:storage_var] = 1
e = Enumerator.new do |y|
p Thread.current[:fiber_local] # for external iteration: nil, for internal iteration: 1
p Fiber[:storage_var] # => 1, inherited
Fiber[:storage_var] += 1
y << 42
end
p e.next # => 42
p Fiber[:storage_var] # => 1 (it ran in a different Fiber)
e.each { p _1 }
p Fiber[:storage_var] # => 2 (it ran in the same Fiber/"stack" as the current Fiber)
Преобразование внешней итерации во внутреннюю итерацию
Внешний итератор можно использовать для реализации внутреннего итератора следующим образом:
def ext_each(e)
while true
begin
vs = e.next_values
rescue StopIteration
return $!.result
end
y = yield(*vs)
e.feed y
end
end
o = Object.new
def o.each
puts yield
puts yield(1)
puts yield(1, 2)
3
end
# use o.each as an internal iterator directly.
puts o.each {|*x| puts x; [:b, *x] }
# => [], [:b], [1], [:b, 1], [1, 2], [:b, 1, 2], 3
# convert o.each to an external iterator for
# implementing an internal iterator.
puts ext_each(o.to_enum) {|*x| puts x; [:b, *x] }
# => [], [:b], [1], [:b, 1], [1, 2], [:b, 1, 2], 3
Методы публичного класса
static VALUE
enumerator_initialize(int argc, VALUE *argv, VALUE obj)
{
VALUE iter = rb_block_proc();
VALUE recv = generator_init(generator_allocate(rb_cGenerator), iter);
VALUE arg0 = rb_check_arity(argc, 0, 1) ? argv[0] : Qnil;
VALUE size = convert_to_feasible_size_value(arg0);
return enumerator_init(obj, recv, sym_each, 0, 0, 0, size, false);
} Создаёт новый объект Enumerator, который можно использовать как Enumerable.
Итерация определяется заданным блоком, в котором объект «yielder», переданный в качестве параметра блока, может использоваться для получения значения путём вызова метода yield (алиас <<):
fib = Enumerator.new do |y|
a = b = 1
loop do
y << a
a, b = b, a + b
end
end
fib.take(10) # => [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Необязательный параметр может использоваться для указания способа вычисления размера ленивым способом (см. Enumerator#size). Он может быть значением или вызываемым объектом.
static VALUE
enumerator_s_produce(int argc, VALUE *argv, VALUE klass)
{
VALUE init, producer;
if (!rb_block_given_p()) rb_raise(rb_eArgError, "no block given");
if (rb_scan_args(argc, argv, "01", &init) == 0) {
init = Qundef;
}
producer = producer_init(producer_allocate(rb_cEnumProducer), init, rb_block_proc());
return rb_enumeratorize_with_size_kw(producer, sym_each, 0, 0, producer_size, RB_NO_KEYWORDS);
} Создаёт бесконечный перечислитель из любого блока, просто вызывая его снова и снова. Результат предыдущей итерации передаётся следующей. Если initial предоставлен, он передаётся в первую итерацию и становится первым элементом перечислителя; если он не предоставлен, первая итерация получает nil, и её результат становится первым элементом итератора.
Возбуждение StopIteration из блока останавливает итерацию.
Enumerator.produce(1, &:succ) # => enumerator of 1, 2, 3, 4, ....
Enumerator.produce { rand(10) } # => infinite random number sequence
ancestors = Enumerator.produce(node) { |prev| node = prev.parent or raise StopIteration }
enclosing_section = ancestors.find { |n| n.type == :section }
Использование ::produce вместе с методами Enumerable, такими как Enumerable#detect, Enumerable#slice_after, Enumerable#take_while, может предоставить перечислительные альтернативы циклам while и until:
# Find next Tuesday
require "date"
Enumerator.produce(Date.today, &:succ).detect(&:tuesday?)
# Simple lexer:
require "strscan"
scanner = StringScanner.new("7+38/6")
PATTERN = %r{\d+|[-/+*]}
Enumerator.produce { scanner.scan(PATTERN) }.slice_after { scanner.eos? }.first
# => ["7", "+", "38", "/", "6"]
static VALUE
enumerator_s_product(int argc, VALUE *argv, VALUE klass)
{
VALUE enums = Qnil, options = Qnil, block = Qnil;
rb_scan_args(argc, argv, "*:&", &enums, &options, &block);
if (!NIL_P(options) && !RHASH_EMPTY_P(options)) {
rb_exc_raise(rb_keyword_error_new("unknown", rb_hash_keys(options)));
}
VALUE obj = enum_product_initialize(argc, argv, enum_product_allocate(rb_cEnumProduct));
if (!NIL_P(block)) {
enum_product_run(obj, block);
return Qnil;
}
return obj;
} Генерирует новый объект перечислителя, который генерирует декартово произведение заданных перечислимых объектов. Это эквивалентно Enumerator::Product.new.
e = Enumerator.product(1..3, [4, 5]) e.to_a #=> [[1, 4], [1, 5], [2, 4], [2, 5], [3, 4], [3, 5]] e.size #=> 6
Когда задан блок, вызывается блок с каждым N-элементным массивом и возвращается nil.
Общедоступные методы экземпляров
static VALUE
enumerator_plus(VALUE obj, VALUE eobj)
{
return new_enum_chain(rb_ary_new_from_args(2, obj, eobj));
} Возвращает объект перечислителя, сгенерированный из этого перечислителя и заданного перечисляемого объекта.
e = (1..3).each + [4, 5] e.to_a #=> [1, 2, 3, 4, 5]
static VALUE
enumerator_each(int argc, VALUE *argv, VALUE obj)
{
struct enumerator *e = enumerator_ptr(obj);
if (argc > 0) {
VALUE args = (e = enumerator_ptr(obj = rb_obj_dup(obj)))->args;
if (args) {
#if SIZEOF_INT < SIZEOF_LONG
/* check int range overflow */
rb_long2int(RARRAY_LEN(args) + argc);
#endif
args = rb_ary_dup(args);
rb_ary_cat(args, argv, argc);
}
else {
args = rb_ary_new4(argc, argv);
}
RB_OBJ_WRITE(obj, &e->args, args);
e->size = Qnil;
e->size_fn = 0;
}
if (!rb_block_given_p()) return obj;
if (!lazy_precheck(e->procs)) return Qnil;
return enumerator_block_call(obj, 0, obj);
} Итерирует по блоку в соответствии со способом построения этого Enumerator. Если блок и аргументы не заданы, возвращает self.
Примеры
"Hello, world!".scan(/\w+/) #=> ["Hello", "world"]
"Hello, world!".to_enum(:scan, /\w+/).to_a #=> ["Hello", "world"]
"Hello, world!".to_enum(:scan).each(/\w+/).to_a #=> ["Hello", "world"]
obj = Object.new
def obj.each_arg(a, b=:b, *rest)
yield a
yield b
yield rest
:method_returned
end
enum = obj.to_enum :each_arg, :a, :x
enum.each.to_a #=> [:a, :x, []]
enum.each.equal?(enum) #=> true
enum.each { |elm| elm } #=> :method_returned
enum.each(:y, :z).to_a #=> [:a, :x, [:y, :z]]
enum.each(:y, :z).equal?(enum) #=> false
enum.each(:y, :z) { |elm| elm } #=> :method_returned
static VALUE
enumerator_each_with_index(VALUE obj)
{
return enumerator_with_index(0, NULL, obj);
} То же, что и Enumerator#with_index(0), т.е. нет начального смещения.
Если блок не задан, возвращается новый Enumerator, который включает индекс.
static VALUE
enumerator_with_object(VALUE obj, VALUE memo)
{
RETURN_SIZED_ENUMERATOR(obj, 1, &memo, enumerator_enum_size);
enumerator_block_call(obj, enumerator_with_object_i, memo);
return memo;
} Итерирует заданный блок для каждого элемента с произвольным объектом, obj, и возвращает obj
Если блок не задан, возвращается новый Enumerator.
Пример
to_three = Enumerator.new do |y|
3.times do |x|
y << x
end
end
to_three_with_string = to_three.with_object("foo")
to_three_with_string.each do |x,string|
puts "#{string}: #{x}"
end
# => foo: 0
# => foo: 1
# => foo: 2
static VALUE
enumerator_feed(VALUE obj, VALUE v)
{
struct enumerator *e = enumerator_ptr(obj);
rb_check_frozen(obj);
if (!UNDEF_P(e->feedvalue)) {
rb_raise(rb_eTypeError, "feed value already set");
}
RB_OBJ_WRITE(obj, &e->feedvalue, v);
return Qnil;
} Устанавливает значение, которое будет возвращено следующим yield внутри e.
Если значение не установлено, yield возвращает nil.
Это значение очищается после того, как оно было передано.
# Array#map passes the array's elements to "yield" and collects the
# results of "yield" as an array.
# Following example shows that "next" returns the passed elements and
# values passed to "feed" are collected as an array which can be
# obtained by StopIteration#result.
e = [1,2,3].map
p e.next #=> 1
e.feed "a"
p e.next #=> 2
e.feed "b"
p e.next #=> 3
e.feed "c"
begin
e.next
rescue StopIteration
p $!.result #=> ["a", "b", "c"]
end
o = Object.new
def o.each
x = yield # (2) blocks
p x # (5) => "foo"
x = yield # (6) blocks
p x # (8) => nil
x = yield # (9) blocks
p x # not reached w/o another e.next
end
e = o.to_enum
e.next # (1)
e.feed "foo" # (3)
e.next # (4)
e.next # (7)
# (10)
static VALUE
enumerator_inspect(VALUE obj)
{
return rb_exec_recursive(inspect_enumerator, obj, 0);
} Создаёт удобочитаемую версию e.
static VALUE
enumerator_next(VALUE obj)
{
VALUE vs = enumerator_next_values(obj);
return ary2sv(vs, 0);
} Возвращает следующий объект в перечислителе и перемещает внутреннюю позицию вперёд. Когда позиция достигает конца, поднимается исключение StopIteration.
Пример
a = [1,2,3] e = a.to_enum p e.next #=> 1 p e.next #=> 2 p e.next #=> 3 p e.next #raises StopIteration
См. заметки на уровне класса об внешних итераторах.
static VALUE
enumerator_next_values(VALUE obj)
{
struct enumerator *e = enumerator_ptr(obj);
VALUE vs;
rb_check_frozen(obj);
if (!UNDEF_P(e->lookahead)) {
vs = e->lookahead;
e->lookahead = Qundef;
return vs;
}
return get_next_values(obj, e);
} Возвращает следующий объект как массив в перечислителе и перемещает внутреннюю позицию вперёд. Когда позиция достигает конца, поднимается исключение StopIteration.
См. заметки на уровне класса об внешних итераторах.
Этот метод может использоваться для различения yield и yield nil.
Пример
o = Object.new def o.each yield yield 1 yield 1, 2 yield nil yield [1, 2] end e = o.to_enum p e.next_values p e.next_values p e.next_values p e.next_values p e.next_values e = o.to_enum p e.next p e.next p e.next p e.next p e.next ## yield args next_values next # yield [] nil # yield 1 [1] 1 # yield 1, 2 [1, 2] [1, 2] # yield nil [nil] nil # yield [1, 2] [[1, 2]] [1, 2]
static VALUE
enumerator_peek(VALUE obj)
{
VALUE vs = enumerator_peek_values(obj);
return ary2sv(vs, 1);
} Возвращает следующий объект в перечислителе, но не перемещает внутреннюю позицию вперёд. Если позиция уже в конце, поднимается исключение StopIteration.
См. заметки на уровне класса об внешних итераторах.
Пример
a = [1,2,3] e = a.to_enum p e.next #=> 1 p e.peek #=> 2 p e.peek #=> 2 p e.peek #=> 2 p e.next #=> 2 p e.next #=> 3 p e.peek #raises StopIteration
static VALUE
enumerator_peek_values_m(VALUE obj)
{
return rb_ary_dup(enumerator_peek_values(obj));
} Возвращает следующий объект как массив, аналогично Enumerator#next_values, но не перемещает внутреннюю позицию вперёд. Если позиция уже в конце, поднимается исключение StopIteration.
См. заметки на уровне класса об внешних итераторах.
Пример
o = Object.new def o.each yield yield 1 yield 1, 2 end e = o.to_enum p e.peek_values #=> [] e.next p e.peek_values #=> [1] p e.peek_values #=> [1] e.next p e.peek_values #=> [1, 2] e.next p e.peek_values # raises StopIteration
static VALUE
enumerator_rewind(VALUE obj)
{
struct enumerator *e = enumerator_ptr(obj);
rb_check_frozen(obj);
rb_check_funcall(e->obj, id_rewind, 0, 0);
e->fib = 0;
e->dst = Qnil;
e->lookahead = Qundef;
e->feedvalue = Qundef;
e->stop_exc = Qfalse;
return obj;
} Перематывает последовательность перечисления в начало.
Если заключённый объект отвечает методу «rewind», он вызывается.
static VALUE
enumerator_size(VALUE obj)
{
struct enumerator *e = enumerator_ptr(obj);
int argc = 0;
const VALUE *argv = NULL;
VALUE size;
if (e->procs) {
struct generator *g = generator_ptr(e->obj);
VALUE receiver = rb_check_funcall(g->obj, id_size, 0, 0);
long i = 0;
for (i = 0; i < RARRAY_LEN(e->procs); i++) {
VALUE proc = RARRAY_AREF(e->procs, i);
struct proc_entry *entry = proc_entry_ptr(proc);
lazyenum_size_func *size_fn = entry->fn->size;
if (!size_fn) {
return Qnil;
}
receiver = (*size_fn)(proc, receiver);
}
return receiver;
}
if (e->size_fn) {
return (*e->size_fn)(e->obj, e->args, obj);
}
if (e->args) {
argc = (int)RARRAY_LEN(e->args);
argv = RARRAY_CONST_PTR(e->args);
}
size = rb_check_funcall_kw(e->size, id_call, argc, argv, e->kw_splat);
if (!UNDEF_P(size)) return size;
return e->size;
} Возвращает размер перечислителя или nil , если его нельзя вычислить лениво.
(1..100).to_a.permutation(4).size # => 94109400 loop.size # => Float::INFINITY (1..100).drop_while.size # => nil
static VALUE
enumerator_with_index(int argc, VALUE *argv, VALUE obj)
{
VALUE memo;
rb_check_arity(argc, 0, 1);
RETURN_SIZED_ENUMERATOR(obj, argc, argv, enumerator_enum_size);
memo = (!argc || NIL_P(memo = argv[0])) ? INT2FIX(0) : rb_to_int(memo);
return enumerator_block_call(obj, enumerator_with_index_i, (VALUE)MEMO_NEW(memo, 0, 0));
} Итерирует заданный блок для каждого элемента с индексом, который начинается со offset. Если блок не задан, возвращается новый Enumerator , который включает индекс, начиная со offset
-
offset -
начальный индекс для использования
Итерирует заданный блок для каждого элемента с произвольным объектом, obj, и возвращает obj
Если блок не задан, возвращается новый Enumerator.
Пример
to_three = Enumerator.new do |y|
3.times do |x|
y << x
end
end
to_three_with_string = to_three.with_object("foo")
to_three_with_string.each do |x,string|
puts "#{string}: #{x}"
end
# => foo: 0
# => foo: 1
# => foo: 2
Ruby Core © 1993–2022 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.