Devez-vous déclarer des méthodes utilisant des surcharges ou des paramètres facultatifs en C # 4.0?

94

Je regardais la présentation d'Anders sur C # 4.0 et un aperçu de C # 5.0 , et cela m'a fait réfléchir à quand les paramètres facultatifs sont disponibles en C #, quelle sera la manière recommandée de déclarer des méthodes qui n'ont pas besoin de tous les paramètres spécifiés?

Par exemple, quelque chose comme la FileStreamclasse a une quinzaine de constructeurs différents qui peuvent être divisés en «familles» logiques, par exemple ceux ci-dessous à partir d'une chaîne, ceux de an IntPtret ceux de a SafeFileHandle.

FileStream(string,FileMode);
FileStream(string,FileMode,FileAccess);
FileStream(string,FileMode,FileAccess,FileShare);
FileStream(string,FileMode,FileAccess,FileShare,int);
FileStream(string,FileMode,FileAccess,FileShare,int,bool);

Il me semble que ce type de modèle pourrait être simplifié en ayant trois constructeurs à la place, et en utilisant des paramètres optionnels pour ceux qui peuvent être par défaut, ce qui rendrait les différentes familles de constructeurs plus distinctes [note: je sais que ce changement ne sera pas fait dans la BCL, je parle hypothétiquement pour ce type de situation].

Qu'est-ce que tu penses? À partir de C # 4.0, sera-t-il plus logique de créer des groupes de constructeurs et de méthodes étroitement liés en une seule méthode avec des paramètres optionnels, ou y a-t-il une bonne raison de s'en tenir au mécanisme traditionnel de surcharge multiple?

Greg Beech
la source

Réponses:

122

Je considérerais ce qui suit:

  • Avez-vous besoin que votre code soit utilisé à partir de langues qui ne prennent pas en charge les paramètres facultatifs? Si tel est le cas, pensez à inclure les surcharges.
  • Y a-t-il des membres de votre équipe qui s'opposent violemment aux paramètres facultatifs? (Parfois, il est plus facile de vivre avec une décision que vous n'aimez pas que d'argumenter.)
  • Êtes-vous certain que vos valeurs par défaut ne changeront pas entre les versions de votre code, ou si elles le peuvent, vos appelants seront-ils d'accord avec cela?

Je n'ai pas vérifié comment les valeurs par défaut vont fonctionner, mais je suppose que les valeurs par défaut seront intégrées dans le code appelant, de la même manière que les références aux constchamps. C'est généralement correct - les modifications apportées à une valeur par défaut sont de toute façon assez importantes - mais ce sont les choses à considérer.

Jon Skeet
la source
21
+1 pour la sagesse sur le pragmatisme: Parfois, il est plus facile de vivre avec une décision que vous n'aimez pas que d'argumenter.
legends2k
13
@romkyns: Non, l'effet des surcharges n'est pas la même chose avec le point 3. Avec les surcharges fournissant les valeurs par défaut, les valeurs par défaut sont dans le code de la bibliothèque - donc si vous changez la valeur par défaut et fournissez une nouvelle version de la bibliothèque, les appelants voir la nouvelle valeur par défaut sans recompilation. Alors qu'avec des paramètres optionnels, vous auriez besoin de recompiler pour "voir" les nouveaux paramètres par défaut. Beaucoup de temps , il n'est pas une distinction importante, mais il est une distinction.
Jon Skeet
salut @JonSkeet, je voudrais savoir si nous utilisons à la fois la fonction ie avec un paramètre optionnel et une autre avec surcharge quelle méthode sera appelée ?? par exemple, Add (int a, int b) et Add (int a, int b, int c = 0) et appel de fonction disons: Add (5,10); quelle méthode sera appelée fonction surchargée ou fonction de paramètre facultative? merci :)
SHEKHAR SHETE
@Shekshar: Avez-vous essayé? Lisez la spécification pour plus de détails, mais fondamentalement, dans un tie-breaker, une méthode où le compilateur n'a pas eu à remplir de paramètres facultatifs l'emporte.
Jon Skeet
@JonSkeet je viens d'essayer avec ci-dessus ... la surcharge de fonction l'emporte sur le paramètre facultatif :)
SHEKHAR SHETE
19

Lorsqu'une surcharge de méthode effectue normalement la même chose avec un nombre différent d'arguments, les valeurs par défaut seront utilisées.

Lorsqu'une surcharge de méthode exécute une fonction différemment en fonction de ses paramètres, la surcharge continuera à être utilisée.

J'ai utilisé l'option facultative dans mes jours VB6 et je l'ai ratée depuis, cela réduira beaucoup de duplication de commentaires XML en C #.

cfeduke
la source
11

J'utilise Delphi avec des paramètres optionnels depuis toujours. Je suis plutôt passé à l'utilisation de surcharges.

Parce que lorsque vous allez créer plus de surcharges, vous serez invariablement en conflit avec un formulaire de paramètre facultatif, et vous devrez alors les convertir en non-facultatif de toute façon.

Et j'aime l'idée qu'il y a généralement une super méthode, et les autres sont des wrappers plus simples autour de celle-là.

Ian Boyd
la source
1
Je suis tout à fait d'accord avec cela, mais il y a l'avertissement que lorsque vous avez une méthode qui prend plusieurs (3+) paramètres, qui sont par nature tous "optionnels" (pourraient être remplacés par une valeur par défaut), vous pourriez vous retrouver avec de nombreuses permutations de la signature de la méthode pour plus aucun bénéfice. Considérez Foo(A, B, C)exige Foo(A), Foo(B), Foo(C), Foo(A, B), Foo(A, C), Foo(B, C).
Dan Lugg
7

J'utiliserai certainement la fonctionnalité de paramètres optionnels de 4.0. Il se débarrasse du ridicule ...

public void M1( string foo, string bar )
{
   // do that thang
}

public void M1( string foo )
{
  M1( foo, "bar default" ); // I have always hated this line of code specifically
}

... et place les valeurs là où l'appelant peut les voir ...

public void M1( string foo, string bar = "bar default" )
{
   // do that thang
}

Beaucoup plus simple et beaucoup moins sujet aux erreurs. J'ai en fait vu cela comme un bogue dans le cas de surcharge ...

public void M1( string foo )
{
   M2( foo, "bar default" );  // oops!  I meant M1!
}

Je n'ai pas encore joué avec le complicateur 4.0, mais je ne serais pas choqué d'apprendre que le complieur émet simplement les surcharges pour vous.

JP Alioto
la source
6

Les paramètres facultatifs sont essentiellement un élément de métadonnées qui dirige un compilateur qui traite un appel de méthode pour insérer les valeurs par défaut appropriées sur le site d'appel. En revanche, les surcharges fournissent un moyen par lequel un compilateur peut sélectionner l'une des nombreuses méthodes, dont certaines peuvent fournir elles-mêmes des valeurs par défaut. Notez que si l'on essaie d'appeler une méthode qui spécifie des paramètres optionnels à partir de code écrit dans un langage qui ne les prend pas en charge, le compilateur exigera que les paramètres "optionnels" soient spécifiés, mais comme appeler une méthode sans spécifier de paramètre optionnel est équivalent à l'appeler avec un paramètre égal à la valeur par défaut, il n'y a aucun obstacle à ce que ces langages appellent de telles méthodes.

Une conséquence importante de la liaison de paramètres facultatifs sur le site d'appel est qu'ils se verront attribuer des valeurs basées sur la version du code cible qui est disponible pour le compilateur. Si un assembly Fooa une méthode Boo(int)avec une valeur par défaut de 5 et que l'assembly Barcontient un appel à Foo.Boo(), le compilateur le traitera comme un Foo.Boo(5). Si la valeur par défaut est modifiée à 6 et l'assembly Fooest recompilé, Barcontinuera à appeler à Foo.Boo(5)moins ou jusqu'à ce qu'il soit recompilé avec cette nouvelle version de Foo. Il faut donc éviter d'utiliser des paramètres optionnels pour des choses qui pourraient changer.

supercat
la source
Re: "Il faut donc éviter d'utiliser des paramètres optionnels pour des choses qui pourraient changer." Je conviens que cela peut être problématique si le changement passe inaperçu par le code client. Cependant, le même problème existe lorsque la valeur par défaut est caché à l' intérieur d' une surcharge de la méthode: void Foo(int value) … void Foo() { Foo(42); }. De l'extérieur, l'appelant ne sait pas quelle valeur par défaut est utilisée, ni quand elle peut changer; il faudrait surveiller la documentation écrite pour cela. Les valeurs par défaut pour les paramètres facultatifs peuvent être vues comme cela: documentation-in-code quelle est la valeur par défaut.
stakx - ne contribue plus
@stakx: Si une surcharge sans paramètre est liée à une surcharge avec un paramètre, changer la valeur "par défaut" de ce paramètre et recompiler la définition de la surcharge changera la valeur qu'elle utilise même si le code appelant n'est pas recompilé .
supercat
C'est vrai, mais cela ne le rend pas plus problématique que l'alternative. Dans un cas (surcharge de méthode), le code d'appel n'a pas son mot à dire dans la valeur par défaut. Cela peut être approprié si le code appelant ne se soucie pas du tout du paramètre facultatif et de sa signification. Dans l'autre cas (paramètre optionnel avec valeur par défaut), le code d'appel précédemment compilé ne sera pas affecté lorsque la valeur par défaut change. Cela peut également être approprié lorsque le code appelant se soucie réellement du paramètre; l'omettre dans la source revient à dire: "La valeur par défaut actuellement suggérée est OK pour moi".
stakx - ne contribue plus
Le point que j'essaie de faire valoir ici est que, bien qu'il y ait des conséquences dans l'une ou l'autre approche (comme vous l'avez souligné), elles ne sont pas intrinsèquement avantageuses ou désavantageuses. Cela dépend également des besoins et des objectifs du code d'appel. À partir de ce point de vue, j'ai trouvé le verdict dans la dernière phrase de votre réponse un peu trop rigide.
stakx - ne contribue plus
@stakx: J'ai dit "éviter d'utiliser" plutôt que "ne jamais utiliser". Si changer X signifie que la prochaine recompilation de Y changera le comportement de Y, cela nécessitera soit qu'un système de construction soit configuré pour que chaque recompilation de X recompile également Y (ralentissant les choses), soit crée le risque qu'un programmeur change X d'une manière qui cassera Y la prochaine fois qu'il sera compilé, et ne découvrira cette rupture que plus tard, lorsque Y est modifié pour une raison totalement indépendante. Les paramètres par défaut ne doivent être utilisés que lorsque leurs avantages l'emportent sur ces coûts.
supercat
4

On peut se demander si des arguments optionnels ou des surcharges doivent être utilisés ou non, mais surtout, chacun a sa propre zone où il est irremplaçable.

Les arguments facultatifs, lorsqu'ils sont utilisés en combinaison avec des arguments nommés, sont extrêmement utiles lorsqu'ils sont combinés avec certaines listes d'arguments longues avec toutes les options d'appels COM.

Les surcharges sont extrêmement utiles lorsque la méthode est capable d'opérer sur de nombreux types d'arguments différents (juste un des exemples), et effectue des castings en interne, par exemple; vous le nourrissez simplement avec n'importe quel type de données qui a du sens (qui est accepté par une surcharge existante). Je ne peux pas battre ça avec des arguments optionnels.

mr.b
la source
3

J'attends avec impatience les paramètres optionnels car ils gardent les valeurs par défaut plus proches de la méthode. Ainsi, au lieu de dizaines de lignes pour les surcharges qui appellent simplement la méthode "développée", vous ne définissez qu'une seule fois la méthode et vous pouvez voir ce que les paramètres optionnels par défaut dans la signature de méthode. Je préfère regarder:

public Rectangle (Point start = Point.Zero, int width, int height)
{
    Start = start;
    Width = width;
    Height = height;
}

Au lieu de cela:

public Rectangle (Point start, int width, int height)
{
    Start = start;
    Width = width;
    Height = height;
}

public Rectangle (int width, int height) :
    this (Point.Zero, width, height)
{
}

Évidemment, cet exemple est vraiment simple mais dans le cas de l'OP avec 5 surcharges, les choses peuvent devenir très vite encombrées.

Mark A. Nicolosi
la source
7
J'ai entendu dire que les paramètres optionnels devraient être les derniers, n'est-ce pas?
Ilya Ryzhenkov
Dépend de votre conception. Peut-être que l'argument «start» est normalement important, sauf quand ce n'est pas le cas. Peut-être avez-vous la même signature ailleurs, ce qui signifie quelque chose de différent. Pour un exemple artificiel, public Rectangle (int width, int height, Point innerSquareStart, Point innerSquareEnd) {}
Robert P
13
D'après ce qu'ils ont dit dans la conférence, les paramètres facultatifs doivent être après les paramètres requis.
Greg Beech
3

L'un de mes aspects préférés des paramètres facultatifs est que vous voyez ce qui arrive à vos paramètres si vous ne les fournissez pas, même sans passer à la définition de la méthode. Visual Studio vous montrera simplement la valeur par défaut du paramètre lorsque vous tapez le nom de la méthode. Avec une méthode de surcharge, vous êtes coincé soit avec la lecture de la documentation (si elle est même disponible), soit avec la navigation directe vers la définition de la méthode (si disponible) et vers la méthode que la surcharge encapsule.

En particulier: l'effort de documentation peut augmenter rapidement avec la quantité de surcharges, et vous finirez probablement par copier des commentaires déjà existants à partir des surcharges existantes. C'est assez ennuyeux, car cela ne produit aucune valeur et casse le principe DRY ). D'un autre côté, avec un paramètre facultatif, il y a exactement un endroit où tous les paramètres sont documentés et vous voyez leur signification ainsi que leurs valeurs par défaut lors de la saisie.

Enfin, si vous êtes le consommateur d'une API, vous n'aurez peut-être même pas la possibilité d'inspecter les détails de l'implémentation (si vous n'avez pas le code source) et n'avez donc aucune chance de voir à quelle super méthode les surchargées emballent. Ainsi, vous êtes coincé avec la lecture du document et espérez que toutes les valeurs par défaut y sont répertoriées, mais ce n'est pas toujours le cas.

Bien sûr, ce n’est pas une réponse qui traite tous les aspects, mais je pense qu’elle en ajoute une qui n’a pas encore été abordée.

LuiBromBeere
la source
1

Bien qu'il existe (supposément?) Deux moyens conceptuellement équivalents disponibles pour modéliser votre API à partir de zéro, ils présentent malheureusement une différence subtile lorsque vous devez considérer la compatibilité descendante d'exécution pour vos anciens clients dans la nature. Mon collègue (merci Brent!) M'a signalé ce merveilleux article: Problèmes de versioning avec des arguments optionnels . Certains en citent:

La raison pour laquelle des paramètres facultatifs ont été introduits dans C # 4 en premier lieu était de prendre en charge l'interopérabilité COM. C'est tout. Et maintenant, nous apprenons toutes les implications de ce fait. Si vous avez une méthode avec des paramètres optionnels, vous ne pouvez jamais ajouter une surcharge avec des paramètres optionnels supplémentaires par crainte de provoquer un changement de rupture lors de la compilation. Et vous ne pouvez jamais supprimer une surcharge existante, car cela a toujours été un changement radical à l'exécution. Vous avez à peu près besoin de le traiter comme une interface. Votre seul recours dans ce cas est d'écrire une nouvelle méthode avec un nouveau nom. Soyez donc conscient de cela si vous prévoyez d'utiliser des arguments facultatifs dans vos API.

RayLuo
la source
1

Une mise en garde des paramètres facultatifs est la gestion des versions, où un refactor a des conséquences inattendues. Un exemple:

Code initial

public string HandleError(string message, bool silent=true, bool isCritical=true)
{
  ...
}

Supposons que c'est l'un des nombreux appelants de la méthode ci-dessus:

HandleError("Disk is full", false);

Ici, l'événement n'est pas silencieux et est traité comme critique.

Maintenant, disons qu'après un refactor, nous constatons que toutes les erreurs invitent de toute façon l'utilisateur, donc nous n'avons plus besoin de l'indicateur silencieux. Alors on l'enlève.

Après refactor

L'ancien appel se compile toujours, et disons qu'il glisse dans le refactor sans changement:

public string HandleError(string message, /*bool silent=true,*/ bool isCritical=true)
{
  ...
}

...

// Some other distant code file:
HandleError("Disk is full", false);

Maintenant falseaura un effet involontaire, l'événement ne sera plus traité comme critique.

Cela pourrait entraîner un défaut subtil, car il n'y aura pas d'erreur de compilation ou d'exécution (contrairement à d'autres mises en garde d'options, telles que ceci ou cela ).

Notez qu'il existe de nombreuses formes de ce même problème. Un autre formulaire est décrit ici .

Notez également que strictement l' utilisation des paramètres nommés lors de l' appel de la méthode évitera la question, par exemple comme ceci: HandleError("Disk is full", silent:false). Cependant, il peut ne pas être pratique de supposer que tous les autres développeurs (ou utilisateurs d'une API publique) le feront.

Pour ces raisons, j'éviterais d'utiliser des paramètres optionnels dans une API publique (ou même une méthode publique si elle pouvait être largement utilisée) à moins qu'il n'y ait d'autres considérations convaincantes.

Zach
la source
0

Les deux paramètres facultatifs, la surcharge de méthode ont leur propre avantage ou inconvénient. Cela dépend de votre préférence pour choisir entre eux.

Paramètre facultatif: disponible uniquement dans .Net 4.0. paramètre facultatif réduit la taille de votre code. Vous ne pouvez pas définir le paramètre out et ref

méthodes surchargées: vous pouvez définir les paramètres de sortie et de référence. La taille du code augmentera mais les méthodes surchargées sont faciles à comprendre.

sushil pandey
la source
0

Dans de nombreux cas, des paramètres facultatifs sont utilisés pour changer d'exécution. Par exemple:

decimal GetPrice(string productName, decimal discountPercentage = 0)
{

    decimal basePrice = CalculateBasePrice(productName);

    if (discountPercentage > 0)
        return basePrice * (1 - discountPercentage / 100);
    else
        return basePrice;
}

Le paramètre Discount ici est utilisé pour alimenter l'instruction if-then-else. Il y a le polymorphisme qui n'a pas été reconnu, puis il a été implémenté comme une instruction if-then-else. Dans de tels cas, il est préférable de diviser les deux flux de contrôle en deux méthodes indépendantes:

decimal GetPrice(string productName)
{
    decimal basePrice = CalculateBasePrice(productName);
    return basePrice;
}

decimal GetPrice(string productName, decimal discountPercentage)
{

    if (discountPercentage <= 0)
        throw new ArgumentException();

    decimal basePrice = GetPrice(productName);

    decimal discountedPrice = basePrice * (1 - discountPercentage / 100);

    return discountedPrice;

}

De cette façon, nous avons même protégé la classe de recevoir un appel sans remise. Cet appel signifierait que l'appelant pense qu'il y a une réduction, mais en fait, il n'y a pas de réduction du tout. Un tel malentendu peut facilement provoquer un bug.

Dans des cas comme celui-ci, je préfère ne pas avoir de paramètres optionnels, mais pour forcer l'appelant à sélectionner explicitement le scénario d'exécution qui convient à sa situation actuelle.

La situation est très similaire à celle des paramètres qui peuvent être nuls. C'est également une mauvaise idée lorsque la mise en œuvre se résume à des déclarations comme if (x == null).

Vous pouvez trouver une analyse détaillée sur ces liens: éviter les paramètres facultatifs et éviter les paramètres nuls

Zoran Horvat
la source
0

Pour ajouter une évidence quand utiliser une surcharge au lieu des options:

Chaque fois que vous avez un certain nombre de paramètres qui n'ont de sens qu'ensemble, n'y introduisez pas d'options.

Ou plus généralement, chaque fois que vos signatures de méthode activent des modèles d'utilisation qui n'ont pas de sens, limitez le nombre de permutations d'appels possibles. Par exemple, en utilisant des surcharges au lieu des options (cette règle est également vraie lorsque vous avez plusieurs paramètres du même type de données, d'ailleurs; ici, des périphériques comme les méthodes d'usine ou les types de données personnalisés peuvent aider).

Exemple:

enum Match {
    Regex,
    Wildcard,
    ContainsString,
}

// Don't: This way, Enumerate() can be called in a way
//         which does not make sense:
IEnumerable<string> Enumerate(string searchPattern = null,
                              Match match = Match.Regex,
                              SearchOption searchOption = SearchOption.TopDirectoryOnly);

// Better: Provide only overloads which cannot be mis-used:
IEnumerable<string> Enumerate(SearchOption searchOption = SearchOption.TopDirectoryOnly);
IEnumerable<string> Enumerate(string searchPattern, Match match,
                              SearchOption searchOption = SearchOption.TopDirectoryOnly);
Sébastien Mach
la source