visual ver varios studio solucion see proyectos how errors errores error correctamente compilar compila como cargaron cambios c# .net undefined-behavior contravariance ambiguity

c# - varios - Sin advertencia o error(o falla en el tiempo de ejecución) cuando la contradicción conduce a la ambigüedad



visual studio 2017 error list (4)

¡Aquí hay un intento de justificación por la falta de advertencia o error que acabo de presentar, y ni siquiera he estado bebiendo!

Resuma su código aún más, ¿cuál es la intención?

ICanEat<string> beast = SomeFactory.CreateRavenousBeast(); beast.Eat("sheep");

Estás alimentando algo. Cómo la bestia realmente se la come a la bestia. Si le dieron sopa, podría decidir usar una cuchara. Puede usar un cuchillo y un tenedor para comer las ovejas. Podría romperlo en pedazos con sus dientes. Pero el punto es que al llamar a esta clase no nos importa cómo se come, solo queremos que se alimente.

En última instancia, depende de la bestia decidir cómo comer lo que se le da. Si la bestia decide que puede comer un ICloneable y un IConvertible , depende de la bestia averiguar cómo manejar ambos casos. ¿Por qué debería el guardabosques cuidar cómo los animales comen sus comidas?

Si causó un error en tiempo de compilación, hace que implementar una nueva interfaz sea un cambio radical. Por ejemplo, si envía a HungryWolf como ICanEat<ICloneable> y luego, meses más tarde, agregue ICanEat<IConvertible> para romper cada lugar donde se usó con un tipo que implemente ambas interfaces.

Y corta en ambos sentidos. Si el argumento genérico solo implementa ICloneable , funcionaría bastante feliz con el lobo. Las pruebas pasan, envíe eso, muchas gracias Sr. Wolf. ¡El año que IConvertible soporte IConvertible a tu tipo y BANG! No es tan útil un error, realmente.

Primero, recuerde que una .NET String es IConvertible e ICloneable .

Ahora, considere el siguiente código bastante simple:

//contravariance "in" interface ICanEat<in T> where T : class { void Eat(T food); } class HungryWolf : ICanEat<ICloneable>, ICanEat<IConvertible> { public void Eat(IConvertible convertibleFood) { Console.WriteLine("This wolf ate your CONVERTIBLE object!"); } public void Eat(ICloneable cloneableFood) { Console.WriteLine("This wolf ate your CLONEABLE object!"); } }

Luego intente lo siguiente (dentro de algún método):

ICanEat<string> wolf = new HungryWolf(); wolf.Eat("sheep");

Cuando uno compila esto, uno no obtiene ningún error o advertencia del compilador. Al ejecutarlo, parece que el método llamado depende del orden de la lista de interfaz en mi declaración de class para HungryWolf . (Intente intercambiar las dos interfaces en la lista separada por comas ( , ).)

La pregunta es simple: ¿no debería esto dar una advertencia en tiempo de compilación (o lanzar en tiempo de ejecución)?

Probablemente no sea el primero en proponer un código como este. Usé la contravariancia de la interfaz, pero puedes hacer un ejemplo completamente análogo con covaraciace de la interfaz. Y, de hecho, el Sr. Lippert hizo eso hace mucho tiempo. En los comentarios en su blog, casi todos están de acuerdo en que debería ser un error. Sin embargo, permiten esto en silencio. ¿Por qué?

---

Pregunta extendida:

Arriba Iconvertible que una String es tanto Iconvertible (interfaz) como ICloneable (interfaz). Ninguna de estas dos interfaces deriva del otro.

Ahora aquí hay un ejemplo con clases base que, en cierto sentido, es un poco peor.

Recuerde que una StackOverflowException es tanto una SystemException (clase base directa) como una Exception (clase base de la clase base). Entonces (si ICanEat<> es como antes):

class Wolf2 : ICanEat<Exception>, ICanEat<SystemException> // also try reversing the interface order here { public void Eat(SystemException systemExceptionFood) { Console.WriteLine("This wolf ate your SYSTEM EXCEPTION object!"); } public void Eat(Exception exceptionFood) { Console.WriteLine("This wolf ate your EXCEPTION object!"); } }

Pruébalo con:

static void Main() { var w2 = new Wolf2(); w2.Eat(new StackOverflowException()); // OK, one overload is more "specific" than the other ICanEat<StackOverflowException> w2Soe = w2; // Contravariance w2Soe.Eat(new StackOverflowException()); // Depends on interface order in Wolf2 }

Todavía no hay advertencia, error o excepción. Todavía depende del orden de la lista de interfaz en class declaración de class . Pero la razón por la que creo que es peor es porque esta vez alguien podría pensar que la resolución de sobrecarga siempre elegiría SystemException porque es más específico que solo Exception .

Estado antes de que se abriera la recompensa: tres respuestas de dos usuarios.

Estado en el último día de la recompensa: todavía no se recibieron nuevas respuestas. Si no aparece ninguna respuesta, tendré que otorgar la recompensa al musulmán Ben Dhaou.


Creo que el compilador hace lo mejor en VB.NET con la advertencia, pero todavía no creo que vaya lo suficientemente lejos. Desafortunadamente, lo "correcto" probablemente requiera no permitir que algo sea potencialmente útil (implementar la misma interfaz con dos parámetros genéricos covariantes o contravariantes) o introducir algo nuevo en el lenguaje.

Tal como está, no hay lugar en el que el compilador pueda asignar un error ahora que no sea la clase HungryWolf . Ese es el punto en el que una clase pretende saber cómo hacer algo que podría ser ambiguo. Está diciendo

Sé cómo comer un ICloneable , o cualquier cosa que implemente o herede de él, de cierta manera.

Y, también sé cómo comer un IConvertible , o cualquier cosa que implemente o herede de él, de cierta manera.

Sin embargo, nunca establece lo que debería hacer si recibe en su placa algo que es a la vez ICloneable e IConvertible . Esto no causa ningún dolor al compilador si se le da una instancia de HungryWolf , ya que puede decir con certeza "¡Oye, no sé qué hacer aquí!" . Pero le dará dolor al compilador cuando se le dé la ICanEat<string> . El compilador no tiene idea de cuál es el tipo real del objeto en la variable, solo que definitivamente implementa ICanEat<string> .

Lamentablemente, cuando un HungryWolf se almacena en esa variable, ambiguamente implementa exactamente la misma interfaz dos veces. Así que seguramente, no podemos lanzar un error al tratar de llamar a ICanEat<string>.Eat(string) , ya que ese método existe y sería perfectamente válido para muchos otros objetos que podrían colocarse en la ICanEat<string> ( batwad ya mencionó este en una de sus respuestas).

Además, aunque el compilador podría quejarse de que la asignación de un objeto HungryWolf a una ICanEat<string> es ambigua, no puede evitar que ocurra en dos pasos. Un HungryWolf se puede asignar a una ICanEat<IConvertible> , que podría pasarse a otros métodos y finalmente asignarse a una ICanEat<string> . Ambas son asignaciones perfectamente legales y sería imposible para el compilador quejarse de una u otra.

Por lo tanto, la opción uno es no permitir que la clase HungryWolf implemente tanto ICanEat<IConvertible> como ICanEat<ICloneable> cuando el parámetro de tipo genérico ICanEat es contravariante, ya que estas dos interfaces podrían unificarse. Sin embargo, esto elimina la capacidad de codificar algo útil sin una solución alternativa.

La segunda opción , desafortunadamente, requeriría un cambio en el compilador , tanto el IL como el CLR. Permitiría a la clase HungryWolf implementar ambas interfaces, pero también requeriría la implementación de la interfaz ICanEat<IConvertible & ICloneable> , donde el parámetro de tipo genérico implementa ambas interfaces. Probablemente esta no sea la mejor sintaxis (¿cómo se ve la firma de este método Eat(T) Eat(IConvertible & ICloneable food) ?). Probablemente, una mejor solución sería un tipo genérico autogenerado sobre la clase implementadora para que la definición de clase sea algo así como:

class HungryWolf: ICanEat<ICloneable>, ICanEat<IConvertible>, ICanEat<TGenerated_ICloneable_IConvertible> where TGenerated_ICloneable_IConvertible: IConvertible, ICloneable { // implementation }

El IL tendría que cambiar, para poder permitir que los tipos de implementación de interfaz se construyan como clases genéricas para una instrucción de callvirt :

.class auto ansi nested private beforefieldinit HungryWolf extends [mscorlib]System.Object implements class NamespaceOfApp.Program/ICanEat`1<class [mscorlib]System.ICloneable>, class NamespaceOfApp.Program/ICanEat`1<class [mscorlib]System.IConvertible>, class NamespaceOfApp.Program/ICanEat`1<class ([mscorlib]System.IConvertible, [mscorlib]System.ICloneable>)!TGenerated_ICloneable_IConvertible>

El CLR tendría que procesar callvirt instrucciones de callvirt construyendo una implementación de interfaz para HungryWolf con string como el parámetro de tipo genérico para TGenerated_ICloneable_IConvertible , y verificando si concuerda mejor que las otras implementaciones de interfaz.

Para la covarianza, todo esto sería más simple, ya que las interfaces adicionales requeridas para implementarse no tendrían que ser parámetros de tipo genérico con restricciones, sino simplemente el tipo base más derivado entre los otros dos tipos , que se conoce en tiempo de compilación.

Si la misma interfaz se implementa más de dos veces, la cantidad de interfaces adicionales necesarias para implementarse crece exponencialmente, pero este sería el costo de la flexibilidad y la seguridad de tipo de implementación de múltiples contravariantes (o covariantes) en una sola clase.

Dudo que esto llegue al marco, pero sería mi solución preferida, especialmente porque la nueva complejidad del lenguaje siempre sería independiente para la clase que desea hacer lo que actualmente es peligroso.

editar:
Agradezco a por recordarme que la covarianza no es más simple que la contravarianza , debido al hecho de que las interfaces comunes también deben tenerse en cuenta. En el caso de string y char[] , el conjunto de elementos comunes más comunes sería { object , ICloneable , IEnumerable<char> } ( IEnumerable está cubierto por IEnumerable<char> ).

Sin embargo, esto requeriría una nueva sintaxis para la restricción de parámetro de tipo genérico de interfaz, para indicar que el parámetro de tipo genérico solo necesita

  • heredar de la clase especificada o una clase que implementa al menos una de las interfaces especificadas
  • implementar al menos una de las interfaces especificadas

Posiblemente algo como:

interface ICanReturn<out T> where T: class { } class ReturnStringsOrCharArrays: ICanReturn<string>, ICanReturn<char[]>, ICanReturn<TGenerated_String_ArrayOfChar> where TGenerated_String_ArrayOfChar: object|ICloneable|IEnumerable<char> { }

El parámetro de tipo genérico TGenerated_String_ArrayOfChar en este caso (donde una o más interfaces son comunes) siempre debe tratarse como object , aunque la clase base común ya se haya derivado del object ; porque el tipo común puede implementar una interfaz común sin heredar de la clase base común .


En tal caso, no se pudo generar un error de compilación porque el código es correcto y debe ejecutarse correctamente con todos los tipos que no hereden de ambos tipos internos al mismo tiempo. El problema es el mismo si heredas de una clase y una interfaz al mismo tiempo. (es decir, use la clase de object base en su código).

Por alguna razón, el compilador VB.Net arrojará una advertencia en tal caso similar a

La interfaz ''ICustom (Of Foo)'' es ambigua con otra interfaz implementada ''ICustom (Of Boo)'' debido a los parámetros ''In'' y ''Out'' en ''Interface ICustom (Of In T)''

Estoy de acuerdo en que el compilador de C # debería lanzar una advertencia similar también. Verifique esta pregunta de desbordamiento de pila . El Sr. Lippert confirmó que el tiempo de ejecución elegirá uno y que este tipo de programación debería evitarse.


Otra explicación: no hay ambigüedad, por lo tanto, no hay advertencia del compilador.

Tengan paciencia conmigo.

ICanEat<string> wolf = new HungryWolf(); wolf.Eat("sheep");

No hay ningún error porque ICanEat<T> solo contiene un método llamado Eat con esos parámetros. Pero di esto y obtendrás un error:

HungryWolf wolf = new HungryWolf(); wolf.Eat("sheep");

HungryWolf es ambiguo, no ICanEat<T> . Al esperar un error de compilación, le está pidiendo al compilador que mire el valor de la variable en el punto en el que se llama a Eat y que resuelva si es ambigua, lo que no es necesariamente algo que pueda deducir. Considere una clase UnambiguousCow que solo implementa ICanEat<string> .

ICanEat<string> beast; if (somethingUnpredictable) beast = new HungryWolf(); else beast = new UnambiguousCow(); beast.Eat("dinner");

¿Dónde y cómo esperarías que se generara un error de compilación?