240 votes

Un guide de référence pour l’API-nouveautés dans .NET

Je voudrais recueillir autant d'informations que possible au sujet de l'API de gestion des versions .NET/CLR, et, en particulier, les changements de l'API de faire ou de ne pas casser les applications clientes. Tout d'abord, définissons quelques termes:

API changement - un changement dans le visible publiquement définition d'un type, y compris l'une quelconque de ses membres publics. Cela inclut changer le type et le nom des membres, en changeant de type de base d'un type, ajout/suppression d'interfaces à partir de la liste de la mise en œuvre des interfaces d'un type, ajout/suppression de membres (y compris les surcharges), la modification des états de visibilité, le changement de nom de la méthode et des paramètres de type, ajouter des valeurs par défaut pour les paramètres de la méthode, de l'ajout/suppression d'attributs sur les types et les membres, et en ajoutant/supprimant des paramètres de type générique sur les types et les membres (ai-je raté quelque chose?). Cela ne comprend pas les changements d'états des corps, ou de toute modification à des membres privés (c'est à dire en ne tenant pas compte de la Réflexion).

Binaire au niveau de pause - une API changement qui en résulte dans le client assemblées compilé avec l'ancienne version de l'API potentiellement pas de chargement avec la nouvelle version. Exemple: retrait d'un membre de la classe.

Au niveau de la Source pause - une API changement qui se traduit dans le code existant écrit pour compiler avec d'anciennes version de l'API peuvent ne pas être de la compilation avec la nouvelle version. Déjà compilé client assemblées travail 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.

Au niveau de la Source de calme sémantique du changement - une API changement qui se traduit dans le code existant écrit pour compiler avec d'anciennes version de l'API tranquillement changer sa sémantique, par exemple par l'appel d'une méthode différente. Le code doit cependant continuer à compiler sans avertissements/erreurs, et a été compilé assemblées devrait fonctionner comme avant. Exemple: mise en œuvre d'une nouvelle interface d'une classe existante que les résultats dans une autre surcharge être choisi lors de la résolution de surcharge.

Le but ultime est de catalogize autant de la rupture et la tranquillité de la sémantique de l'API de changements que possible, et de décrire exactement l'effet de rupture, et quelles sont les langues et ne sont pas affectés par elle. Pour développer sur le dernier: alors que certains changements affectent toutes les langues universelle (par exemple, l'ajout d'un nouveau membre à une interface de briser les implémentations de cette interface dans n'importe quelle langue), certains de besoin très spécifique de la langue de la sémantique d'entrer dans le jeu pour faire une pause. Cette plupart implique généralement la surcharge de méthode, et, en général, n'importe quoi avoir à faire avec les conversions de type. Il ne semble pas être un moyen de définir le "plus petit dénominateur commun" ici même pour CLS-conforme langues (c'est à dire ceux qui satisfont au moins aux règles de CLS "consommateur", tel que défini dans les spécifications CLI) - bien que je vais apprécier si quelqu'un me corrige comme étant mal ici - donc cela devra aller de la langue par langue. Ceux qui présentent le plus d'intérêt sont naturellement ceux qui viennent avec .NET: C#, VB et F#; mais d'autres, comme IronPython, IronRuby, Delphi Prism, etc sont également pertinents. La plus un angle cas, il est le plus intéressant, il sera, comme des membres de la suppression sont assez évidentes, mais de subtiles interactions entre par exemple la surcharge de méthode, en option/paramètres par défaut, lambda, l'inférence de type, et les opérateurs de conversion peut être très surprenant à la fois.

Quelques exemples pour lancer ce:

L'ajout de nouvelles surcharges de méthode

Genre: au niveau de la source pause

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

API avant modification:

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 client code du travail avant le changement et cassé après:

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

L'ajout de nouveaux la surcharge de l'opérateur de conversion implicite

Genre: au niveau de la source de rupture.

Langues concernées: C#, VB

Langues qui ne sont pas touchés: F#

API avant modification:

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 client code du travail avant le changement et cassé après:

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

Notes: F# n'est pas cassé, parce qu'il n'a pas de langue de niveau de soutien pour les opérateurs surchargés, ni explicite, ni implicite, qui ont toutes deux pour être appelé directement comme op_Explicit et op_Implicit méthodes.

L'ajout de nouvelles méthodes d'instance

Genre: au niveau de la source de calme sémantique de changement.

Langues concernées: C#, VB

Langues qui ne sont pas touchés: F#

API avant modification:

public class Foo
{
}

API après modification:

public class Foo
{
    public void Bar();
}

Exemple de code client qui souffre d'un calme sémantique changement:

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

new Foo().Bar();

Notes: F# n'est pas cassé, parce qu'il n'a pas de langue de support au niveau de l' ExtensionMethodAttribute, et nécessite CLS méthodes d'extension pour être appelé en tant que méthodes statiques.

46voto

Justin Drury Points 443
<h2>Modifier une signature de méthode<p>Genre : Rupture de niveau binaire</p><p>Langues concernées : c# (VB et F # plus probable, mais non testé)</p><p>API avant changement</p><pre><code></code></pre><p>API après changement</p><pre><code></code></pre><p>Exemple de code client avant changement</p><pre><code></code></pre></h2>

43voto

Eldritch Conundrum Points 1683

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

Type de séjour: Binaire au niveau de pause

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

C'est parce que le C# compile les valeurs par défaut des paramètres directement dans la convocation de l'assemblée. Cela signifie que si vous n'avez pas de recompilation, vous obtiendrez un MissingMethodException parce que l'ancienne assemblée tente d'appeler une méthode avec moins d'arguments.

API Avant de Changer

public void Foo(int a) { }

Après le Changement de l'API

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

Exemple de code client qui est cassé par la suite

Foo(5);

Le code du client doit être recompilé en Foo(5, null) au niveau du bytecode. Les appelés de l'assemblée ne contiennent Foo(int, string), pas Foo(int). C'est parce que les valeurs de paramètre par défaut sont purement d'un langage, l' .Net runtime ne connaissent rien à leur sujet. (Cela aussi expliquer pourquoi les valeurs par défaut doivent être des constantes de compilation en C#).

26voto

Pavel Minaev Points 60647

Celui-ci a été 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 rupture, mais c'est assez surprenant que j'ai décidé de l'inclure:

Refactoring des membres de la classe dans une classe de base

Genre: pas une pause!

Langues concernées: aucun (c'est à dire aucun sont cassés)

API avant modification:

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 à travailler tout au long de la changer (même si je m'attendais à une 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 .NET langue qui a une construction analogue à l'implémentation d'interface explicite pour virtuel de la classe de base des membres - "explicite " override". Je m'attendais qu'à la suite de la même genre de casse comme lors du déplacement de l'interface les membres d'une interface de base (puisqu'IL a généré explicite remplacement est la même que pour la mise en œuvre explicite). À ma grande surprise, ce n'est pas le cas - même si généré IL précise encore que BarOverride remplacements Foo::Bar plutôt que d' FooBase::Bar, assemblée, un chargeur est assez intelligent pour se substituer l'un à l'autre correctement sans qu'aucune réclamation - apparemment, le fait qu' Foo est une classe est ce qui fait la différence. Allez comprendre...

21voto

Pavel Minaev Points 60647

C'est peut-être pas si évidente cas particulier de "ajout/suppression de membres d'interface", et j'ai pensé qu'il mérite sa propre entrée dans la lumière d'une autre affaire dont je vais post suivant. Donc:

Refactoring de l'interface de membres dans une interface de base

Genre: casse à la fois source et binaire niveaux

Langues concernées: C#, VB, C++/CLI, F# (source break; binaire affecte naturellement n'importe quelle langue)

API avant modification:

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

API après modification:

interface IFooBase 
{
    void Bar();
}

interface IFoo : IFooBase
{
    void Baz();
}

Exemple de code client qui est cassé par le changement au niveau de la source:

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

Exemple de code client qui est cassé par le changement au niveau binaire;

(new Foo()).Bar();

Notes:

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

Binaire rupture est due au fait que les méthodes d'interface sont parfaitement qualifiés généré IL explicite les implémentations, et le nom de l'interface, il doit aussi être exact.

Implicite de mise en œuvre (c'est à dire, C# et C++/CLI, mais pas VB) fonctionnera bien sur à la fois la source et binaire. Les appels de méthode ne cassent pas non plus.

13voto

Pavel Minaev Points 60647

C'est vraiment une chose très rare dans la pratique, mais néanmoins surprenant quand ça arrive.

L'ajout de nouvelles non surchargé membres

Genre: niveau de la source de pause ou à la tranquillité de la sémantique du changement.

Langues concernées: C#, VB

Langues qui ne sont pas touchés: F#, C++/CLI

API avant modification:

public class Foo
{
}

API après modification:

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

Exemple de code client 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:

Ici, le problème est causé par lambda inférence de type en C# et VB, en présence de résolution de surcharge. Une forme limitée de duck-typing est employée ici pour rompre les liens où plus d'un type de matches, en vérifiant si le corps de la lambda de sens que pour un type donné - si un seul type de résultats dans compilable corps, qui est choisi.

Le danger ici est que le code client peut disposer d'une méthode surchargée groupe 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 tout de son code, puis s'appuie sur l'algorithme d'inférence de types pour déterminer la bonne méthode basée uniquement sur la présence ou de l'absence de membres, puis d'ajouter un nouveau membre à l'un de vos types avec le même nom que dans l'un des clients de types peuvent potentiellement jeter l'inférence, ce qui provoque toute ambiguïté lors de la résolution de surcharge.

Notez que les types d' Foo et Bar dans cet exemple ne sont pas liés en aucune façon, pas par héritage ou autrement. La simple utilisation d'une méthode unique groupe est suffisant pour déclencher cela, et si cela se produit dans le code client, vous n'avez aucun contrôle sur elle.

L'exemple de code ci-dessus montre une situation plus simple où c'est une source de niveau de rupture (c'est à dire erreur de compilation des résultats). Cependant, cela peut aussi être un silencieux sémantique de changement, si la surcharge qui a été choisi par inférence avait d'autres arguments qui seraient autrement provoquer pour être classé ci-dessous (par exemple, les arguments optionnels avec des valeurs par défaut, ou de l'incompatibilité entre déclarés et argument réel nécessitant une conversion implicite). Dans un tel scénario, la résolution de surcharge n'échouent plus, mais d'une autre surcharge sera tranquillement sélectionné par le compilateur. Dans la pratique, cependant, il est très difficile de courir dans ce cas, sans soigneusement méthode de construction des signatures de provoquer volontairement.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X