Error de invocación ambigua del compilador: método anónimo y grupo de métodos con Func <> o Action

102

Tengo un escenario en el que quiero usar la sintaxis de grupo de métodos en lugar de métodos anónimos (o sintaxis lambda) para llamar a una función.

La función tiene dos sobrecargas, una que toma una Action, la otra toma una Func<string>.

Felizmente puedo llamar a las dos sobrecargas usando métodos anónimos (o sintaxis lambda), pero obtengo un error de compilador de invocación ambigua si uso la sintaxis del grupo de métodos. Puedo solucionarlo mediante la conversión explícita a Actiono Func<string>, pero no creo que esto deba ser necesario.

¿Alguien puede explicar por qué deberían requerirse los moldes explícitos?

Ejemplo de código a continuación.

class Program
{
    static void Main(string[] args)
    {
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods();
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods();

        // These both compile (lambda syntax)
        classWithDelegateMethods.Method(() => classWithSimpleMethods.GetString());
        classWithDelegateMethods.Method(() => classWithSimpleMethods.DoNothing());

        // These also compile (method group with explicit cast)
        classWithDelegateMethods.Method((Func<string>)classWithSimpleMethods.GetString);
        classWithDelegateMethods.Method((Action)classWithSimpleMethods.DoNothing);

        // These both error with "Ambiguous invocation" (method group)
        classWithDelegateMethods.Method(classWithSimpleMethods.GetString);
        classWithDelegateMethods.Method(classWithSimpleMethods.DoNothing);
    }
}

class ClassWithDelegateMethods
{
    public void Method(Func<string> func) { /* do something */ }
    public void Method(Action action) { /* do something */ }
}

class ClassWithSimpleMethods
{
    public string GetString() { return ""; }
    public void DoNothing() { }
}

Actualización de C # 7.3

Según el comentario de 0xcde a continuación el 20 de marzo de 2019 (¡nueve años después de que publiqué esta pregunta!), Este código se compila a partir de C # 7.3 gracias a candidatos de sobrecarga mejorados .

Richard Ev
fuente
Probé su código y recibo un error de tiempo de compilación adicional: 'void test.ClassWithSimpleMethods.DoNothing ()' tiene el tipo de retorno incorrecto (que está en la línea 25, que es donde está el error de ambigüedad)
Matt Ellen
@ Matt: También veo ese error. Los errores que cité en mi publicación fueron los problemas de compilación que VS destaca incluso antes de que pruebes una compilación completa.
Richard Ev
1
Por cierto, esta fue una gran pregunta. Amo todo lo que me obliga a cumplir con las especificaciones :)
Jon Skeet
1
Tenga en cuenta que su código de muestra se compilará si usa C # 7.3 ( <LangVersion>7.3</LangVersion>) o posterior gracias a los candidatos de sobrecarga mejorados .
0xced el

Respuestas:

97

En primer lugar, permítanme decirles que la respuesta de Jon es correcta. Esta es una de las partes más complicadas de la especificación, tan bueno para Jon por sumergirse en ella de cabeza.

En segundo lugar, déjeme decir que esta línea:

Existe una conversión implícita de un grupo de métodos a un tipo de delegado compatible

(énfasis agregado) es profundamente engañoso y desafortunado. Hablaré con Mads sobre cómo eliminar la palabra "compatible" aquí.

La razón por la que esto es engañoso y desafortunado es porque parece que está llamando a la sección 15.2, "Compatibilidad de delegados". La sección 15.2 describió la relación de compatibilidad entre métodos y tipos de delegados , pero esta es una cuestión de convertibilidad de grupos de métodos y tipos de delegados , que es diferente.

Ahora que lo hemos sacado del camino, podemos revisar la sección 6.6 de la especificación y ver lo que obtenemos.

Para resolver la sobrecarga, primero debemos determinar qué sobrecargas son candidatos aplicables . Un candidato es aplicable si todos los argumentos se pueden convertir implícitamente a los tipos de parámetros formales. Considere esta versión simplificada de su programa:

class Program
{
    delegate void D1();
    delegate string D2();
    static string X() { return null; }
    static void Y(D1 d1) {}
    static void Y(D2 d2) {}
    static void Main()
    {
        Y(X);
    }
}

Así que vamos a repasarlo línea por línea.

Existe una conversión implícita de un grupo de métodos a un tipo de delegado compatible.

Ya he hablado de cómo la palabra "compatible" es desafortunada aquí. Hacia adelante. Nos preguntamos cuando hacemos una resolución de sobrecarga en Y (X), ¿el grupo de métodos X se convierte en D1? ¿Se convierte a D2?

Dado un tipo de delegado D y una expresión E que se clasifica como un grupo de métodos, existe una conversión implícita de E a D si E contiene al menos un método que es aplicable [...] a una lista de argumentos construida mediante el uso del parámetro tipos y modificadores de D, como se describe a continuación.

Hasta aquí todo bien. X puede contener un método que sea aplicable con las listas de argumentos de D1 o D2.

La aplicación en tiempo de compilación de una conversión de un grupo de métodos E a un tipo de delegado D se describe a continuación.

Esta línea realmente no dice nada interesante.

Tenga en cuenta que la existencia de una conversión implícita de E a D no garantiza que la aplicación de la conversión en tiempo de compilación tenga éxito sin errores.

Esta línea es fascinante. ¡Significa que hay conversiones implícitas que existen, pero que están sujetas a convertirse en errores! Esta es una regla extraña de C #. Para divagar un momento, aquí hay un ejemplo:

void Q(Expression<Func<string>> f){}
string M(int x) { ... }
...
int y = 123;
Q(()=>M(y++));

Una operación de incremento es ilegal en un árbol de expresión. Sin embargo, la lambda aún se puede convertir al tipo de árbol de expresión, aunque si alguna vez se usa la conversión, ¡es un error! El principio aquí es que podríamos querer cambiar las reglas de lo que puede ir en un árbol de expresión más adelante; cambiar esas reglas no debería cambiar las reglas del sistema de tipos . Queremos obligarlo a que sus programas sean inequívocos ahora , de modo que cuando cambiemos las reglas de los árboles de expresión en el futuro para mejorarlos, no introduzcamos cambios importantes en la resolución de sobrecarga .

De todos modos, este es otro ejemplo de este tipo de regla extraña. Puede existir una conversión a los efectos de la resolución de sobrecargas, pero su uso real puede ser un error. Aunque, de hecho, esa no es exactamente la situación en la que nos encontramos aquí.

Hacia adelante:

Se selecciona un único método M correspondiente a una invocación de método de la forma E (A) [...] La lista de argumentos A es una lista de expresiones, cada una clasificada como una variable [...] del parámetro correspondiente en el formato formal. -lista-de-parámetros de D.

OKAY. Así que sobrecargamos la resolución en X con respecto a D1. La lista de parámetros formales de D1 está vacía, así que sobrecargamos la resolución en X () y joy, encontramos un método "string X ()" que funciona. De manera similar, la lista de parámetros formales de D2 está vacía. Nuevamente, encontramos que "string X ()" es un método que también funciona aquí.

El principio aquí es que determinar la convertibilidad del grupo de métodos requiere seleccionar un método de un grupo de métodos usando la resolución de sobrecarga , y la resolución de sobrecarga no considera los tipos de retorno .

Si el algoritmo [...] produce un error, se produce un error en tiempo de compilación. De lo contrario, el algoritmo produce un único mejor método M que tiene el mismo número de parámetros que D y se considera que la conversión existe.

Solo hay un método en el grupo de métodos X, por lo que debe ser el mejor. Hemos probado con éxito que existe una conversión de X a D1 y de X a D2.

Ahora bien, ¿esta línea es relevante?

El método seleccionado M debe ser compatible con el tipo de delegado D; de lo contrario, se producirá un error en tiempo de compilación.

De hecho, no, no en este programa. Nunca llegamos a activar esta línea. Porque, recuerde, lo que estamos haciendo aquí es intentar resolver la sobrecarga en Y (X). Tenemos dos candidatos Y (D1) e Y (D2). Ambos son aplicables. ¿Qué es mejor ? En ninguna parte de la especificación describimos la mejora entre estas dos posibles conversiones .

Ahora bien, ciertamente se podría argumentar que una conversión válida es mejor que una que produce un error. Eso estaría diciendo efectivamente, en este caso, que la resolución de sobrecarga SÍ considera los tipos de retorno, que es algo que queremos evitar. La pregunta entonces es qué principio es mejor: (1) mantener el invariante de que la resolución de sobrecarga no considera los tipos de retorno, o (2) intentar elegir una conversión que sabemos que funcionará sobre una que sabemos que no lo hará.

Esta es una llamada de juicio. Con lambdas , que hacemos en cuenta el tipo de retorno en este tipo de conversiones, en el apartado 7.4.3.3:

E es una función anónima, T1 y T2 son tipos delegados o tipos de árboles de expresión con listas de parámetros idénticas, existe un tipo de retorno inferido X para E en el contexto de esa lista de parámetros, y se cumple una de las siguientes:

  • T1 tiene un tipo de retorno Y1 y T2 tiene un tipo de retorno Y2, y la conversión de X a Y1 es mejor que la conversión de X a Y2

  • T1 tiene un tipo de retorno Y, y T2 es un retorno nulo

Es lamentable que las conversiones de grupos de métodos y las conversiones lambda sean inconsistentes a este respecto. Sin embargo, puedo vivir con eso.

De todos modos, no tenemos una regla de "mejora" para determinar qué conversión es mejor, X a D1 o X a D2. Por lo tanto, damos un error de ambigüedad en la resolución de Y (X).

Eric Lippert
fuente
8
Cracking: muchas gracias tanto por la respuesta como (con suerte) por la mejora resultante en la especificación :) Personalmente, creo que sería razonable que la resolución de sobrecargas tuviera en cuenta el tipo de retorno para las conversiones de grupos de métodos para hacer el comportamiento más intuitivo, pero Entiendo que lo haría a costa de la coherencia. (Lo mismo puede decirse de la inferencia de tipo genérico aplicada a las conversiones de grupos de métodos cuando solo hay un método en el grupo de métodos, como creo que hemos discutido antes).
Jon Skeet
35

EDITAR: Creo que lo tengo.

Como dice zinglon, es porque hay una conversión implícita de GetStringa Actionaunque la aplicación en tiempo de compilación fallaría. Aquí está la introducción a la sección 6.6, con algo de énfasis (mío):

Existe una conversión implícita (§6.1) de un grupo de métodos (§7.1) a un tipo de delegado compatible. Dado un tipo de delegado D y una expresión E que se clasifica como un grupo de métodos, existe una conversión implícita de E a D si E contiene al menos un método que es aplicable en su forma normal (§7.4.3.1) a una lista de argumentos construida mediante el uso de los tipos de parámetros y modificadores de D , como se describe a continuación.

Ahora, me estaba confundiendo la primera oración, que habla de una conversión a un tipo de delegado compatible. Actionno es un delegado compatible para ningún método en el GetStringgrupo de métodos, pero el GetString()método es aplicable en su forma normal a una lista de argumentos construida mediante el uso de los tipos de parámetros y modificadores de D. Tenga en cuenta que esto no se refiere al tipo de retorno de D. Es por eso que se está confundiendo ... porque solo verificaría la compatibilidad del delegado GetString()al aplicar la conversión, no verificaría su existencia.

Creo que es instructivo dejar la sobrecarga fuera de la ecuación brevemente y ver cómo se puede manifestar esta diferencia entre la existencia de una conversión y su aplicabilidad . Aquí tienes un ejemplo breve pero completo:

using System;

class Program
{
    static void ActionMethod(Action action) {}
    static void IntMethod(int x) {}

    static string GetString() { return ""; }

    static void Main(string[] args)
    {
        IntMethod(GetString);
        ActionMethod(GetString);
    }
}

Ninguna de las expresiones de invocación de métodos en las Maincompilaciones, pero los mensajes de error son diferentes. Aquí está el de IntMethod(GetString):

Test.cs (12,9): error CS1502: la mejor coincidencia de método sobrecargado para 'Program.IntMethod (int)' tiene algunos argumentos no válidos

En otras palabras, la sección 7.4.3.1 de la especificación no puede encontrar ningún miembro de función aplicable.

Ahora aquí está el error para ActionMethod(GetString):

Test.cs (13,22): error CS0407: 'string Program.GetString ()' tiene el tipo de retorno incorrecto

Esta vez resolvió el método al que quiere llamar, pero no pudo realizar la conversión requerida. Desafortunadamente, no puedo encontrar la parte de la especificación donde se realiza la verificación final; parece que podría estar en 7.5.5.1, pero no puedo ver exactamente dónde.


Se eliminó la respuesta anterior, excepto por esta parte, porque espero que Eric pueda arrojar luz sobre el "por qué" de esta pregunta ...

Todavía mirando ... mientras tanto, si decimos "Eric Lippert" tres veces, ¿cree que tendremos una visita (y por lo tanto una respuesta)?

Jon Skeet
fuente
@Jon - ¿podría ser eso classWithSimpleMethods.GetStringy classWithSimpleMethods.DoNothingno somos delegados?
Daniel A. White
@Daniel: No, esas expresiones son expresiones de grupo de métodos, y los métodos sobrecargados solo deben considerarse aplicables cuando hay una conversión implícita del grupo de métodos al tipo de parámetro relevante. Consulte la sección 7.4.3.1 de la especificación.
Jon Skeet
Al leer la sección 6.6, parece que la conversión de classWithSimpleMethods.GetString a Action se considera que existe ya que las listas de parámetros son compatibles, pero que la conversión (si se intenta) falla en tiempo de compilación. Por lo tanto, una conversión implícita no existe para los dos tipos de delegados y la llamada es ambigua.
Zinglon
@zinglon: ¿Cómo está leyendo §6.6 para determinar que una conversión de ClassWithSimpleMethods.GetStringa Actiones válida? Para que un método Msea ​​compatible con un tipo de delegado D(§15.2) "existe una conversión de identidad o referencia implícita del tipo de retorno de Mal tipo de retorno de D".
Jason
@Jason: La especificación no dice que la conversión sea válida, dice que existe . De hecho, no es válido porque falla en el momento de la compilación. Los dos primeros puntos de §6.6 determinan si existe la conversión. Los siguientes puntos determinan si la conversión se realizará correctamente. Desde el punto 2: "De lo contrario, el algoritmo produce un único mejor método M que tiene el mismo número de parámetros que D y se considera que la conversión existe". §15.2 se invoca en el punto 3.
zinglon
1

El uso de Func<string>y Action<string>(obviamente muy diferente de Actiony Func<string>) en el ClassWithDelegateMethodselimina la ambigüedad.

La ambigüedad también ocurre entre Actiony Func<int>.

También obtengo el error de ambigüedad con esto:

class Program
{ 
    static void Main(string[] args) 
    { 
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods(); 
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods(); 

        classWithDelegateMethods.Method(classWithSimpleMethods.GetOne);
    } 
} 

class ClassWithDelegateMethods 
{ 
    public void Method(Func<int> func) { /* do something */ }
    public void Method(Func<string> func) { /* do something */ } 
}

class ClassWithSimpleMethods 
{ 
    public string GetString() { return ""; } 
    public int GetOne() { return 1; }
} 

La experimentación adicional muestra que cuando se pasa un grupo de métodos por sí mismo, el tipo de retorno se ignora por completo al determinar qué sobrecarga usar.

class Program
{
    static void Main(string[] args)
    {
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods();
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods();

        //The call is ambiguous between the following methods or properties: 
        //'test.ClassWithDelegateMethods.Method(System.Func<int,int>)' 
        //and 'test.ClassWithDelegateMethods.Method(test.ClassWithDelegateMethods.aDelegate)'
        classWithDelegateMethods.Method(classWithSimpleMethods.GetX);
    }
}

class ClassWithDelegateMethods
{
    public delegate string aDelegate(int x);
    public void Method(Func<int> func) { /* do something */ }
    public void Method(Func<string> func) { /* do something */ }
    public void Method(Func<int, int> func) { /* do something */ }
    public void Method(Func<string, string> func) { /* do something */ }
    public void Method(aDelegate ad) { }
}

class ClassWithSimpleMethods
{
    public string GetString() { return ""; }
    public int GetOne() { return 1; }
    public string GetX(int x) { return x.ToString(); }
} 
Matt Ellen
fuente
0

La sobrecarga con Funcy Actiones similar (porque ambos son delegados) a

string Function() // Func<string>
{
}

void Function() // Action
{
}

Si se da cuenta, el compilador no sabe a cuál llamar porque solo se diferencian por los tipos de retorno.

Daniel A. White
fuente
No creo que sea realmente así, porque no se puede convertir un Func<string>en un Action... y no se puede convertir un grupo de métodos que consiste solo en un método que devuelve una cadena en Actionuno.
Jon Skeet
2
No puede enviar un delegado que no tiene parámetros y vuelve stringa un Action. No veo por qué hay ambigüedad.
Jason
3
@dtb: Sí, eliminar la sobrecarga elimina el problema, pero eso no explica realmente por qué hay un problema.
Jon Skeet