2011-11-01 15 views
18

Durante la lectura de la documentación de Microsoft, me encontré con un ejemplo de código tan interesante:¿Por qué hay una restricción para convertir explícitamente un genérico a un tipo de clase pero no hay restricciones para convertir un genérico a un tipo de interfaz?

interface ISomeInterface 
{...} 
class SomeClass 
{...} 
class MyClass<T> 
{ 
    void SomeMethod(T t) 
    { 
     ISomeInterface obj1 = (ISomeInterface)t;//Compiles 
     SomeClass  obj2 = (SomeClass)t;  //Does not compile 
    } 
} 

Esto significa que puede emitir su genérico para la interfaz explícita, pero no a la clase a menos que tenga una restricción. Bueno, todavía no puedo entender la lógica detrás de la decisión, ya que tanto la interfaz como el tipo de clase emiten excepciones, entonces, ¿por qué uno debería protegerse contra solo una de estas excepciones?

Por cierto, no es una forma de evitar el error de compilación, pero esto no elimina el desorden lógica en mi cabeza:

class MyOtherClass 
{...} 

class MyClass<T> 
{ 

    void SomeMethod(T t) 

    { 
     object temp = t; 
     MyOtherClass obj = (MyOtherClass)temp; 

    } 
} 
+0

Por curiosidad: ¿puede escribir "SomeClass obj2 = (SomeClass) (object) t;"? –

+0

sí, esto se hace mediante el segundo fragmento –

+1

Compruebe este http://philipm.at/2011/1014/, lo encontró al intentar encontrar la explicación. Alerta de spoiler: ¡podría confundirte más !;) –

Respuesta

5

eso es exactamente lo que se obtiene en circunstancias normales - sin genéricos - cuando se intenta convertir entre las clases sin relación de herencia:

public interface IA 
{ 
} 

public class B 
{ 
} 

public class C 
{ 
} 

public void SomeMethod(B b) 
{ 
    IA o1 = (IA) b; <-- will compile 
    C o2 = (C)b; <-- won't compile 
} 

Así que sin una limitación, la clase genérica se comportará como si no existe una relación entre las clases.

Continuación ...

Bueno, digamos que alguien hace esto:

public class D : B, IA 
{ 
} 

y luego llama:

SomeMethod(new D()); 

Ahora verá por qué el compilador permite la pase de reparto de interfaz. Realmente no se puede saber en tiempo de compilación si se implementa o no una interfaz.

Recuerde que la clase D puede muy bien ser escrita por alguien que está utilizando su conjunto, años después de que lo compiló. Por lo tanto, no hay posibilidad de que el compilador se niegue a compilarlo. Debe verificarse en tiempo de ejecución.

+0

buen punto, pero sigue confundiendo por qué eligieron permitir un lanzamiento y no permitir el segundo. –

+0

Actualicé mi respuesta para incluir eso. –

+0

Si probara 'D o3 = (D) b;' en 'SomeMethod (B b)' ¿compilaría eso? Me da la impresión de que se aplica el mismo argumento de que lo que pases podría ser un descendiente válido para el elenco pero que podría no ser ... (y sí, estoy siendo flojo y no lo estoy intentando en este momento). – Chris

1

No tiene nada de malo. La única diferencia es que, en el primer caso, el compilador puede detectar en el momento de la compilación donde no hay posible conversión, pero no puede estar tan seguro de las interfaces, por lo que el error, en este caso, aumentará solo en tiempo de ejecución . Así,

// Compiles 
ISomeInterface obj1 = (ISomeInterface)t; 

// Сompiles too! 
SomeClass obj2 = (SomeClass)(object)t;  

producirá mismos errores en tiempo de ejecución.

El motivo puede ser: el compilador no sabe qué clase de interfaces implementa, pero conoce la herencia de las clases (de ahí que el método (SomeClass)(object)t funciona). En otras palabras: la conversión inválida está prohibida en CLR, la única diferencia es que en algunos casos se puede detectar en tiempo de compilación y en algunos no. La razón principal detrás de eso, incluso si el compilador conoce todas las interfaces de la clase, no sabe acerca de sus descendientes, que pueden implementarlo, y son válidos para ser T. Consideremos siguiente escenario:

namespace ConsoleApplication2 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      MyClass<SomeClass> mc = new MyClass<SomeClass>(); 

      mc.SomeMethod(new SomeClassNested()); 

     } 
    } 

    public interface ISomeInterface 
    { 
    } 

    public class SomeClass 
    { 

    } 

    public class SomeClassNested : SomeClass, ISomeInterface 
    { 

    } 

    public class MyClass<T> 
    { 
     public void SomeMethod(T t) 
     { 
      // Compiles, no errors at runtime 
      ISomeInterface obj1 = (ISomeInterface)t; 
     } 
    } 
} 
+0

¿por qué crees que él no sabe? todo está escrito en el ensamblaje en las tablas de metadatos. –

+0

@Karel, todo está en ensamblaje, eso es seguro, pero eso no significa que el compilador lo sepa. El compilador es algo relativamente "tonto". –

+0

¿Puedes vincular alguna referencia a los compiladores que no pueden descubrir qué interfaces implementa una clase? Me parece mal, pero estoy abierto a la educación sobre el tema. – Chris

0

creo que la diferencia entre la fundición de una interfaz y fundición a una clase reside en el hecho de que C# soporta múltiples "herencia" sólo para las interfaces. Qué significa eso?El compilador solo puede determinar en tiempo de compilación si un molde es válido o no para una clase porque C# no permite herencia múltiple para las clases.

Por otro lado, el compilador no sabe en tiempo de compilación si su clase no implementa la interfaz utilizada en el elenco. ¿Por qué? Alguien podría heredar de su clase e implementar la interfaz utilizada en su elenco. Entonces, el compilador no es consciente de esto en tiempo de compilación. (Ver SomeMethod4() a continuación).

Sin embargo, el compilador puede determinar si lanzar o no a una interfaz es válido, si su clase está sellada.

Consideremos el siguiente ejemplo:

interface ISomeInterface 
{} 
class SomeClass 
{} 

sealed class SealedClass 
{ 
} 

class OtherClass 
{ 
} 

class DerivedClass : SomeClass, ISomeInterface 
{ 
} 

class MyClass 
{ 
    void OtherMethod(SomeClass s) 
    { 
    ISomeInterface t = (ISomeInterface)s; // Compiles! 
    } 

    void OtherMethod2(SealedClass sc) 
    { 
    ISomeInterface t = (ISomeInterface)sc; // Does not compile! 
    } 

    void OtherMethod3(SomeClass c) 
    { 
    OtherClass oc = (OtherClass)c; // Does not compile because compiler knows 
    }        // that SomeClass does not inherit from OtherClass! 

    void OtherMethod4() 
    { 
    OtherMethod(new DerivedClass()); // In this case the cast to ISomeInterface inside 
    }         // the OtherMethod is valid! 
} 

Lo mismo es cierto para los genéricos.

Espero, esto ayuda.

2

La gran diferencia es que se garantiza que una interfaz es un tipo de referencia. Los tipos de valor son los generadores de problemas. Se menciona explícitamente en la especificación del lenguaje C#, capítulo 6.2.6, con un excelente ejemplo que demuestra el problema:


Las reglas anteriores no permiten una conversión explícita directa de un parámetro de tipo no restringido a un no tipo de interfaz, que podría ser sorprendente. El motivo de esta regla es evitar confusiones y dejar clara la semántica de tales conversiones. Por ejemplo, considere la siguiente declaración:

class X<T> 
{ 
    public static long F(T t) { 
     return (long)t;    // Error 
    } 
} 

Si la conversión explícita directa de t en INT se permitió, se podría esperar fácilmente que X.F (7) volvería 7L. Sin embargo, no lo haría, ya que las conversiones numéricas estándar solo se tienen en cuenta cuando se sabe que los tipos son numéricos en el momento de la compilación. Con el fin de hacer que la semántica clara, el ejemplo anterior en lugar debe ser escrito:

class X<T> 
{ 
    public static long F(T t) { 
     return (long)(object)t;  // Ok, but will only work when T is long 
    } 
} 

Este código será ahora compilar, pero la ejecución XF (7) sería entonces una excepción en tiempo de ejecución, ya que un int en caja no se puede convertir directamente a un largo

Cuestiones relacionadas