2010-10-25 19 views
43

considerar dos métodos de extensión:llamada ambigua entre dos C# extensión métodos genéricos uno donde T: clase y otra donde T: struct

public static T MyExtension<T>(this T o) where T:class 
public static T MyExtension<T>(this T o) where T:struct 

y una clase:

class MyClass() { ... } 

ahora llamamos el método de extensión en una instancia de la clase anterior:

var o = new MyClass(...); 
o.MyExtension(); //compiler error here.. 
o.MyExtension<MyClass>(); //tried this as well - still compiler error.. 

el compilador dice que una llamada al método es una cal ambigua Cuando lo llamo en una clase. Hubiera pensado que podría determinar qué método de extensión llamar, ya que MyClass es una clase, no una estructura.

+0

¡Neat find! Pero, ¿cuál es tu pregunta? Una solución? –

+2

Buena pregunta. Pensé que tenía una respuesta simple, pero resultó que no. Espero que no te importe que mi "respuesta" sea más una exploración de lo que está sucediendo que una respuesta en sí misma. –

+0

Gracias por sus comentarios. Eamon - lo siento, mi pregunta no es tan clara - es realmente por qué el compilador no puede determinar el mejor método para usar. Después de leer los comentarios y preguntas, y el enlace proporcionado por LukeH, es porque el compilador no tiene en cuenta las restricciones de tipo al determinar el mejor método para usar. –

Respuesta

34

EDIT: Tengo ahora blogged about this con más detalle.


Mi original (y ahora creo incorrecta) pensamiento: las limitaciones genéricas no son tomados en cuenta durante las fases de inferencia de resolución de sobrecarga y tipo - que sólo son utilizados para validar el resultado de la resolución de sobrecarga.

EDITAR: Bien, después de un lote de dar la vuelta en esto, creo que estoy allí. Básicamente mi primer pensamiento fue casi correcto.

Las restricciones de tipo genérico solo actúan para eliminar los métodos de un conjunto de candidatos en un conjunto limitado de circunstancias muy ... en particular, solo cuando el tipo de un parámetro en sí es genérico; no solo un parámetro de tipo, sino un tipo genérico que usa un parámetro de tipo genérico. En ese punto, son las restricciones en los parámetros de tipo del tipo genérico los que se validan, no las restricciones en los parámetros de tipo del método genérico al que se llama.

Por ejemplo:

// Constraint won't be considered when building the candidate set 
void Foo<T>(T value) where T : struct 

// The constraint *we express* won't be considered when building the candidate 
// set, but then constraint on Nullable<T> will 
void Foo<T>(Nullable<T> value) where T : struct 

Así que si se intenta llamar Foo<object>(null) el método anterior no será parte del conjunto de candidatos, porque Nullable<object> value no satisface las restricciones de Nullable<T>. Si hay otros métodos aplicables, la llamada aún podría tener éxito.

Ahora bien, en el caso anterior, las limitaciones son exactamente lo mismo ... pero no es necesario. Por ejemplo, considere:

class Factory<TItem> where TItem : new() 

void Foo<T>(Factory<T> factory) where T : struct 

Si se intenta llamar Foo<object>(null), el método seguirá siendo parte del conjunto de candidatos - porque cuando TItem es object, la restricción expresa en Factory<TItem> todavía mantiene, y que es lo que está controlada al construir el conjunto de candidatos. Si esto resulta ser el mejor método, entonces falla la validación posterior, cerca del extremo de 7.6.5.1:

Si el mejor método es un método genérico, los argumentos de tipo (suministrado o inferida) se comparan con las restricciones (§4.4.4) declaradas en el método genérico. Si cualquier argumento de tipo no satisface la (s) restricción (es) correspondiente (s) en el parámetro de tipo, se produce un error de tiempo de enlace.

Eric's blog post contiene más detalles sobre esto.

+0

+1, y paginación Eric Lippert ... – Richard

+0

@Jon: estoy bastante seguro de que su respuesta original es correcta, aunque estoy de acuerdo en que es difícil ver exactamente dónde y cómo se codifica en la especificación. La sección 7.6.5.1 parece ambigua a este respecto, pero la idea de que las restricciones no son parte de la firma está bien establecida (y la afirman con confianza los que conocen estas cosas, por ejemplo http://blogs.msdn.com/b /ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx). – LukeH

+1

Mi especulación sobre por qué su ejemplo no se compila ... Una de las reglas en 7.5.3.2 dice * "... si todos los parámetros de Mp tienen un argumento correspondiente, mientras que los argumentos predeterminados deben ser sustituidos por al menos un parámetro opcional en Mq, entonces Mp es mejor que Mq "*. De acuerdo con esa regla, y suponiendo que las restricciones * no * se toman en consideración, entonces su segundo método resulta ser una mejor coincidencia para 'Foo ()', aunque posteriormente causará un error cuando las restricciones están validados (Es mejor porque no requiere sustituciones, mientras que el primer método sí lo hace). – LukeH

10

Eric Lippert explica mejor que nunca, here.

Me he encontrado con esto. Mi solución fue

public void DoSomthing<T> (T theThing){ 
    if (typeof (T).IsValueType) 
     DoSomthingWithStruct (theThing); 
    else 
     DoSomthingWithClass (theThing); 
} 

// edit - seems I just lived with boxing 

public void DoSomthingWithStruct (object theThing) 
public void DoSomthingWithClass(object theThing) 
+0

Courtney, no pude conseguir esto para compilar: dentro del método DoSomething, dice que espera que un tipo de valor llame a DoSomthingWithStruct y un tipo de referencia para llamar a DoSomthingWithClass. –

+0

Ah, tendré que comprobar exactamente qué era lo que hice mañana –

+0

Para evitar el boxeo, se debe definir una clase estática 'SomethingDoer ' con una propiedad de solo lectura de campo respaldada de tipo 'Acción ' llamada 'DoSomething' ; el constructor de clase debe usar Reflection para construir un delegado para llamar a 'DoSomethingWithStruct (T param) donde T: struct',' DoSomethingWithClass (T param) donde T: class', o 'DoSomethingWithNullable (Nullable param)' y almacenar en ese campo La reflexión solo tendría que usarse una vez para cualquier parámetro de tipo dado; después de eso, el delegado invocaría el método apropiado directamente. – supercat

4

yo encontramos este modo extraño "interesante" para hacer que en .NET 4.5 utilizando valores de los parámetros por defecto :) Tal vez es más útil para fines especulativos \ educativos que para el uso real, pero me gustaría mostrar it:

/// <summary>Special magic class that can be used to differentiate generic extension methods.</summary> 
public class MagicValueType<TBase> 
    where TBase : struct 
{ 
} 

/// <summary>Special magic class that can be used to differentiate generic extension methods.</summary> 
public class MagicRefType<TBase> 
    where TBase : class 
{ 
} 

struct MyClass1 
{ 
} 

class MyClass2 
{ 
} 

// Extensions 
public static class Extensions 
{ 
    // Rainbows and pink unicorns happens here. 
    public static T Test<T>(this T t, MagicRefType<T> x = null) 
     where T : class 
    { 
     Console.Write("1:" + t.ToString() + " "); 
     return t; 
    } 

    // More magic, other pink unicorns and rainbows. 
    public static T Test<T>(this T t, MagicValueType<T> x = null) 
     where T : struct 
    { 
     Console.Write("2:" + t.ToString() + " "); 
     return t; 
    } 
} 

class Program 
{ 
    static void Main(string[] args) 
    { 

     MyClass1 t1 = new MyClass1(); 
     MyClass2 t2 = new MyClass2(); 

     MyClass1 t1result = t1.Test(); 
     Console.WriteLine(t1result.ToString()); 

     MyClass2 t2result = t2.Test(); 
     Console.WriteLine(t2result.ToString()); 

     Console.ReadLine(); 
    } 
} 
+0

¿Por qué es necesario el atributo '[Serializable]'? – Juan

+0

No es. Mi error :) –

Cuestiones relacionadas