remarks cref c# generics methods interface type-inference

cref - Argumento genérico de tipo de método C#no deducido del uso



remarks c# (3)

Recientemente, he experimentado con una implementación del patrón de visitantes, en la que he intentado aplicar métodos de aceptación y visita con interfaces genéricas:

public interface IVisitable<out TVisitable> where TVisitable : IVisitable<TVisitable> { TResult Accept<TResult>(IVisitor<TResult, TVisitable> visitor); }

-cuyo propósito es 1) marcar cierto tipo "Foo" como visitable por tal visitante, que a su vez es un "visitante de tal tipo Foo" y 2) aplicar el método de aceptación de la firma correcta en el tipo visitable de implementación, como tal :

public class Foo : IVisitable<Foo> { public TResult Accept<TResult>(IVisitor<TResult, Foo> visitor) => visitor.Visit(this); }

Hasta ahora todo bien, la interfaz del visitante:

public interface IVisitor<out TResult, in TVisitable> where TVisitable : IVisitable<TVisitable> { TResult Visit(TVisitable visitable); }

-debe 1) marcar al visitante como "capaz de visitar" el TVisitable 2) cuál es el tipo de resultado (TResult) para este TVisitable debe ser 3) imponer el método de visita de una firma correcta para cada TVable que la implementación del visitante pueda "visitar" , al igual que:

public class CountVisitor : IVisitor<int, Foo> { public int Visit(Foo visitable) => 42; } public class NameVisitor : IVisitor<string, Foo> { public string Visit(Foo visitable) => "Chewie"; }

Muy agradable y bellamente, esto me permite escribir:

var theFoo = new Foo(); int count = theFoo.Accept(new CountVisitor()); string name = theFoo.Accept(new NameVisitor());

Muy bien.

Ahora comienzan los tiempos tristes, cuando agrego otro tipo visitable, como:

public class Bar : IVisitable<Bar> { public TResult Accept<TResult>(IVisitor<TResult, Bar> visitor) => visitor.Visit(this); }

que es visitable por decir solo el CountVisitor :

public class CountVisitor : IVisitor<int, Foo>, IVisitor<int, Bar> { public int Visit(Foo visitable) => 42; public int Visit(Bar visitable) => 7; }

que de repente rompe la inferencia de tipo en el método Aceptar! (Esto destruye todo el diseño)

var theFoo = new Foo(); int count = theFoo.Accept(new CountVisitor());

dando me:

"Los argumentos de tipo para el método ''Foo.Accept<TResult>(IVisitor<TResult, Foo>)'' no se pueden inferir del uso".

¿Podría alguien por favor explicar por qué es eso? Solo hay una versión de la IVisitor<T, Foo> que CountVisitor implementa o, si el IVisitor<T, Bar> no se puede eliminar por alguna razón, ambas tienen la misma T - int , = ninguna otra El tipo funcionaría allí de todos modos. ¿Se rinde la inferencia de tipos tan pronto como hay más de un candidato adecuado? (Dato curioso: ReSharper cree que la int en theFoo.Accept<int>(...) es redundante: P, aunque no se compilara sin ella)


¿Se rinde la inferencia de tipos tan pronto como hay más de un candidato adecuado?

Sí, en este caso lo hace. Al intentar inferir el parámetro de tipo genérico del método ( TResult ), el algoritmo de inferencia de tipo parece fallar en CountVisitor con dos inferencias al tipo IVisitor<TResult, TVisitable> .

De la especificación C # 5 (la más reciente que pude encontrar), §7.5.2:

Tr M<X1…Xn>(T1 x1 … Tm xm)

Con una llamada de método de la forma M(E1 …Em) la tarea de inferencia de tipo es encontrar argumentos de tipo únicos S1…Sn para cada uno de los parámetros de tipo X1…Xn para que la llamada M<S1…Sn>(E1…Em) vuelve válido.

El primer paso que toma el compilador es el siguiente (§7.5.2.1):

Para cada uno de los argumentos del método Ei :

  • Si Ei es una función anónima, se realiza una inferencia de tipo de parámetro explícito (§7.5.2.7) de Ei a Ti

  • De lo contrario, si Ei tiene un tipo U y xi es un parámetro de valor, entonces se realiza una inferencia de límite inferior de U a Ti .

Solo tiene un argumento, por lo que tenemos que la única Ei es la expresión new CountVisitor() . Claramente no es una función anónima, así que estamos en el segundo punto. Es trivial ver que en nuestro caso, U es del tipo CountVisitor . El bit " xi es un parámetro de valor" básicamente significa que no es una variable out , in , ref , etc., que es el caso aquí.

En este punto, ahora necesitamos hacer una inferencia de límite inferior de CountVisitor a IVisitor<TResult, TVisitable> La parte relevante de §7.5.2.9 (donde debido a un cambio de variable, tenemos V = IVisitor<TResult, TVisitable> in nuestro caso):

  • De lo contrario, los conjuntos U1…Uk y V1…Vk se determinan verificando si se cumple alguno de los siguientes casos:
    • V es un tipo de matriz V1[…] y U es un tipo de matriz U1[…] (o un parámetro de tipo cuyo tipo base efectivo es U1[…] ) del mismo rango
    • V es uno de IEnumerable<V1> , ICollection<V1> o IList<V1> y U es un tipo de matriz unidimensional U1[] (o un parámetro de tipo cuyo tipo de base efectivo es U1[] )
    • V es una clase construida, estructura, interfaz o delegado tipo C<V1…Vk> y hay un tipo único C<U1…Uk> tal que U (o, si U es un parámetro de tipo, su clase base efectiva o cualquier miembro de su conjunto de interfaz efectivo) es idéntico a, se hereda de (directa o indirectamente), o implementa (directa o indirectamente) C<U1…Uk> .

(La restricción de "unicidad" significa que en la interfaz del caso C<T>{} class U: C<X>, C<Y>{} , no se hace ninguna inferencia al inferir de U a C<T> porque U1 podría ser X o Y )

Podemos omitir los dos primeros casos, ya que claramente no son aplicables, el tercer caso es el que caemos. El compilador intenta encontrar un tipo único C<U1…Uk> que CountVisitor implementa y encuentra dos de estos tipos, IVisitor<int, Foo> e IVisitor<int, Bar> . Tenga en cuenta que el ejemplo que da la especificación es casi idéntico a su ejemplo.

Debido a la restricción de unicidad, no se hace ninguna inferencia para este argumento de método. Como el compilador no puede inferir ningún tipo de información del argumento, no tiene nada que seguir para intentar inferir TResult y, por lo tanto, falla.

En cuanto a por qué existe una restricción de unicidad, supongo que simplifica el algoritmo y, por lo tanto, la implementación del compilador. Si está interesado, aquí hay un enlace al código fuente donde Roslyn (compilador C # moderno) implementa inferencia de tipo de método genérico.


En C #, puede simplificar el patrón de visitante eliminando el "doble envío" haciendo uso de la palabra clave dynamic .

Puedes implementar tu Visitante así:

public class CountVisitor : IVisitor<int, IVisitable> { public int Visit( IVisitable v ) { dynamic d = v; Visit(d); } private int Visit( Foo f ) { return 42; } private int Visit( Bar b ) { return 7; } }

Al hacer esto, no necesitarás tener el método de aceptación implementado en Foo y Bar aunque aún deben implementar una interfaz común para que el Visitor trabaje sin rumbo.


Parece que la inferencia de tipos funciona de manera codiciosa, primero tratando de hacer coincidir los tipos genéricos del método , luego los tipos genéricos de la clase. Entonces si tu dices

int count = theFoo.Accept<int>(new CountVisitor());

funciona, lo cual es extraño, ya que Foo es el único candidato para el tipo genérico de clase.

Primero, si reemplaza el tipo genérico del método por un tipo genérico de segunda clase, funciona:

public interface IVisitable<R, out T> where T: IVisitable<int, T> { R Accept(IVisitor<R, T> visitor); } public class Foo : IVisitable<int, Foo> { public int Accept(IVisitor<int, Foo> visitor) => visitor.Visit(this); } public class Bar : IVisitable<int, Bar> { public int Accept(IVisitor<int, Bar> visitor) => visitor.Visit(this); } public interface IVisitor<out TResult, in T> where T: IVisitable<int, T> { TResult Visit(T visitable); } public class CountVisitor : IVisitor<int, Foo>, IVisitor<int, Bar> { public int Visit(Foo visitable) => 42; public int Visit(Bar visitable) => 7; } class Program { static void Main(string[] args) { var theFoo = new Foo(); int count = theFoo.Accept(new CountVisitor()); } }

En segundo lugar (y esta es la parte extraña que resalta cómo funciona la inferencia de tipos), observe qué sucede si reemplaza int por una string en el visitante de la Bar :

public class CountVisitor : IVisitor<int, Foo> , IVisitor<string, Bar> { public int Visit(Foo visitable) => 42; public string Visit(Bar visitable) => "42"; }

Primero, obtienes el mismo error, pero observa lo que sucede si fuerzas una cadena:

int count = theFoo.Accept<string>(new CountVisitor());

error CS1503: Argumento 1: no se puede convertir de ''CountVisitor'' a ''IVisitor<string, Foo>''

Lo que sugiere que el compilador primero mira los métodos genéricos del método ( TResult en su caso) y falla inmediatamente si encuentra más candidatos. Ni siquiera mira más allá, en los tipos genéricos de clase.

Intenté encontrar una especificación de inferencia de tipo de Microsoft, pero no pude encontrar ninguna.