2009-05-28 18 views
6

Hoy encontré un problema de compilación que me desconcertaba. Considere estas dos clases de contenedor.Genéricos, herencia y resolución de métodos fallidos del compilador de C#

public class BaseContainer<T> : IEnumerable<T> 
{ 
    public void DoStuff(T item) { throw new NotImplementedException(); } 

    public IEnumerator<T> GetEnumerator() { } 
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { } 
} 
public class Container<T> : BaseContainer<T> 
{ 
    public void DoStuff(IEnumerable<T> collection) { } 

    public void DoStuff <Tother>(IEnumerable<Tother> collection) 
     where Tother: T 
    { 
    } 
} 

Los primero define DoStuff(T item) y los segundos sobrecargas de TI con DoStuff <Tother>(IEnumerable<Tother>) específicamente para moverse por la ausencia de covariance/contravariance de C# (hasta 4 escucho).

Este código

Container<string> c = new Container<string>(); 
c.DoStuff("Hello World"); 

realiza un error de compilación bastante extraño. Tenga en cuenta la ausencia de <char> de la llamada a método.

El tipo char 'no se puede utilizar como parámetro de tipo 'Tother' en el tipo genérico o método 'Container.DoStuff (System.Collections.Generic.IEnumerable)'. No hay conversión de boxeo de 'char' a 'cadena'.

En esencia, el compilador está tratando de atascar mi llamada a DoStuff(string) en Container.DoStuff<char>(IEnumerable<char>) PORQUE string implementos IEnumerable<char>, en lugar de utilizar BaseContainer.DoStuff(string).

La única forma que he encontrado para hacer esta compilación es añadir DoStuff(T) a la clase derivada

public class Container<T> : BaseContainer<T> 
{ 
    public new void DoStuff(T item) { base.DoStuff(item); } 

    public void DoStuff(IEnumerable<T> collection) { } 

    public void DoStuff <Tother>(IEnumerable<Tother> collection) 
     where Tother: T 
    { 
    } 
} 

¿Por qué es el compilador tratando de atascar una cadena como IEnumerable<char> cuando: 1) se sabe que puede' t (dada la presencia de un error de compilación) y 2) tiene un método en la clase base que compila bien? ¿Estoy malentendiendo algo sobre genéricos o cosas de métodos virtuales en C#? ¿Hay alguna otra solución que no sea agregar un new DoStuff(T item) al Container?

+3

acepto esto parece raro, pero es correcta de acuerdo con la especificación. Esto es consecuencia de la interacción de dos reglas: (1) la verificación de la aplicabilidad de la resolución de sobrecarga ocurre ANTES de la verificación de restricciones, y (2) los métodos aplicables en las clases derivadas son SIEMPRE mejores que los métodos aplicables en las clases base. Ambas son reglas razonablemente sensatas; simplemente interactúan particularmente mal en tu caso. –

+1

Para detalles, consulte la sección 7.5.5.1, específicamente los bits que dicen: (1) "Si el mejor método es un método genérico, los argumentos de tipo (suministrados o inferidos) se comparan con las restricciones ..." y (2) " el conjunto de métodos candidatos se reduce para contener solo métodos de los tipos más derivados ... " –

+2

En última instancia, su problema aquí es un problema de diseño. Está sobrecargando un método "DoStuff" para que signifique "hacer cosas en un único valor de tipo T" y "hacer cosas en una secuencia de valores de tipo T". Esto entra en graves problemas de "resolución de intención" de muchas maneras, por ejemplo, cuando el "tipo T" es en sí mismo una secuencia. Encontrará que las clases de colección existentes en el BCL se han diseñado cuidadosamente para evitar este problema; Los métodos que toman un ítem se llaman "Frob", los métodos que toman una secuencia de ítems se llaman "FrobRange", por ejemplo "Add" y "AddRange" en las listas. –

Respuesta

3

Editar

Ok ... Me parece ver su confusión ahora. Habría esperado que DoStuff (cadena) mantuviera el parámetro como una cadena y recorriera la Lista de métodos de BaseClass buscando primero una firma adecuada y, en su defecto, tratando de convertir el parámetro a otro tipo.

Pero sucedió al revés ... En vez Container.DoStuff(string) fueron, meh "theres un método de clase base hay que encaja a la perfección, pero voy a convertir en una interfaz IEnumerable y tener un ataque al corazón de lo que está disponible en la clase actual en lugar ...

Hmmm ... estoy seguro de Jon o Marc sería capaz de meter su cuchara en este punto con el párrafo C# Spec específica que cubre este caso particular rincón

original

Ambos métodos esperan una Columna IEnumerable lection

Estás pasando una cadena individual.

El compilador está tomando esa cadena y venir,

Ok, tengo una cadena, Ambos métodos esperar una IEnumerable<T>, así que voy a convertir esta cadena en un IEnumerable<char> ... Hecho

correcta, verifica el primer método ... hmmm ... esta clase es un Container<string> pero tengo una IEnumerable<char> de manera que no es correcto.

Comprobar el segundo método, hmmm .... Me tiene un IEnumerable<char> pero Char no implementa cuerda de manera que es no está bien tampoco.

error del compilador

Entonces, ¿qué # s la revisión, así que depende por completo lo que su tratando de lograr ... ambos de los siguientes sería válida, en esencia, su uso de tipos es simplemente incorrecto en tu encarnación.

 Container<char> c1 = new Container<char>(); 
     c1.DoStuff("Hello World"); 

     Container<string> c2 = new Container<string>(); 
     c2.DoStuff(new List<string>() { "Hello", "World" }); 
+0

Entonces, el compilador elige una sobrecarga solo por los tipos de los parámetros y no por las restricciones impuestas sobre ellos (¿no como un conjunto de parámetros + restricciones? Eso parece bastante débil y medio horneado. * Puede * encontrar un método que * pueda * usar, pero está optando * no * por. –

+0

no, no puede ... ninguno de sus métodos satisface el tipo de su paso. El contenedor actualmente solo puede ejecutar DoStuff (IEnumerable ) O DoStuff (IEnumerable ) <- que no es nada ya que string es clase sellada. –

+0

No puede? ¿Qué pasó con BaseContainer.DoStuff (T)? En cuanto a su edición de corrección. Quiero un contenedor de cuerda y la cadena "Hello World" para ser DoStuff'ed. Si no puedo arreglar la clase, DoStuff (nueva cadena [] {"Hello World"}); es lo que quiero, pero esa es una API realmente mala para proporcionar (creo). –

1

Creo que tiene algo que ver con el hecho de que char es un tipo de valor y la cadena es un tipo de referencia. Parece que está definiendo

TOther : T 

y char no deriva de la cadena.

+0

De ahí mi desconcierto por qué está escogiendo DoStuff (IEnumerable ) como el método que estoy invocando. No estoy especificando DoStuff no es un derivado de cadena. No tiene sentido de ninguna manera. –

+0

Me pregunto si la cadena se puede convertir a IEnumerable y el compilador infiere Tother para ser char? – n8wrl

+0

Creo que eso es exactamente lo que está haciendo, pero no estoy invocando DoStuff , solo DoStuff. Nunca he visto el genérico asumido así. –

2

El compilador intentará hacer coincidir el parámetro con IEnumerable <T>. El tipo String implementa IEnumerable <char>, por lo que se supone que T es "char".

Después de eso, el compilador comprueba la otra condición "donde OtherT: T" no se cumple, y esa condición. De ahí el error del compilador.

2

Mi GUESS, y esto es una suposición porque realmente no sé, es que primero se ve en la clase derivada para resolver la llamada al método (porque su objeto es el del tipo derivado). Si, y solo si no puede, pasa a mirar los métodos de las clases base para resolverlo. En su caso, dado que PUEDE resolverlo utilizando la sobrecarga

DoStuff <Tother>(IEnumerable<Tother> collection) 

, intentó atascarse en eso. Por lo tanto, PUEDE resolverlo en lo que respecta al parámetro, pero luego se topa con un obstáculo en las restricciones. En ese punto, ya se ha resuelto su sobrecarga, por lo que no busca más, pero solo genera un error. ¿Tener sentido?

+0

Excepto que supone implícitamente Tother = char independientemente del hecho de que no especifique eso. Nunca he visto el genérico en un método simplemente asumido porque puede confundir el argumento en uno. –

+1

¿Nunca has usado LINQ? La mayoría de LINQ se basa en la idea de que el parámetro de tipo se puede suponer si el tipo se proporciona en uno de los parámetros del método. –

+2

Está especificando que Tother es char, al pasar un IEnumerable , también conocido como "cadena". Es posible que no esté familiarizado con la función "inferencia del tipo de método" de C#, pero ha estado presente desde C# 2.0. Consulte la sección "inferencia de tipo" de mi blog si desea detalles sobre cómo funcionan los algoritmos de inferencia de tipo de método; son bastante fascinantes –

3

Como se ha explicado Eric Lippert, el compilador elige el método DoStuff<Tother>(IEnumerable<Tother>) where Tother : T {} porque elige métodos antes de comprobar las limitaciones. Como la cadena puede hacer IEnumerable<>, el compilador lo compara con ese método de clase hijo. El compilador funciona correctamente como se describe en la especificación C#.

El orden de resolución de método que desee puede forzarse implementando DoStuff como extension method. Los métodos de extensión se comprueban después métodos de la clase base, por lo que no van a tratar de igualar string contra DoStuff 's IEnumerable<Tother> hasta después de que se ha tratado de compararlo con DoStuff<T>.

El siguiente código muestra el orden de resolución de método, la covarianza y la herencia deseados. Copie/pegue en un nuevo proyecto.

Este inconveniente mayor que se me ocurre es que hasta el momento no se puede utilizar en los métodos base primordiales, pero creo que hay formas de evitar que (pregunte si está interesado).

using System; 
using System.Collections.Generic; 

namespace MethodResolutionExploit 
{ 
    public class BaseContainer<T> : IEnumerable<T> 
    { 
     public void DoStuff(T item) { Console.WriteLine("\tbase"); } 
     public IEnumerator<T> GetEnumerator() { return null; } 
     System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return null; } 
    }   
    public class Container<T> : BaseContainer<T> { } 
    public class ContainerChild<T> : Container<T> { } 
    public class ContainerChildWithOverride<T> : Container<T> { } 
    public static class ContainerExtension 
    { 
     public static void DoStuff<T, Tother>(this Container<T> container, IEnumerable<Tother> collection) where Tother : T 
     { 
      Console.WriteLine("\tContainer.DoStuff<Tother>()"); 
     } 
     public static void DoStuff<T, Tother>(this ContainerChildWithOverride<T> container, IEnumerable<Tother> collection) where Tother : T 
     { 
      Console.WriteLine("\tContainerChildWithOverride.DoStuff<Tother>()"); 
     } 
    } 

    class someBase { } 
    class someChild : someBase { } 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      Console.WriteLine("BaseContainer:"); 
      var baseContainer = new BaseContainer<string>(); 
      baseContainer.DoStuff(""); 

      Console.WriteLine("Container:"); 
      var container = new Container<string>(); 
      container.DoStuff(""); 
      container.DoStuff(new List<string>()); 

      Console.WriteLine("ContainerChild:"); 
      var child = new ContainerChild<string>(); 
      child.DoStuff(""); 
      child.DoStuff(new List<string>()); 

      Console.WriteLine("ContainerChildWithOverride:"); 
      var childWithOverride = new ContainerChildWithOverride<string>(); 
      childWithOverride.DoStuff(""); 
      childWithOverride.DoStuff(new List<string>()); 

      //note covariance 
      Console.WriteLine("Covariance Example:"); 
      var covariantExample = new Container<someBase>(); 
      var covariantParameter = new Container<someChild>(); 
      covariantExample.DoStuff(covariantParameter); 

      // this won't work though :(
      // var covariantExample = new Container<Container<someBase>>(); 
      // var covariantParameter = new Container<Container<someChild>>(); 
      // covariantExample.DoStuff(covariantParameter); 

      Console.ReadKey(); 
     } 
    } 
} 

Aquí está la salida:

BaseContainer: 
     base 
Container: 
     base 
     Container.DoStuff<Tother>() 
ContainerChild: 
     base 
     Container.DoStuff<Tother>() 
ContainerChildWithOverride: 
     base 
     ContainerChildWithOverride.DoStuff<Tother>() 
Covariance Example: 
     Container.DoStuff<Tother>() 

hay algún problema con esta solución?

+0

Hmm, idea interesante. No puse esas sobrecargas en BaseContainer porque intento mantener esa clase muy básica. Fácilmente podría poner DoStuff en BaseContainer, pero ese tipo derrota mi objetivo de mantener BaseContainer básico. La extensión significa que técnicamente mantengo BaseContainer como quiero, pero ... :) –

+0

Colin, * no * pone las sobrecargas en el BaseContainer. es decir, no destruye el intellisense de BaseContainer. Pruébalo por favor. Si no entendí bien, por favor aclare. – dss539

+0

Mis disculpas, leí mal el prototipo del método. El uso de una extensión en una clase genérica, sin embargo, significa que tengo que invocarlo por c.DoStuff en lugar de simplemente c.DoStuff , ¿verdad? –

0

No estoy muy claro en lo que estamos tratando de lograr, lo que te impide simplemente usando dos métodos, DoStuff(T item) and DoStuff(IEnumerable<T> collection)?

+0

El problema es cuando T en sí es un IEnumerable ya que el compilador se ajusta a DoStuff (IEnumerable ), no a DoStuff (T). –

Cuestiones relacionadas