Je regardais le discours d'Anders sur C # 4.0 et un aperçu de C # 5.0 , et cela m'a fait réfléchir sur le moment où des paramètres facultatifs sont disponibles en C #, quelle est la méthode recommandée pour déclarer des méthodes ne nécessitant pas tous les paramètres spécifié?
Par exemple, quelque chose comme la classe FileStream
a une quinzaine de constructeurs différents qui peuvent être divisés en «familles» logiques, par exemple. ceux ci-dessous d'une chaîne, ceux d'un IntPtr
et ceux d'un 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 plutôt trois constructeurs, et en utilisant des paramètres optionnels pour ceux qui peuvent être configurés par défaut, ce qui rendrait les différentes familles de constructeurs plus distincts [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 la version 4.0, est-il plus logique de regrouper des groupes de constructeurs et de méthodes étroitement liés en une seule méthode avec des paramètres facultatifs, ou existe-t-il une bonne raison de s'en tenir au mécanisme traditionnel de surcharge multiple?
Je considérerais ce qui suit:
Je n'ai pas vérifié comment les valeurs par défaut vont fonctionner, mais je supposerais que les valeurs par défaut seront intégrées au code d'appel, de la même manière que les références aux champs const
. C’est d’habitude correct: les modifications apportées à une valeur par défaut sont quand même assez importantes, mais ce sont les éléments à prendre en compte.
Lorsqu'une surcharge de méthode effectue normalement la même chose avec un nombre différent d'arguments, les valeurs par défaut sont utilisées.
Lorsqu'une surcharge de méthode exécute une fonction différemment en fonction de ses paramètres, la surcharge continue à être utilisée.
J'avais utilisé optionnel à l'époque de mon VB6 et l'ai manqué depuis, cela réduira beaucoup de duplication de commentaires XML en C #.
j'utilise Delphi, avec des paramètres facultatifs, depuis toujours. J'ai opté pour des surcharges.
En effet, lorsque vous créez davantage de surcharges, vous vous en prendrez toujours à une forme de paramètre facultative. et vous devrez alors les convertir en non-facultatif de toute façon.
Et j'aime l'idée qu'il y ait généralement une super méthode, et le reste est plus simple.
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 effectivement vu cela comme un bug 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 4.0 complier, mais je ne serais pas choqué d'apprendre que le compliant n'émet que des surcharges pour vous.
Les paramètres facultatifs sont essentiellement des métadonnées qui orientent un compilateur qui traite un appel de méthode pour insérer les valeurs par défaut appropriées sur le site de l'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 on essaie d'appeler une méthode qui spécifie des paramètres optionnels à partir d'un code écrit dans un langage qui ne les prend pas en charge, le compilateur exigera que les paramètres "optionnels" soient spécifiés, équivalent à l'appeler avec un paramètre égal à la valeur par défaut, il n'y a pas d'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 que des valeurs leur seront attribuées en fonction de la version du code cible disponible pour le compilateur. Si un Assembly Foo
a une méthode Boo(int)
avec une valeur par défaut de 5 et si Assembly Bar
contient un appel à Foo.Boo()
, le compilateur le traitera comme un Foo.Boo(5)
. Si la valeur par défaut est définie sur 6 et que Assembly Foo
est recompilé, Bar
continuera d'appeler Foo.Boo(5)
à moins que ou jusqu'à ce qu'il soit recompilé avec cette nouvelle version de Foo
. Il faut donc éviter d’utiliser des paramètres optionnels pour les choses qui pourraient changer.
Un de mes aspects préférés des paramètres facultatifs est que vous voyez ce qu'il advient de vos paramètres si vous ne les fournissez pas, même sans aller à la définition de la méthode. Visual Studio vous montrera simplement la valeur par défaut pour le paramètre lorsque vous tapez le nom de la méthode. Avec une méthode de surcharge, vous devez soit lire la documentation (si elle est disponible), soit naviguer directement vers la définition de la méthode (si disponible) et vers la méthode encapsulée par la surcharge.
En particulier: l'effort de documentation peut augmenter rapidement avec le nombre de surcharges, et vous finirez probablement par copier les commentaires existants à partir des surcharges existantes. Ceci est assez énervant, car il ne produit aucune valeur et brise le principe DRY ). D'autre part, avec un paramètre facultatif, il y a un seul emplacement où tous les paramètres sont documentés et vous voyez leur signification ainsi que leurs valeurs default lors de la frappe.
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 donc de ne pas savoir à quelle super-méthode les surchargés enveloppent. Vous êtes donc 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.
On peut se demander si des arguments optionnels ou des surcharges devraient être utilisés ou non, mais surtout, chacun a son propre domaine où ils sont irremplaçables.
Les arguments facultatifs, lorsqu'ils sont utilisés en combinaison avec des arguments nommés, sont extrêmement utiles lorsqu'ils sont combinés à des appels longue liste-arguments-tout-optionnels 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 (un exemple parmi d'autres), et effectue des transtypages en interne, par exemple. vous venez de le nourrir avec n'importe quel type de données qui a du sens (qui est accepté par une surcharge existante). Ne peut pas battre cela avec des arguments optionnels.
J'attends avec impatience les paramètres facultatifs, car ils conservent 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 définissez la méthode une seule fois et vous pouvez voir les paramètres par défaut des paramètres facultatifs dans la signature de la 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 très simple, mais dans le cas de l'OP avec 5 surcharges, les choses peuvent devenir très encombrées très rapidement.
L'un des inconvénients des paramètres facultatifs est le contrôle de version, dans lequel un refactor a des conséquences inattendues. Un exemple:
Code initial
public string HandleError(string message, bool silent=true, bool isCritical=true)
{
...
}
Supposons qu'il s'agit d'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, supposons qu'après un refactor, nous trouvions que toutes les erreurs invitaient l'utilisateur de toute façon, de sorte que nous n'avions plus besoin de l'indicateur silencieux. Alors on l'enlève.
Après refactor
L’appel précédent est toujours compilé et disons qu’il passe sans changement dans le refactor:
public string HandleError(string message, /*bool silent=true,*/ bool isCritical=true)
{
...
}
...
// Some other distant code file:
HandleError("Disk is full", false);
Maintenant, false
aura un effet inattendu, l'événement ne sera plus traité comme étant 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 des options, telles que this ou this ).
Notez qu'il existe plusieurs formes de ce même problème. Une autre forme est décrite ici .
Notez également que l'utilisation stricte de paramètres nommés lors de l'appel de la méthode évitera le problème, comme celui-ci: 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 utilisée à grande échelle) à moins d’autres considérations convaincantes.
Les deux paramètres facultatifs, surcharge de méthode, ont leur propre avantage ou leur inconvénient.Il dépend de votre préférence pour choisir entre eux.
Paramètre facultatif: Disponible uniquement en .Net 4.0 . Paramètre facultatif réduire la taille de votre code . Vous ne pouvez pas définir les paramètres out et ref
méthodes surchargées: Vous pouvez définir les paramètres Out et ref . La taille du code augmentera mais les méthodes surchargées sont faciles à comprendre.
Pour ajouter une évidence quand utiliser une surcharge à la place des options:
Chaque fois que vous avez un certain nombre de paramètres qui ne font que du sens, n'introduisez pas de paramètres en option.
Ou plus généralement, chaque fois que vos signatures de méthodes permettent des schémas 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 si vous avez plusieurs paramètres du même type de données, d'ailleurs, ici, des périphériques tels que des méthodes d'usine ou des types de données personnalisés peuvent vous 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);
Dans de nombreux cas, des paramètres facultatifs sont utilisés pour commuter l'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 sert ici à 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 déclaration if-then-else. Dans ce cas, il est préférable de scinder 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 manière, nous avons même empêché la classe de recevoir un appel avec zéro remise. Cet appel signifierait que l'appelant pense qu'il y a un rabais, mais en réalité, il n'y a pas de rabais du tout. Un tel malentendu peut facilement causer un bogue.
Dans ce cas, je préfère ne pas avoir de paramètres facultatifs, mais forcer l'appelant à sélectionner explicitement le scénario d'exécution qui convient à sa situation actuelle.
La situation est très similaire à celle d'avoir des paramètres pouvant être nuls. C'est également une mauvaise idée lorsque la mise en œuvre se résume en des déclarations telles que if (x == null)
.
Vous pouvez trouver une analyse détaillée sur ces liens: Eviter les paramètres facultatifs et Eviter les paramètres nuls
Bien qu'ils soient (supposés?) Comme deux méthodes équivalentes sur le plan conceptuel pour modéliser votre API, ils présentent malheureusement une différence subtile lorsque vous devez envisager la compatibilité en amont de l'exécution avec vos anciens clients. Mon collègue (merci Brent!) M'a signalé ce message magnifique: Problèmes de gestion des versions avec arguments facultatifs . Quelques citations:
La raison pour laquelle des paramètres facultatifs ont été introduits dans C # 4 dans le fichier La première place était de soutenir COM Interop. C'est tout. Et maintenant, nous sommes en apprendre davantage sur toutes les implications de ce fait. Si tu as un méthode avec des paramètres facultatifs, vous ne pouvez jamais ajouter une surcharge avec paramètres optionnels supplémentaires par peur de provoquer une compilation briser le changement. Et vous ne pouvez jamais supprimer une surcharge existante, telle que cela a toujours été un changement radical à l'exécution. Vous avez à peu près besoin le traiter comme une interface. Votre seul recours dans ce cas est de écrire une nouvelle méthode avec un nouveau nom. Soyez donc conscient de cela si vous envisagez de utilisez des arguments optionnels dans vos API.