mars
2011
Pour ceux qui ne suivraient pas l’actualité C#, le langage s’est doté en avril 2010 de paramètres optionnels et de paramètres nommés avec la sortie du framework .NET 4.0 (pour plus de détail, je vous invite à lire le tour d’horizon des nouveautés de C# 4.0, Jérôme Lambert).
C’est intéressant car dans le même temps, il s’agissait de fonctionnalités évoquées lors des discussions sur le projet Coin de Java 7. Et si on remonte plus loin on peut voir que Java 6 prévoyait une API permettant l’accès aux noms des paramètres. Toutefois tout ceci a été abandonné et remis à plus tard…
Bref encore une fois .NET et Java ont pris une approche diamétralement opposé sur des fonctionnalités qui semblent basiques et qui sont déjà implémentés dans plusieurs autres langages…
On pourrait penser que le monde Java est encore à la traîne, or ce n’est jamais aussi facile…
Ce billet me trottait dans la tête depuis pas mal de temps. J’ai vu passer sur le web plusieurs billets vantant les mérites de ces fonctionnalités sans jamais pointer les problèmes que cela pourrait engendrer…
Mais commençons tout d’abord par présenter ces fonctionnalités !
Les paramètres nommés
Les paramètres nommés permettent de passer les paramètres d’une méthode selon leurs noms et non plus selon leurs ordres d’apparition dans la déclaration de la méthode.
Par exemple si on prend la méthode suivante :
public void SetPosition(int x, int y) {
// ...
}
Son utilisation se fait normalement en respectant l’ordre des paramètres, mais on peut désormais utiliser le nom des paramètres lors de l’appel de fonction.
Ainsi ces trois appels sont équivalent :
SetPosition(10, 20);
SetPosition(x:10, y:20);
SetPosition(y:20, x:10);
L’intérêt étant de rendre le code bien plus clair lors de la relecture, en particulier lorsqu’on utilise un grand nombre de paramètres de même type.
Les paramètres optionnels
Les paramètres optionnels permettent de définir des valeurs par défauts, ce qui permet donc de les rendre optionnels.
Par exemple :
public void SetBounds(int x=0, int y=0, int width=100, int height=100) {
// ...
}
On peut donc ignorer certains paramètres (ils prendront alors la valeur par défaut indiqué dans la signature de la méthode) :
SetBounds(5, 15, 200, 200);
SetBounds(5, 15, 200);
SetBounds(5, 15);
SetBounds(5);
SetBounds();
Le tout sans avoir à surcharger la méthode comme c’est généralement le cas en Java ou même en C# sans les paramètres optionnels.
Bien sûr on peut également cumuler les paramètres nommés aux paramètres optionnels, ce qui permet de définir seulement une partie des paramètres indépendamment de leurs ordres d’apparitions :
SetBounds(width:400, height:400);
SetBounds(y:100, width:50);
SetBounds(x:5, y:5);
// ...
Comment ça marche ?
Tout ceci correspond en fait à du sucre syntaxique.
C’est à dire que le compilateur se charge automatiquement de remplacer votre code utilisant les paramètres nommés/optionnels afin de le traduire en un code « standard » qui appellerait la méthode avec tous ses paramètres dans le bon ordre.
Ainsi :
- Dans le cas des paramètres nommés, le compilateur se charge de remettre les paramètres dans le bon ordre.
- Dans le cas des paramètres optionnels, le compilateur rajoute implicitement les paramètres en utilisant la valeur par défaut.
Après la compilation il n’y a aucune différence entre un appel de méthode via paramètres nommés/optionnels et un appel de méthode « standard ».
Par exemple voici en commentaire l’équivalent du code réellement généré par le compilateur dans les exemples précités de cet article :
SetPosition(10, 20);			// SetPosition(10, 20);
SetPosition(x:10, y:20);		// SetPosition(10, 20);
SetPosition(y:20, x:10);		// SetPosition(10, 20);
SetBounds(5, 15, 200, 200);		// SetBounds(5, 15, 200, 200);
SetBounds(5, 15, 200);			// SetBounds(5, 15, 200, 100);
SetBounds(5, 15);			// SetBounds(5, 15, 100, 100);
SetBounds(5);				// SetBounds(5, 0, 100, 100);
SetBounds();				// SetBounds(0, 0, 100, 100);
SetBounds(width:400, height:400);	// SetBounds(0, 0, 400, 400);
SetBounds(y:100, width:50);		// SetBounds(0, 100, 50, 100);
SetBounds(x:5, y:5);			// SetBounds(5, 5, 100, 100);
C’est sûr que cela semble apporter beaucoup de facilités aux développeurs et aux concepteurs d’API… Comme on peut le voir un peu partout sur le web sur les pages décrivant ces fonctionnalités.
Seulement tout ceci peut avoir un impact sur la compatibilité et l’évolution des APIs, ce qui est rarement indiqué dans les articles louant les mérites de ces nouvelles fonctionnalités.
Pourtant ces problèmes sont bien signalés sur Visual C# Developer Center (toutes les citations en anglais de cet article sont tirés de ce document).
Incompatibilités ?
Avant toute chose je rappelle que dans les langages managés comme C#/Java il existe deux niveaux de compatibilités :
- La compatibilité des sources
C’est à dire qu’un programme compilable avec une version antérieur reste compilable correctement avec une version plus récente de la plateforme.
Une incompatibilité à ce niveau signifie que l’on doit modifier notre code si on veut le recompiler avec la nouvelle API.
C’est problématique dans le sens où cela peut gêner la migration de l’application sur une nouvelle plateforme. - La compatibilité des binaires
C’est à dire qu’un programme compilé avec une version antérieur peut s’exécuter tel quel sans erreur et de la même manière sur une version plus récente de la plateforme, sans avoir à être recompilé.
Une incompatibilité à ce niveau signifie que l’on doit recompiler notre code pour l’exécuter avec la nouvelle API.
C’est problématique car cela implique que notre application, bien que correcte, ne pourra pas tourner sur tous les environnements !
On peut se permettre quelques impasses au niveau de la compatibilité des sources du moment que la compatibilité binaire reste possible. Cette dernière est déjà bien plus primordiale si on veut garantir son fonctionnement sur les plateformes futures (compatibilité ascendante).
Ces problèmes de compatibilité sont généralement pris en compte pour chaque version du framework/JDK, mais ils devraient également être pris en compte par les concepteurs d’APIs de librairies tierces.
Exemple concret
Imaginons que je souhaite développez une librairie tierce contenant, entre autres, une méthode permettant de récupérer le contenu d’un fichier CSV dans une liste à deux dimensions, le tout défini comme ceci :
public List<List<string>> ReadCsvFile(string file, string code="utf-8",
bool trim=false, bool ignore=false) {
...
}
Les paramètres de type string
définissant respectivement le nom du fichier et l’encodage du texte, tandis que les bool
indiquent que les champs doivent être nettoyés (suppression des blancs en début/fin) et si on doit ignorer la première ligne (qui correspond généralement à une entête). Enfin comme le langage me le permet, au lieu de surcharger la méthode j’ai utiliser les paramètres optionnels pour définir les valeurs par défaut.
Je ne suis pas développeur C# donc je m’excuse d’avance pour toute erreur. Je passe également outre le code de la méthode (l’intérêt n’est pas là).
Imaginons maintenant que ma librairie est publiée et utilisée par divers développeurs…
Le nom des paramètres devient critique
De mon coté je continue à faire évoluer cette librairie, par exemple en y ajoutant de nouvelles classes et méthodes.
J’en profite également pour améliorer le code existant. Je me dis que les noms des paramètres de ma méthode ne sont pas très bien choisis : ils ne sont pas très clair et peuvent prêter à confusion.
J’en profite donc pour améliorer cela dans la nouvelle version :
public List<List<string>> ReadCsvFile(string fileName, string charsetName="utf-8",
bool trimFields=false, bool ignoreFirstLine=false) {
...
}
Et là, c’est le drame !
Ce changement apparemment anodin vient de casser la compatibilité des sources de ma librairie !
En effet tous les développeurs qui utilisaient ma librairie devront modifier leurs codes-sources s’ils ont utilisés les paramètres nommés pour appeler ma méthode.
Par exemple, le code suivant qui compilait parfaitement avec la version précédente me génère désormais une erreur puisque le compilateur ne connait plus les paramètres « file
» et « trim
» :
List<string> list = ReadLines(file="text.txt", trim=true); // compile error
Heureusement ce n’est pas si grave puisque la compatibilité des binaires n’a pas été affectée (souvenez-vous que le compilateur génère en fait un appel de méthode « standard »). C’est à dire que les anciens codes devront être modifiés afin d’être recompilé, mais les versions binaires déjà compilés fonctionnement même si ils sont liés à ma nouvelle librairie.
C’est toutefois problématique car cela implique que chaque fois qu’on renomme un paramètre d’une méthode public en C#, cela cause potentiellement une incompatibilité des sources. Et c’est également une régression dans le sens où il est impossible de passer outre cela alors qu’il est possible de renommer une classe/méthode proprement en assurant une compatibilité sources/binaires (en la dupliquant et en utilisation le mécanisme de dépréciation Obsolete/Deprecated
).
Bref les noms de paramètre font désormais partie de l’interface publique des composants, et ne devraient pas être modifiés sous peine d’engendrer des incompatibilités des sources…
Of course, you don’t want to upset the developers who use your components either. For that reason, you must consider the names of your parameters as part of the public interface to your component. Changing the names of parameters will break client code at compile time.
Les paramètres ne peuvent plus évoluer
J’ai des retours d’utilisateur : le séparateur du format CSV n’est pas standardisé. Certains utilisent une virgule tandis que d’autres lui préfère le point-virgule. Je décide donc de faire une nouvelle version afin de gérer ce cas particulier.
C’est un simple choix de séparateur qui n’est pas bien difficile à implémenter, et en toute logique je me contente d’ajouter un paramètre optionnel à ma méthode :
public List<string> ReadCsvFile(string fileName, string charsetName="utf-8",
bool trimFields=false, bool ignoreFirstLine=false, string separator=",") {
...
}
Prudent après les problèmes rencontrés par le changement des noms de paramètres, j’effectue quelques tests en recompilant d’anciens codes sans problème !
Ouf ! Apparemment cela n’a causé aucune incompatibilité des sources !
Et là, c’est le drame !
Nouveau problème : je viens de casser la compatibilité binaire de ma librairie !
En effet souvenez vous que le remplacement des paramètres optionnels est géré par le compilateur qui se charge de renseigner les valeurs absentes.
Ainsi les codes compilés avec une version précédente de ma librairie généraient un appel de la méthode ReadCsvFile(string,string,bool,bool)
qui n’existe désormais plus ! Cela aboutira invariablement à une exception lors de l’exécution.
Il faut impérativement recompiler le code source afin qu’il génère un appel vers la nouvelle méthode ReadCsvFile(string,string,bool,bool,<span style="color:red">string</span>)
…
Therefore, adding parameters, even if they are optional parameters, is a breaking change at runtime. If they have default values, it’s not a breaking change at compile time.
Et devinez quel est la solution ?
once you start creating future releases, you must create overloads for additional parameters
Il faut revenir à la surcharge afin d’avoir deux signatures de méthodes pour traiter correctement les anciens appels de méthode, par exemple dans notre cas il faudrait faire ceci :
// Une méthode avec 4 paramètres pour la compatibilité binaire des anciennes applications :
[System.Obsolete("for compatibility with previous release")]
public List<string> ReadCsvFile(string fileName, string charsetName,
bool trimFields, bool ignoreFirstLine) {
return ReadCsvFile(fileName, charsetName, trimFields, ignoreFirstLine, ",");
}
// Notre méthode modifiée avec un paramètre supplémentaire :
public List<string> ReadCsvFile(string fileName, string charsetName="utf-8",
bool trimFields=false, bool ignoreFirstLine=false, string separator=",") {
...
}
Je trouve cela très troublant. Je ne m’attends pas du tout à un tel comportement en ajoutant un paramètre optionnel !
Et je ne pense pas être le seul dans ce cas…
Et pourtant on était prévenu…
L’article du Visual C# Developer Center se conclue sur ces quelques recommandations :
Now, after that explanation, the guidance should be clearer. For your initial release, use optional and named parameters to create whatever combination of overloads your users may want to use. However, once you start creating future releases, you must create overloads for additional parameters. That way, existing client applications will still function. Furthermore, in any future release, avoid changing parameter names. They are now part of your public interface.
En clair :
- Vous pouvez utiliser les paramètres optionnels et nommés pour votre toute première version.
- Mais pour toutes les versions suivantes, vous devez surcharger les méthodes, et vous ne devriez plus changer le nom des paramètres.
Personnellement je conseillerais plutôt l’inverse : N’utilisez pas les paramètres optionnels et nommés ! Je pense vraiment que cette implémentation des paramètres nommés et optionnels est dangereuse du fait de ses effets de bord qui ne sont pas forcément évident de prime abord.
Bien sûr cela concerne principalement les développeurs de librairies.
Dans le code interne à un projet ces fonctionnalités peuvent être utile et ne poseront pas de problème de compatibilité (l’EDI pouvant refactoriser le code qui sera recompiler de toute manière).
Mais lorsqu’on développe une librairie, il faut bien prendre en compte les limitations de ces fonctionnalités.
On privilégiera la surcharge de méthode standard lorsque le nombre de paramètre reste réduit, mais au delà de 4-5 paramètres il est préférable de revoir sa conception (par exemple on pourrait envisager l’utilisation d’une classe « option » créée via le pattern « Builder »).
4 Commentaires + Ajouter un commentaire
Tutoriels
Discussions
- [REFLEXION] Connaitre toutes les classes qui implémentent une interface
- L'apparition du mot-clé const est-il prévu dans une version à venir du JDK?
- jre 1.5, tomcat 6.0 et multi processeurs
- Difference de performances Unix/Windows d'un programme?
- Classes, méthodes private
- Définition exacte de @Override
- Recuperation du nom des parametres
- [ fuite ] memoire
- Possibilité d'accéder au type générique en runtime
Si on change une valeur par défaut, il faudra recompiler le code appelant pour que ce soit pris en compte, puisque c’est le compilateur qui rajoute cette valeur dans la liste des paramètres de l’appel de la méthode.
En clair les codes compilés avec une version précédente continueront à utiliser l’ancienne valeur par défaut.
(on rencontre le même style de problème en Java si on modifie la valeur d’une constante)
a++
Clair et précis… merci pour ce billet !
La compatibilité statique/dynamique est effectivement un gros bazard pour le consommateur de services nommés (qu’il soit développeur ou administrateur)…
J’imagine aussi que le simple changement de la valeur par défaut a les mêmes conséquences puisque la signature doit être modifiée…
Oui mais c’est d’autant plus troublant de voir dans une API qu’une méthode à paramètres optionnels soit surchargée par une autre méthode avec quelques paramètres en moins… Cela va à l’encontre du « principe de moindre surprise ».
Il aurait été préférable que l’ajout d’un paramètre optionnel ne génère pas d’incompatibilité (mais dans ce cas là cela implique une modification du code généré et on s’éloigne du simple sucre syntaxique).
Sinon dans le cas de surcharge je suppose que cela doit générer une erreur d’ambiguïté. Mais là le problème vient vraiment du développeur qui fait n’importe quoi
(et on peut déjà obtenir cela avec une surcharge incorrect – mais bon comme dans cet exemple généralement il faut presque le faire exprès)
a++
Si l’usage permet de limiter la surcharge de méthode, je ne pense pas que cela l’annule (comme le prouve les recommandations pour les versions ultérieures)
Du coup, je me pose une question.
Supposons une méthode surchargée :
maMethod(string a, string b, string c= »aucun »)
maMethod(string a, string b, int c=0)
Si je peux écrire maMethod(a= »s1″, b= »s2″)
Que fait le compilo ? à moins que ce ne soit interdit ?