¿Por qué el compilador de C # no genera errores en el código cuando un método estático llama a un método de instancia?

110

El siguiente código tiene un método estático, Foo(), llamando a un método de instancia, Bar():

public sealed class Example
{
    int count;

    public static void Foo( dynamic x )
    {
        Bar(x);
    }

    void Bar( dynamic x )
    {
        count++;
    }
}

Se compila sin errores * pero genera una excepción de enlace en tiempo de ejecución en tiempo de ejecución. La eliminación del parámetro dinámico de estos métodos provoca un error del compilador, como se esperaba.

Entonces, ¿por qué tener un parámetro dinámico permite compilar el código? ReSharper tampoco lo muestra como un error.

Edición 1: * en Visual Studio 2008

Edición 2: agregada sealedya que es posible que una subclase pueda contener un Bar(...)método estático . Incluso la versión sellada se compila cuando no es posible que se pueda llamar a otro método que no sea el método de instancia en tiempo de ejecución.

Mike Scott
fuente
8
+1 para muy buena pregunta
cuongle
40
Ésta es una pregunta de Eric-Lippert.
Olivier Jacot-Descombes
3
Estoy bastante seguro de que Jon Skeet también sabría qué hacer con esto;) @ OlivierJacot-Descombes
Thousand
2
@Olivier, Jon Skeet probablemente quería que el código se compilara, por lo que el compilador lo permite :-))
Mike Scott
5
Este es otro ejemplo de por qué no debería usarlo a dynamicmenos que realmente lo necesite.
Servicio

Respuestas:

71

ACTUALIZACIÓN: la respuesta a continuación se escribió en 2012, antes de la introducción de C # 7.3 (mayo de 2018) . En Novedades de C # 7.3 , la sección Candidatos de sobrecarga mejorados , elemento 1, se explica cómo han cambiado las reglas de resolución de sobrecarga para que las sobrecargas no estáticas se descarten antes. Entonces, ¡la respuesta a continuación (y toda esta pregunta) tiene en su mayoría solo interés histórico por ahora!


(Pre C # 7.3 :)

Por alguna razón, la resolución de sobrecarga siempre encuentra la mejor coincidencia antes de buscar estática o no estática. Pruebe este código con todos los tipos estáticos:

class SillyStuff
{
  static void SameName(object o) { }
  void SameName(string s) { }

  public static void Test()
  {
    SameName("Hi mom");
  }
}

Esto no se compilará porque la mejor sobrecarga es la que toma un archivo string. Pero bueno, ese es un método de instancia, así que el compilador se queja (en lugar de tomar la segunda mejor sobrecarga).

Adición: Creo que la explicación del dynamicejemplo de la pregunta original es que, para ser coherentes, cuando los tipos son dinámicos también encontramos primero la mejor sobrecarga (comprobando solo el número de parámetro y los tipos de parámetro, etc., no estático vs. -static), y solo entonces compruebe si hay estática. Pero eso significa que la verificación estática tiene que esperar hasta el tiempo de ejecución. De ahí el comportamiento observado.

Adición tardía: algunos antecedentes sobre por qué eligieron hacer cosas en este orden divertido se pueden inferir de esta publicación de blog de Eric Lippert .

Jeppe Stig Nielsen
fuente
No hay sobrecargas en la pregunta original. Las respuestas que muestran una sobrecarga estática no son relevantes. No es válido responder "bueno, si escribiste esto ..." ya que yo no escribí eso :-)
Mike Scott
5
@MikeScott Solo trato de convencerlo de que la resolución de sobrecarga en C # siempre es así: (1) Encuentre la mejor coincidencia sin tener en cuenta estática / no estática. (2) Ahora sabemos qué sobrecarga usar, luego verificamos la estática. Debido a esto, cuando dynamicse introdujo en el lenguaje, creo que los diseñadores de C # dijeron: "No consideraremos (2) el tiempo de compilación cuando sea una dynamicexpresión". Entonces, mi propósito aquí es tener una idea de por qué eligieron no verificar estático versus instancia hasta el tiempo de ejecución. Yo diría que esta verificación ocurre en el momento de la vinculación .
Jeppe Stig Nielsen
bastante justo, pero todavía no explica por qué en este caso el compilador no puede resolver la llamada al método de instancia. En otras palabras, la forma en que el compilador hace la resolución es simplista: no reconoce el caso simple como mi ejemplo, donde no hay posibilidad de que no pueda resolver la llamada. La ironía es: al tener un solo método Bar () con un parámetro dinámico, el compilador ignora ese único método Bar ().
Mike Scott
45
Escribí esta parte del compilador de C # y Jeppe tiene razón. Vota esto. La resolución de sobrecarga ocurre antes de verificar si un método dado es un método estático o de instancia, y en este caso posponemos la resolución de sobrecarga al tiempo de ejecución y, por lo tanto, también la verificación estática / de instancia hasta el tiempo de ejecución. Además, el compilador hace un "mejor esfuerzo" para encontrar estáticamente errores dinámicos, lo que no es en absoluto exhaustivo.
Chris Burrows
30

Foo tiene un parámetro "x" que es dinámico, lo que significa que Bar (x) es una expresión dinámica.

Sería perfectamente posible que Example tuviera métodos como:

static Bar(SomeType obj)

En cuyo caso se resolvería el método correcto, por lo que la declaración Bar (x) es perfectamente válida. El hecho de que exista un método de instancia Bar (x) es irrelevante y ni siquiera se considera: por definición , dado que Bar (x) es una expresión dinámica, hemos diferido la resolución al tiempo de ejecución.

Marc Gravell
fuente
14
pero cuando elimina el método Bar de instancia, ya no se compila.
Justin Harvey
1
@Justin interesante - ¿una advertencia? ¿O un error? De cualquier manera, puede estar validando solo hasta el grupo de métodos, dejando la resolución de sobrecarga completa al tiempo de ejecución.
Marc Gravell
1
@Marc, dado que no hay otro método Bar (), no está respondiendo la pregunta. ¿Puede explicar esto dado que solo hay un método Bar () sin sobrecargas? ¿Por qué diferir al tiempo de ejecución cuando no hay forma de llamar a ningún otro método? ¿O hay? Nota: he editado el código para sellar la clase, que aún se compila.
Mike Scott
1
@mike en cuanto a por qué diferir el tiempo de ejecución: porque eso es lo que significa
Marc Gravell
2
@ Mike imposible no es el punto; lo importante es si es necesario . El punto con la dinámica es que no es el trabajo del compilador.
Marc Gravell
9

La expresión "dinámica" se vinculará durante el tiempo de ejecución, por lo que si define un método estático con la firma correcta o un método de instancia, el compilador no lo comprobará.

El método "correcto" se determinará durante el tiempo de ejecución. El compilador no puede saber si hay un método válido allí durante el tiempo de ejecución.

La palabra clave "dinámica" se define para lenguajes dinámicos y de secuencia de comandos, donde el método se puede definir en cualquier momento, incluso durante el tiempo de ejecución. Cosas locas

Aquí, una muestra que maneja entradas pero no cadenas, debido al método, está en la instancia.

class Program {
    static void Main(string[] args) {
        Example.Foo(1234);
        Example.Foo("1234");
    }
}
public class Example {
    int count;

    public static void Foo(dynamic x) {
        Bar(x);
    }

    public static void Bar(int a) {
        Console.WriteLine(a);
    }

    void Bar(dynamic x) {
        count++;
    }
}

Puede agregar un método para manejar todas las llamadas "incorrectas", que no se pudieron manejar

public class Example {
    int count;

    public static void Foo(dynamic x) {
        Bar(x);
    }

    public static void Bar<T>(T a) {
        Console.WriteLine("Error handling:" + a);
    }

    public static void Bar(int a) {
        Console.WriteLine(a);
    }

    void Bar(dynamic x) {
        count++;
    }
}
oberfreak
fuente
¿No debería el código de llamada en su ejemplo ser Example.Bar (...) en lugar de Example.Foo (...)? ¿No es Foo () irrelevante en su ejemplo? Realmente no entiendo tu ejemplo. ¿Por qué causaría un problema agregar el método genérico estático? ¿Podría editar su respuesta para incluir ese método en lugar de darlo como una opción?
Mike Scott
pero el ejemplo que publiqué tiene solo un método de instancia única y sin sobrecargas, por lo que en el momento de la compilación sabe que no hay métodos estáticos posibles que puedan resolverse. Solo si agrega al menos uno, la situación cambia y el código es válido.
Mike Scott
Pero este ejemplo todavía tiene más de un método Bar (). Mi ejemplo tiene un solo método. Así que no hay posibilidad de llamar a ningún método Bar () estático. La llamada se puede resolver en tiempo de compilación.
Mike Scott
@Mike puede ser! = Is; con dinámica, no es necesario hacerlo
Marc Gravell
@MarcGravell, ¿aclara?
Mike Scott