web-dev-qa-db-ja.com

C#では、匿名メソッドにyieldステートメントを含めることができないのはなぜですか?

私はこのようなことをするのがいいと思った(ラムダが利回りを返す):

public IList<T> Find<T>(Expression<Func<T, bool>> expression) where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();

    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

しかし、匿名メソッドではyieldを使用できないことがわかりました。なぜだろうか。 yield docs は許可されていないとだけ言っています。

許可されていなかったので、リストを作成してアイテムを追加しました。

84
Lance Fisher

Eric Lippertは最近、いくつかのケースで収量が許可されない理由に関する一連のブログ投稿を書きました。

EDIT2:

  • パート7(これは後で投稿され、この質問に具体的に対処します)

あなたはおそらくそこに答えを見つけるでしょう...


EDIT1:これは、パート5のコメントで説明されています。AbhijeetPatelのコメントに対するEricの回答です。

Q:

エリック、

また、匿名メソッドまたはラムダ式内で「yields」が許可されない理由についての洞察を提供できますか

A:

良い質問。匿名のイテレータブロックが必要です。ローカル変数で閉じた小さなシーケンスジェネレータをインプレースで自分で構築できるのは本当に素晴らしいことです。そうしない理由は簡単です。利益がコストを上回ることはありません。シーケンスジェネレータをインプレースで作成することのすばらしさは、実際には大規模なものではかなり小さく、ほとんどのシナリオでは名目上の方法で十分に機能します。したがって、利点はそれほど魅力的ではありません。

費用が大きい。イテレータの書き換えはコンパイラで最も複雑な変換であり、匿名メソッドの書き換えは2番目に複雑です。匿名メソッドは他の匿名メソッド内に配置でき、匿名メソッドはイテレータブロック内に配置できます。したがって、最初にすべての匿名メソッドを書き換えて、それらがクロージャークラスのメソッドになるようにします。これは、メソッドに対してILを発行する前にコンパイラが行う最後から2番目の処理です。そのステップが完了すると、イテレータリライタはイテレータブロックに匿名メソッドがないと想定できます。それらはすべて既に書き換えられています。そのため、イテレータのリライタは、未実現の匿名メソッドが存在することを心配せずに、イテレータの書き換えに集中できます。

また、イテレータブロックは、匿名メソッドとは異なり、「ネスト」することはありません。イテレータリライタは、すべてのイテレータブロックが「トップレベル」であると想定できます。

匿名メソッドにイテレータブロックを含めることが許可されている場合、これらの仮定は両方とも範囲外になります。匿名メソッドを含む反復子ブロックを含む匿名メソッドを含む匿名メソッドを含む反復子ブロックを持つことができます。次に、ネストされたイテレータブロックとネストされた匿名メソッドを同時に処理できる書き換えパスを作成し、2つの最も複雑なアルゴリズムを1つのはるかに複雑なアルゴリズムにマージする必要があります。設計、実装、テストするのは本当に難しいでしょう。私たちはそうするのに十分スマートです、私は確信しています。ここにはスマートなチームがあります。しかし、「必要ではないが必要」という機能に大きな負担をかけたくはありません。 -エリック

103
Thomas Levesque

Eric Lippertは、 iterator blocks に関する制限(およびそれらの選択に影響する設計上の決定)に関する優れたシリーズの記事を書いています。

特に、イテレータブロックは、洗練されたコンパイラコード変換によって実装されます。これらの変換は、匿名関数またはラムダ内で発生する変換に影響を与え、特定の状況では、コードを他のコンストラクトと互換性のない他のコンストラクトに「変換」しようとします。

結果として、彼らは相互作用を禁じられています。

イテレータブロックが内部でどのように機能するかは、 here でうまく処理されます。

非互換性の簡単な例として:

public IList<T> GreaterThan<T>(T t)
{
    IList<T> list = GetList<T>();
    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

コンパイラは同時にこれを次のようなものに変換しようとしています:

// inner class
private class Magic
{
    private T t;
    private IList<T> list;
    private Magic(List<T> list, T t) { this.list = list; this.t = t;}

    public IEnumerable<T> DoIt()
    {
        var items = () => {
            foreach (var item in list)
                if (fun.Invoke(item))
                    yield return item;
        }
    }
}

public IList<T> GreaterThan<T>(T t)
{
    var magic = new Magic(GetList<T>(), t)
    var items = magic.DoIt();
    return items.ToList();
}

同時に、イテレータの側面は、小さなステートマシンを作成する作業を行おうとしています。特定の簡単な例は、かなりの量の健全性チェック(最初に(おそらく任意に)ネストされたクロージャーを処理する)で動作し、最下位レベルの結果クラスがイテレーター状態マシンに変換できるかどうかを確認します。

しかし、これは

  1. 非常に多くの作業。
  2. 少なくとも、イテレータブロックアスペクトが、クロージャアスペクトが特定の変換を効率的に適用することを防ぐことができない場合、すべての場合におそらく動作しませんでした(完全なクロージャクラスではなく、ローカル変数をインスタンス変数に昇格させるなど)。
    • 実装が不可能または十分に困難な場所でオーバーラップする可能性がわずかでもあった場合、微妙な重大な変更が多くの​​ユーザーに失われるため、結果としてサポートの問題の数が多くなる可能性があります。
  3. 非常に簡単に回避できます。

あなたの例のように:

public IList<T> Find<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    return FindInner(expression).ToList();
}

private IEnumerable<T> FindInner<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();
    foreach (var item in list)
        if (fun.Invoke(item))
            yield return item;
}
21
ShuggyCoUk

残念ながら、なぜこれが許可されなかったのかわかりません。もちろん、これがどのように機能するかを想像することは完全に可能であるためです。

ただし、ローカル変数を処理するかどうかに応じて、既存のクラスのメソッドまたはまったく新しいクラスのいずれかにメソッドが抽出されるという意味で、匿名メソッドはすでに「コンパイラマジック」の一部です。

さらに、yieldを使用するイテレータメソッドも、コンパイラマジックを使用して実装されます。

私の推測では、これらの2つのうちの1つは、他の魔法の部分ではコードを識別できず、現在のバージョンのC#コンパイラーでこの作業を行わないことに決めたと思われます。もちろん、それはまったく意識的な選択ではないかもしれませんし、誰もそれを実装しようと考えなかったので、それはただ動かないだけです。

100%正確な質問については、 Microsoft Connect サイトを使用して質問を報告することをお勧めします。見返りに有用なものが得られると確信しています。

私はこれをします:

IList<T> list = GetList<T>();
var fun = expression.Compile();

return list.Where(item => fun.Invoke(item)).ToList();

もちろん、Linqメソッドには.NET 3.5から参照されるSystem.Core.dllが必要です。以下を含めます。

using System.Linq;

乾杯、

スライ

1
Sly1024

たぶん、それは単なる構文上の制限です。 C#に非常に似ているVisual Basic .NETでは、書くのが面倒ですが、完全に可能です。

_Sub Main()
    Console.Write("x: ")
    Dim x = CInt(Console.ReadLine())
    For Each elem In Iterator Function()
                         Dim i = x
                         Do
                             Yield i
                             i += 1
                             x -= 1
                         Loop Until i = x + 20
                     End Function()
        Console.WriteLine($"{elem} to {x}")
    Next
    Console.ReadKey()
End Sub
_

括弧_' here_;にも注意してください。ラムダ関数_Iterator Function_..._End Function_returnsan IEnumerable(Of Integer) butnot notそのようなオブジェクト自体。そのオブジェクトを取得するために呼び出す必要があります。

[1]によって変換されたコードは、C#7.3(CS0149)でエラーを発生させます。

_static void Main()
{
    Console.Write("x: ");
    var x = System.Convert.ToInt32(Console.ReadLine());
    // ERROR: CS0149 - Method name expected 
    foreach (var elem in () =>
    {
        var i = x;
        do
        {
            yield return i;
            i += 1;
            x -= 1;
        }
        while (!i == x + 20);
    }())
        Console.WriteLine($"{elem} to {x}");
    Console.ReadKey();
}
_

私は、コンパイラが処理するのが難しいという他の回答で示された理由に強く反対します。 VB.NETの例にあるIterator Function()は、ラムダイテレータ専用に作成されています。

VBには、Iteratorキーワードがあります。 C#に対応するものはありません。私見、これがC#の機能ではない本当の理由はありません。

@Thomasの Part#7 のコメントに記載されているように、本当に、本当に本当にイテレータ関数が必要な場合は、現在Visual Basicまたは(私はチェックしていません)F#を使用しますレベスクの答え(F#の場合はCtrl + Fを実行)。

0
Bolpat