Los métodos generics en .NET no pueden inferir sus tipos de devolución. ¿Por qué?

Dado:

static TDest Gimme(TSource source) { return default(TDest); } 

¿Por qué no puedo hacer esto?

 string dest = Gimme(5); 

sin obtener el error del comstackdor:

error CS0411: The type arguments for method 'Whatever.Gimme(TSource)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

El 5 se puede inferir como int , pero hay una restricción donde el comstackdor no puede / no puede resolver el tipo de devolución como una string . He leído en varios lugares que esto es por diseño, pero no hay una explicación real. Leí en algún lado que esto podría cambiar en C # 4, pero no es así.

¿Alguien sabe por qué los tipos de devolución no se pueden inferir de los métodos generics? ¿Es esta una de esas preguntas donde la respuesta es tan obvia que te está mirando a la cara? ¡Espero que no!

El principio general aquí es que la información de tipo fluye solo “en una dirección”, desde el interior hacia el exterior de una expresión. El ejemplo que das es extremadamente simple. Supongamos que queremos que la información de tipo fluya “en ambos sentidos” al hacer una inferencia de tipo en un método R G(A a) , y consideremos algunos de los escenarios descabellados que crea:

 N(G(5)) 

Supongamos que hay diez sobrecargas diferentes de N, cada una con un tipo de argumento diferente. ¿Deberíamos hacer diez inferencias diferentes para R? Si lo hiciéramos, ¿deberíamos elegir de alguna manera el “mejor”?

 double x = b ? G(5) : 123; 

¿Qué debe inferirse que es el tipo de retorno de G? Int, porque la otra mitad de la expresión condicional es int? ¿O doble, porque en última instancia, esto se va a asignar al doble? Ahora quizás comiences a ver cómo va esto; si vas a decir que piensas de afuera hacia adentro, ¿qué tan lejos vas ? Podría haber muchos pasos en el camino. Vea lo que sucede cuando comenzamos a combinar estos:

 N(b ? G(5) : 123) 

¿Ahora que hacemos? Tenemos diez sobrecargas de N para elegir. ¿Decimos que R es int? Podría ser int o cualquier tipo que int sea implícitamente convertible a. Pero de esos tipos, ¿cuáles son implícitamente convertibles a un tipo de argumento de N? ¿Nos escribimos un pequeño progtwig de prólogo y le pedimos al motor de prólogo que resuelva cuáles son todos los posibles tipos de retorno que R podría tener para satisfacer cada una de las posibles sobrecargas en N, y luego de alguna manera elegir el mejor?

(No estoy bromeando, hay idiomas que básicamente escriben un pequeño progtwig de prólogo y luego usan un motor lógico para determinar qué tipo de cosas son. F # por ejemplo, hace una inferencia de tipo mucho más compleja que C #. Tipo de Haskell el sistema es en realidad Turing Complete; puede codificar problemas arbitrariamente complejos en el sistema de tipos y pedirle al comstackdor que los resuelva. Como veremos más adelante, lo mismo ocurre con la resolución de sobrecarga en C #: no puede codificar el problema de detención en C # escriba el sistema como puede en Haskell, pero puede codificar los problemas NP-HARD en problemas de resolución de sobrecarga).

Esta es una expresión muy simple. Supongamos que tienes algo así como

 N(N(b ? G(5) * G("hello") : 123)); 

Ahora tenemos que resolver este problema varias veces para G, y posiblemente para N también, y tenemos que resolverlos en combinación . Tenemos cinco problemas de resolución de sobrecarga para resolver y todos , para ser justos, deben considerar tanto sus argumentos como su tipo de contexto. Si hay diez posibilidades para N, entonces hay potencialmente cien posibilidades a considerar para N (N (…)) y mil para N (N (N (…))) y muy rápidamente nos tendrías a nosotros resolviendo problemas que fácilmente tenían miles de millones de combinaciones posibles y hacían que el comstackdor fuera muy lento.

Esta es la razón por la cual tenemos la regla de que la información de tipo solo fluye en una dirección. Previene este tipo de problemas de huevo y gallina, en los que se intenta determinar el tipo externo del tipo interno y determinar el tipo interno del tipo externo y provocar una explosión combinatoria de posibilidades.

¡Observe que la información de tipo fluye en ambos sentidos para lambdas! Si dice N(x=>x.Length) entonces, consideramos todas las posibles sobrecargas de N que tienen tipos de funciones o expresiones en sus argumentos y probamos todos los tipos posibles para x. Y por supuesto, hay situaciones en las que puede hacer fácilmente que el comstackdor pruebe miles de millones de combinaciones posibles para encontrar la combinación única que funciona. Las reglas de inferencia de tipo que hacen posible hacer eso para los métodos generics son extremadamente complejas e incluso ponen nervioso a Jon Skeet. Esta característica hace que la resolución de sobrecarga sea NP-HARD .

Conseguir que la información del tipo fluya en ambos sentidos para las lambdas, de modo que la resolución de sobrecarga genérica funcione de manera correcta y eficiente me llevó aproximadamente un año. Es una característica tan compleja que solo queríamos asumirlo si de manera absolutamente positiva obtuviéramos un retorno sorprendente de esa inversión. Hacer que LINQ funcione valió la pena. Pero no existe una característica correspondiente como LINQ que justifique el inmenso gasto de hacer que este trabajo en general.

Tu tienes que hacer:

 string dest = Gimme(5); 

Debe especificar cuáles son sus tipos en la llamada al método genérico. ¿Cómo podría saber que quería una cadena en la salida?

System.String es un mal ejemplo porque es una clase sellada, pero dicen que no. ¿Cómo podría el comstackdor saber que no deseaba una de sus subclases si no especificaba el tipo en la llamada?

Toma este ejemplo:

 System.Windows.Forms.Control dest = Gimme(5); 

¿Cómo sabría el comstackdor qué control hacer en realidad? Debería especificarlo así:

 System.Windows.Forms.Control dest = Gimme(5); 

Llamar a Gimme(5) ignorando el valor de retorno es una statement legal de cómo el comstackdor sabrá qué tipo devolver.

Esta fue una decisión de diseño, supongo. También me parece útil durante la progtwigción en Java.

A diferencia de Java, C # parece evolucionar hacia un lenguaje de progtwigción funcional, y usted puede obtener la inferencia de tipos al revés, para que pueda tener:

 var dest = Gimme(5); 

que inferirá el tipo de dest. Supongo que mezclar esto y la inferencia de estilo java podría ser bastante difícil de implementar.

Si se supone que una función devuelve uno de un pequeño número de tipos, puede hacer que devuelva una clase con conversiones ensanchadas definidas a esos tipos. No creo que sea posible hacerlo de forma genérica, ya que el operador ctype de ampliación no acepta un parámetro de tipo genérico.

Uso esta técnica cuando necesito hacer algo como eso:

 static void Gimme(out T myVariable) { myVariable = default(T); } 

y úsalo así:

 Gimme(out int myVariable); Print(myVariable); //myVariable is already declared and usable. 

Tenga en cuenta que la statement en línea de las variables de salida está disponible desde C # 7.0

 public class ReturnString : IReq { } public class ReturnInt : IReq { } public interface IReq { } public class Handler { public T MakeRequest(IReq requestObject) { return default(T); } } var handler = new Handler(); string stringResponse = handler.MakeRequest(new ReturnString()); int intResponse = handler.MakeRequest(new ReturnInt());