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 Action
o 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 .
<LangVersion>7.3</LangVersion>
) o posterior gracias a los candidatos de sobrecarga mejorados .Respuestas:
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:
(é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:
Así que vamos a repasarlo línea por línea.
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?
Hasta aquí todo bien. X puede contener un método que sea aplicable con las listas de argumentos de D1 o D2.
Esta línea realmente no dice nada interesante.
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:
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:
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 .
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?
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:
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).
fuente
EDITAR: Creo que lo tengo.
Como dice zinglon, es porque hay una conversión implícita de
GetString
aAction
aunque 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):Ahora, me estaba confundiendo la primera oración, que habla de una conversión a un tipo de delegado compatible.
Action
no es un delegado compatible para ningún método en elGetString
grupo de métodos, pero elGetString()
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 delegadoGetString()
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:
Ninguna de las expresiones de invocación de métodos en las
Main
compilaciones, pero los mensajes de error son diferentes. Aquí está el deIntMethod(GetString)
: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)
: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)?
fuente
classWithSimpleMethods.GetString
yclassWithSimpleMethods.DoNothing
no somos delegados?ClassWithSimpleMethods.GetString
aAction
es válida? Para que un métodoM
sea compatible con un tipo de delegadoD
(§15.2) "existe una conversión de identidad o referencia implícita del tipo de retorno deM
al tipo de retorno deD
".El uso de
Func<string>
yAction<string>
(obviamente muy diferente deAction
yFunc<string>
) en elClassWithDelegateMethods
elimina la ambigüedad.La ambigüedad también ocurre entre
Action
yFunc<int>
.También obtengo el error de ambigüedad con esto:
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.
fuente
La sobrecarga con
Func
yAction
es similar (porque ambos son delegados) aSi se da cuenta, el compilador no sabe a cuál llamar porque solo se diferencian por los tipos de retorno.
fuente
Func<string>
en unAction
... y no se puede convertir un grupo de métodos que consiste solo en un método que devuelve una cadena enAction
uno.string
a unAction
. No veo por qué hay ambigüedad.