Почему в C# анонимный метод не может содержать оператор 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 и добавил в него элементы.
5 ответов
Эрик Липперт недавно написал серию постов в блоге о том, почему в некоторых случаях доходность не допускается.
EDIT2:
- Часть 7 (эта была опубликована позже и конкретно посвящена этому вопросу)
Вы, вероятно, найдете ответ там...
РЕДАКТИРОВАТЬ1: это объясняется в комментариях к части 5, в ответе Эрика на комментарий Абхиджита Пателя:
Q:
Эрик,
Можете ли вы также дать некоторое представление о том, почему "выходы" не разрешены внутри анонимного метода или лямбда-выражения
A:
Хороший вопрос. Я хотел бы иметь анонимные блоки итераторов. Было бы замечательно иметь возможность создать на месте небольшой генератор последовательностей, закрывающий локальные переменные. Причина не проста: выгоды не перевешивают затраты. Удивительность создания генераторов последовательностей на самом деле довольно мала в общей схеме, и номинальные методы делают работу достаточно хорошо в большинстве сценариев. Таким образом, преимущества не настолько убедительны.
Затраты большие. Перезапись итератора является наиболее сложным преобразованием в компиляторе, а перезапись анонимного метода - вторым по сложности. Анонимные методы могут быть внутри других анонимных методов, а анонимные методы могут быть внутри блоков итераторов. Поэтому сначала мы переписываем все анонимные методы, чтобы они стали методами класса замыкания. Это второе, что делает компилятор перед тем, как выдавать 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();
}
и в то же время аспект итератора пытается сделать свою работу, чтобы сделать небольшой конечный автомат. Некоторые простые примеры могут работать с достаточной проверкой работоспособности (сначала имея дело с (возможно, произвольно) вложенными замыканиями), а затем посмотреть, могут ли результирующие классы самого нижнего уровня быть преобразованы в конечные автоматы итераторов.
Однако это будет
- Довольно много работы.
- Невозможно работать во всех случаях без, по крайней мере, аспекта блока итератора, способного предотвратить применение аспектом замыкания определенных преобразований для повышения эффективности (например, продвижение локальных переменных в переменные экземпляра, а не в полноценный класс замыкания).
- Если бы была даже небольшая вероятность совпадения, когда это было бы невозможно или достаточно сложно не реализовать, то число возникающих проблем с поддержкой, вероятно, было бы высоким, так как незначительное изменение было бы потеряно для многих пользователей.
- Это может быть очень легко обойти.
В вашем примере так:
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($"{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#).