En C #, ¿por qué un método anónimo no puede contener una declaración de rendimiento?
-
10-07-2019 - |
Pregunta
Pensé que sería bueno hacer algo como esto (con la lambda haciendo un rendimiento):
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();
}
Sin embargo, descubrí que no puedo usar el rendimiento en un método anónimo. Me pregunto por qué. Los documentos de rendimiento solo dicen que no está permitido.
Como no estaba permitido, acabo de crear Lista y le agregué los elementos.
Solución
Eric Lippert recientemente escribió una serie de publicaciones de blog sobre por qué el rendimiento no está permitido en algunos casos.
EDIT2:
- Parte 7 (esta se publicó más tarde y aborda específicamente esta pregunta)
Probablemente encontrarás la respuesta allí ...
EDIT1: esto se explica en los comentarios de la Parte 5, en la respuesta de Eric al comentario de Abhijeet Patel:
Q:
Eric,
¿También puede proporcionar alguna idea sobre por qué " rinde " no están permitidos dentro de un método anónimo o expresión lambda
A:
Buena pregunta. Me encantaría tener bloques iteradores anónimos. Podría ser totalmente increíble poder construir usted mismo un pequeño generador de secuencia en el lugar que cerró sobre locales variables La razón por la que no es sencillo: los beneficios no superan los costos. La genialidad de haciendo generadores de secuencia en el lugar es en realidad bastante pequeño en el gran esquema de cosas y métodos nominales hacer el trabajo lo suficientemente bien en la mayoría escenarios Entonces los beneficios no son que convincente.
Los costos son grandes. Iterador reescribir es lo más complicado transformación en el compilador, y reescritura de método anónimo es el El segundo más complicado. Anónimo los métodos pueden estar dentro de otros anónimos métodos y métodos anónimos pueden ser dentro de bloques iteradores. Por lo tanto, lo que hacemos es reescribir todo métodos anónimos para que se conviertan métodos de una clase de cierre. Esto es lo penúltimo del compilador hace antes de emitir IL para un método. Una vez que se realiza ese paso, el iterador reescritor puede asumir que no hay métodos anónimos en el iterador bloquear; todos han sido reescritos ya. Por lo tanto el iterador reescritor puede concentrarse en reescribiendo el iterador, sin preocupado de que pueda haber una método anónimo no realizado allí.
Además, los bloques iteradores nunca "anidan", a diferencia de los métodos anónimos. El iterador Rewriter puede suponer que todos los iteradores los bloques son "nivel superior".
Si se permiten métodos anónimos contienen bloques iteradores, entonces ambos esos supuestos salen por la ventana. Puede tener un bloque iterador que contiene un método anónimo que contiene un método anónimo que contiene un bloque iterador que contiene un método anónimo y ... qué asco. Ahora tenemos que escribir una reescritura pase que puede manejar iterador anidado bloques y métodos anónimos anidados en Al mismo tiempo, fusionando nuestros dos más algoritmos complicados en un lejano Algoritmo más complicado. Sería ser realmente difícil de diseñar, implementar, y prueba. Somos lo suficientemente inteligentes como para hacer Entonces, estoy seguro. Tenemos un equipo inteligente aquí. Pero no queremos enfrentarnos esa gran carga para a & quo
Otros consejos
Eric Lippert ha escrito una excelente serie de artículos sobre las limitaciones (y las decisiones de diseño que influyen en esas elecciones) en bloques iteradores
En particular, los bloques iteradores se implementan mediante algunas sofisticadas transformaciones de código del compilador. Estas transformaciones impactarían con las transformaciones que suceden dentro de funciones anónimas o lambdas de tal manera que, en ciertas circunstancias, ambos intentarían 'convertir' el código en alguna otra construcción que fuera incompatible con la otra.
Como resultado, se les prohíbe la interacción.
El funcionamiento de los bloques iteradores bajo el capó se trata bien aquí .
Como un simple ejemplo de incompatibilidad:
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();
}
El compilador desea simultáneamente convertir esto en algo como:
// 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();
}
y al mismo tiempo el aspecto del iterador intenta hacer su trabajo para hacer una pequeña máquina de estados. Ciertos ejemplos simples podrían funcionar con una buena cantidad de verificación de cordura (primero tratando con los cierres anidados (posiblemente arbitrariamente)) y luego viendo si las clases resultantes de nivel inferior podrían transformarse en máquinas de estado iterador.
Sin embargo, esto sería
- Mucho trabajo.
- No podría funcionar en todos los casos sin, al menos, que el aspecto del bloque iterador sea capaz de evitar que el aspecto de cierre aplique ciertas transformaciones para la eficiencia (como promover variables locales a variables de instancia en lugar de una clase de cierre completa).
- Si hubiera incluso una pequeña posibilidad de superposición donde era imposible o lo suficientemente difícil de no implementar, entonces el número de problemas de soporte resultantes probablemente sería alto ya que el cambio sutil se perdería en muchos usuarios.
- Se puede solucionar muy fácilmente.
En su ejemplo así:
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;
}
Desafortunadamente no sé por qué no permitieron esto, ya que, por supuesto, es completamente posible imaginar cómo funcionaría esto.
Sin embargo, los métodos anónimos ya son una pieza de "compilador mágico" en el sentido de que el método se extraerá a un método en la clase existente, o incluso a una clase completamente nueva, dependiendo de si trata con variables locales o no.
Además, los métodos de iterador que usan yield
también se implementan usando la magia del compilador.
Supongo que uno de estos dos hace que el código no sea identificable para la otra pieza de magia, y que se decidió no perder tiempo haciendo que esto funcione para las versiones actuales del compilador de C #. Por supuesto, puede que no sea una opción consciente, y que simplemente no funciona porque nadie pensó en implementarla.
Para una pregunta 100% precisa, le sugiero que use el sitio Microsoft Connect e informe una pregunta, Estoy seguro de que obtendrá algo utilizable a cambio.
Haría esto:
IList<T> list = GetList<T>();
var fun = expression.Compile();
return list.Where(item => fun.Invoke(item)).ToList();
Por supuesto, necesita el System.Core.dll referenciado desde .NET 3.5 para el método Linq. E incluye:
using System.Linq;
Saludos,
Sly
Quizás sea solo una limitación de sintaxis. En Visual Basic .NET, que es muy similar a C #, es perfectamente posible mientras que es incómodo escribir
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
Observe también los paréntesis 'aquí
; la función lambda Función Iterator
... End Function
devuelve un IEnumerable (Of Integer)
pero es no tal objeto en sí. Se debe llamar para obtener ese objeto.
El código convertido por [1] genera errores en C # 7.3 (CS0149):
<*> Estoy totalmente en desacuerdo con la razón dada en las otras respuestas que es difícil de manejar para el compilador. La Iterator Function ()
que ve en el ejemplo de VB.NET se creó específicamente para los iteradores lambda.
En VB, existe la palabra clave Iterator
; no tiene contraparte de C #. En mi humilde opinión, no hay una razón real por la que esto no sea una característica de C #.
Entonces, si realmente desea funciones de iterador anónimo, actualmente use Visual Basic o (no lo he verificado) F #, como se indica en un comentario de Parte # 7 en la respuesta de @Thomas Levesque (do Ctrl + F para F #).