8

Rubyでは- 2 つの Enumerator をエレガントに比較すると、

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

1 に答える 1

7

手元にある 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_convertたまたま次のtry_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");

Ruby iteratorメソッドid_eachの内部参照も同様です。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にあるものを(基本的に、既定の引数を使用して)呼び出していますargv[i]

そのため、最初のforandifブロックの最終結果はargv、おそらく 2 つの組み合わせではなく、配列または列挙子でいっぱいになります。ただし、ロジックがどのように機能するかに注意してください。配列ではないものが見つかった場合、すべてが列挙子になります。関数の最初の部分は、enum_zip列挙子で配列をラップします (これは本質的に無料であるか、少なくとも心配する必要がないほど安価です) が、列挙子を配列に展開しません (これは非常に高価になる可能性があります)。以前のバージョンでは逆の方向に進んでいた可能性があります (列挙子よりも配列を優先)。それは、読者または歴史家の課題として残しておきます。

次の部分:

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

がブロックなしで呼び出されたresult場合、新しい空の配列を作成し、そのままにします。ここで、何が返さzipれるかに注意する必要があります。zip

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

ブロックがある場合、返すものは何もなく、そのままにしておくresultことができますQnil。ブロックがない場合は、result配列を返すことができるように配列が必要です。

からparse.c、それNODE_DOT2は 2 つのドットの範囲であることがわかりますが、新しいノードを単純な 3 つの要素の構造体として使用しているように見えます。rb_new_nodeオブジェクトを割り当て、いくつかのビットを設定し、構造体に 3 つの値を割り当てるだけです。

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、3 つの要素を持つ構造体ができました。この の使用はNODE_DOT2、便利なクラッジのようです。

このrb_block_call関数は、コアの内部反復子のようです。そして、再び友人に会うid_eachので、each反復を行います。zip_i次に、との間の選択肢が表示されzip_aryます。これは、内部配列が作成され、 にプッシュされる場所resultです。との唯一の違いはzip_izip_aryの StopIteration 例外処理のようzip_iです。

この時点で圧縮が完了し、配列の配列result(ブロックがない場合) または(ブロックがある場合) のいずれかにQnilなりresultます。


エグゼクティブ サマリー: 最初のループでは、列挙子を配列に展開することを明示的に回避しています。zip_iとのzip_ary呼び出しは、戻り値として配列の配列を作成する必要がある場合にのみ、非一時配列で機能します。したがって、zip配列以外の列挙子を少なくとも 1 つ指定して呼び出し、ブロック形式を使用すると、すべてが列挙子になり、「内部で配列が作成されるという zip の問題」は発生しません。1.8 またはその他の Ruby 実装のレビューは、読者の演習として残されています。

于 2011-06-27T12:46:02.637 に答える