47 votes

Appel ambigu entre deux méthodes génériques d'extension C# : l'une où T:class et l'autre où T:struct.

Considérons deux méthodes d'extension :

public static T MyExtension<T>(this T o) where T:class
public static T MyExtension<T>(this T o) where T:struct

Et un cours :

class MyClass() { ... }

Maintenant, appelez la méthode d'extension sur une instance de la classe ci-dessus :

var o = new MyClass(...);
o.MyExtension(); //compiler error here..
o.MyExtension<MyClass>(); //tried this as well - still compiler error..

Le compilateur dit que l'appel de la méthode est un appel ambigu lorsque je l'appelle sur une classe. J'aurais pensé qu'il pouvait déterminer quelle méthode d'extension appeler, puisque MyClass est une classe et non un struct ?

0 votes

Belle trouvaille ! Mais quelle est votre question ? Une solution de contournement ?

2 votes

Bonne question. Je pensais avoir une réponse simple, mais il s'avère que ce n'est pas le cas. J'espère que cela ne vous dérange pas que ma "réponse" soit davantage une exploration de ce qui se passe qu'une réponse en soi.

0 votes

Merci pour vos commentaires. Eamon - désolé, ma question n'est pas très claire - il s'agit en fait de savoir pourquoi le compilateur ne peut pas déterminer la meilleure méthode à utiliser. Après avoir lu les commentaires et les questions, ainsi que le lien fourni par LukeH, c'est parce que le compilateur ne prend pas en compte les contraintes de type pour déterminer la meilleure méthode à utiliser.

36voto

Jon Skeet Points 692016

EDIT : J'ai maintenant a publié un blog à ce sujet de manière plus détaillée.


Ma pensée initiale (et je pense maintenant qu'elle est incorrecte) : les contraintes génériques ne sont pas prises en compte pendant les phases de résolution des surcharges et d'inférence de type - elles ne sont utilisées que pour valider le résultat de la résolution des surcharges.

EDIT : Ok, après un lot d'aller et venir sur ce sujet, je pense que j'y suis. En gros, ma première pensée était presque correct.

Les contraintes de type générique n'agissent que pour supprimer les méthodes d'un ensemble de candidats dans un système de gestion de l'information. muy un ensemble limité de circonstances... en particulier, seulement lorsque le type d'un paramètre lui-même est générique ; pas seulement un paramètre de type, mais un type générique qui utilise un paramètre de type générique. À ce stade, ce sont les contraintes sur les paramètres de type du type générique qui sont validées, et non les contraintes sur les paramètres de type de la méthode générique que vous appelez.

Par exemple :

// Constraint won't be considered when building the candidate set
void Foo<T>(T value) where T : struct

// The constraint *we express* won't be considered when building the candidate
// set, but then constraint on Nullable<T> will
void Foo<T>(Nullable<T> value) where T : struct

Donc si vous essayez d'appeler Foo<object>(null) la méthode ci-dessus ne le fera pas faire partie de l'ensemble des candidats, car Nullable<object> value ne satisfait pas aux contraintes de Nullable<T> . S'il existe d'autres méthodes applicables, l'appel peut encore aboutir.

Maintenant, dans le cas ci-dessus, les contraintes sont exactement les mêmes... mais elles ne doivent pas l'être. Par exemple, considérons :

class Factory<TItem> where TItem : new()

void Foo<T>(Factory<T> factory) where T : struct

Si vous essayez d'appeler Foo<object>(null) la méthode fera toujours partie de l'ensemble des candidats, car lorsque l TItem es object la contrainte exprimée dans Factory<TItem> tient toujours, et c'est ce qui est vérifié lors de la construction de l'ensemble des candidats. Si cette méthode s'avère être la meilleure, elle échouera à la validation ultérieure, à proximité de l'option fin de 7.6.5.1 :

Si la meilleure méthode est une méthode générique, les arguments de type (fournis ou inférés) sont vérifiés par rapport aux contraintes (§4.4.4) déclarées sur la méthode générique. Si un argument de type ne satisfait pas la ou les contraintes correspondantes sur le paramètre de type, une erreur de liaison se produit.

Eric article de blog contient plus de détails à ce sujet.

0 votes

@Jon : Je suis presque sûr que votre réponse initiale est correcte, bien que je convienne qu'il est difficile de voir exactement où/comment cela est codifié dans la spécification. La section 7.6.5.1 semble ambiguë sur ce point, mais l'idée que les contraintes ne font pas partie de la signature est bien établie (et affirmée avec confiance par ceux qui connaissent ces choses, par exemple blogs.msdn.com/b/ericlippert/archive/2009/12/10/ ).

1 votes

Ma spéculation sur la raison pour laquelle votre exemple ne compile pas... Une des règles de 7.5.3.2 dit que "... si tous les paramètres de Mp ont un argument correspondant alors que les arguments par défaut doivent être substitués pour au moins un paramètre optionnel dans Mq, alors Mp est meilleur que Mq" . Selon cette règle -- et en supposant que les contraintes ne sont pas pris en considération - alors votre deuxième méthode s'avère être une meilleure adéquation pour Foo<int>() même si cela provoquera par la suite une erreur lors de la validation des contraintes. (Elle est meilleure car elle ne nécessite aucune substitution alors que la première méthode le fait).

0 votes

Merci Jon et LukeH - il semble que les contraintes de type ne soient pas utilisées pour déterminer la méthode à utiliser. Je créerais bien moi-même une réponse à cette question, mais je préfère que quelqu'un de plus compétent y réponde :-)

10voto

Courtney D Points 1567

Eric Lippert l'explique mieux que je ne le pourrais jamais, aquí .

J'ai moi-même rencontré ce problème. Ma solution était

public void DoSomthing<T> (T theThing){
    if (typeof (T).IsValueType)
        DoSomthingWithStruct (theThing);
    else
        DoSomthingWithClass (theThing);  
}

// edit - seems I just lived with boxing

public void DoSomthingWithStruct (object theThing)
public void DoSomthingWithClass(object theThing)

0 votes

Courtney, je n'ai pas réussi à le compiler - dans la méthode DoSomething, il est indiqué que l'on attend un type de valeur pour appeler DoSomthingWithStruct et un type de référence pour appeler DoSomthingWithClass.

0 votes

Ah, je vais devoir vérifier ce que j'ai fait exactement demain.

0 votes

Pour éviter la mise en boîte, il faut définir une classe statique SomethingDoer<T> avec une propriété en lecture seule adossée à un champ, de type Action<T> appelé DoSomething ; le constructeur de la classe doit utiliser Reflection pour construire un délégué à appeler DoSomethingWithStruct<T>(T param) where T:struct , DoSomethingWithClass<T>(T param) where T:class o DoSomethingWithNullable<T>(Nullable<T> param) et le stocker dans ce champ. Reflection ne devrait être utilisé qu'une seule fois pour un paramètre de type donné ; ensuite, le délégué invoquerait directement la méthode appropriée.

5voto

Salvatore Previti Points 5842

J'ai trouvé une façon étrange et "intéressante" de le faire dans .NET 4.5 en utilisant des valeurs de paramètres par défaut :) Peut-être que c'est plus utile pour l'éducation \speculative que pour une utilisation réelle mais je voudrais le montrer :

/// <summary>Special magic class that can be used to differentiate generic extension methods.</summary>
public class MagicValueType<TBase>
    where TBase : struct
{
}

/// <summary>Special magic class that can be used to differentiate generic extension methods.</summary>
public class MagicRefType<TBase>
    where TBase : class
{
}

struct MyClass1
{
}

class MyClass2
{
}

// Extensions
public static class Extensions
{
    // Rainbows and pink unicorns happens here.
    public static T Test<T>(this T t, MagicRefType<T> x = null)
        where T : class
    {
        Console.Write("1:" + t.ToString() + " ");
        return t;
    }

    // More magic, other pink unicorns and rainbows.
    public static T Test<T>(this T t, MagicValueType<T> x = null)
        where T : struct
    {
        Console.Write("2:" + t.ToString() + " ");
        return t;
    }
}

class Program
{
    static void Main(string[] args)
    {

        MyClass1 t1 = new MyClass1();
        MyClass2 t2 = new MyClass2();

        MyClass1 t1result = t1.Test();
        Console.WriteLine(t1result.ToString());

        MyClass2 t2result = t2.Test();
        Console.WriteLine(t2result.ToString());

        Console.ReadLine();
    }
}

0 votes

Pourquoi le [Serializable] attribut nécessaire ?

0 votes

Je sais que c'est vieux, mais je viens de tomber dessus et je voulais ajouter que vous pouvez le faire beaucoup plus simplement. Tout argument par défaut dans la signature qui est différent entre les deux fonctionnera. Par exemple public static T Test<T>(this T value) where T : class => value; vs public static T Test<T>(this T value, object _ = null) where T : struct => value;

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X