Resolución de sobrecarga del método C # no seleccionar la anulación genérica concreta


Este programa completo de C# ilustra el problema:

public abstract class Executor<T>
{
    public abstract void Execute(T item);
}

class StringExecutor : Executor<string>
{
    public void Execute(object item)
    {
        // why does this method call back into itself instead of binding
        // to the more specific "string" overload.
        this.Execute((string)item);
    }

    public override void Execute(string item) { }
}

class Program
{
    static void Main(string[] args)
    {
        object item = "value";
        new StringExecutor()
            // stack overflow
            .Execute(item); 
    }
}

Me encontré con una excepción de StackOverlowException que rastreé hasta este patrón de llamadas donde estaba tratando de reenviar llamadas a una sobrecarga más específica. Para mi sorpresa, sin embargo, la invocación no estaba seleccionando la sobrecarga más específica, sino llamando de nuevo a sí misma. Claramente tiene algo que ver con que el tipo base sea genérico, pero no entiendo por qué no seleccionaría la sobrecarga de ejecución(cadena).

¿alguien tiene ¿alguna idea de esto?

El código anterior se simplificó para mostrar el patrón, la estructura real es un poco más complicada, pero el problema es el mismo.

Author: MarkPflug, 2017-03-30

3 answers

Parece que esto se menciona en la especificación C # 5.0, 7.5.3 Resolución de sobrecarga:

La resolución de sobrecarga selecciona el miembro de función a invocar en los siguientes contextos distintos dentro de C#:

  • Invocación de un método nombrado en una expresión de invocación (§7.6.5.1).
  • Invocación de un constructor de instancia nombrado en una expresión de creación de objetos (§7.6.10.1).
  • Invocación de un accessor de indexador a través de un elemento-access (§7.6.6).
  • Invocación de un operador predefinido o definido por el usuario al que se hace referencia en una expresión (§7.3.3 y §7.3.4).

Cada uno de estos contextos define el conjunto de miembros de función candidatos y la lista de argumentos en su propia manera única, como se describe en detalle en las secciones enumeradas anteriormente. Por ejemplo, el conjunto de los candidatos a una invocación de método no incluyen los métodos marcados override (§7.4), y los métodos en una clase base no son candidatos si los hay método en un derivado la clase es aplicable (§7.6.5.1).

Cuando miramos 7.4:

Una búsqueda de miembro de un nombre N con parámetros de tipo K en un tipo T se procesa de la siguiente manera:

* Primero, se determina un conjunto de miembros accesibles llamados N:

  • Si T es un parámetro de tipo, entonces el conjunto es la unión de los conjuntos de
    miembros accesibles nombrados N en cada uno de los tipos especificados como restricción primaria o restricción secundaria (§10.1.5) para T, junto con conjunto de miembros accesibles llamados N en object.

  • De lo contrario, el conjunto consiste en todos los miembros accesibles (§3.5) nombrados N en T, incluidos los miembros heredados y los miembros accesibles nombrados N en objeto. Si T es un tipo construido, el conjunto de barras se obtiene sustituyendo argumentos de tipo como se describe en §10.3.2. Los miembros que incluyen un modificador de anulación se excluyen del conjunto.

Si elimina override el compilador elige el Execute(string) sobrecarga cuando lanzas el objeto.

 30
Author: Selman Genç,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-29 23:28:29

Como se menciona en el artículo de Jon Skeet sobre overloading, cuando se invoca un método en una clase que también anula un método con el mismo nombre desde una clase base, el compilador siempre tomará el método in-class en lugar de la anulación, independientemente de la "especificidad" del tipo, siempre que la firma sea "compatible".

Jon continúa señalando que este es un excelente argumento para evitar la sobrecarga a través de los límites de la herencia, ya que este es exactamente el tipo de comportamiento inesperado que puede ocurrir.

 23
Author: David L,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-29 23:27:01

Como han señalado otras respuestas, esto es por diseño.

Consideremos un ejemplo menos complicado:

class Animal
{
  public virtual void Eat(Apple a) { ... }
}
class Giraffe : Animal
{
  public void Eat(Food f) { ... }
  public override void Eat(Apple a) { ... }
}

La pregunta es por qué giraffe.Eat(apple) se resuelve a Giraffe.Eat(Food) y no a lo virtual Animal.Eat(Apple).

Esto es consecuencia de dos reglas:{[16]]}

(1) El tipo del receptor es más importante que el tipo de cualquier argumento al resolver sobrecargas.

Espero que esté claro por qué debe ser así. La persona que escribe la clase derivada tiene estrictamente más conocimiento que la persona que escribe la clase base, porque la persona que escribe la clase derivada utiliza la clase base, y no viceversa.

La persona que escribió Giraffe dijo: "Tengo una manera para que un Giraffe coma cualquier alimento" y eso requiere un conocimiento especial de los aspectos internos de la digestión de la jirafa. Esa información no está presente en la implementación de la clase base, que solo sabe comer manzanas.

Así que la resolución de sobrecarga siempre debe priorizar la elección de un método aplicable de un clase derivada sobre la elección de un método de una clase base, independientemente de la mejora de las conversiones de tipo de argumento.

(2) Elegir sobreescribir o no sobreescribir un método virtual no es parte del área de superficie pública de una clase. Es un detalle de implementación privada. Por lo tanto, no se debe tomar ninguna decisión al hacer una resolución de sobrecarga que cambiaría dependiendo de si un método se sobrescribe o no.

La resolución de sobrecarga nunca debe decir " Voy a elegir virtual Animal.Eat(Apple) porque fue anulado".

Ahora bien, podrías decir "OK, supongamos que estoy dentro de Jirafa cuando estoy haciendo la llamada."Code inside Giraffe tiene todo el conocimiento de los detalles de implementación privada, ¿verdad? Así que podría tomar la decisión de llamar virtual Animal.Eat(Apple) en lugar de Giraffe.Eat(Food) cuando se enfrenta a giraffe.Eat(apple), ¿verdad? Porque sabe que hay una implementación que entiende las necesidades de las jirafas que comen manzanas.

Eso es una cura peor que la enfermedad. Ahora tenemos una situación donde idénticocódigo tiene diferente comportamiento dependiendo de donde se ejecuta! Usted puede imaginar tener una llamada a giraffe.Eat(apple) fuera de la clase, refactorizarla para que esté dentro de la clase, y de repente cambios de comportamiento observables!

O, se podría decir, hey, me doy cuenta de que mi lógica Jirafa es en realidad lo suficientemente general para pasar a una clase base, pero no a Animal, así que voy a refactorizar mi código Giraffe a:

class Mammal : Animal 
{
  public void Eat(Food f) { ... } 
  public override void Eat(Apple a) { ... }
}
class Giraffe : Mammal
{
  ...
}

Y ahora todas las llamadas a giraffe.Eat(apple) dentro Giraffe de repente tienen diferente comportamiento de resolución de sobrecarga después de la refactorización? Que sería muy inesperado!

C# es un lenguaje de pozo de éxito; queremos asegurarnos de que refactorizaciones simples como cambiar dónde en una jerarquía se anula un método no causen cambios sutiles en el comportamiento.

Resumiendo:

  • La resolución de sobrecarga prioriza a los receptores sobre otros argumentos porque llamar a código especializado que conoce los aspectos internos del receptor es mejor que llamar a código más general que no lo hace.
  • Si y dónde se sobrescribe un método no se considera durante la resolución de sobrecarga; todos los métodos se tratan como si nunca se sobrescribieran para propósitos de resolución de sobrecarga. Es un detalle de implementación, no parte de la superficie del tipo.
  • Los problemas de resolución de sobrecarga se resuelven mod modulo accesibilidad de ¡por supuesto! -- de la misma manera, sin importar dónde se produzca el problema en el código. No tenemos un algoritmo para la resolución donde el receptor es del tipo del código que contiene, y otro para cuando la llamada está en una clase diferente.

Se pueden encontrar reflexiones adicionales sobre temas relacionados aquí: https://ericlippert.com/2013/12/23/closer-is-better / y aquí https://blogs.msdn.microsoft.com/ericlippert/2007/09/04/future-breaking-changes-part-three/

 16
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-31 05:39:22