web-dev-qa-db-ja.com

C#は匿名メソッド本体内でrefまたはoutパラメーターを使用できません

渡された整数をインクリメントするアクションを作成できる関数を作成しようとしています。ただし、最初の試行では、「匿名メソッド本体内でrefまたはoutパラメータを使用できません」というエラーが発生します。

public static class IntEx {
    public static Action CreateIncrementer(ref int reference) {
        return () => {
            reference += 1;
        };
    }
}

コンパイラがこれを好まない理由は理解していますが、それでも、任意の整数を指すことができるNiceインクリメンタファクトリを提供するための優雅な方法が欲しいです。これを行うには、次のような方法しかありません。

public static class IntEx {
    public static Action CreateIncrementer(Func<int> getter, Action<int> setter) {
        return () => setter(getter() + 1);
    }
}

しかしもちろん、それは発信者が使用するためのより多くの苦痛です。呼び出し側に、参照を渡すだけでなく2つのラムダを作成するよう要求する。この機能を提供するより上品な方法はありますか、それとも2つのラムダオプションを使用する必要がありますか?

41
Dax Fohl

さて、私はそれが実際に安全でないコンテキストの場合、ポインタで可能ですisであることがわかりました:

public static class IntEx {
    unsafe public static Action CreateIncrementer(int* reference) {
        return () => {
            *reference += 1;
        };
    }
}

ただし、次のように、ガベージコレクターは、ガベージコレクション中に参照を移動することにより、これで大混乱を引き起こす可能性があります。

class Program {
    static void Main() {
        new Program().Run();
        Console.ReadLine();
    }

    int _i = 0;
    public unsafe void Run() {
        Action incr;
        fixed (int* p_i = &_i) {
            incr = IntEx.CreateIncrementer(p_i);
        }
        incr();
        Console.WriteLine(_i); // Yay, incremented to 1!
        GC.Collect();
        incr();
        Console.WriteLine(_i); // Uh-oh, still 1!
    }
}

変数をメモリ内の特定の場所に固定することで、この問題を回避できます。これを行うには、コンストラクタに次を追加します。

    public Program() {
        GCHandle.Alloc(_i, GCHandleType.Pinned);
    }

これにより、ガベージコレクターがオブジェクトを移動できなくなるため、まさに私たちが探しているものになります。ただし、ピンを解放するにはデストラクタを追加する必要があり、オブジェクトの存続期間を通じてメモリを断片化します。本当に簡単ではありません。これは、C++では物事が動き回らず、リソース管理は当然のことですが、すべてが自動であるはずのC#ではそれほど意味がありません。

したがって、この話の教訓は、そのメンバーのintを参照型でラップし、それで済ますことです。

(そして、はい、それは質問をする前にそれを機能させていた方法ですが、すべてのReference <int>メンバー変数を取り除き、通常のintを使用する方法があるかどうかを理解しようとしていました。まあ。 )

31
Dax Fohl

これは不可能です。

コンパイラーは、匿名メソッドで使用されるすべてのローカル変数とパラメーターを、自動生成されたクロージャークラスのフィールドに変換します。

CLRでは、refタイプをフィールドに格納することはできません。

たとえば、refパラメータなどのローカル変数で値の型を渡すと、値の有効期間はスタックフレームを超えて延長されます。

25
SLaks

永続化を防ぐメカニズムを備えた変数参照を作成できるようにすることは、ランタイムにとって有用な機能だったかもしれません。このような機能により、インデクサーは配列のように動作することができます(たとえば、 "myDictionary [5] .X = 9;"を介してDictionary <Int32、Point>にアクセスできます)。そのような参照が他のタイプのオブジェクトにダウンキャストできなかったり、フィールドとして使用できなかったり、参照自体によって渡されなかったりした場合、そのような機能は安全に提供できたと思いますそれ自体は)。残念ながら、CLRにはそのような機能はありません。

あなたが求めているものを実装するには、 発信者 クロージャー内で参照パラメーターを使用するすべての関数は、そのような関数に渡したいすべての変数をクロージャー内でラップする必要があります。パラメータがそのような方法で使用されることを示す特別な宣言があった場合、コンパイラが必要な動作を実装することは現実的かもしれません。多分.net 5.0コンパイラですが、それがどれほど役立つかはわかりません。

ところで、私の理解では、Javaでのクロージャは値によるセマンティクスを使用しますが、.netでのクロージャは参照によるものです。参照によるセマンティクスの時折の使用を理解できますがVB VB6までのバージョンのデフォルトの参照渡しパラメーター渡しのセマンティクスの使用に似ています。デフォルトのデリゲートを作成するときに変数の値をキャプチャしたい場合関数を呼び出す(たとえば、デリゲートが作成されたときにXの値を使用してデリゲートにMyFunction(X)を呼び出させたい場合)、追加のtempを指定したラムダを使用する方がよいか、単にデリゲートファクトリを使用する方がよいラムダ式を気にしないでください。

2
supercat