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


87

私はこのようなことをするのが良いだろうと思いました(ラムダが利回りを返すことで):

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

しかし、無名メソッドでは利回りを使用できないことがわかりました。なぜだろう。歩留まりのドキュメントは、それが許可されていないと言います。

許可されなかったので、リストを作成して項目を追加しただけです。


C#5.0で匿名asyncラムダを許可awaitできるようになったので、なぜyield内部で匿名イテレータがまだ実装されていないのかを知りたいと思います。多かれ少なかれ、それは同じステートマシンジェネレータです。
noseratio 14年

回答:


113

Eric Lippertは最近、なぜ利回りが許可されない場合があるかについて一連のブログ投稿を書きました。

EDIT2:

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

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


EDIT1:これはパート5のコメントで説明されており、Abhijeet Patelのコメントに対するEricの回答:

Q:

エリック、

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

A:

良い質問。匿名のイテレータブロックが欲しいです。ローカル変数をクローズする小さなシーケンスジェネレーターをインプレースで構築できるとしたら、とてもすばらしいでしょう。そうではない理由は簡単です。メリットがコストを上回らないことです。シーケンスジェネレーターをインプレースにすることの素晴らしさは、実際にはグランドスキームではかなり小さく、名目上の方法はほとんどのシナリオで十分に機能します。したがって、メリットはそれほど魅力的ではありません。

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

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

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


2
興味深いことに、特にローカル機能があるので。
Mafii 2017年

4
この回答はローカル関数で利回りを返すため、古くなっているのではないでしょうか。
ジョシュア

2
@Joshuaですが、ローカル関数は匿名メソッドと同じではありません...匿名メソッドでは、yield returnはまだ許可されていません。
トーマスレベスク2018

21

Eric Lippertは、反復子ブロックの制限(およびそれらの選択に影響を与える設計決定)に関する優れた一連の記事を書いています

特に、イテレータブロックは、いくつかの高度なコンパイラコード変換によって実装されます。これらの変換は、匿名関数またはラムダ内で行われる変換に影響を与えるため、特定の状況では、どちらもコードを他の構成と互換性のない他の構成に「変換」しようとします。

その結果、彼らは相互作用を禁止されています。

反復子ブロックが内部でどのように機能するかは、ここでうまく処理されます

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

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

2
コンパイラーがすべてのクロージャーをいったん解除すると、通常のイテレーター変換を実行できないという明確な理由はありません。実際に問題が発生するケースをご存知ですか?ところで、あなたのMagicクラスはでなければなりませんMagic<T>
Qwertie 2013

3

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

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

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

私の推測では、これらの2つのうちの1つは他の魔法の部分からコードを識別できなくなり、現在のバージョンのC#コンパイラーでこの作業を行うことに時間を費やすことはないと決定されました。もちろん、それはまったく賢い選択ではないかもしれませんし、誰もそれを実装するつもりがなかったので機能しないだけです。

100%正確な質問の場合、Microsoft Connectサイトを使用して質問を報告することをお勧めします。見返りに何か役に立つものがあると確信しています。


1

私はこれを行います:

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;

乾杯、

スライ


0

たぶんそれは単なる構文上の制限です。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 返しますIEnumerable(Of Integer)が、ないようなオブジェクトそのもの。そのオブジェクトを取得するために呼び出す必要があります。

[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();
}

コンパイラーが処理するのが難しいという他の回答で示される理由に強く同意しません。Iterator Function()あなたはVB.NETの例でわかるが、具体的ラムダイテレータのために作成されます。

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

だから、本当に、本当にのコメントで述べたようにVisual Basicまたは(私はそれをチェックしていない)F#を使用して、現在、匿名のイテレータの機能が必要な場合はパート#7 @Thomasレベスクの答えに(F#用のCtrl + Fを行います)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.