Question

Je voudrais rassembler autant d'informations que possible sur le versioning API .NET / CLR, et plus précisément comment les changements de l'API font ou ne se cassent pas les applications clientes. Tout d'abord, nous allons définir certains termes:

Changement API - un changement dans la définition publiquement visible d'un type, y compris l'un de ses membres du public. Ceci inclut le changement de type et les membres, changement de type d'un type de base, l'ajout / suppression d'interfaces à partir de la liste des interfaces mises en œuvre d'un type, l'ajout / suppression de membres (y compris les surcharges), changer la visibilité du membre, en renommant les paramètres de méthode et de type, en ajoutant des valeurs par défaut pour les paramètres de la méthode, l'ajout / suppression des attributs sur les types et les membres, et l'ajout / suppression des paramètres de type générique sur les types et les membres (ai-je raté quelque chose?). Cela ne comprend pas de changements dans les organismes membres, ou tout changement aux membres privés (à savoir que nous ne prenons pas en compte la réflexion).

rupture niveau binaire - un changement d'API qui se traduit par des ensembles de clients compilés contre une version plus ancienne de l'API de chargement potentiellement pas avec la nouvelle version. Exemple: changement de signature de la méthode, même si elle permet d'être appelé de la même manière que précédemment (à savoir: void pour renvoyer des valeurs par défaut de type / paramètre de surcharge).

rupture niveau de la source - un changement d'API qui se traduit par le code existant écrit pour compiler contre ancienne version de l'API potentiellement ne pas compiler avec la nouvelle version. Déjà compilé ensembles client fonctionnent comme avant, cependant. Exemple:. L'ajout d'une nouvelle surcharge qui peut entraîner une ambiguïté dans les appels de méthode qui étaient sans ambiguïté précédente

Source niveau sémantique calme changement - un changement d'API qui se traduit par le code existant écrit pour compiler contre ancienne version de l'API tranquillement changer sa sémantique, par exemple en appelant une autre méthode. Le code devrait toutefois continuer à compiler sans avertissements / erreurs et assemblées précédemment compilées devrait fonctionner comme auparavant. Exemple:. La mise en œuvre d'une nouvelle interface sur une classe existante qui se traduit par une surcharge différente étant choisie lors de la résolution de surcharge

Le but ultime est de catalogize autant de rupture et sémantique calme modifications de l'API que possible, et de décrire l'effet exact de la rupture, et quelles langues sont et ne sont pas affectés par celle-ci. Pour développer ce dernier: alors que certains changements affectent toutes les langues universellement (par exemple l'ajout d'un nouveau membre à une interface cassera mise en oeuvre de cette interface dans toutes les langues), certains exigent la sémantique des langages très spécifiques pour entrer en jeu pour faire une pause. Ce plus implique généralement la surcharge de méthode, et, en général, tout ce qui a à voir avec les conversions implicites de type. Il ne semble pas être un moyen de définir le ici « dénominateur commun », même pour les langues CLS conforme à l '(ceux qui répondent au moins aux règles du « consommateur CLS », comme défini dans la spécification CLI) - bien que je vais apprécier si quelqu'un me corrige comme tort ici - donc cela devra aller la langue par langue. Ceux de plus grand intérêt sont naturellement ceux qui viennent avec .NET de la boîte: C #, VB et F #; mais d'autres, comme IronPython, IronRuby, Delphi Prism etc sont également pertinents. Plus d'un cas d'angle, il est, plus intéressant, il sera - des choses comme la suppression de membres sont assez soi, mais les interactions subtiles entre par exemple la surcharge de méthode, les paramètres en option / par défaut, l'inférence de type lambda et les opérateurs de conversion peut être très surprenant parfois.

Quelques exemples kickstart ceci:

Ajout de nouvelles surcharges de méthode

Type: coupure niveau source

Langues affectées: C #, VB, F #

API avant changement:

public class Foo
{
    public void Bar(IEnumerable x);
}

API après le changement:

public class Foo
{
    public void Bar(IEnumerable x);
    public void Bar(ICloneable x);
}

code client Exemple de travail avant le changement et rompu après:

new Foo().Bar(new int[0]);

Ajout de nouveaux opérateurs de surcharge conversion implicite

Type:. Rupture niveau source

Langues uneffected: C #, VB

Langues non affectées: F #

API avant changement:

public class Foo
{
    public static implicit operator int ();
}

API après le changement:

public class Foo
{
    public static implicit operator int ();
    public static implicit operator float ();
}

code client Exemple de travail avant le changement et rompu après:

void Bar(int x);
void Bar(float x);
Bar(new Foo());

Notes: F # est pas cassé, car il n'a pas un soutien au niveau de la langue pour les opérateurs surchargées, ni explicite ni implicite - les deux doivent appeler directement les méthodes de op_Explicit et op_Implicit

.

Ajout de nouvelles méthodes d'instance

Type:. Niveau source calme changement sémantique

Langues affectées: C #, VB

Langues non affectées: F #

API avant changement:

public class Foo
{
}

API après le changement:

public class Foo
{
    public void Bar();
}

code client de l'échantillon qui subit un changement de sémantique calme:

public static class FooExtensions
{
    public void Bar(this Foo foo);
}

new Foo().Bar();

Notes: F # est pas cassé, parce qu'il ne prend pas en charge au niveau de la langue pour ExtensionMethodAttribute, et nécessite des méthodes d'extension de CLS être appelé comme méthodes statiques.

Était-ce utile?

La solution

Modification de la signature de la méthode

Genre: niveau binaire Pause

Langues affectées: C # (VB et F # plus probable, mais non testé)

API avant le changement

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

API après le changement

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

code client Exemple de travail avant le changement

Foo.bar(13);

Autres conseils

Ajout d'un paramètre avec une valeur par défaut.

Type de rupture: rupture niveau binaire

Même si le code source d'appel n'a pas besoin de changer, il doit encore être recompilé (tout comme lors de l'ajout d'un paramètre régulier).

C'est parce que C # compile les valeurs par défaut des paramètres directement dans l'ensemble de l'appel. Cela signifie que si vous ne recompilez pas, vous obtiendrez un MissingMethodException parce que l'ancien ensemble essaie d'appeler une méthode avec moins d'arguments.

API avant le changement

public void Foo(int a) { }

API Après changement

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

code client de l'échantillon qui est cassé suite

Foo(5);

Le code client doit être recompilé en Foo(5, null) au niveau du bytecode. L'assemblée appelée ne contiendra Foo(int, string), non Foo(int). En effet, les valeurs des paramètres par défaut sont une fonctionnalité du langage, le moteur d'exécution .Net ne sait rien sur eux. (Cela explique aussi pourquoi les valeurs par défaut doivent être des constantes de compilation en C #).

Celui-ci était très non évidente quand 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 il est assez surprenant que j'ai décidé de l'inclure:

Les membres de la classe refactorisation dans une classe de base

Type: pas une rupture

Langues affectées: aucune (ce ne sont ni cassées)

API avant changement:

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

API après le changement:

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

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

Exemple de code qui ne cesse de travailler tout au long du changement (même si je m'y attendais à la pause):

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

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

Notes:

C ++ / CLI est la seule langue .NET qui a une construction analogue à la mise en œuvre d'interface explicite pour les membres de la classe de base virtuelle - « dérogation explicite ». J'attendais que pour aboutir à la même sorte de rupture que lors du déplacement de corps d'interface à une interface de base (depuis IL généré pour la commande explicite est la même que pour la mise en œuvre explicite). À ma grande surprise, ce n'est pas le cas - même si l'IL généré précise encore que les remplacements de BarOverride Foo::Bar plutôt que FooBase::Bar, chargeur de montage est assez intelligent pour substituer l'un à l'autre correctement sans aucune plainte - apparemment, le fait que Foo est une classe est ce que qui fait la différence. Aller figure ...

Celui-ci est peut-être pas si évident cas particulier de « ajout / suppression de membres d'interface », et je me suis dit qu'il mérite sa propre entrée à la lumière d'une autre affaire que je vais poster prochain. Donc:

éléments d'interface Refactoring dans une interface de base

Type: pauses, tant au niveau de la source et binaire

Langues affectées: C #, VB, C ++ / CLI, F # (pour la pause de la source, un binaire affecte naturellement toutes les langues)

API avant changement:

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

API après le changement:

interface IFooBase 
{
    void Bar();
}

interface IFoo : IFooBase
{
    void Baz();
}

code client de l'échantillon qui est cassé par le changement au niveau de la source:

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

code client de l'échantillon qui est cassé par le changement au niveau binaire;

(new Foo()).Bar();

Notes:

Pour la rupture au niveau de la source, le problème est que C #, VB et C ++ / CLI ont tous besoin exactement nom de l'interface dans la déclaration de mise en œuvre de membre d'interface; ainsi, si le membre est déplacé à une interface de base, le code ne sera plus compiler.

pause binaire est due au fait que les méthodes d'interface sont pleinement qualifiés dans l'IL généré pour les implémentations explicites et le nom de l'interface, il doit aussi être exacte.

mise en œuvre implicite lorsqu'elles sont disponibles (à savoir C # et C ++ / CLI, mais pas VB) fonctionnera bien à la fois source et le niveau binaire. Les appels de méthode ne se cassent pas non plus.

Réorganiser valeurs énumérées

Type de rupture: Source niveau / sémantique binaire niveau silencieux changement

Langues affectées: tous

Réorganiser les valeurs énumérées garderont la compatibilité au niveau source comme littéraux ont le même nom, mais leurs indices ordinaux seront mis à jour, ce qui peut causer certains types de ruptures au niveau source silencieux.

Pire encore est le silence des pauses niveau binaire qui peut être introduit si le code client n'est pas recompilé contre la nouvelle version de l'API. valeurs enum sont des constantes de compilation et en tant que tels les utilisations d'entre eux sont cuits dans IL de l'ensemble de la clientèle. Ce cas peut être particulièrement difficile à repérer à la fois.

API avant le changement

public enum Foo
{
   Bar,
   Baz
}

API Après changement

public enum Foo
{
   Baz,
   Bar
}

code client Sample qui fonctionne mais est cassé après:

Foo.Bar < Foo.Baz

Celui-ci est vraiment une chose très rare dans la pratique, mais néanmoins un surprenant quand il arrive.

Ajout de nouveaux membres non surchargés

Type:. Rupture de niveau source ou changement sémantique calme

Langues affectées: C #, VB

Langues non affectées: F #, C ++ / CLI

API avant changement:

public class Foo
{
}

API après le changement:

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

code client de l'échantillon qui est cassé par le changement:

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());        
    }
}

Notes:

Le problème est causé par l'inférence de type lambda ici en C # et VB en présence de la résolution de surcharge. Une forme limitée de frappe de canard est employé ici pour rompre les liens où plus d'un correspond de type, en vérifiant si le corps du lambda est logique pour un type donné -. Si seulement un résultat de type dans le corps compilable, que l'on est choisi

Le danger ici est que le code client peut avoir un groupe de méthode surchargée 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 son code repose alors sur l'algorithme d'inférence de type pour déterminer la bonne méthode fondée uniquement sur la présence ou l'absence de membres, puis ajouter un nouveau membre à un de vos types avec le même nom que dans l'un des types peuvent potentiellement jeter l'inférence du client off, ce qui entraîne une ambiguïté lors de la résolution de surcharge.

Notez que les types Foo et Bar dans cet exemple ne sont pas liés de quelque façon, et non par héritage, ni autrement. La simple utilisation d'entre eux dans un seul groupe de méthode est suffisant pour déclencher, et si cela se produit dans le code client, vous n'avez pas le contrôle.

Le code exemple ci-dessus montre une situation plus simple lorsque cela est une rupture au niveau de la source (à savoir les résultats d'erreur du compilateur). Cependant, cela peut aussi être un changement silencieux sémantique, si la surcharge qui a été choisie par inférence avait d'autres arguments qui seraient autrement l'amener à être classés ci-dessous (par exemple, les arguments facultatifs avec des valeurs par défaut, ou incompatibilité de type entre déclaré et argument réel nécessitant une implicite conversion). Dans un tel scénario, la résolution de surcharge ne sera plus l'échec, mais une surcharge différente sera tranquillement sélectionnée par le compilateur. Dans la pratique, cependant, il est très difficile de courir dans ce cas, sans construire soigneusement les signatures de méthode pour provoquer délibérément.

Convertir une implémentation d'interface implicite dans un un explicite.

Type de rupture: source et binaire

Langues concernées: Toutes

Ceci est vraiment juste une variation de l'évolution de l'accessibilité d'une méthode -. Juste un peu plus subtile car il est facile de négliger le fait que tous les accès aux méthodes d'une interface sont nécessairement par une référence au type de l'interface

API avant le changement:

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

API Après le changement:

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

Exemple de code client qui fonctionne avant le changement et se décompose ensuite:

new Foo().GetEnumerator(); // fails because GetEnumerator() is no longer public

Convertir une implémentation d'interface explicite dans un un implicite.

Type de rupture: Source

Langues concernées: Toutes

Le refactoring d'une implémentation d'interface explicite dans une une implicite est plus subtile dans la façon dont il peut briser une API. Sur la surface, il semblerait que cela devrait être lorsqu'il est combiné avec l'héritage relativement cependant, en toute sécurité, il peut causer des problèmes.

API avant le changement:

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

API Après le changement:

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

Exemple de code client qui fonctionne avant le changement et se décompose ensuite:

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"

Modification d'un champ à une propriété

Type de rupture: API

Langues concernées: Visual Basic et C # *

Info: Lorsque vous modifiez un champ normal ou variable dans une propriété de Visual Basic, code externe référencement que membre de quelque manière que devra être recompilé.

API avant le changement:

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

API Après le changement:

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    

code client Sample qui fonctionne mais est cassé après:

Foo.Bar = "foobar"

Ajout Namespace

break niveau de la source / sémantique calme niveau Source changement

En raison de la résolution de l'espace de noms façon dont fonctionne dans vb.Net, l'ajout d'un espace de noms à une bibliothèque peut provoquer le code Visual Basic qui a compilé avec une version précédente de l'API pour ne pas compiler avec une nouvelle version.

code client de l'échantillon:

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 compilera pas.

Il devient plus compliqué avec les importations d'espace de nom au niveau du projet. Si Imports System est omis dans le code ci-dessus, mais l'espace de noms System est importé au niveau du projet, le code peut encore donner lieu à une erreur.

Cependant, si l'Api comprend une DataRow de classe dans son espace de noms Api.SomeNamespace.Data, le code compilera mais dr sera une instance de System.Data.DataRow lorsqu'il est compilé avec l'ancienne version de l'API et Api.SomeNamespace.Data.DataRow lorsqu'il est compilé avec la nouvelle version de l'API .

Argument Renommer

rupture niveau de la source

Modification des noms des arguments est un changement de rupture dans vb.net de la version 7 (?) (Version .Net 1?) Et c # .net de la version 4 (version .Net 4).

API avant changement:

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

API après le changement:

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

code client de l'échantillon:

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

Paramètres Ref

rupture niveau de la source

Ajout à l'exception que l'on paramètre un remplacement de procédé avec la même signature est passé par référence plutôt que par valeur provoquera source de vb qui fait référence à l'API dans l'impossibilité de régler la fonction. Visual Basic n'a aucun moyen (?) Pour différencier ces méthodes au point d'appel à moins qu'ils aient des noms d'arguments, de sorte qu'un tel changement pourrait entraîner les deux membres d'être inutilisable à partir du code vb.

API avant changement:

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

API après le changement:

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

code client de l'échantillon:

Api.SomeNamespace.Foo.Bar(str)

champ pour modifier la propriété

rupture niveau binaire / pause niveau source

Outre la rupture évidente au niveau binaire, cela peut provoquer une rupture au niveau de la 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 le changement:

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

code client de l'échantillon:

FooBar(ref Api.SomeNamespace.Foo.Bar);

Changement API:

  1. Ajout de la [Obsolete] attribut (vous couvert un peu ce avec des attributs mentionnant, mais cela peut être un changement de rupture lors de l'utilisation d'alerte-en erreur.)

rupture niveau binaire:

  1. Déplacement d'un type à partir d'un ensemble à l'autre
  2. Modification de l'espace de noms d'un type
  3. Ajout d'un type de classe de base d'un autre assemblage.
  4. Ajout d'un nouveau membre (event protégé) qui utilise un type comme une contrainte d'argument de modèle à partir d'un autre ensemble (classe 2).

    protected void Something<T>() where T : Class2 { }
    
  5. Modification d'une classe enfant (classe 3) pour dériver à partir d'un type dans un autre ensemble 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 { }
    

sémantique calme niveau Source changement:

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

(pas sûr où ceux-ci correspondent)

Modifications de déploiement:

  1. Ajout / suppression des dépendances / références
  2. dépendances Mise à jour vers les nouvelles versions
  3. Modification de la 'plate-forme cible' entre les architectures x86, x64 ou anycpu
  4. Bâtiment / test sur un autre cadre d'installation (à savoir 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)

Bootstrap / changements de configuration:

  1. Ajout / Suppression / Modification des options de configuration personnalisée (à savoir les paramètres App.config)
  2. Avec l'utilisation massive de IoC / DI dans les applications d'aujourd'hui, il est nécessaire de reconfigurer somethings et / ou modifier le code de bootstrapping pour le code dépendant DI.

Mise à jour:

Désolé, je ne savais pas que la seule raison pour cela rompait pour moi était que je les ai utilisés dans des contraintes de modèle.

Ajout des méthodes de surcharge à mort l'utilisation des paramètres par défaut

Type de rupture: sémantique calme niveau Source changer

Parce que le compilateur transforme les appels de méthode avec les valeurs manquantes des paramètres par défaut à un appel explicite à la valeur par défaut du côté appelant, la compatibilité pour est donnée existant code compilé; On trouvera une méthode avec la signature correcte pour tout le code compilé précédemment.

De l'autre côté, les appels sans utilisation de paramètres optionnels sont maintenant compilés comme un appel à la nouvelle méthode qui manque le paramètre optionnel. Tout fonctionne toujours bien, mais si le code appelé réside dans un autre ensemble, le code compilé récemment l'appelant est maintenant dépendante de la nouvelle version de cette assemblée. Déploiement des ensembles appelant le code refondus sans déployer aussi l'ensemble du code refactorisé réside dans se traduit par « méthode non trouvée » exceptions.

API avant le changement

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

API après le changement

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

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

Code de démonstration qui fonctionnera toujours

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

Code de l'échantillon qui est maintenant dépendante de la nouvelle version lors de la compilation

  public int CodeDependentToNewVersion()
  {
     return MyMethod(5); 
  }

Changement de nom d'une interface

Kinda Break: Source et Binary

Langues Affectés:. Très probablement tous, testé en C #

API avant le changement:

public interface IFoo
{
    void Test();
}

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

API Après le changement:

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

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

code client Sample qui fonctionne mais est cassé après:

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

Surcharge méthode avec un paramètre de type Nullable

Type: rupture 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);
}

code client Exemple de travail avant le changement et rompu après:

new Foo().Bar(null);

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

Promotion à une méthode d'extension

Type: coupure niveau source

Langues affectées: (peut-être d'autres) C # v6 et plus

API avant changement:

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

API après le changement:

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

code client Exemple de travail avant le changement et rompu après:

using static Foo;

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

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

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top