【问题标题】:Does Ruby's Enumerable#zip create arrays internally?Ruby 的 Enumerable#zip 是否在内部创建数组?
【发布时间】:2011-09-23 04:25:18
【问题描述】:

Ruby - Compare two Enumerators elegantly,有人说

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;
}

【问题讨论】:

    标签: c ruby array-merge yarv


    【解决方案1】:

    我将使用 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 调用。这看起来很像 Array.try_converttry_convert 的 C 版本,只是恰好看起来像这样:

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

    正在调用to_enum(基本上是default argument)对argv[i] 中的任何内容。

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

    下一部分:

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

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

    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 只是一个有点摆弄的宏。现在我们将memo 作为一个三元素结构。 NODE_DOT2 的这种用法看起来很方便。

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

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


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

    【讨论】:

    • "如果没有块,则没有什么可以返回,结果可以保持为 Qnil" - 你的意思是“如果有块”吗?
    • @Andrew:正确,已更正。毕竟我是在 05:00 写的 :)
    • “备忘录”是什么意思?是memoization的缩写吗?
    • @Andrew:他们可能在"memoization" 意义上使用“备忘录”,但我认为“一张纸”更能代表使用;请记住,Matz 是日本人,因此他的口语英语与我们的不符。我可能会称它为“状态”或“上下文”。如果您查看rb_block_call 内部,您会看到该参数被称为相当通用的data2,它只是在迭代过程中携带的一小包数据。
    • @mu 太短:或者它可能意味着“记忆”,我猜。
    猜你喜欢
    • 2013-09-05
    • 2013-04-01
    • 2016-08-31
    • 1970-01-01
    • 2014-08-14
    • 2011-11-30
    • 2015-02-07
    • 2020-11-15
    • 2013-09-07
    相关资源
    最近更新 更多