web-dev-qa-db-fra.com

Pourquoi C # n'infère-t-il pas mes types génériques?

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:

  1. Je fournis un objet qui implémente IQuery<TResult>.
  2. La seule version IQuery<TResult> que le type implémente est IQuery<string> et donc TResult doit être string.
  3. Avec ces informations, le compilateur a TResult et TQuery.

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 .

58
Steven

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:

http://blogs.msdn.com/b/ericlippert/archive/2007/11/05/c-3-0-retour-type-inference-does-not-work-on-member-groups.aspx

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:

http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx

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.

46
Eric Lippert

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 .

15
Reed Copsey

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.

11
Jon Hanna

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.

4
Ed S.

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».

2
Baptiste Dupy

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.
}
1
Thorarin

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:

https://github.com/dotnet/csharplang/issues/997

https://github.com/dotnet/roslyn/pull/7850

0
Roman Artiukhin