Je voudrais rassembler autant d'informations que possible concernant le contrôle de version d'API dans .NET / CLR, et en particulier comment les changements d'API font ou ne cassent pas les applications clientes. Définissons d'abord quelques termes:
Modification de l'API - un changement dans la définition publiquement visible d'un type, y compris l'un de ses membres publics. Cela inclut la modification du type et des noms de membre, la modification du type de base d'un type, l'ajout / la suppression d'interfaces de la liste des interfaces implémentées d'un type, l'ajout / la suppression de membres (y compris les surcharges), la modification de la visibilité des membres, la modification du nom de la méthode et des paramètres de type, l'ajout de valeurs par défaut pour les paramètres de méthode, ajouter / supprimer des attributs sur les types et les membres, et ajouter / supprimer des paramètres de type génériques sur les types et les membres (ai-je oublié quelque chose?). Cela n'inclut aucun changement dans les comités membres, ni aucun changement dans les membres privés (c'est-à-dire que nous ne prenons pas en compte la réflexion).
Interruption au niveau binaire - une modification de l'API qui se traduit par des assemblys clients compilés avec une ancienne version de l'API ne pouvant pas se charger avec la nouvelle version. Exemple: modification de la signature d'une méthode, même si elle permet d'être appelée de la même manière que précédemment (ie: void pour renvoyer des surcharges de valeurs par défaut de type / paramètre).
Saut au niveau de la source - un changement d'API qui se traduit par un code existant écrit pour être compilé avec une ancienne version de l'API qui ne peut pas être compilée avec la nouvelle version. Cependant, les assemblys clients déjà compilés fonctionnent comme auparavant. Exemple: ajouter une nouvelle surcharge qui peut entraîner une ambiguïté dans les appels de méthode qui étaient sans ambiguïté auparavant.
Modification de la sémantique silencieuse au niveau de la source - une modification de l'API qui se traduit par un code existant écrit pour être compilé avec une version plus ancienne de l'API, modifie discrètement sa sémantique, par exemple en appelant une méthode différente. Le code doit cependant continuer à compiler sans avertissements / erreurs, et les assemblys précédemment compilés devraient fonctionner comme avant. Exemple: implémentation d'une nouvelle interface sur une classe existante qui se traduit par une surcharge différente choisie lors de la résolution de surcharge.
Le but ultime est de cataloguer autant de changements d'API de sémantique de rupture et de silence que possible, et de décrire l'effet exact de la rupture, et les langues qui sont et ne sont pas affectées par celui-ci. Pour développer cette dernière: alors que certains changements affectent universellement toutes les langues (par exemple, l'ajout d'un nouveau membre à une interface interrompra les implémentations de cette interface dans n'importe quelle langue), certains nécessitent une sémantique de langage très spécifique pour entrer en jeu pour faire une pause. Cela implique généralement une surcharge de méthode et, en général, tout ce qui a à voir avec les conversions de types implicites. Il ne semble pas y avoir de moyen de définir le "dénominateur le moins commun" ici même pour les langages conformes CLS (c'est-à-dire ceux qui se conforment au moins aux règles du "consommateur CLS" telles que définies dans les spécifications CLI) - bien que je ' J'apprécierai si quelqu'un me corrige comme se trompant ici - donc cela devra aller langue par langue. Ceux qui sont les plus intéressants sont naturellement ceux fournis avec .NET dès la sortie de la boîte: C #, VB et F #; mais d'autres, comme IronPython, IronRuby, Delphi Prism, etc. sont également pertinents. Plus il s'agit d'un cas d'angle, plus il sera intéressant - des choses comme la suppression de membres sont assez évidentes, mais des interactions subtiles entre, par exemple, la surcharge de méthode, les paramètres facultatifs / par défaut, l'inférence de type lambda et les opérateurs de conversion peuvent être très surprenantes a l'heure.
Quelques exemples pour démarrer ceci:
Ajout de nouvelles surcharges de méthode
Type: coupure au niveau de la source
Langues affectées: C #, VB, F #
API avant changement:
public class Foo
{
public void Bar(IEnumerable x);
}
API après modification:
public class Foo
{
public void Bar(IEnumerable x);
public void Bar(ICloneable x);
}
Exemple de code client fonctionnant avant le changement et cassé après:
new Foo().Bar(new int[0]);
Ajout de nouvelles surcharges d'opérateurs de conversion implicites
Type: coupure au niveau de la source.
Langues affectées: C #, VB
Langues non concernées: F #
API avant changement:
public class Foo
{
public static implicit operator int ();
}
API après modification:
public class Foo
{
public static implicit operator int ();
public static implicit operator float ();
}
Exemple de code client fonctionnant avant le changement et cassé après:
void Bar(int x);
void Bar(float x);
Bar(new Foo());
Remarques: F # n'est pas rompu, car il ne prend pas en charge le niveau de langue pour les opérateurs surchargés, ni explicites ni implicites - les deux doivent être appelés directement en tant que op_Explicit
et op_Implicit
méthodes.
Ajout de nouvelles méthodes d'instance
Type: modification de la sémantique silencieuse au niveau de la source.
Langues affectées: C #, VB
Langues non concernées: F #
API avant changement:
public class Foo
{
}
API après modification:
public class Foo
{
public void Bar();
}
Exemple de code client qui subit une modification sémantique discrète:
public static class FooExtensions
{
public void Bar(this Foo foo);
}
new Foo().Bar();
Remarques: F # n'est pas rompu, car il ne prend pas en charge le niveau de langue ExtensionMethodAttribute
et nécessite que les méthodes d'extension CLS soient appelées comme méthodes statiques.
la source
Réponses:
Modification d'une signature de méthode
Type: Pause au niveau binaire
Langues affectées: C # (VB et F # le plus probable, mais non testé)
API avant changement
API après modification
Exemple de code client fonctionnant avant le changement
la source
bar
.Ajout d'un paramètre avec une valeur par défaut.
Type de pause: pause au niveau binaire
Même si le code source appelant n'a pas besoin de changer, il doit quand même être recompilé (comme lors de l'ajout d'un paramètre normal).
En effet, C # compile les valeurs par défaut des paramètres directement dans l'assembly appelant. Cela signifie que si vous ne recompilez pas, vous obtiendrez une MissingMethodException car l'ancien assembly essaie d'appeler une méthode avec moins d'arguments.
API avant modification
API après modification
Exemple de code client qui est rompu par la suite
Le code client doit être recompilé au
Foo(5, null)
niveau du bytecode. L'assembly appelé contient uniquementFoo(int, string)
, pasFoo(int)
. En effet, les valeurs de paramètre par défaut sont purement une fonction de langue, le runtime .Net n'en sait rien. (Cela explique également pourquoi les valeurs par défaut doivent être des constantes de compilation en C #).la source
Func<int> f = Foo;
Celui-ci était très peu évident lorsque je l'ai découvert, surtout à la lumière de la différence avec la même situation pour les interfaces. Ce n'est pas une pause du tout, mais c'est assez surprenant que j'ai décidé de l'inclure:
Refactorisation des membres de la classe dans une classe de base
Genre: pas une pause!
Langues affectées: aucune (c.-à-d. Aucune n'est cassée)
API avant changement:
API après modification:
Exemple de code qui continue de fonctionner tout au long du changement (même si je m'attendais à ce qu'il se casse):
Remarques:
C ++ / CLI est le seul langage .NET qui a une construction analogue à l'implémentation d'interface explicite pour les membres de la classe de base virtuelle - "dérogation explicite". Je m'attendais à ce que cela entraîne le même type de rupture que lors du déplacement des membres de l'interface vers une interface de base (car l'IL généré pour la substitution explicite est le même que pour l'implémentation explicite). À ma grande surprise, ce n'est pas le cas - même si l'IL généré spécifie toujours que les
BarOverride
remplacementsFoo::Bar
plutôt queFooBase::Bar
, le chargeur d'assemblage est suffisamment intelligent pour se substituer correctement les uns aux autres sans aucune plainte - apparemment, le fait que ceFoo
soit une classe fait la différence. Allez comprendre...la source
Celui-ci est un cas spécial peut-être pas si évident "d'ajout / suppression de membres d'interface", et j'ai pensé qu'il mérite sa propre entrée à la lumière d'un autre cas que je vais poster ensuite. Alors:
Refactorisation des membres de l'interface dans une interface de base
Type: coupures aux niveaux source et binaire
Langues affectées: C #, VB, C ++ / CLI, F # (pour le saut de source; le binaire affecte naturellement n'importe quel langage)
API avant changement:
API après modification:
Exemple de code client rompu par une modification au niveau source:
Exemple de code client rompu par une modification au niveau binaire;
Remarques:
Pour la coupure au niveau source, le problème est que C #, VB et C ++ / CLI nécessitent tous un nom d'interface exact dans la déclaration d'implémentation du membre d'interface; ainsi, si le membre est déplacé vers une interface de base, le code ne sera plus compilé.
La rupture binaire est due au fait que les méthodes d'interface sont entièrement qualifiées en IL généré pour les implémentations explicites, et le nom de l'interface doit également être exact.
L'implémentation implicite là où elle est disponible (c'est-à-dire C # et C ++ / CLI, mais pas VB) fonctionnera bien au niveau source et binaire. Les appels de méthode ne se cassent pas non plus.
la source
Implements IFoo.Bar
ce que la référence sera transparenteIFooBase.Bar
?Réorganisation des valeurs énumérées
Type de rupture: changement de la sémantique silencieuse au niveau source / niveau binaire
Langues concernées: toutes
La réorganisation des valeurs énumérées conservera la compatibilité au niveau source car les littéraux ont le même nom, mais leurs indices ordinaux seront mis à jour, ce qui peut provoquer certains types de ruptures silencieuses au niveau source.
Pire encore, les sauts au niveau binaire silencieux peuvent être introduits si le code client n'est pas recompilé avec la nouvelle version de l'API. Les valeurs d'énumération sont des constantes au moment de la compilation et, à ce titre, toute utilisation de celles-ci est intégrée dans l'IL de l'assembly client. Ce cas peut parfois être particulièrement difficile à repérer.
API avant modification
API après modification
Exemple de code client qui fonctionne mais est cassé par la suite:
la source
Celui-ci est vraiment très rare dans la pratique, mais néanmoins surprenant quand il se produit.
Ajout de nouveaux membres non surchargés
Type: coupure au niveau source ou changement sémantique discret.
Langues affectées: C #, VB
Langues non affectées: F #, C ++ / CLI
API avant changement:
API après modification:
Exemple de code client rompu par une modification:
Remarques:
Le problème ici est causé par l'inférence de type lambda en C # et VB en présence d'une résolution de surcharge. Une forme limitée de typage canard est utilisée ici pour rompre les liens où plus d'un type correspond, en vérifiant si le corps du lambda a du sens pour un type donné - si un seul type donne un corps compilable, celui-ci est choisi.
Le danger ici est que le code client peut avoir un groupe de méthodes surchargé où certaines méthodes prennent des arguments de ses propres types, et d'autres prennent des arguments de types exposés par votre bibliothèque. Si l'un de ses codes s'appuie ensuite sur l'algorithme d'inférence de type pour déterminer la méthode correcte basée uniquement sur la présence ou l'absence de membres, l'ajout d'un nouveau membre à l'un de vos types avec le même nom que dans l'un des types du client peut potentiellement générer une inférence désactivé, ce qui entraîne une ambiguïté lors de la résolution de la surcharge.
Notez que les types
Foo
etBar
dans cet exemple ne sont liés d'aucune façon, ni par héritage ni autrement. Leur simple utilisation dans un seul groupe de méthodes suffit pour déclencher cela, et si cela se produit dans le code client, vous n'avez aucun contrôle sur celui-ci.L'exemple de code ci-dessus illustre une situation plus simple où il s'agit d'une interruption au niveau de la source (c'est-à-dire des résultats d'erreur du compilateur). Cependant, cela peut également être un changement sémantique silencieux, si la surcharge qui a été choisie via l'inférence avait d'autres arguments qui autrement entraîneraient son classement ci-dessous (par exemple, des arguments facultatifs avec des valeurs par défaut, ou une incompatibilité de type entre l'argument déclaré et réel nécessitant un implicite conversion). Dans un tel scénario, la résolution de surcharge n'échouera plus, mais une surcharge différente sera discrètement sélectionnée par le compilateur. En pratique, cependant, il est très difficile de se lancer dans ce cas sans construire soigneusement des signatures de méthode pour le provoquer délibérément.
la source
Convertissez une implémentation d'interface implicite en implémentation explicite.
Type de rupture: source et binaire
Langues concernées: toutes
C'est vraiment juste une variante de la modification de l'accessibilité d'une méthode - c'est juste un peu plus subtil car il est facile d'ignorer le fait que tous les accès aux méthodes d'une interface ne sont pas nécessairement par une référence au type de l'interface.
API avant modification:
API après modification:
Exemple de code client qui fonctionne avant le changement et qui est rompu après:
la source
Convertissez une implémentation d'interface explicite en implémentation implicite.
Type de pause: Source
Langues concernées: toutes
La refactorisation d'une implémentation d'interface explicite en implémentation implicite est plus subtile dans la façon dont elle peut casser une API. En surface, il semblerait que cela devrait être relativement sûr, cependant, lorsqu'il est combiné avec l'héritage, il peut causer des problèmes.
API avant modification:
API après modification:
Exemple de code client qui fonctionne avant le changement et qui est rompu après:
la source
Foo
n'avait pas de méthode publique nomméeGetEnumerator
, et vous appelez la méthode via une référence de typeFoo
.. .yield return "Bar"
:) mais oui, je vois où cela va maintenant -foreach
appelle toujours la méthode publique nomméeGetEnumerator
, même si ce n'est pas la vraie implémentation deIEnumerable.GetEnumerator
. Cela semble avoir un angle de plus: même si vous n'avez qu'une seule classe, et qu'elle implémenteIEnumerable
explicitement, cela signifie que c'est un changement de rupture de source pour ajouter une méthode publique qui lui est nomméeGetEnumerator
, car maintenantforeach
va utiliser cette méthode sur l'implémentation de l'interface. En outre, le même problème s'applique à laIEnumerator
mise en œuvre ...Changer un champ en propriété
Type de rupture: API
Langues affectées: Visual Basic et C # *
Info: Lorsque vous changez un champ ou une variable normale en une propriété dans Visual Basic, tout code extérieur référençant ce membre de quelque manière que ce soit devra être recompilé.
API avant modification:
API après modification:
Exemple de code client qui fonctionne mais qui est rompu par la suite:
la source
out
et lesref
arguments des méthodes, contrairement aux champs, et ne peuvent pas être la cible de l'&
opérateur unaire .Ajout d'espace de noms
Saut au niveau de la source / changement sémantique silencieux au niveau de la source
En raison de la façon dont la résolution d'espace de noms fonctionne dans vb.Net, l'ajout d'un espace de noms à une bibliothèque peut empêcher le code Visual Basic compilé avec une version précédente de l'API de compiler avec une nouvelle version.
Exemple de code client:
Si une nouvelle version de l'API ajoute l'espace de noms
Api.SomeNamespace.Data
, le code ci-dessus ne sera pas compilé.Cela devient plus compliqué avec les importations d'espace de noms au niveau du projet. Si
Imports System
est omis du code ci-dessus, mais que l'System
espace de noms est importé au niveau du projet, le code peut toujours entraîner une erreur.Cependant, si l'API inclut une classe
DataRow
dans sonApi.SomeNamespace.Data
espace de noms, le code sera compilé maisdr
sera une instance de laSystem.Data.DataRow
compilation avec l'ancienne version de l'API et de laApi.SomeNamespace.Data.DataRow
compilation avec la nouvelle version de l'API.Renommage d'argument
Rupture au niveau de la source
La modification des noms d'arguments est un changement de rupture dans vb.net depuis la version 7 (?) (.Net version 1?) Et c # .net depuis la version 4 (.Net version 4).
API avant changement:
API après modification:
Exemple de code client:
Paramètres de référence
Rupture au niveau de la source
L'ajout d'un remplacement de méthode avec la même signature, sauf qu'un paramètre est passé par référence au lieu de par valeur, entraînera l'incapacité de la source vb qui référence l'API à résoudre la fonction. Visual Basic n'a aucun moyen (?) De différencier ces méthodes au point d'appel, sauf si elles ont des noms d'arguments différents, donc une telle modification pourrait rendre les deux membres inutilisables à partir du code vb.
API avant changement:
API après modification:
Exemple de code client:
Changement de champ en propriété
Saut au niveau binaire / Saut au niveau source
Outre la coupure évidente au niveau binaire, cela peut provoquer une coupure au niveau source si le membre est passé à une méthode par référence.
API avant changement:
API après modification:
Exemple de code client:
la source
Changement d'API:
Pause au niveau binaire:
Ajout d'un nouveau membre (protégé contre les événements) qui utilise un type d'un autre assembly (Class2) comme contrainte d'argument de modèle.
Modification d'une classe enfant (Class3) pour dériver d'un type dans un autre assembly lorsque la classe est utilisée comme argument de modèle pour cette classe.
Changement de la sémantique silencieuse au niveau de la source:
(je ne sais pas où ils correspondent)
Modifications du déploiement:
Modifications d'amorçage / de configuration:
Mettre à jour:
Désolé, je ne savais pas que la seule raison pour laquelle cela ne fonctionnait pas était que je les utilisais dans les contraintes de modèle.
la source
TypeForwardedToAttribute
est utilisé.-Werror
sur votre système de build que vous expédiez avec des tarballs de version. Cet indicateur est le plus utile pour le développeur du code et le plus souvent inutile pour le consommateur.Ajout de méthodes de surcharge pour supprimer l'utilisation des paramètres par défaut
Type de rupture: changement sémantique silencieux au niveau de la source
Étant donné que le compilateur transforme les appels de méthode avec des valeurs de paramètre par défaut manquantes en un appel explicite avec la valeur par défaut du côté appelant, la compatibilité du code compilé existant est donnée; une méthode avec la signature correcte sera trouvée pour tout le code précédemment compilé.
De l'autre côté, les appels sans utilisation de paramètres facultatifs sont désormais compilés en tant qu'appel à la nouvelle méthode qui manque le paramètre facultatif. Tout fonctionne toujours correctement, mais si le code appelé réside dans un autre assembly, le nouveau code compilé l'appelant dépend désormais de la nouvelle version de cet assembly. Le déploiement d'assemblys appelant le code refactorisé sans déployer également l'assembly dans lequel réside le code refactored entraîne des exceptions de «méthode introuvable».
API avant changement
API après modification
Exemple de code qui fonctionnera toujours
Exemple de code qui dépend maintenant de la nouvelle version lors de la compilation
la source
Renommer une interface
Sorte de rupture: source et binaire
Langues affectées: Très probablement toutes, testées en C #.
API avant modification:
API après modification:
Exemple de code client qui fonctionne mais est cassé par la suite:
la source
Méthode de surcharge avec un paramètre de type nullable
Type: interruption au niveau de la source
Langues affectées: C #, VB
API avant un changement:
API après le changement:
Exemple de code client fonctionnant avant la modification et cassé après:
Exception: l'appel est ambigu entre les méthodes ou propriétés suivantes.
la source
Promotion vers une méthode d'extension
Type: coupure au niveau de la source
Langues affectées: C # v6 et supérieur (peut-être d'autres?)
API avant changement:
API après modification:
Exemple de code client fonctionnant avant le changement et cassé après:
Plus d'informations: https://github.com/dotnet/csharplang/issues/665
la source