c# type-inference nested-generics

c# - Genéricos anidados: ¿Por qué el compilador no puede inferir los argumentos de tipo en este caso?



type-inference nested-generics (3)

Estaba jugando con un proyecto de hobby cuando me topé con un error de inferencia de tipo que no entendía. Lo he simplificado al siguiente ejemplo trivial.

Tengo las siguientes clases y funciones:

class Foo { } class Bar { } class Baz { } static T2 F<T1, T2>(Func<T1, T2> f) { return default(T2); } static T3 G<T1, T2, T3>(Func<T1, Func<T2, T3>> f) { return default(T3); }

Ahora considera los siguientes ejemplos:

// 1. F with explicit type arguments - Fine F<Foo, Bar>(x => new Bar()); // 2. F with implicit type arguments - Also fine, compiler infers <Foo, Bar> F((Foo x) => new Bar()); // 3. G with explicit type arguments - Still fine... G<Foo, Bar, Baz>(x => y => new Baz()); // 4. G with implicit type arguments - Bang! // Compiler error: Type arguments cannot be inferred from usage G((Foo x) => (Bar y) => new Baz());

El último ejemplo produce un error del compilador, pero me parece que debería ser capaz de inferir los argumentos de tipo sin ningún problema.

PREGUNTA: ¿Por qué el compilador no puede inferir <Foo, Bar, Baz> en este caso?

ACTUALIZACIÓN: descubrí que simplemente envolver el segundo lambda en una función de identidad hará que el compilador deduzca todos los tipos correctamente:

static Func<T1, T2> I<T1, T2>(Func<T1, T2> f) { return f; } // Infers G<Foo, Bar, Baz> and I<Bar, Baz> G((Foo x) => I((Bar y) => new Baz()));

¿Por qué puede hacer todos los pasos individuales perfectamente, pero no toda la inferencia a la vez? ¿Hay alguna sutileza en el orden en que el compilador analiza los tipos lambda implícitos y los tipos genéricos implícitos?


La lambda no se puede inferir cuál es su tipo de retorno ya que no está asignado y no puede ser determinado por el compilador. Echa un vistazo a este link sobre cómo el compilador determina los tipos de retorno de las lambdas. Si hubieras tenido:

Func<Bar, Baz> f = (Bar y) => new Baz(); G((Foo x) => f);

entonces el compilador habría podido calcular el tipo de retorno de la lambda en función de a lo que está asignado, pero como ahora no está asignado a nada, el compilador se esfuerza por determinar cuál es el tipo de retorno para (Bar y) => new Baz(); sería.


Para el compilador, una función lambda es distinta de una función, es decir, usar una función lambda para una función implica una conversión de tipo. El compilador no realiza conversiones de tipo "anidadas" al especializar genéricos. Eso, sin embargo, sería requerido en tu ejemplo:

Tipo de (Foo x) => (Bar y) => new Baz () es lambda (Foo, lambda (Bar, Baz)) , pero se requeriría Func (T1, Func (T2, T3)) , es decir, dos conversiones , que están anidadas.


Porque el algoritmo como se describe en la especificación de C # no tiene éxito en este caso. Echemos un vistazo a la especificación para ver por qué esto es.

La descripción del algoritmo es larga y complicada, por lo que abreviaré mucho esto.

Los tipos relevantes mencionados en el algoritmo tienen los siguientes valores para usted:

  • Eᵢ = la lambda anónima (Foo x) => (Bar y) => new Baz()
  • Tᵢ = el tipo de parámetro ( Func<T1, Func<T2, T3>> )
  • Xᵢ = los tres parámetros de tipo genérico ( T1 , T2 , T3 )

En primer lugar, está la primera fase, que en su caso solo hace una cosa:

7.5.2.1 La primera fase

Para cada uno de los argumentos del método Eᵢ (en su caso, solo hay uno, la lambda):

  • Si Eᵢ es una función anónima [es], se realiza una inferencia de tipo de parámetro explícita (§7.5.2.7) de Eᵢ a Tᵢ
  • De lo contrario, [no relevante]
  • De lo contrario, [no relevante]
  • De lo contrario, no se hace ninguna inferencia para este argumento.

Me saltaré los detalles de la inferencia de tipo de parámetro explícito aquí; basta con decir que para la llamada G((Foo x) => (Bar y) => new Baz()) , infiere que T1 = Foo .

Luego viene la segunda fase, que es efectivamente un bucle que trata de reducir el tipo de cada parámetro de tipo genérico hasta que los encuentra a todos o se da por vencido. El único punto importante es el último:

7.5.2.2 La segunda fase

La segunda fase procede de la siguiente manera:

  • [...]
  • De lo contrario, para todos los argumentos Eᵢ con el tipo de parámetro Tᵢ correspondiente donde los tipos de salida (§7.5.2.4) contienen variables de tipo no fijadas Xj pero los tipos de entrada (§7.5.2.3) no, una inferencia de tipo de salida (§7.5.2.6) es hecho de Eᵢ a Tᵢ . Luego se repite la segunda fase.

[Traducido y aplicado a su caso, esto significa:

  • De lo contrario, si el tipo de retorno del delegado (es decir, Func<T2,T3> ) contiene una variable de tipo todavía no determinada (lo hace) pero sus tipos de parámetros (es decir, T1 ) no lo hacen (no lo hacen, ya sabemos que T1 = Foo ), se hace una inferencia de tipo de salida (§7.5.2.6).]

La inferencia de tipo de salida ahora procede de la siguiente manera; de nuevo, solo un punto es relevante, esta vez es el primero:

7.5.2.6 Inferencias de tipo de salida

Se hace una inferencia de tipo de salida de una expresión E a un tipo T de la siguiente manera:

  • Si E es una función anónima [es] con el tipo de retorno inferido U (§7.5.2.12) y T es un tipo de árbol de expresión o tipo delegado con el tipo de retorno Tb , entonces se realiza una inferencia de límite inferior (§7.5.2.9) de la U a la Tb .
  • De lo contrario, [resto cortado]

El "tipo de retorno inferido" U es lambda anónima (Bar y) => new Baz() y Tb es Func<T2,T3> . Inferencia de límite inferior de cue.

No creo que deba citar todo el algoritmo de inferencia de límite inferior ahora (es largo); es suficiente decir que no menciona funciones anónimas. Se ocupa de las relaciones de herencia, las implementaciones de interfaz, la covarianza de matrices, la interfaz y la contravarianza delegada, ... pero no las lambdas. Por lo tanto, se aplica su último punto de bala:

  • De lo contrario, no se hacen inferencias.

Luego volvemos a la segunda fase, que se da por vencida porque no se han hecho inferencias para T2 y T3 .

La moraleja de la historia: el algoritmo de inferencia de tipos no es recursivo con las lambdas. Solo puede inferir tipos del parámetro y tipos de retorno de la lambda externa, no lambdas anidadas en su interior. Solo la inferencia de límite inferior es recursiva (de modo que puede tomar construcciones genéricas anidadas como List<Tuple<List<T1>, T2>> aparte) pero ni inferencias de tipo de salida (§7.5.2.6) ni inferencias explícitas de tipo de parámetro (§7.5 .2.7) son recursivos y nunca se aplican a las lambdas internas.

Apéndice

Cuando agrega una llamada a esa función de identificación I :

  • G((Foo x) => I((Bar y) => new Baz()));

luego la inferencia de tipo se aplica primero a la llamada a I , lo que hace I el tipo de retorno de I se infiera como Func<Bar, Baz> . Luego, la " U tipo de retorno inferido" de la lambda externa es el tipo de delegado Func<Bar, Baz> y Tb es Func<T2, T3> . Por lo tanto , la inferencia de límite inferior tendrá éxito porque se enfrentará con dos tipos de delegados explícitos ( Func<Bar, Baz> y Func<T2, T3> ) pero no con funciones / lambdas anónimas. Es por esto que la función de identificar hace que tenga éxito.