gpt4 book ai didi

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

转载 作者:数据小太阳 更新时间:2023-10-29 06:54:12 26 4
gpt4 key购买 nike

Ruby - Compare two Enumerators elegantly ,据说

The problem with zip is that it creates arrays internally, no matter what Enumerable you pass. There's another problem with length of input params

我查看了 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));
}
}

只有在没有给定 block 时才创建数组的数组

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

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

    /* 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);

如果没有给出 block ,返回一个数组数组,否则返回 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 <i>obj</i> into an array, using +to_ary+ method.
* Returns converted array or +nil+ if <i>obj</i> 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 \a n 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(本质上是 default argument)。

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

下一部分:

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

如果调用 zip 时没有 block ,则创建一个新的空数组并将其保留在 result 中。这里我们应该注意什么 zip returns :

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

如果有一个 block ,那么就没有什么可以返回,result可以保持为Qnil;如果没有 block ,那么我们需要在 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 只是一个微不足道的宏。现在我们有了 memo 作为一个三元素结构。 NODE_DOT2 的这种使用似乎是一种方便的拼凑。

rb_block_call 函数似乎是核心内部迭代器。我们再次看到我们的 friend id_each,所以我们将进行一次 each 迭代。然后我们看到在 zip_izip_ary 之间进行选择;这是创建内部数组并将其推送到 result 的地方。 zip_izip_ary 之间的唯一区别似乎是 zip_i 中的 StopIteration 异常处理。

此时我们已经完成了压缩,我们要么在 result 中有数组的数组(如果没有 block ),要么在 中有 Qnil >result(如果有 block )。


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

关于c - Ruby 的 Enumerable#zip 是否在内部创建数组?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6487747/

26 4 0