Ruby的Enumerable#zip是否在内部创建数组?

在Ruby中 -有人说, 优雅地比较两个枚举器

zip的问题在于它在内部创建数组,无论您传递的是什么Enumerable。 输入参数的长度还有另一个问题

我看了一下YARV中Enumerable#zip的实现,并看到了

static VALUE enum_zip(int argc, VALUE *argv, VALUE obj) { int i; ID conv; NODE *memo; VALUE result = Qnil; VALUE args = rb_ary_new4(argc, argv); int allary = TRUE; argv = RARRAY_PTR(args); for (i=0; i<argc; i++) { VALUE ary = rb_check_array_type(argv[i]); if (NIL_P(ary)) { allary = FALSE; break; } argv[i] = ary; } if (!allary) { CONST_ID(conv, "to_enum"); for (i=0; i<argc; i++) { argv[i] = rb_funcall(argv[i], conv, 1, ID2SYM(id_each)); } } if (!rb_block_given_p()) { result = rb_ary_new(); } /* use NODE_DOT2 as memo(v, v, -) */ memo = rb_node_newnode(NODE_DOT2, result, args, 0); rb_block_call(obj, id_each, 0, 0, allary ? zip_ary : zip_i, (VALUE)memo); return result; } 

我是否正确理解了以下位?

检查是否所有参数都是数组,如果是,则使用直接引用替换对数组的某些间接引用

  for (i=0; i<argc; i++) { VALUE ary = rb_check_array_type(argv[i]); if (NIL_P(ary)) { allary = FALSE; break; } argv[i] = ary; } 

如果它们不是所有数组,请改为创建枚举器

  if (!allary) { CONST_ID(conv, "to_enum"); for (i=0; i<argc; i++) { argv[i] = rb_funcall(argv[i], conv, 1, ID2SYM(id_each)); } } 

仅在未给出块时才创建数组数组

  if (!rb_block_given_p()) { result = rb_ary_new(); } 

如果一切都是数组,请使用zip_ary ,否则使用zip_i ,并在每组值上调用一个块

  /* use NODE_DOT2 as memo(v, v, -) */ memo = rb_node_newnode(NODE_DOT2, result, args, 0); rb_block_call(obj, id_each, 0, 0, allary ? zip_ary : zip_i, (VALUE)memo); 

如果没有给出块,则返回一个数组数组,否则返回nil( Qnil )?

  return result; } 

我将使用1.9.2-p0,就像我手边的东西一样。

rb_check_array_type函数如下所示:

 VALUE rb_check_array_type(VALUE ary) { return rb_check_convert_type(ary, T_ARRAY, "Array", "to_ary"); } 

并且rb_check_convert_type看起来像这样:

 VALUE rb_check_convert_type(VALUE val, int type, const char *tname, const char *method) { VALUE v; /* always convert T_DATA */ if (TYPE(val) == type && type != T_DATA) return val; v = convert_type(val, tname, method, FALSE); if (NIL_P(v)) return Qnil; if (TYPE(v) != type) { const char *cname = rb_obj_classname(val); rb_raise(rb_eTypeError, "can't convert %s to %s (%s#%s gives %s)", cname, tname, cname, method, rb_obj_classname(v)); } return v; } 

请注意convert_type调用。 这看起来很像C版的Array.try_converttry_convert恰好看起来像这样:

 /* * call-seq: * Array.try_convert(obj) -> array or nil * * Try to convert obj into an array, using +to_ary+ method. * Returns converted array or +nil+ if obj cannot be converted * for any reason. This method can be used to check if an argument is an * array. * * Array.try_convert([1]) #=> [1] * Array.try_convert("1") #=> nil * * if tmp = Array.try_convert(arg) * # the argument is an array * elsif tmp = String.try_convert(arg) * # the argument is a string * end * */ static VALUE rb_ary_s_try_convert(VALUE dummy, VALUE ary) { return rb_check_array_type(ary); } 

所以,是的,第一个循环是在argv中查找不是数组的任何东西,如果找到这样的东西则设置allary标志。

enum.c ,我们看到:

 id_each = rb_intern("each"); 

所以id_each是Ruby each迭代器方法的内部引用。 在vm_eval.c ,我们有:

 /*! * Calls a method * \param recv receiver of the method * \param mid an ID that represents the name of the method * \param n the number of arguments * \param ... arbitrary number of method arguments * * \pre each of arguments after \an must be a VALUE. */ VALUE rb_funcall(VALUE recv, ID mid, int n, ...) 

所以这:

 argv[i] = rb_funcall(argv[i], conv, 1, ID2SYM(id_each)); 

argv[i]任何内容上调用to_enum (基本上是默认参数 )。

因此,第一个forif块的最终结果是argv要么充满了数组,要么充满了枚举数,而不是两者的混合。 但请注意逻辑是如何工作的:如果找到的东西不是数组,那么一切都变成了枚举器。 enum_zip函数的第一部分将数组包装在枚举器中(基本上是免费的或至少足够便宜而不用担心)但不会将枚举器扩展为数组(这可能非常昂贵)。 早期的版本可能已经走了另一条路(更喜欢数组而不是枚举数),我会将其作为读者或历史学家的练习。

下一部分:

 if (!rb_block_given_p()) { result = rb_ary_new(); } 

如果在没有块的情况下调用zip则创建一个新的空数组并将其保留在result 。 在这里我们应该注意zip返回的内容 :

 enum.zip(arg, ...) → an_array_of_array enum.zip(arg, ...) {|arr| block } → nil 

如果有一个区块,则没有任何东西可以返回, result可以保持为Qnil ; 如果没有块,那么我们需要一个result的数组,以便可以返回一个数组。

parse.c ,我们看到NODE_DOT2是一个双点范围,但看起来他们只是将新节点用作一个简单的三元素结构; rb_new_node只是分配一个对象,设置一些位,并在结构中分配三个值:

 NODE* rb_node_newnode(enum node_type type, VALUE a0, VALUE a1, VALUE a2) { NODE *n = (NODE*)rb_newobj(); n->flags |= T_NODE; nd_set_type(n, type); n->u1.value = a0; n->u2.value = a1; n->u3.value = a2; return n; } 

nd_set_type只是一个nd_set_type宏。 现在我们将memo作为三元素结构。 这种NODE_DOT2使用似乎是一种方便的方法。

rb_block_call函数似乎是核心内部迭代器。 我们再次看到我们的朋友id_each ,所以我们将进行each迭代。 然后我们看到zip_izip_ary之间的选择; 这是创建内部数组并将其推送到resultzip_izip_ary之间的唯一区别似乎是zip_ary中的StopIterationexception处理。

此时我们已经完成了压缩,我们要么在result有数组数组(如果没有块),要么我们在resultQnil (如果有块)。


执行摘要 :第一个循环显式避免将枚举数扩展到数组中。 如果zip_izip_ary调用必须构建一个数组数组作为返回值,则它们只能用于非临时数组。 因此,如果您使用至少一个非数组枚举器调用zip并使用块forms,那么它一直是枚举器,并且“zip的问题是它在内部创建数组”不会发生。 回顾1.8或其他Ruby实现是留给读者的练习。

Interesting Posts