Un guide définitif des changements révolutionnaires de l'API dans .NET

227

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_Explicitet op_Implicitmé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 ExtensionMethodAttributeet nécessite que les méthodes d'extension CLS soient appelées comme méthodes statiques.

Pavel Minaev
la source
Certes, Microsoft couvre déjà cela ... msdn.microsoft.com/en-us/netframework/aa570326.aspx
Robert Harvey
1
@Robert: votre lien concerne quelque chose de très différent - il décrit des changements de rupture spécifiques dans .NET Framework lui-même. Il s'agit d'une question plus large qui décrit les modèles génériques qui peuvent introduire des changements de rupture dans vos propres API (en tant qu'auteur de bibliothèque / framework). Je ne suis pas au courant d'un tel document de MS qui serait complet, bien que tous les liens vers ceux-ci, même incomplets, soient certainement les bienvenus.
Pavel Minaev,
Dans l'une de ces catégories de «coupure», existe-t-il des cas dans lesquels le problème n'apparaîtra qu'à l'exécution?
Rohit
1
Oui, catégorie "pause binaire". Dans ce cas, vous disposez déjà d'un assembly tiers compilé avec toutes les versions de votre assembly. Si vous déposez une nouvelle version de votre assembly sur place, l'assembly tiers cesse de fonctionner - soit il ne se charge tout simplement pas au moment de l'exécution, soit il fonctionne de manière incorrecte.
Pavel Minaev,
3
J'ajouterais ceux dans le post et les commentaires blogs.msdn.com/b/ericlippert/archive/2012/01/09/…
Lukasz Madon

Réponses:

42

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

public static class Foo
{
    public static void bar(int i);
}

API après modification

public static class Foo
{
    public static bool bar(int i);
}

Exemple de code client fonctionnant avant le changement

Foo.bar(13);
Justin Drury
la source
15
En fait, cela peut aussi être une rupture au niveau de la source, si quelqu'un essaie de créer un délégué pour bar.
Pavel Minaev, le
C'est vrai aussi. J'ai trouvé ce problème particulier lorsque j'ai apporté des modifications aux utilitaires d'impression dans l'application de mon entreprise. Lorsque la mise à jour a été publiée, toutes les DLL faisant référence à ces utilitaires n'ont pas été recompilées et publiées, ce qui lève une exception de méthode introuvable.
Justin Drury le
1
Cela revient au fait que les types de retour ne comptent pas pour la signature de la méthode. Vous ne pouvez pas surcharger deux fonctions basées uniquement sur le type de retour. Même problème.
Jason Short
1
sous-question à cette réponse: est-ce que quelqu'un connaît l'implication de l'ajout d'une valeur par défaut dotnet4 'public static void bar (int i = 0);' ou changer cette valeur par défaut d'une valeur à une autre?
k3b
1
Pour ceux qui vont atterrir sur cette page, je pense que pour C # (et "je pense" la plupart des autres langages OOP), les types de retour ne contribuent pas à la signature de la méthode. Oui, la réponse est juste que les changements de signature contribuent au changement de niveau binaire. MAIS l'exemple ne semble pas correct à mon humble avis le bon exemple que je peux penser est de AVANT la somme décimale publique (int a, int b) Après la somme décimale publique (décimal a, décimal b) Veuillez vous référer à ce lien MSDN 3.6 Signatures et surcharge
Bhanu Chhabra
40

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

public void Foo(int a) { }

API après modification

public void Foo(int a, string b = null) { }

Exemple de code client qui est rompu par la suite

Foo(5);

Le code client doit être recompilé au Foo(5, null)niveau du bytecode. L'assembly appelé contient uniquement Foo(int, string), pas Foo(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 #).

Eldritch Conundrum
la source
2
c'est un changement Func<int> f = Foo;
décisif
26

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:

class Foo
{
    public virtual void Bar() {}
    public virtual void Baz() {}
}

API après modification:

class FooBase
{
    public virtual void Bar() {}
}

class Foo : FooBase
{
    public virtual void Baz() {}
}

Exemple de code qui continue de fonctionner tout au long du changement (même si je m'attendais à ce qu'il se casse):

// C++/CLI
ref class Derived : Foo
{
   public virtual void Baz() {{

   // Explicit override    
   public virtual void BarOverride() = Foo::Bar {}
};

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 BarOverrideremplacements Foo::Barplutôt que FooBase::Bar, le chargeur d'assemblage est suffisamment intelligent pour se substituer correctement les uns aux autres sans aucune plainte - apparemment, le fait que ce Foosoit une classe fait la différence. Allez comprendre...

Pavel Minaev
la source
3
Tant que la classe de base est dans le même assembly. Sinon, c'est un changement de rupture binaire.
Jeremy
@Jeremy quel genre de code casse dans ce cas? Est-ce que l'utilisation de Baz () par un appelant externe sera interrompue ou s'agit-il uniquement d'un problème avec les personnes qui tentent d'étendre Foo et de remplacer Baz ()?
ChaseMedallion
@ChaseMedallion ça casse si vous êtes un utilisateur d'occasion. Par exemple, la DLL compilée fait référence à une ancienne version de Foo et vous référencez cette DLL compilée, mais vous utilisez également une version plus récente de la DLL Foo. Cela rompt avec une étrange erreur, ou du moins c'est le cas pour moi dans les bibliothèques que j'ai développées auparavant.
Jeremy
19

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:

interface IFoo
{
    void Bar();
    void Baz();
}

API après modification:

interface IFooBase 
{
    void Bar();
}

interface IFoo : IFooBase
{
    void Baz();
}

Exemple de code client rompu par une modification au niveau source:

class Foo : IFoo
{
   void IFoo.Bar() { ... }
   void IFoo.Baz() { ... }
}

Exemple de code client rompu par une modification au niveau binaire;

(new Foo()).Bar();

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.

Pavel Minaev
la source
Ce n'est pas vrai pour toutes les langues. Pour VB, ce n'est pas un changement de code source de rupture. Pour C # c'est le cas.
Jeremy
Est- Implements IFoo.Barce que la référence sera transparente IFooBase.Bar?
Pavel Minaev
Oui, en fait, vous pouvez référencer un membre directement ou indirectement via l'interface héritée lorsque vous l'implémentez. Cependant, c'est toujours un changement binaire de rupture.
Jeremy
15

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

public enum Foo
{
   Bar,
   Baz
}

API après modification

public enum Foo
{
   Baz,
   Bar
}

Exemple de code client qui fonctionne mais est cassé par la suite:

Foo.Bar < Foo.Baz
glopes
la source
12

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:

public class Foo
{
}

API après modification:

public class Foo
{
    public void Frob() {}
}

Exemple de code client rompu par une modification:

class Bar
{
    public void Frob() {}
}

class Program
{
    static void Qux(Action<Foo> a)
    {
    }

    static void Qux(Action<Bar> a)
    {
    }

    static void Main()
    {
        Qux(x => x.Frob());        
    }
}

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 Fooet Bardans 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.

Pavel Minaev
la source
9

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:

public class Foo : IEnumerable
{
    public IEnumerator GetEnumerator();
}

API après modification:

public class Foo : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator();
}

Exemple de code client qui fonctionne avant le changement et qui est rompu après:

new Foo().GetEnumerator(); // fails because GetEnumerator() is no longer public
LBushkin
la source
7

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:

public class Foo : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() { yield return "Foo"; }
}

API après modification:

public class Foo : IEnumerable
{
    public IEnumerator GetEnumerator() { yield return "Foo"; }
}

Exemple de code client qui fonctionne avant le changement et qui est rompu après:

class Bar : Foo, IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() // silently hides base instance
    { yield return "Bar"; }
}

foreach( var x in new Bar() )
    Console.WriteLine(x);    // originally output "Bar", now outputs "Foo"
LBushkin
la source
Désolé, je ne suis pas tout à fait à jour - sûrement l'exemple de code avant le changement d'API ne se compilerait pas du tout, car avant le changement Foon'avait pas de méthode publique nommée GetEnumerator, et vous appelez la méthode via une référence de type Foo.. .
Pavel Minaev
En effet, j'ai essayé de simplifier un exemple de mémoire et ça a fini par «foobar» (pardonnez le jeu de mots). J'ai mis à jour l'exemple pour démontrer correctement le cas (et être compilable).
LBushkin
Dans mon exemple, le problème est causé par plus que la simple transition d'une méthode d'interface d'implicite à publique. Cela dépend de la façon dont le compilateur C # détermine la méthode à appeler dans une boucle foreach. Étant donné les règles de résolution du compilateur, il passe de la version de la classe dérivée à la version de la classe de base.
LBushkin
Vous avez oublié yield return "Bar":) mais oui, je vois où cela va maintenant - foreachappelle toujours la méthode publique nommée GetEnumerator, même si ce n'est pas la vraie implémentation de IEnumerable.GetEnumerator. Cela semble avoir un angle de plus: même si vous n'avez qu'une seule classe, et qu'elle implémente IEnumerableexplicitement, cela signifie que c'est un changement de rupture de source pour ajouter une méthode publique qui lui est nommée GetEnumerator, car maintenant foreachva utiliser cette méthode sur l'implémentation de l'interface. En outre, le même problème s'applique à la IEnumeratormise en œuvre ...
Pavel Minaev
6

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:

Public Class Foo    
    Public Shared Bar As String = ""    
End Class

API après modification:

Public Class Foo
    Private Shared _Bar As String = ""
    Public Shared Property Bar As String
        Get
            Return _Bar
        End Get
        Set(value As String)
            _Bar = value
        End Set
    End Property
End Class    

Exemple de code client qui fonctionne mais qui est rompu par la suite:

Foo.Bar = "foobar"
Hagelt18
la source
2
Cela casserait également les choses en C #, car les propriétés ne peuvent pas être utilisées pour outet les refarguments des méthodes, contrairement aux champs, et ne peuvent pas être la cible de l' &opérateur unaire .
Pavel Minaev,
5

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:

Imports System
Imports Api.SomeNamespace

Public Class Foo
    Public Sub Bar()
        Dim dr As Data.DataRow
    End Sub
End Class

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 Systemest omis du code ci-dessus, mais que l' Systemespace de noms est importé au niveau du projet, le code peut toujours entraîner une erreur.

Cependant, si l'API inclut une classe DataRowdans son Api.SomeNamespace.Dataespace de noms, le code sera compilé mais drsera une instance de la System.Data.DataRowcompilation avec l'ancienne version de l'API et de la Api.SomeNamespace.Data.DataRowcompilation 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:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
    }
}

API après modification:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string y) {
           ...
        }
    }
}

Exemple de code client:

Api.SomeNamespace.Foo.Bar(x:"hi"); //C#
Api.SomeNamespace.Foo.Bar(x:="hi") 'VB

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:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
    }
}

API après modification:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
        public static void Bar(ref string x) {
           ...
        }
    }
}

Exemple de code client:

Api.SomeNamespace.Foo.Bar(str)

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:

namespace SomeNamespace {
    public class Foo {
        public int Bar;
    }
}

API après modification:

namespace SomeNamespace {
    public class Foo {
        public int Bar { get; set; }
    }
}

Exemple de code client:

FooBar(ref Api.SomeNamespace.Foo.Bar);
jswolf19
la source
4

Changement d'API:

  1. Ajout de l'attribut [Obsolète] (vous avez couvert cela en mentionnant les attributs; cependant, cela peut être un changement de rupture lors de l'utilisation de l'avertissement en tant qu'erreur.)

Pause au niveau binaire:

  1. Déplacement d'un type d'un assemblage à un autre
  2. Modification de l'espace de noms d'un type
  3. Ajout d'un type de classe de base à partir d'un autre assembly.
  4. 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.

    protected void Something<T>() where T : Class2 { }
  5. 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.

    protected class Class3 : Class2 { }
    protected void Something<T>() where T : Class3 { }

Changement de la sémantique silencieuse au niveau de la source:

  1. Ajout / suppression / modification des remplacements de Equals (), GetHashCode () ou ToString ()

(je ne sais pas où ils correspondent)

Modifications du déploiement:

  1. Ajout / suppression de dépendances / références
  2. Mise à jour des dépendances vers des versions plus récentes
  3. Changement de la «plate-forme cible» entre x86, Itanium, x64 ou anycpu
  4. Construire / tester sur une installation de framework différente (c'est-à-dire que l'installation de 3.5 sur une boîte .Net 2.0 permet des appels d'API qui nécessitent alors .Net 2.0 SP2)

Modifications d'amorçage / de configuration:

  1. Ajout / suppression / modification des options de configuration personnalisées (par exemple, paramètres App.config)
  2. Avec l'utilisation intensive d'IoC / DI dans les applications d'aujourd'hui, il est parfois nécessaire de reconfigurer et / ou de changer le code d'amorçage pour le code dépendant de DI.

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.

csharptest.net
la source
"Ajout d'un nouveau membre (protégé contre les événements) qui utilise un type d'un autre assembly." - IIRC, le client a seulement besoin de référencer les assemblys dépendants qui contiennent les types de base des assemblys qu'il référence déjà; il n'a pas à référencer des assemblys qui sont simplement utilisés (même si les types sont dans les signatures de méthode); Je n'en suis pas sûr à 100%. Avez-vous une référence pour des règles précises à ce sujet? En outre, le déplacement d'un type peut être ininterrompu s'il TypeForwardedToAttributeest utilisé.
Pavel Minaev
Ce "TypeForwardedTo" est une nouveauté pour moi, je vais le vérifier. Quant à l'autre, je n'y suis pas non plus à 100% ... laisse-moi voir si peut repro et je mettrai à jour le post.
csharptest.net
Alors, ne forcez pas -Werrorsur 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.
binki
@binki excellent point, le traitement des avertissements comme des erreurs ne devrait être suffisant que dans les builds DEBUG.
csharptest.net
3

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

  public int MyMethod(int mandatoryParameter, int optionalParameter = 0)
  {
     return mandatoryParameter + optionalParameter;
  }    

API après modification

  public int MyMethod(int mandatoryParameter, int optionalParameter)
  {
     return mandatoryParameter + optionalParameter;
  }

  public int MyMethod(int mandatoryParameter)
  {
     return MyMethod(mandatoryParameter, 0);
  }

Exemple de code qui fonctionnera toujours

  public int CodeNotDependentToNewVersion()
  {
     return MyMethod(5, 6); 
  }

Exemple de code qui dépend maintenant de la nouvelle version lors de la compilation

  public int CodeDependentToNewVersion()
  {
     return MyMethod(5); 
  }
Tu ne sais pas
la source
1

Renommer une interface

Sorte de rupture: source et binaire

Langues affectées: Très probablement toutes, testées en C #.

API avant modification:

public interface IFoo
{
    void Test();
}

public class Bar
{
    IFoo GetFoo() { return new Foo(); }
}

API après modification:

public interface IFooNew // Of the exact same definition as the (old) IFoo
{
    void Test();
}

public class Bar
{
    IFooNew GetFoo() { return new Foo(); }
}

Exemple de code client qui fonctionne mais est cassé par la suite:

new Bar().GetFoo().Test(); // Binary only break
IFoo foo = new Bar().GetFoo(); // Source and binary break
Aidiakapi
la source
1

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:

public class Foo
{
    public void Bar(string param);
}

API après le changement:

public class Foo
{
    public void Bar(string param);
    public void Bar(int? param);
}

Exemple de code client fonctionnant avant la modification et cassé après:

new Foo().Bar(null);

Exception: l'appel est ambigu entre les méthodes ou propriétés suivantes.

Bohdan Spilnyi
la source
0

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:

public static class Foo
{
    public static void Bar(string x);
}

API après modification:

public static class Foo
{
    public void Bar(this string x);
}

Exemple de code client fonctionnant avant le changement et cassé après:

using static Foo;

class Program
{
    static void Main() => Bar("hello");
}

Plus d'informations: https://github.com/dotnet/csharplang/issues/665

rory.ap
la source