Почему в C # анонимный метод не может содержать оператор yield?

87

Я подумал, что было бы неплохо сделать что-то вроде этого (с лямбдой, возвращающей 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 в анонимном методе. Интересно почему. Документы по выходу просто говорят, что это запрещено.

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

Лэнс Фишер
источник
Теперь, когда у нас могут быть анонимные asyncлямбды, разрешающие awaitвнутри в C # 5.0, мне было бы интересно узнать, почему они до сих пор не реализовали анонимные итераторы с yieldвнутри. Более-менее, это тот же генератор конечных автоматов.
Nosratio

Ответы:

113

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

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

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

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


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

Вопрос:

Эрик,

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

А:

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

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

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

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

Томас Левеск
источник
2
Интересно, тем более что сейчас есть локальные функции.
Mafii
4
Интересно, устарел ли этот ответ, потому что он потребует возврата доходности в локальной функции.
Джошуа
2
@Joshua, но локальная функция - это не то же самое, что анонимный метод ... yield return по-прежнему не разрешен в анонимных методах.
Thomas Levesque
21

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

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

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

Здесь хорошо разбирается, как работают блоки итератора .

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

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;
}
ShuggyCoUk
источник
2
Нет ясной причины, по которой компилятор не может после того, как он снял все замыкания, выполнить обычное преобразование итератора. Вы знаете случай, который действительно мог бы вызвать трудности? Кстати, ваш Magicкласс должен быть Magic<T>.
Qwertie
3

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

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

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

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

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

Лассе В. Карлсен
источник
1

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

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

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

Конечно, для метода Linq вам понадобится System.Core.dll, на который есть ссылка в .NET 3.5. И включают:

using System.Linq;

Привет,

Хитрый


источник
0

Возможно, это просто синтаксическое ограничение. В 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 #, как указано в комментарии к Части № 7 в ответе @Thomas Levesque (нажмите Ctrl + F для F #).

Болпат
источник