web-dev-qa-db-fra.com

Paramètres facultatifs C # sur les méthodes remplacées

Il semble que dans .NET Framework, il existe un problème avec les paramètres facultatifs lorsque vous remplacez la méthode. La sortie du code ci-dessous est: "bbb" "aaa". Mais la sortie que j'attends est: "bbb" "bbb". Y a-t-il une solution pour cela. Je sais que cela peut être résolu avec une surcharge de méthode, mais je me demande la raison de cela. Le code fonctionne également très bien en Mono.

class Program
{
    class AAA
    {
        public virtual void MyMethod(string s = "aaa")
        {
            Console.WriteLine(s);
        }

        public virtual void MyMethod2()
        {
            MyMethod();
        }
    }

    class BBB : AAA
    {
        public override void MyMethod(string s = "bbb")
        {
            base.MyMethod(s);
        }

        public override void MyMethod2()
        {
            MyMethod();
        }
    }

    static void Main(string[] args)
    {
        BBB asd = new BBB();
        asd.MyMethod();
        asd.MyMethod2();
    }
}
70
SARI

Une chose à noter ici, c'est que la version remplacée est appelée à chaque fois. Remplacez le remplacement par:

public override void MyMethod(string s = "bbb")
{
  Console.Write("derived: ");
  base.MyMethod(s);
}

Et la sortie est:

derived: bbb
derived: aaa

Une méthode dans une classe peut effectuer une ou deux des opérations suivantes:

  1. Il définit une interface pour appeler d'autres codes.
  2. Il définit une implémentation à exécuter lorsqu'elle est appelée.

Elle ne peut pas faire les deux, car une méthode abstraite ne fait que la première.

Dans BBB l'appel MyMethod() appelle une méthode définie dans AAA.

Étant donné qu'il existe une substitution dans BBB, l'appel de cette méthode entraîne l'appel d'une implémentation dans BBB.

Maintenant, la définition dans AAA informe le code appelant de deux choses (enfin, quelques autres aussi qui n'ont pas d'importance ici).

  1. La signature void MyMethod(string).
  2. (Pour les langues qui le prennent en charge) la valeur par défaut pour le paramètre unique est "aaa" Et donc lors de la compilation du code de la forme MyMethod() si aucune méthode correspondant à MyMethod() ne peut être trouvé, vous pouvez le remplacer par un appel à `MyMethod (" aaa ").

Donc, c'est ce que fait l'appel dans BBB: le compilateur voit un appel à MyMethod(), ne trouve pas de méthode MyMethod() mais trouve une méthode MyMethod(string). Il voit également qu'à l'endroit où il est défini, il y a une valeur par défaut de "aaa", donc au moment de la compilation, il change cela en un appel à MyMethod("aaa").

Depuis BBB, AAA est considéré comme l'endroit où les méthodes de AAA sont définies, même si elles sont remplacées dans BBB, de sorte qu'elles peut être remplacé.

Au moment de l'exécution, MyMethod(string) est appelée avec l'argument "aaa". Puisqu'il existe un formulaire substitué, c'est le formulaire appelé, mais il n'est pas appelé avec "bbb" car cette valeur n'a rien à voir avec l'implémentation au moment de l'exécution mais avec la définition au moment de la compilation.

L'ajout de this. Modifie la définition examinée et modifie donc l'argument utilisé dans l'appel.

Edit: Pourquoi cela me semble plus intuitif.

Personnellement, et comme je parle de ce qui est intuitif, cela ne peut être que personnel, je trouve cela plus intuitif pour la raison suivante:

Si je codais BBB, que ce soit en appelant ou en remplaçant MyMethod(string), je considérerais cela comme "faisant AAA des trucs" - c'est BBBs "faire AAA trucs", mais ça fait AAA truc tout de même. Par conséquent, que ce soit en appelant ou en remplaçant, je vais être conscient du fait que c'est AAA qui a défini MyMethod(string).

Si j'appelais du code utilisant BBB, je penserais à "utiliser BBB stuff". Je ne suis peut-être pas très conscient de ce qui était initialement défini dans AAA, et je penserais peut-être que cela n'est qu'un détail d'implémentation (si je n'utilisais pas également l'interface AAA à proximité) .

Le comportement du compilateur correspond à mon intuition, c'est pourquoi lors de la première lecture de la question, il m'a semblé que Mono avait un bug. Après examen, je ne vois pas comment l'un remplit mieux le comportement spécifié que l'autre.

Pour cette question cependant, tout en restant à un niveau personnel, je n'utiliserais jamais de paramètres facultatifs avec des méthodes abstraites, virtuelles ou remplacées, et si le remplacement de quelqu'un d'autre le faisait, je ferais correspondre les leurs.

23
Jon Hanna

Vous pouvez lever l'ambiguïté en appelant:

this.MyMethod();

(dans MyMethod2())

Que ce soit un bug est délicat; cela semble cependant incohérent. Resharper vous avertit simplement de ne pas modifier la valeur par défaut dans un remplacement, si cela vous aide; p Bien sûr, resharper aussi vous indique que this. Est redondant et propose à supprimez-le pour vous ... ce qui change le comportement - donc le resharper n'est pas parfait non plus.

Il y ressemble pourrait se qualifier comme un bug de compilation, je vous l'accorde. Il faudrait que je regarde vraiment attentivement pour être sûr ... où est Eric quand tu as besoin de lui, hein?


Éditer:

Le point clé ici est la spécification de la langue; regardons le §7.5.3:

Par exemple, l'ensemble des candidats pour un appel de méthode n'inclut pas les méthodes marquées override (§7.4), et les méthodes dans une classe de base ne sont pas candidates si une méthode dans une classe dérivée est applicable (§7.6.5.1).

(et en effet le §7.4 omet clairement les méthodes override de la considération)

Il y a un conflit ici .... il indique que les méthodes base ne sont pas utilisées s'il existe une méthode applicable dans une classe dérivée - ce qui nous conduirait à la dérivée méthode, mais en même temps, elle indique que les méthodes marquées override ne sont pas prises en compte.

Mais, le §7.5.1.1 déclare alors:

Pour les méthodes et indexeurs virtuels définis dans les classes, la liste des paramètres est choisie dans la déclaration ou la substitution la plus spécifique du membre de la fonction, en commençant par le type statique du récepteur et en parcourant ses classes de base.

puis le §7.5.1.2 explique comment les valeurs sont évaluées au moment de l'appel:

Lors du traitement au moment de l'exécution d'un appel de membre de fonction (§7.5.4), les expressions ou références de variables d'une liste d'arguments sont évaluées dans l'ordre, de gauche à droite, comme suit:

...(couper)...

Lorsque des arguments sont omis d'un membre de fonction avec les paramètres facultatifs correspondants, les arguments par défaut de la déclaration de membre de fonction sont implicitement transmis. Parce que ceux-ci sont toujours constants, leur évaluation n'aura pas d'impact sur l'ordre d'évaluation des arguments restants.

Cela souligne explicitement qu'il examine la liste d'arguments, qui a été précédemment définie au §7.5.1.1 comme provenant de déclaration ou remplacement le plus spécifique. Il semble raisonnable que ce soit la "déclaration de méthode" à laquelle il est fait référence au §7.5.1.2, donc la valeur transmise doit être du plus dérivé jusqu'au type statique.

Cela suggérerait: csc a un bogue, et il devrait utiliser la version dérivée ("bbb bbb") à moins qu'il ne soit restreint (via base., Ou casté en un type de base ) pour regarder les déclarations de la méthode de base (§7.6.8).

34
Marc Gravell

Cela ressemble à un bug pour moi. Je le crois est bien spécifié et qu'il devrait se comporter de la même manière que si vous appelez la méthode avec le préfixe explicite this.

J'ai simplifié l'exemple pour utiliser uniquement une méthode virtuelle nique, et montrer à la fois quelle implémentation est appelée et quelle est la valeur du paramètre:

using System;

class Base
{
    public virtual void M(string text = "base-default")
    {
        Console.WriteLine("Base.M: {0}", text);
    }   
}

class Derived : Base
{
    public override void M(string text = "derived-default")
    {
        Console.WriteLine("Derived.M: {0}", text);
    }

    public void RunTests()
    {
        M();      // Prints Derived.M: base-default
        this.M(); // Prints Derived.M: derived-default
        base.M(); // Prints Base.M: base-default
    }
}

class Test
{
    static void Main()
    {
        Derived d = new Derived();
        d.RunTests();
    }
}

Donc, tout ce dont nous devons nous soucier, ce sont les trois appels dans RunTests. Les bits importants de la spécification pour les deux premiers appels sont la section 7.5.1.1, qui parle de la liste des paramètres à utiliser lors de la recherche des paramètres correspondants:

Pour les méthodes et indexeurs virtuels définis dans les classes, la liste des paramètres est choisie dans la déclaration ou la substitution la plus spécifique du membre de la fonction, en commençant par le type statique du récepteur et en parcourant ses classes de base.

Et la section 7.5.1.2:

Lorsque des arguments sont omis d'un membre de fonction avec les paramètres facultatifs correspondants, les arguments par défaut de la déclaration de membre de fonction sont implicitement transmis.

Le "paramètre facultatif correspondant" est le bit qui lie 7.5.2 à 7.5.1.1.

Pour M() et this.M(), cette liste de paramètres doit être celle de Derived car le type statique du récepteur est Derived, en effet, vous pouvez dire que le compilateur traite cela comme la liste des paramètres plus tôt dans la compilation, comme si vous définissez le paramètre obligatoire dans Derived.M(), les deux des appels échouent - donc la M() appelle nécessite le paramètre pour avoir une valeur par défaut dans Derived, mais l'ignore ensuite!

En effet, cela empire: si vous fournissez une valeur par défaut pour le paramètre dans Derived mais le rendez obligatoire dans Base, l'appel M() finit par utiliser null comme valeur d'argument. Si rien d'autre, je dirais que cela prouve que c'est un bug: cette valeur null ne peut pas provenir de n'importe où valide. (C'est null car c'est la valeur par défaut du type string; il utilise toujours juste la valeur par défaut pour le type de paramètre.)

La section 7.6.8 de la spécification traite de base.M (), qui dit que ainsi comme comportement non virtuel, l'expression est considérée comme ((Base) this).M(); il est donc tout à fait correct que la méthode de base soit utilisée pour déterminer la liste des paramètres effectifs. Cela signifie que la dernière ligne est correcte.

Juste pour rendre les choses plus faciles pour quiconque veut voir le bogue vraiment étrange décrit ci-dessus, où une valeur non spécifiée n'importe où est utilisée:

using System;

class Base
{
    public virtual void M(int x)
    {
        // This isn't called
    }   
}

class Derived : Base
{
    public override void M(int x = 5)
    {
        Console.WriteLine("Derived.M: {0}", x);
    }

    public void RunTests()
    {
        M();      // Prints Derived.M: 0
    }

    static void Main()
    {
        new Derived().RunTests();
    }
}
15
Jon Skeet

As-tu essayé:

 public override void MyMethod2()
    {
        this.MyMethod();
    }

Vous dites donc à votre programme d'utiliser la méthode surchargée.

10
basti

Le comportement est définitivement très étrange; il n'est pas clair pour moi s'il s'agit en fait d'un bogue dans le compilateur, mais c'est possible.

Le campus a reçu une bonne quantité de neige la nuit dernière et Seattle n'est pas très bon pour gérer la neige. Mon bus ne circule pas ce matin, donc je ne pourrai pas entrer dans le bureau pour comparer ce que C # 4, C # 5 et Roslyn disent à propos de cette affaire et s'ils ne sont pas d'accord. J'essaierai de publier une analyse plus tard cette semaine une fois de retour au bureau et je pourrai utiliser les outils de débogage appropriés.

9
Eric Lippert

Cela peut être dû à l'ambiguïté et le compilateur donne la priorité à la classe de base/super. Le changement ci-dessous au code de votre classe BBB avec l'ajout d'une référence au mot clé this, donne la sortie 'bbb bbb':

class BBB : AAA
{
    public override void MyMethod(string s = "bbb")
    {
        base.MyMethod(s);
    }

    public override void MyMethod2()
    {
        this.MyMethod(); //added this keyword here
    }
}

Cela implique notamment que vous devez toujours utiliser le mot clé this chaque fois que vous appelez des propriétés ou des méthodes sur l'instance actuelle de la classe en tant que meilleure pratique.

Je serais inquiet si cette ambiguïté dans les méthodes de base et enfant ne déclenche même pas un avertissement du compilateur (sinon une erreur), mais si c'est le cas, cela n'était pas vu, je suppose.

================================================== ================

EDIT: Considérez ci-dessous des exemples d'extraits de ces liens:

http://geekswithblogs.net/BlackRabbitCoder/archive/2011/07/28/c.net-little-pitfalls-default-parameters-are-compile-time-substitutions.aspx

http://geekswithblogs.net/BlackRabbitCoder/archive/2010/06/17/c-optional-parameters---pros-and-pitfalls.aspx

Piège: les valeurs des paramètres facultatifs sont au moment de la compilation Il y a une chose et une seule chose à garder à l'esprit lors de l'utilisation des paramètres facultatifs. Si vous gardez cette chose à l'esprit, il est probable que vous puissiez bien comprendre et éviter tout piège potentiel avec leur utilisation: Cette seule chose est la suivante: les paramètres facultatifs sont du sucre syntaxique à la compilation!

Piège: méfiez-vous des paramètres par défaut dans l'héritage et la mise en œuvre de l'interface

Désormais, le deuxième piège potentiel concerne l'héritage et la mise en œuvre de l'interface. Je vais illustrer avec un puzzle:

   1: public interface ITag 
   2: {
   3:     void WriteTag(string tagName = "ITag");
   4: } 
   5:  
   6: public class BaseTag : ITag 
   7: {
   8:     public virtual void WriteTag(string tagName = "BaseTag") { Console.WriteLine(tagName); }
   9: } 
  10:  
  11: public class SubTag : BaseTag 
  12: {
  13:     public override void WriteTag(string tagName = "SubTag") { Console.WriteLine(tagName); }
  14: } 
  15:  
  16: public static class Program 
  17: {
  18:     public static void Main() 
  19:     {
  20:         SubTag subTag = new SubTag();
  21:         BaseTag subByBaseTag = subTag;
  22:         ITag subByInterfaceTag = subTag; 
  23:  
  24:         // what happens here?
  25:         subTag.WriteTag();       
  26:         subByBaseTag.WriteTag(); 
  27:         subByInterfaceTag.WriteTag(); 
  28:     }
  29: } 

Ce qui se produit? Eh bien, même si l'objet dans chaque cas est SubTag dont la balise est "SubTag", vous obtiendrez:

1: Sous-étiquette 2: BaseTag 3: ITag

Mais n'oubliez pas de vous assurer:

N'insérez pas de nouveaux paramètres par défaut au milieu d'un ensemble existant de paramètres par défaut, cela peut entraîner un comportement imprévisible qui ne génère pas nécessairement une erreur de syntaxe - ajoutez à la fin de la liste ou créez une nouvelle méthode. Soyez extrêmement prudent sur la façon dont vous utilisez les paramètres par défaut dans les hiérarchies d'héritage et les interfaces - choisissez le niveau le plus approprié pour ajouter les valeurs par défaut en fonction de l'utilisation attendue.

================================================== ========================

5
VS1

Je pense que c'est parce que ces valeurs par défaut sont fixées au moment de la compilation. Si vous utilisez le réflecteur, vous verrez ce qui suit pour MyMethod2 dans BBB.

public override void MyMethod2()
{
    this.MyMethod("aaa");
}
1
chandmk

D'accord en général avec @Marc Gravell.

Cependant, je voudrais mentionner que le problème est assez ancien dans le monde C++ ( http://www.devx.com/tips/Tip/12737 ), et la réponse ressemble à "contrairement au virtuel fonctions, qui sont résolues au moment de l'exécution, les arguments par défaut sont résolus statiquement, c'est-à-dire au moment de la compilation. " Donc, ce comportement du compilateur C # avait plutôt été accepté délibérément en raison de la cohérence, malgré son caractère inattendu, semble-t-il.

0
Alexey Khoroshikh

Quoi qu'il en soit, il a besoin d'un correctif

Je le considérerais certainement comme un bogue, soit parce que les résultats sont incorrects, soit si les résultats sont attendus, le compilateur ne devrait pas vous laisser le déclarer comme "remplacement", ou au moins fournir un avertissement.

Je vous recommanderais de signaler cela à Microsoft.

Mais est-ce juste ou faux?

Cependant, que ce soit le comportement attendu ou non, analysons d'abord les deux points de vue.

considérons que nous avons le code suivant:

void myfunc(int optional = 5){ /* Some code here*/ } //Function implementation
myfunc(); //Call using the default arguments

Il existe deux façons de le mettre en œuvre:

  1. Ces arguments facultatifs sont traités comme des fonctions surchargées, ce qui donne les résultats suivants:

    void myfunc(int optional){ /* Some code here*/ } //Function implementation
    void myfunc(){ myfunc(5); } //Default arguments implementation
    myfunc(); //Call using the default arguments
    
  2. Que la valeur par défaut est incorporée dans l'appelant, résultant ainsi dans le code suivant:

    void myfunc(int optional){ /* Some code here*/ } //Function implementation
    myfunc(5); //Call and embed default arguments
    

Il existe de nombreuses différences entre les deux approches, mais nous allons d'abord examiner comment le framework .Net l'interprète.

  1. Dans .Net, vous pouvez uniquement remplacer une méthode par une méthode qui contient le même nombre d'arguments, mais vous ne pouvez pas remplacer par une méthode contenant plus d'arguments, même si elles sont toutes facultatives (ce qui entraînerait un appel ayant la même signature que la méthode substituée), disons par exemple que vous avez:

    class bassClass{ public virtual void someMethod()}
    class subClass :bassClass{ public override void someMethod()} //Legal
    //The following is illegal, although it would be called as someMethod();
    //class subClass:bassClass{ public override void someMethod(int optional = 5)} 
    
  2. Vous pouvez surcharger une méthode avec des arguments par défaut avec une autre méthode sans arguments, (cela a des implications désastreuses comme je le discuterai dans quelques instants), donc le code suivant est légal:

    void myfunc(int optional = 5){ /* Some code here*/ } //Function with default
    void myfunc(){ /* Some code here*/ } //No arguments
    myfunc(); //Call which one?, the one with no arguments!
    
  3. lors de l'utilisation de la réflexion, il faut toujours fournir une valeur par défaut.

Tout cela suffit pour prouver que .Net a pris la deuxième implémentation, donc le comportement que l'OP a vu est correct, au moins selon .Net.

Problèmes avec l'approche .Net

Cependant, il existe de réels problèmes avec l'approche .Net.

  1. Cohérence

    • Comme dans le problème de l'OP lors de la substitution de la valeur par défaut dans une méthode héritée, les résultats peuvent être imprévisibles

    • Lorsque l'implantation d'origine de la valeur par défaut est modifiée et que les appelants n'ont pas à être recompilés, nous pourrions nous retrouver avec des valeurs par défaut qui ne sont plus valides

    • La réflexion vous oblige à fournir la valeur par défaut, que l'appelant n'a pas à connaître
  2. Code de rupture

    • Lorsque nous avons une fonction avec des arguments par défaut et que nous ajoutons une fonction sans arguments, tous les appels seront désormais acheminés vers la nouvelle fonction, cassant ainsi tout le code existant, sans aucune notification ni avertissement!

    • Une situation similaire se produira, si nous supprimons plus tard la fonction sans arguments, tous les appels seront automatiquement acheminés vers la fonction avec les arguments par défaut, à nouveau sans notification ni avertissement! bien que ce ne soit pas l'intention du programmeur

    • De plus, il ne doit pas nécessairement s'agir d'une méthode d'instance régulière, une méthode d'extension fera les mêmes problèmes, car une méthode d'extension sans paramètres aura priorité sur une méthode d'instance avec des paramètres par défaut!

Résumé: RESTEZ LOIN DES ARGUMENTS OPTIONNELS ET UTILISEZ DES SURCHARGES AU LIEU (COMME LE CADRE .NET LUI-MÊME)

0
yoel halb