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

StackOverflow https://stackoverflow.com/questions/1217729

Вопрос

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

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

Однако я обнаружил, что не могу использовать выход в анонимном методе.Мне интересно, почему.А предоставить документы просто скажи, что это запрещено.

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

Это было полезно?

Решение

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

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

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

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


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

Вопрос:

Эрик,

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

А:

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

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

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

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

Другие советы

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

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

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

Хорошо рассмотрено, как работают блоки итераторов под капотом. здесь.

Простой пример несовместимости:

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

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

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

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

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

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

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

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;

Ваше здоровье,

хитрый

Может быть, это просто ограничение синтаксиса. В 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(
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(<*>quot;{elem} to {x}");
    Console.ReadKey();
}
quot;{elem} to {x}") Next Console.ReadKey() End Sub

Также обратите внимание на круглые скобки 'здесь ; лямбда-функция Функция итератора ... End Function возвращает значение IEnumerable (Of Integer) , но не такой объект сам по себе. Он должен быть вызван для получения этого объекта.

Преобразованный код в [1] вызывает ошибки в C # 7.3 (CS0149):

<*>

Я категорически не согласен с причиной, приведенной в других ответах, что компилятору трудно справиться. Iterator Function () , которую вы видите в примере VB.NET, специально создан для лямбда-итераторов.

В VB есть ключевое слово Iterator ; у него нет аналога C #. ИМХО, нет реальной причины, чтобы это не было особенностью C #.

Так что если вы действительно, действительно хотите анонимные итераторные функции, в настоящее время используйте Visual Basic или (я не проверял это) F #, как указано в комментарии Часть № 7 в ответе @Thomas Levesque (сделайте Ctrl + F для F #).

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top