Чому в 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();
}

Однак я з’ясував, що не можу використовувати вихід в анонімному методі. Мені цікаво чому. У документації про вихід просто сказано, що це не дозволено.

Оскільки це було заборонено, я просто створив Список і додав до нього елементи.


Тепер, коли ми можемо мати анонімні asyncлямбди, що дозволяють awaitвсередині в C # 5.0, мені було б цікаво дізнатись, чому вони все ще не реалізували анонімні ітератори з yieldinside. Більш-менш, це той самий генератор державного автомата.
noseratio 13.03.14

Відповіді:


113

Нещодавно Ерік Ліпперт написав низку дописів у блозі про те, чому в деяких випадках не дозволяється врожай.

EDIT2:

  • Частина 7 (ця була опублікована пізніше і конкретно стосується цього питання)

Ви, мабуть, знайдете там відповідь ...


EDIT1: це пояснюється в коментарях до частини 5, у відповіді Еріка на коментар Абхієет Патель:

Q:

Ерік,

Чи можете ви також дати деяке розуміння того, чому "врожаї" заборонені в анонімному методі чи лямбда-виразі

A:

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

Витрати великі. Переписування ітератора є найскладнішим перетворенням у компіляторі, а анонімний метод переписування - другим за складністю. Анонімні методи можуть бути всередині інших анонімних методів, а анонімні методи можуть бути всередині блоків ітераторів. Отже, спочатку ми переписуємо всі анонімні методи, щоб вони стали методами класу закриття. Це друге останнє, що робить компілятор перед випуском IL для методу. Після завершення цього кроку переписувач ітераторів може припустити, що в блоці ітератора немає анонімних методів; всі вони вже переписані. Тому переписувач ітераторів може просто зосередитися на переписуванні ітератора, не побоюючись, що там може бути нереалізований анонімний метод.

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

Якщо анонімним методам дозволено містити блоки ітераторів, то обидва ці припущення виходять з вікна. Ви можете мати блок ітераторів, який містить анонімний метод, який містить анонімний метод, що містить блок ітераторів, що містить анонімний метод, і ... yuck. Тепер нам потрібно написати перезапис, який може одночасно обробляти вкладені блоки ітераторів та вкладені анонімні методи, об’єднуючи наші два найскладніші алгоритми в один набагато складніший алгоритм. Було б дуже важко розробити, впровадити та протестувати. Я впевнений, що ми досить розумні, щоб це зробити. У нас тут розумна команда. Але ми не хочемо брати на себе цей великий тягар за функцію "приємно мати, але не потрібно". - Еріку


2
Цікаво, тим більше, що зараз існують місцеві функції.
Mafii

4
Цікаво, чи ця відповідь застаріла, оскільки вона вимагатиме повернення доходу в локальній функції.
Джошуа

2
@Joshua, але локальна функція - це не те саме, що анонімний метод ... повернення доходу все ще не дозволяється в анонімних методах.
Томас Левеск,

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

2
Немає чіткої причини, чому компілятор не може, як тільки він зняв усі закриття, зробити звичайне перетворення ітератора. Чи знаєте ви про випадок, який насправді міг би скласти певні труднощі? До речі, ваш Magicклас повинен бути Magic<T>.
Кверті

3

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

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

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

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

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


1

Я б зробив це:

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;

Ура,

Хитрий


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 #. IMHO, немає жодної реальної причини, що це не особливість C #.

Отже, якщо ви дійсно, дуже хочете анонімні функції ітераторів, наразі використовуйте Visual Basic або (я не перевіряв) F #, як зазначено у коментарі до частини 7 у відповіді @Thomas Levesque (зробіть Ctrl + F для F #).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.