En C #, ¿por qué un método anónimo no puede contener una declaración de rendimiento?

87

Pensé que sería bueno hacer algo como esto (con lambda haciendo un retorno de 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 simplemente dicen que no está permitido.

Como no estaba permitido, simplemente creé la Lista y le agregué los elementos.

Lance Fisher
fuente
Ahora que podemos tener asynclambdas anónimos que permitan el awaitinterior en C # 5.0, me interesaría saber por qué todavía no han implementado iteradores anónimos con yieldinside. Más o menos, es el mismo generador de máquina de estados.
noseratio

Respuestas:

113

Eric Lippert escribió recientemente una serie de publicaciones de blog sobre por qué no se permite el rendimiento en algunos casos.

EDIT2:

  • Parte 7 (esta se publicó más tarde y aborda específicamente esta pregunta)

Probablemente encontrará 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 información sobre por qué no se permiten "rendimientos" dentro de un método anónimo o expresión lambda?

UNA :

Buena pregunta. Me encantaría tener bloques de iteradores anónimos. Sería totalmente increíble poder construir usted mismo un pequeño generador de secuencias en el lugar que cerrara las variables locales. La razón por la que no es sencilla: los beneficios no superan los costos. La maravilla de hacer generadores de secuencias en el lugar es en realidad bastante pequeña en el gran esquema de las cosas y los métodos nominales hacen el trabajo lo suficientemente bien en la mayoría de los escenarios. Entonces los beneficios no son tan convincentes.

Los costos son elevados. La reescritura de iteradores es la transformación más complicada en el compilador, y la reescritura de métodos anónimos es la segunda más complicada. Los métodos anónimos pueden estar dentro de otros métodos anónimos y los métodos anónimos pueden estar dentro de bloques de iteradores. Por lo tanto, lo que hacemos es primero reescribir todos los métodos anónimos para que se conviertan en métodos de una clase de cierre. Esto es lo penúltimo que hace el compilador antes de emitir IL para un método. Una vez que se realiza ese paso, el reescritor del iterador puede asumir que no hay métodos anónimos en el bloque del iterador; ya se han reescrito todos. Por lo tanto, el reescritor del iterador puede concentrarse en reescribir el iterador, sin preocuparse de que pueda haber un método anónimo no realizado allí.

Además, los bloques de iteradores nunca "anidan", a diferencia de los métodos anónimos. El reescritor de iteradores puede asumir que todos los bloques de iteradores son de "nivel superior".

Si se permite que los métodos anónimos contengan bloques de iteradores, ambas suposiciones desaparecen por la ventana. Puede tener un bloque iterador que contenga un método anónimo que contenga un método anónimo que contenga un bloque iterador que contenga un método anónimo, y ... puaj. Ahora tenemos que escribir un pase de reescritura que pueda manejar bloques de iteradores anidados y métodos anónimos anidados al mismo tiempo, fusionando nuestros dos algoritmos más complicados en uno mucho más complicado. Sería muy difícil de diseñar, implementar y probar. Somos lo suficientemente inteligentes para hacerlo, estoy seguro. Tenemos un equipo inteligente aquí. Pero no queremos asumir la gran carga de una característica "agradable de tener pero no necesaria". - Eric

Thomas Levesque
fuente
2
Interesante, especialmente porque ahora hay funciones locales.
Mafii
4
Me pregunto si esta respuesta está desactualizada porque tendrá un rendimiento de rendimiento en una función local.
Joshua
2
@Joshua, pero una función local no es lo mismo que un método anónimo ... el rendimiento de rendimiento aún no está permitido en métodos anónimos.
Thomas Levesque
21

Eric Lippert ha escrito una excelente serie de artículos sobre las limitaciones (y las decisiones de diseño que influyen en esas elecciones) de los bloques de iteradores.

En particular, los bloques de iteradores se implementan mediante algunas transformaciones sofisticadas del código del compilador. Estas transformaciones impactarían con las transformaciones que ocurren 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.

Aquí se trata bien cómo funcionan los bloques de iteradores bajo el capó .

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 quiere convertir esto simultáneamente 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 está tratando de hacer su trabajo para hacer una pequeña máquina de estado. 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 del nivel inferior podrían transformarse en máquinas de estado de iterador.

Sin embargo, esto sería

  1. Mucho trabajo.
  2. Posiblemente no podría funcionar en todos los casos sin que al menos el aspecto del bloque del iterador pueda evitar que el aspecto del 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 una pequeña posibilidad de superposición donde fuera imposible o lo suficientemente difícil como para no ser implementado, entonces la cantidad de problemas de soporte resultantes probablemente sería alta, ya que muchos usuarios perderían el sutil cambio de ruptura.
  3. Se puede solucionar muy fácilmente.

En tu 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;
}
ShuggyCoUk
fuente
2
No hay una razón clara por la que el compilador no pueda, una vez que ha eliminado todos los cierres, realizar la transformación de iterador habitual. ¿Conoce algún caso que realmente presente alguna dificultad? Por cierto, tu Magicclase debería serlo Magic<T>.
Qwertie
3

Desafortunadamente, no sé por qué no lo permitieron, ya que, por supuesto, es completamente posible imaginar cómo funcionaría.

Sin embargo, los métodos anónimos ya son una pieza de "magia del compilador" 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 se utilizan yieldtambién se implementan mediante la magia del compilador.

Supongo que uno de estos dos hace que el código no sea identificable para la otra pieza mágica, y que se decidió no dedicar tiempo a hacer que esto funcione para las versiones actuales del compilador de C #. Por supuesto, puede que no sea una elección consciente en absoluto, y que simplemente no funciona porque nadie pensó en implementarlo.

Para una pregunta 100% precisa, le sugiero que use el sitio de Microsoft Connect e informe una pregunta, estoy seguro de que obtendrá algo utilizable a cambio.

Lasse V. Karlsen
fuente
1

Yo haría esto:

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

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

Por supuesto, necesita System.Core.dll referenciado desde .NET 3.5 para el método Linq. E incluye:

using System.Linq;

Salud,

Astuto


fuente
0

Tal vez sea solo una limitación de sintaxis. En Visual Basic .NET, que es muy similar a C #, es perfectamente posible, aunque 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($"{elem} to {x}")
    Next
    Console.ReadKey()
End Sub

También tenga en cuenta los paréntesis ' here; la función lambda Iterator Function... End Function devuelve un IEnumerable(Of Integer)pero no es un objeto en sí mismo. Debe ser llamado para obtener ese objeto.

El código convertido por [1] genera errores en 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();
}

Estoy totalmente en desacuerdo con la razón dada en las otras respuestas que es difícil de manejar para el compilador. El Iterator Function()que ve en el ejemplo de VB.NET se crea específicamente para iteradores lambda.

En VB, existe la Iteratorpalabra clave; 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ónimas, actualmente use Visual Basic o (no lo he verificado) F #, como se indica en un comentario de la Parte # 7 en la respuesta de @Thomas Levesque (haga Ctrl + F para F #).

Bolpat
fuente