Je m'amuse beaucoup avec Funcy (amusement voulu) avec des méthodes génériques. Dans la plupart des cas, l'inférence de type C # est assez intelligente pour savoir quels arguments génériques elle doit utiliser sur mes méthodes génériques, mais j'ai maintenant une conception où le compilateur C # échoue, alors que je pense qu'il aurait pu réussir à trouver types corrects.
Quelqu'un peut-il me dire si le compilateur est un peu idiot dans ce cas ou s'il existe une raison très claire pour laquelle il ne peut pas déduire mes arguments génériques?
Voici le code:
Classes et définitions d'interface:
interface IQuery<TResult> { }
interface IQueryProcessor
{
TResult Process<TQuery, TResult>(TQuery query)
where TQuery : IQuery<TResult>;
}
class SomeQuery : IQuery<string>
{
}
Un code qui ne compile pas:
class Test
{
void Test(IQueryProcessor p)
{
var query = new SomeQuery();
// Does not compile :-(
p.Process(query);
// Must explicitly write all arguments
p.Process<SomeQuery, string>(query);
}
}
Pourquoi est-ce? Qu'est-ce que j'oublie ici?
Voici le message d'erreur du compilateur (il ne laisse pas beaucoup d'imagination):
Les arguments de type de la méthode IQueryProcessor.Process (TQuery) ne peuvent pas être déduits de l'utilisation. Essayez de spécifier le tapez des arguments explicitement.
La raison pour laquelle je pense que C # devrait pouvoir en déduire est la suivante:
IQuery<TResult>
.IQuery<TResult>
que le type implémente est IQuery<string>
et donc TResult doit être string
.SOLUTION
Pour moi, la meilleure solution consistait à modifier l'interface IQueryProcessor
et à utiliser le typage dynamique dans la mise en œuvre:
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
// Implementation
sealed class QueryProcessor : IQueryProcessor {
private readonly Container container;
public QueryProcessor(Container container) {
this.container = container;
}
public TResult Process<TResult>(IQuery<TResult> query) {
var handlerType =
typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult));
dynamic handler = container.GetInstance(handlerType);
return handler.Handle((dynamic)query);
}
}
L’interface IQueryProcessor
prend maintenant un paramètre IQuery<TResult>
. De cette façon, il peut renvoyer une TResult
et cela résoudra les problèmes du point de vue du consommateur. Nous devons utiliser la réflexion dans l'implémentation pour obtenir l'implémentation réelle, car les types de requête concrets sont nécessaires (dans mon cas). Mais voici le typage dynamique à la rescousse qui fera le reflet pour nous. Vous pouvez en savoir plus à ce sujet dans cet article article .
Un groupe de personnes a souligné que C # ne fait pas d'inférences basées sur des contraintes. C'est correct et pertinent à la question. Les inférences sont effectuées en examinant arguments et leurs types de paramètres formels correspondants, ce qui est la seule source d'informations d'inférence.
Un groupe de personnes a ensuite lié à cet article:
Cet article est à la fois obsolète et sans rapport avec la question. Il est obsolète car il décrit une décision de conception que nous avons prise en C # 3.0 et que nous avons ensuite inversée en C # 4.0, principalement en fonction de la réponse à cet article. Je viens d'ajouter une mise à jour à cet effet à l'article.
Cela n'a aucune importance, car l'article traite de l'inférence de type return à partir d'arguments de groupe de méthodes et de paramètres de délégation génériques. Ce n'est pas la situation que l'affiche originale demande.
Mon article pertinent à lire est plutôt celui-ci:
UPDATE: J'ai entendu dire que C # 7.3 avait légèrement modifié les règles applicables aux applications de contraintes, rendant ainsi obsolète l'article de dix ans susmentionné. Lorsque j'aurai le temps, je passerai en revue les modifications apportées par mes anciens collègues et verrai s'il est utile de publier une correction sur mon nouveau blog. en attendant, soyez prudent et voyez ce que C # 7.3 fait dans la pratique.
C # n'inférera pas les types génériques basés sur le type de retour d'une méthode générique, mais uniquement les arguments de la méthode.
Il n'utilise pas non plus les contraintes dans l'inférence de type, ce qui élimine la contrainte générique de fournir le type pour vous.
Pour plus de détails, voir l'article d'Eric Lippert sur le sujet .
Il n'utilise pas de contraintes pour déduire des types. Au contraire, il déduit les types (lorsque cela est possible) puis vérifie les contraintes.
Par conséquent, bien que la seule TResult
possible pouvant être utilisée avec un paramètre SomeQuery
, elle ne le verra pas.
Notez également qu'il serait parfaitement possible que SomeQuery
implémente également IQuery<int>
, ce qui est l'une des raisons pour lesquelles cette limitation du compilateur n'est peut-être pas une mauvaise idée.
La spécification énonce cela assez clairement:
Section 7.4.2 Inférence de type
Si le nombre d'arguments fourni est différent du nombre de paramètres de la méthode, l'inférence échoue immédiatement. Sinon, supposons que la méthode générique ait la signature suivante:
Tr M (T1 x1… Tm xm)
Avec un appel de méthode de la forme M (E1… Em), la tâche de l'inférence de type consiste à trouver les arguments de type uniques S1… Sn pour chacun des paramètres de type X1… Xn afin que l'appel M (E1… Em) soit valide. .
Comme vous pouvez le constater, le type de retour n'est pas utilisé pour l'inférence de type. Si l'appel de méthode ne mappe pas directement sur les arguments de type, l'inférence échoue immédiatement.
Le compilateur ne suppose pas seulement que vous vouliez string
comme argument TResult
, et ne le peut pas. Imaginez une TResult
dérivée de chaîne. Les deux seraient valables, alors lequel choisir? Mieux vaut être explicite.
Le pourquoi a été bien répondu, mais il existe une solution alternative. Je suis régulièrement confronté aux mêmes problèmes mais dynamic
ou toute solution utilisant la réflexion ou l’allocation de données est hors de question dans mon cas (joie des jeux vidéo,
Donc, au lieu de cela, je passe le retour sous forme de paramètres out
qui sont alors correctement déduits.
interface IQueryProcessor
{
void Process<TQuery, TResult>(TQuery query, out TResult result)
where TQuery : IQuery<TResult>;
}
class Test
{
void Test(IQueryProcessor p)
{
var query = new SomeQuery();
// Instead of
// string result = p.Process<SomeQuery, string>(query);
// You write
string result;
p.Process(query, out result);
}
}
Le seul inconvénient auquel je peux penser, c'est qu'il interdit l'utilisation de «var».
Je ne reviendrai pas dans le pourquoi, je ne me fais pas d'illusions de mieux expliquer Eric Lippert.
Cependant, il existe une solution qui n'exige pas de liaison tardive ni de paramètres supplémentaires pour votre appel de méthode. Ce n’est pas très intuitif, alors je laisserai au lecteur le soin de décider s’il s’agit d’une amélioration.
Tout d'abord, modifiez IQuery
pour le rendre auto-référençant:
public interface IQuery<TQuery, TResult> where TQuery: IQuery<TQuery, TResult>
{
}
Votre IQueryProcessor
ressemblerait à ceci:
public interface IQueryProcessor
{
Task<TResult> ProcessAsync<TQuery, TResult>(IQuery<TQuery, TResult> query)
where TQuery: IQuery<TQuery, TResult>;
}
Un type de requête réel:
public class MyQuery: IQuery<MyQuery, MyResult>
{
// Neccessary query parameters
}
Une implémentation du processeur pourrait ressembler à ceci:
public Task<TResult> ProcessAsync<TQuery, TResult>(IQuery<TQuery, TResult> query)
where TQuery: IQuery<TQuery, TResult>
{
var handler = serviceProvider.Resolve<QueryHandler<TQuery, TResult>>();
// etc.
}
Une autre solution de contournement à ce problème consiste à ajouter un paramètre supplémentaire pour la résolution de type . Par exemple, nous pouvons ajouter l'extension suivante:
static class QueryProcessorExtension
{
public static TResult Process<TQuery, TResult>(
this IQueryProcessor processor, TQuery query,
//Additional parameter for TQuery -> IQuery<TResult> type resolution:
Func<TQuery, IQuery<TResult>> typeResolver)
where TQuery : IQuery<TResult>
{
return processor.Process<TQuery, TResult>(query);
}
}
Maintenant, nous pouvons utiliser cette extension comme suit:
void Test(IQueryProcessor p)
{
var query = new SomeQuery();
//You can now call it like this:
p.Process(query, x => x);
//Instead of
p.Process<SomeQuery, string>(query);
}
Ce qui est loin d’être idéal mais beaucoup mieux que de fournir explicitement des types.
P.S. Liens associés à ce problème dans le référentiel dotnet: