Почему в C # анонимный метод не может содержать оператор yield? - PullRequest
84 голосов
/ 02 августа 2009

Я подумал, что было бы неплохо сделать что-то вроде этого (с лямбдой, делающей возврат доходности):

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 в анонимном методе. Мне интересно почему. выход документов просто скажем, что это не разрешено.

Поскольку это было запрещено, я просто создал List и добавил в него элементы.

Ответы [ 5 ]

102 голосов
/ 02 августа 2009

Эрик Липперт недавно написал серию постов в блоге о том, почему в некоторых случаях доходность запрещена.

РЕДАКТИРОВАТЬ2:

  • Part 7 (этот был опубликован позже и специально посвящен этому вопросу)

Возможно, вы найдете ответ там ...


РЕДАКТИРОВАТЬ1: это объясняется в комментариях к части 5, в ответе Эрика на комментарий Абхиджита Пателя:

Q:

Эрик

Можете ли вы дать некоторое представление о почему «доходность» не допускается внутри анонимный метод или лямбда-выражение

A:

Хороший вопрос. Я хотел бы иметь анонимные блоки итераторов. Это было бы совершенно потрясающе иметь возможность строить сам маленький генератор последовательности на месте, которое закрыто над местным переменные. Причина, почему нет просто: преимущества не перевешивают расходы. Удивительность создание генераторов последовательности на месте на самом деле довольно маленький в большом схема вещей и именные методы сделать работу достаточно хорошо в большинстве сценарии. Таким образом, преимущества не это неотразимо.

Затраты большие. Итератор переписывание самое сложное преобразование в компилятор, и анонимный метод переписывания является второй самый сложный. анонимное методы могут быть внутри других анонимных методы, и анонимные методы могут быть внутри блоков итераторов. Следовательно, что мы делаем, сначала мы переписываем все анонимные методы, чтобы они стали методы класса замыкания. Это вторая вещь компилятор делает до испускания IL для метода. Как только этот шаг сделан, итератор переписчик может предположить, что нет анонимные методы в итераторе блок; все они были переписаны уже. Поэтому итератор переписчик может просто сосредоточиться на переписывая итератор, без опасаясь, что там может быть там нереализованный анонимный метод.

Кроме того, блоки итераторов никогда не «гнездятся», в отличие от анонимных методов. Итератор рерайтер может предположить, что все итераторы блоки "верхнего уровня".

Если анонимным методам разрешено содержат блоки итератора, тогда оба эти предположения выходят в окно. Вы можете иметь блок итератора, который содержит анонимный метод, который содержит анонимный метод, который содержит блок итератора, который содержит анонимный метод и ... Тьфу. Теперь мы должны написать переписывание проход, который может обрабатывать вложенный итератор блоки и вложенные анонимные методы в в то же время, объединяя наши два самых сложные алгоритмы в один далеко более сложный алгоритм. Было бы быть очень сложным в разработке, реализации, и проверить. Мы достаточно умны, чтобы сделать так что я уверен. У нас умная команда Вот. Но мы не хотим брать на себя что большое бремя для "приятно иметь но не обязательно "фича. - Эрик

21 голосов
/ 02 августа 2009

Эрик Липперт написал превосходную серию статей об ограничениях (и проектных решениях, влияющих на этот выбор) для блоков итераторов

В частности, блоки итераторов реализованы с помощью некоторых сложных преобразований кода компилятора. Эти преобразования будут влиять на преобразования, которые происходят внутри анонимных функций или лямбд, так что при определенных обстоятельствах они оба будут пытаться «преобразовать» код в какую-то другую конструкцию, несовместимую с другой.

В результате им запрещено взаимодействие.

Как хорошо работают блоки итератора под капотом здесь .

В качестве простого примера несовместимости:

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;
}
3 голосов
/ 02 августа 2009

К сожалению, я не знаю, почему они этого не допустили, поскольку, конечно, вполне возможно представить, как это будет работать.

Однако анонимные методы уже являются частью "магии компилятора" в том смысле, что метод будет извлечен либо в метод существующего класса, либо даже в совершенно новый класс, в зависимости от того, имеет ли он дело с локальными переменными или нет.

Кроме того, методы итератора, использующие yield, также реализованы с использованием магии компилятора.

Я предполагаю, что одно из этих двух дел делает код неидентифицируемым для другой части магии, и было решено не тратить время на выполнение этой работы для текущих версий компилятора C #. Конечно, это может быть вовсе не осознанный выбор, и он просто не работает, потому что никто не думал его реализовать.

Для 100% точного вопроса я бы предложил вам воспользоваться сайтом Microsoft Connect и сообщить о нем. Я уверен, что вы получите что-то полезное взамен.

1 голос
/ 20 сентября 2009

Я бы сделал это:

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

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

Конечно, вам нужен файл System.Core.dll, указанный в .NET 3.5 для метода Linq. И включают в себя:

using System.Linq;

Приветствия

Sly

0 голосов
/ 08 октября 2018

Может быть, это просто ограничение синтаксиса. В Visual Basic .NET, который очень похож на C #, вполне возможно, хотя неудобно писать

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 #, как указано в комментарии к Part # 7 в ответе @Thomas Levesque ( сделать Ctrl + F для F #).

...