Eine endgültige Anleitung zu API-brechenden Änderungen in .NET


227

Ich möchte so viele Informationen wie möglich über die API-Versionierung in .NET / CLR sammeln und insbesondere darüber, wie API-Änderungen Clientanwendungen beschädigen oder nicht. Definieren wir zunächst einige Begriffe:

API-Änderung - Eine Änderung der öffentlich sichtbaren Definition eines Typs, einschließlich eines seiner öffentlichen Mitglieder. Dies umfasst das Ändern von Typ- und Mitgliedsnamen, das Ändern des Basistyps eines Typs, das Hinzufügen / Entfernen von Schnittstellen aus der Liste der implementierten Schnittstellen eines Typs, das Hinzufügen / Entfernen von Mitgliedern (einschließlich Überladungen), das Ändern der Sichtbarkeit von Mitgliedern, das Umbenennen von Methoden- und Typparametern sowie das Hinzufügen von Standardwerten für Methodenparameter, Hinzufügen / Entfernen von Attributen für Typen und Elemente und Hinzufügen / Entfernen von generischen Typparametern für Typen und Elemente (habe ich etwas verpasst?). Dies schließt keine Änderungen in den Mitgliedsorganen oder Änderungen an privaten Mitgliedern ein (dh wir berücksichtigen die Reflexion nicht).

Unterbrechung auf Binärebene - Eine API-Änderung, die dazu führt, dass Client-Assemblys, die mit einer älteren Version der API kompiliert wurden, möglicherweise nicht mit der neuen Version geladen werden. Beispiel: Ändern der Methodensignatur, auch wenn sie auf die gleiche Weise wie zuvor aufgerufen werden kann (dh: void, um Überladungen von Typ- / Parameter-Standardwerten zurückzugeben).

Unterbrechung auf Quellenebene - Eine API-Änderung, die dazu führt, dass vorhandener Code zum Kompilieren mit einer älteren Version der API geschrieben wird, die möglicherweise nicht mit der neuen Version kompiliert werden kann. Bereits kompilierte Client-Assemblys funktionieren jedoch wie zuvor. Beispiel: Hinzufügen einer neuen Überladung, die zu Mehrdeutigkeiten bei Methodenaufrufen führen kann, die zuvor eindeutig waren.

Leise Semantikänderung auf Quellenebene - Eine API-Änderung, die dazu führt, dass vorhandener Code, der zum Kompilieren mit einer älteren Version der API geschrieben wurde, seine Semantik leise ändert, z. B. durch Aufrufen einer anderen Methode. Der Code sollte jedoch weiterhin ohne Warnungen / Fehler kompiliert werden, und zuvor kompilierte Assemblys sollten wie zuvor funktionieren. Beispiel: Implementieren einer neuen Schnittstelle in einer vorhandenen Klasse, die dazu führt, dass während der Überlastungsauflösung eine andere Überlastung ausgewählt wird.

Das ultimative Ziel ist es, so viele Änderungen der Semantik-API wie möglich zu katalogisieren und die genauen Auswirkungen von Unterbrechungen zu beschreiben und zu beschreiben, welche Sprachen davon betroffen sind und welche nicht. Um letzteres zu erweitern: Während einige Änderungen alle Sprachen allgemein betreffen (z. B. das Hinzufügen eines neuen Mitglieds zu einer Schnittstelle führt zu einer Unterbrechung der Implementierung dieser Schnittstelle in einer beliebigen Sprache), erfordern einige eine sehr spezifische Sprachsemantik, um eine Unterbrechung zu erreichen. Dies beinhaltet in der Regel eine Methodenüberladung und im Allgemeinen alles, was mit impliziten Typkonvertierungen zu tun hat. Es scheint hier keine Möglichkeit zu geben, den "kleinsten gemeinsamen Nenner" zu definieren, selbst für CLS-konforme Sprachen (dh solche, die mindestens den in der CLI-Spezifikation definierten Regeln des "CLS-Verbrauchers" entsprechen) - obwohl ich ' Ich würde es begrüßen, wenn mich jemand hier als falsch korrigiert - also muss dies Sprache für Sprache gehen. Am interessantesten sind natürlich diejenigen, die standardmäßig mit .NET geliefert werden: C #, VB und F #; Aber auch andere wie IronPython, IronRuby, Delphi Prism usw. sind relevant. Je mehr es sich um einen Eckfall handelt, desto interessanter wird es sein - Dinge wie das Entfernen von Mitgliedern sind ziemlich selbstverständlich, aber subtile Wechselwirkungen zwischen z. B. Methodenüberladung, optionalen / Standardparametern, Lambda-Typ-Inferenz und Konvertierungsoperatoren können sehr überraschend sein manchmal.

Einige Beispiele, um dies zu starten:

Hinzufügen neuer Methodenüberladungen

Art: Unterbrechung auf Quellenebene

Betroffene Sprachen: C #, VB, F #

API vor Änderung:

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

API nach Änderung:

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

Beispiel für einen Clientcode, der vor der Änderung funktioniert und danach beschädigt wird:

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

Hinzufügen neuer Überladungen von impliziten Konvertierungsoperatoren

Art: Unterbrechung auf Quellenebene.

Betroffene Sprachen: C #, VB

Nicht betroffene Sprachen: F #

API vor Änderung:

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

API nach Änderung:

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

Beispiel für einen Clientcode, der vor der Änderung funktioniert und danach beschädigt wird:

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

Anmerkungen: F # ist nicht fehlerhaft, da es keine Unterstützung auf Sprachebene für überladene Operatoren bietet, weder explizit noch implizit - beide müssen direkt als op_Explicitund op_ImplicitMethoden aufgerufen werden.

Hinzufügen neuer Instanzmethoden

Art: Änderung der stillen Semantik auf Quellenebene.

Betroffene Sprachen: C #, VB

Nicht betroffene Sprachen: F #

API vor Änderung:

public class Foo
{
}

API nach Änderung:

public class Foo
{
    public void Bar();
}

Beispiel für einen Clientcode, der eine stille Änderung der Semantik erfährt:

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

new Foo().Bar();

Anmerkungen: F # ist nicht fehlerhaft, da es keine Unterstützung auf Sprachebene ExtensionMethodAttributebietet und erfordert, dass CLS-Erweiterungsmethoden als statische Methoden aufgerufen werden.


Sicherlich deckt Microsoft dies bereits ab ... msdn.microsoft.com/en-us/netframework/aa570326.aspx
Robert Harvey

1
@Robert: In Ihrem Link geht es um etwas ganz anderes - er beschreibt bestimmte wichtige Änderungen in .NET Framework . Dies ist eine umfassendere Frage, die generische Muster beschreibt , die zu grundlegenden Änderungen in Ihren eigenen APIs führen können (als Bibliotheks- / Framework-Autor). Mir ist kein solches Dokument von MS bekannt, das vollständig wäre, obwohl Links zu solchen, auch wenn sie unvollständig sind, auf jeden Fall willkommen sind.
Pavel Minaev

Gibt es in einer dieser "Unterbrechungs" -Kategorien eine, in der das Problem erst zur Laufzeit auftritt?
Rohit

1
Ja, Kategorie "Binärunterbrechung". In diesem Fall haben Sie bereits eine Assembly eines Drittanbieters für alle Versionen Ihrer Assembly kompiliert. Wenn Sie eine neue Version Ihrer Baugruppe an Ort und Stelle ablegen, funktioniert die Baugruppe eines Drittanbieters nicht mehr - entweder wird sie zur Laufzeit einfach nicht geladen oder sie funktioniert nicht ordnungsgemäß.
Pavel Minaev

3
Ich würde diese in den Beitrag und Kommentare blogs.msdn.com/b/ericlippert/archive/2012/01/09/…
Lukasz Madon

Antworten:


42

Ändern einer Methodensignatur

Art: Binäre Pause

Betroffene Sprachen: C # (VB und F # höchstwahrscheinlich, aber nicht getestet)

API vor Änderung

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

API nach Änderung

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

Beispiel für einen Clientcode, der vor der Änderung funktioniert

Foo.bar(13);

15
Tatsächlich kann es auch zu einer Unterbrechung auf Quellenebene kommen, wenn jemand versucht, einen Delegaten für zu erstellen bar.
Pavel Minaev

Das stimmt auch. Ich habe dieses spezielle Problem festgestellt, als ich einige Änderungen an den Druckdienstprogrammen in meiner Unternehmensanwendung vorgenommen habe. Als das Update veröffentlicht wurde, wurden nicht alle DLLs, die auf diese Dienstprogramme verwiesen haben, neu kompiliert und freigegeben, sodass eine methodnotfound-Ausnahme ausgelöst wurde.
Justin Drury

1
Dies geht auf die Tatsache zurück, dass Rückgabetypen für die Signatur der Methode nicht zählen. Sie können auch nicht zwei Funktionen überladen, die ausschließlich auf dem Rückgabetyp basieren. Gleiches Problem.
Jason Short

1
Unterfrage zu dieser Antwort: Kennt jemand die Bedeutung des Hinzufügens eines dotnet4-Standardwerts 'public static void bar (int i = 0);' oder diesen Standardwert von einem Wert in einen anderen ändern?
K3B

1
Für diejenigen, die auf dieser Seite landen werden, denke ich, dass für C # (und "ich denke" die meisten anderen OOP-Sprachen) Return Types nicht zur Methodensignatur beiträgt. Ja, die Antwort ist richtig, dass die Signaturänderungen zur Änderung der Binärebene beitragen. ABER das Beispiel scheint meiner Meinung nach nicht korrekt zu sein. Das richtige Beispiel, das ich mir vorstellen kann, ist VOR der öffentlichen Dezimalsumme (int a, int b) Nach der öffentlichen Dezimalsumme (dezimal a, dezimal b). Bitte verweisen Sie auf diesen MSDN-Link. 3.6 Signaturen und Überladung
Bhanu Chhabra

40

Hinzufügen eines Parameters mit einem Standardwert.

Art der Pause: Pause auf Binärebene

Auch wenn sich der aufrufende Quellcode nicht ändern muss, muss er dennoch neu kompiliert werden (genau wie beim Hinzufügen eines regulären Parameters).

Dies liegt daran, dass C # die Standardwerte der Parameter direkt in die aufrufende Assembly kompiliert. Wenn Sie nicht neu kompilieren, erhalten Sie eine MissingMethodException, da die alte Assembly versucht, eine Methode mit weniger Argumenten aufzurufen.

API vor Änderung

public void Foo(int a) { }

API nach Änderung

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

Beispiel für einen Clientcode, der anschließend beschädigt wird

Foo(5);

Der Client-Code muss auf Foo(5, null)Bytecode-Ebene neu kompiliert werden . Die aufgerufene Assembly enthält nur Foo(int, string), nicht Foo(int). Dies liegt daran, dass Standardparameterwerte nur eine Sprachfunktion sind und die .Net-Laufzeit nichts über sie weiß. (Dies erklärt auch, warum Standardwerte Konstanten zur Kompilierungszeit in C # sein müssen).


2
Dies ist eine bahnbrechende Änderung, selbst für die Quellcode-Ebene: Func<int> f = Foo;// Dies wird mit der geänderten Signatur
fehlschlagen

26

Dieser war sehr offensichtlich, als ich ihn entdeckte, insbesondere angesichts des Unterschieds mit der gleichen Situation für Schnittstellen. Es ist überhaupt keine Pause, aber es ist überraschend genug, dass ich beschlossen habe, sie aufzunehmen:

Refactoring von Klassenmitgliedern in eine Basisklasse

Art: keine Pause!

Betroffene Sprachen: keine (dh keine sind kaputt)

API vor Änderung:

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

API nach Änderung:

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

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

Beispielcode, der während der Änderung weiterhin funktioniert (obwohl ich damit gerechnet habe, dass er nicht funktioniert):

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

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

Anmerkungen:

C ++ / CLI ist die einzige .NET-Sprache, deren Konstrukt der expliziten Schnittstellenimplementierung für Mitglieder der virtuellen Basisklasse entspricht - "explizite Überschreibung". Ich habe voll und ganz erwartet, dass dies zu der gleichen Art von Bruch führt wie beim Verschieben von Schnittstellenmitgliedern auf eine Basisschnittstelle (da IL, das für die explizite Überschreibung generiert wird, dasselbe ist wie für die explizite Implementierung). Zu meiner Überraschung ist dies nicht der Fall - obwohl die generierte IL immer noch angibt, dass BarOverrideÜberschreibungen Foo::Barund nicht der FooBase::BarAssembly Loader intelligent genug sind, um sich ohne Beschwerden korrekt durch einen anderen zu ersetzen -, macht anscheinend die Tatsache, dass Fooes sich um eine Klasse handelt, den Unterschied aus. Stelle dir das vor...


3
Solange sich die Basisklasse in derselben Assembly befindet. Andernfalls handelt es sich um eine binäre Änderung.
Jeremy

@ Jeremy, welche Art von Code bricht in diesem Fall? Wird die Verwendung von Baz () durch einen externen Anrufer unterbrochen oder ist dies nur ein Problem bei Personen, die versuchen, Foo zu erweitern und Baz () zu überschreiben?
ChaseMedallion

@ChaseMedallion es bricht, wenn Sie ein Benutzer aus zweiter Hand sind. Beispielsweise verweist die kompilierte DLL auf eine ältere Version von Foo, und Sie verweisen auf diese kompilierte DLL, verwenden jedoch auch eine neuere Version der Foo-DLL. Es bricht mit einem seltsamen Fehler, oder zumindest für mich in Bibliotheken, die ich zuvor entwickelt habe.
Jeremy

19

Dies ist ein vielleicht nicht so offensichtlicher Sonderfall des "Hinzufügens / Entfernens von Schnittstellenmitgliedern", und ich dachte, er verdient seinen eigenen Eintrag angesichts eines anderen Falls, den ich als nächstes veröffentlichen werde. So:

Refactoring von Schnittstellenmitgliedern in eine Basisschnittstelle

Art: Unterbrechungen sowohl auf Quell- als auch auf Binärebene

Betroffene Sprachen: C #, VB, C ++ / CLI, F # (für Quellumbruch; binär wirkt sich natürlich auf jede Sprache aus)

API vor Änderung:

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

API nach Änderung:

interface IFooBase 
{
    void Bar();
}

interface IFoo : IFooBase
{
    void Baz();
}

Beispiel für einen Clientcode, der durch Änderungen auf Quellenebene beschädigt wird:

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

Beispiel für einen Clientcode, der durch Änderung auf Binärebene beschädigt wird;

(new Foo()).Bar();

Anmerkungen:

Das Problem besteht darin, dass C #, VB und C ++ / CLI für die Unterbrechung der Quellenebene alle einen genauen Schnittstellennamen in der Deklaration der Implementierung des Schnittstellenmitglieds erfordern . Wenn das Mitglied auf eine Basisschnittstelle verschoben wird, wird der Code nicht mehr kompiliert.

Die binäre Unterbrechung ist auf die Tatsache zurückzuführen, dass die Schnittstellenmethoden in der generierten IL für explizite Implementierungen vollständig qualifiziert sind und der Schnittstellenname dort ebenfalls genau sein muss.

Die implizite Implementierung, sofern verfügbar (dh C # und C ++ / CLI, jedoch nicht VB), funktioniert sowohl auf Quell- als auch auf Binärebene einwandfrei. Methodenaufrufe werden auch nicht unterbrochen.


Das gilt nicht für alle Sprachen. Für VB ist dies keine Änderung des Quellcodes. Für C # ist es.
Jeremy

So Implements IFoo.Barwird transparent Referenz IFooBase.Bar?
Pavel Minaev

Ja, tatsächlich können Sie ein Mitglied direkt oder indirekt über die erbende Schnittstelle referenzieren, wenn Sie es implementieren. Dies ist jedoch immer eine brechende binäre Änderung.
Jeremy

15

Aufzählung der aufgezählten Werte

Art der Unterbrechung: Änderung der stillen Semantik auf Quellen- / Binärebene

Betroffene Sprachen: alle

Durch das Neuordnen von Aufzählungswerten bleibt die Kompatibilität auf Quellenebene erhalten, da Literale denselben Namen haben, ihre Ordnungsindizes jedoch aktualisiert werden, was zu einigen Arten stiller Unterbrechungen auf Quellenebene führen kann.

Noch schlimmer sind die stillen Unterbrechungen auf Binärebene, die eingeführt werden können, wenn der Clientcode nicht mit der neuen API-Version neu kompiliert wird. Aufzählungswerte sind Konstanten zur Kompilierungszeit, und als solche werden alle Verwendungen davon in die IL der Client-Assembly eingebrannt. Dieser Fall kann manchmal besonders schwer zu erkennen sein.

API vor Änderung

public enum Foo
{
   Bar,
   Baz
}

API nach Änderung

public enum Foo
{
   Baz,
   Bar
}

Beispiel für einen Clientcode, der funktioniert, aber danach fehlerhaft ist:

Foo.Bar < Foo.Baz

12

Dieser ist in der Praxis wirklich sehr selten, aber dennoch überraschend, wenn er passiert.

Hinzufügen neuer nicht überladener Mitglieder

Art: Unterbrechung der Quellenebene oder Änderung der stillen Semantik.

Betroffene Sprachen: C #, VB

Nicht betroffene Sprachen: F #, C ++ / CLI

API vor Änderung:

public class Foo
{
}

API nach Änderung:

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

Beispiel für einen Clientcode, der durch Änderung beschädigt wird:

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

Anmerkungen:

Das Problem wird hier durch die Inferenz des Lambda-Typs in C # und VB bei Vorhandensein einer Überlastauflösung verursacht. Eine begrenzte Form der Ententypisierung wird hier verwendet, um Bindungen zu lösen, bei denen mehr als ein Typ übereinstimmt, indem geprüft wird, ob der Lambda-Körper für einen bestimmten Typ sinnvoll ist - wenn nur ein Typ zu einem kompilierbaren Körper führt, wird dieser ausgewählt.

Hier besteht die Gefahr, dass Client-Code eine überladene Methodengruppe aufweist, in der einige Methoden Argumente seines eigenen Typs und andere Argumente von Typen verwenden, die von Ihrer Bibliothek verfügbar gemacht werden. Wenn sich einer seiner Codes dann auf einen Typinferenzalgorithmus stützt, um die richtige Methode zu bestimmen, die ausschließlich auf der Anwesenheit oder Abwesenheit von Mitgliedern basiert, kann das Hinzufügen eines neuen Mitglieds zu einem Ihrer Typen mit demselben Namen wie in einem der Clienttypen möglicherweise eine Inferenz auslösen Aus, was zu Mehrdeutigkeiten während der Überlastungsauflösung führt.

Beachten Sie, dass Typen FooundBar in diesem Beispiel in keiner Weise miteinander verbunden sind, weder durch Vererbung noch auf andere Weise. Die bloße Verwendung in einer einzelnen Methodengruppe reicht aus, um dies auszulösen. Wenn dies im Clientcode auftritt, haben Sie keine Kontrolle darüber.

Der obige Beispielcode zeigt eine einfachere Situation, in der dies eine Unterbrechung auf Quellenebene ist (dh Compilerfehler resultieren). Dies kann jedoch auch eine stille Semantikänderung sein, wenn die über Inferenz ausgewählte Überladung andere Argumente hatte, die andernfalls zu einer Rangfolge führen würden (z. B. optionale Argumente mit Standardwerten oder Typinkongruenz zwischen deklariertem und tatsächlichem Argument, das ein implizites Argument erfordert Umwandlung). In einem solchen Szenario schlägt die Überlastungsauflösung nicht mehr fehl, aber der Compiler wählt leise eine andere Überladung aus. In der Praxis ist es jedoch sehr schwierig, auf diesen Fall zu stoßen, ohne sorgfältig Methodensignaturen zu erstellen, um ihn absichtlich zu verursachen.


9

Konvertieren Sie eine implizite Schnittstellenimplementierung in eine explizite.

Art der Pause: Quelle und Binär

Betroffene Sprachen: Alle

Dies ist wirklich nur eine Variation der Änderung der Zugänglichkeit einer Methode - es ist nur ein wenig subtiler, da leicht übersehen werden kann, dass nicht jeder Zugriff auf die Methoden einer Schnittstelle notwendigerweise durch einen Verweis auf den Typ der Schnittstelle erfolgt.

API vor Änderung:

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

API nach Änderung:

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

Beispiel für einen Client-Code, der vor der Änderung funktioniert und danach fehlerhaft ist:

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

7

Konvertieren Sie eine explizite Schnittstellenimplementierung in eine implizite.

Art der Pause: Quelle

Betroffene Sprachen: Alle

Das Refactoring einer expliziten Schnittstellenimplementierung in eine implizite ist subtiler darin, wie eine API beschädigt werden kann. Oberflächlich betrachtet scheint dies relativ sicher zu sein. In Kombination mit der Vererbung kann dies jedoch zu Problemen führen.

API vor Änderung:

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

API nach Änderung:

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

Beispiel für einen Client-Code, der vor der Änderung funktioniert und danach fehlerhaft ist:

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"

Entschuldigung, ich folge nicht ganz - sicherlich würde der Beispielcode vor der API-Änderung überhaupt nicht kompiliert werden, da vor der Änderung Fookeine öffentliche Methode namens benannt GetEnumeratorwurde und Sie die Methode über eine Referenz vom Typ aufrufen Foo. .
Pavel Minaev

In der Tat habe ich versucht, ein Beispiel aus dem Gedächtnis zu vereinfachen, und es endete mit "foobar" (entschuldigen Sie das Wortspiel). Ich habe das Beispiel aktualisiert, um den Fall korrekt zu demonstrieren (und kompilierbar zu sein).
LBushkin

In meinem Beispiel wird das Problem nicht nur durch den Übergang einer Schnittstellenmethode von implizit zu öffentlich verursacht. Dies hängt davon ab, wie der C # -Compiler bestimmt, welche Methode in einer foreach-Schleife aufgerufen werden soll. Angesichts der Auflösungsregeln, die der Compiler verwendet, wechselt er von der Version in der abgeleiteten Klasse zur Version in der Basisklasse.
LBushkin

Du hast es vergessen yield return "Bar":) aber ja, ich sehe, wohin das jetzt führt - foreachruft immer die öffentliche Methode namens auf GetEnumerator, auch wenn es nicht die eigentliche Implementierung für ist IEnumerable.GetEnumerator. Dies scheint einen weiteren Blickwinkel zu haben: Selbst wenn Sie nur eine Klasse haben und diese IEnumerableexplizit implementiert wird , bedeutet dies, dass es sich um eine Änderung handelt, die eine Quelle bricht, wenn eine öffentliche Methode mit dem Namen hinzugefügt wird GetEnumerator, da foreachdiese Methode jetzt über die Schnittstellenimplementierung verwendet wird. Das gleiche Problem gilt auch für die IEnumeratorImplementierung ...
Pavel

6

Ändern eines Feldes in eine Eigenschaft

Art der Pause: API

Betroffene Sprachen: Visual Basic und C # *

Info: Wenn Sie ein normales Feld oder eine Variable in Visual Basic in eine Eigenschaft ändern, muss jeder externe Code, der auf dieses Mitglied verweist, neu kompiliert werden.

API vor Änderung:

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

API nach Änderung:

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    

Beispiel für einen Clientcode, der funktioniert, aber danach fehlerhaft ist:

Foo.Bar = "foobar"

2
Tatsächlich würde dies auch in C # zu Problemen führen, da Eigenschaften outund refArgumente von Methoden im Gegensatz zu Feldern nicht verwendet werden können und nicht das Ziel des unären &Operators sein können.
Pavel Minaev

5

Namespace-Addition

Pause auf Quellenebene / Änderung der stillen Semantik auf Quellenebene

Aufgrund der Funktionsweise der Namespace-Auflösung in vb.Net kann das Hinzufügen eines Namespace zu einer Bibliothek dazu führen, dass Visual Basic-Code, der mit einer früheren Version der API kompiliert wurde, nicht mit einer neuen Version kompiliert wird.

Beispiel für einen Clientcode:

Imports System
Imports Api.SomeNamespace

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

Wenn eine neue Version der API den Namespace hinzufügt Api.SomeNamespace.Data, wird der obige Code nicht kompiliert.

Bei Namespace-Importen auf Projektebene wird dies komplizierter. Wenn Imports Systemder obige Code nicht enthalten ist, der SystemNamespace jedoch auf Projektebene importiert wird, kann der Code dennoch zu einem Fehler führen.

Wenn die API jedoch eine Klasse DataRowin ihren Api.SomeNamespace.DataNamespace einschließt , wird der Code kompiliert, drist jedoch eine Instanz dessen, System.Data.DataRowwenn er mit der alten Version der API und Api.SomeNamespace.Data.DataRowmit der neuen Version der API kompiliert wird.

Umbenennen von Argumenten

Unterbrechung auf Quellenebene

Das Ändern der Namen von Argumenten ist eine grundlegende Änderung in vb.net von Version 7 (?) (.Net Version 1?) Und c # .net von Version 4 (.Net Version 4).

API vor Änderung:

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

API nach Änderung:

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

Beispiel für einen Clientcode:

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

Ref Parameter

Unterbrechung auf Quellenebene

Wenn Sie eine Methodenüberschreibung mit derselben Signatur hinzufügen, mit der Ausnahme, dass ein Parameter als Referenz und nicht als Wert übergeben wird, kann die vb-Quelle, die auf die API verweist, die Funktion nicht auflösen. Visual Basic hat keine Möglichkeit (?), Diese Methoden am Aufrufpunkt zu unterscheiden, es sei denn, sie haben unterschiedliche Argumentnamen. Eine solche Änderung kann dazu führen, dass beide Mitglieder vom vb-Code nicht mehr verwendet werden können.

API vor Änderung:

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

API nach Änderung:

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

Beispiel für einen Clientcode:

Api.SomeNamespace.Foo.Bar(str)

Feld zur Eigenschaftsänderung

Unterbrechung auf Binärebene / Unterbrechung auf Quellenebene

Neben der offensichtlichen Unterbrechung auf Binärebene kann dies zu einer Unterbrechung auf Quellenebene führen, wenn das Element als Referenz an eine Methode übergeben wird.

API vor Änderung:

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

API nach Änderung:

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

Beispiel für einen Clientcode:

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

4

API-Änderung:

  1. Hinzufügen des Attributs [Veraltet] (Sie haben dies durch Erwähnen von Attributen abgedeckt; dies kann jedoch eine wichtige Änderung sein, wenn Sie "Warnung als Fehler" verwenden.)

Pause auf Binärebene:

  1. Verschieben eines Typs von einer Baugruppe in eine andere
  2. Ändern des Namespace eines Typs
  3. Hinzufügen eines Basisklassentyps aus einer anderen Assembly.
  4. Hinzufügen eines neuen Mitglieds (ereignisgeschützt), das einen Typ aus einer anderen Assembly (Klasse 2) als Einschränkung für Vorlagenargumente verwendet.

    protected void Something<T>() where T : Class2 { }
  5. Ändern einer untergeordneten Klasse (Klasse 3), um sie von einem Typ in einer anderen Assembly abzuleiten, wenn die Klasse als Vorlagenargument für diese Klasse verwendet wird.

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

Änderung der stillen Semantik auf Quellenebene:

  1. Hinzufügen / Entfernen / Ändern von Überschreibungen von Equals (), GetHashCode () oder ToString ()

(nicht sicher, wo diese passen)

Bereitstellungsänderungen:

  1. Hinzufügen / Entfernen von Abhängigkeiten / Referenzen
  2. Aktualisieren von Abhängigkeiten auf neuere Versionen
  3. Ändern der 'Zielplattform' zwischen x86, Itanium, x64 oder anycpu
  4. Erstellen / Testen auf einer anderen Framework-Installation (dh Installieren von 3.5 auf einer .NET 2.0-Box ermöglicht API-Aufrufe, für die dann .NET 2.0 SP2 erforderlich ist)

Bootstrap / Konfigurationsänderungen:

  1. Hinzufügen / Entfernen / Ändern von benutzerdefinierten Konfigurationsoptionen (dh App.config-Einstellungen)
  2. Aufgrund der starken Verwendung von IoC / DI in heutigen Anwendungen ist es erforderlich, den Bootstrapping-Code für DI-abhängigen Code neu zu konfigurieren und / oder zu ändern.

Aktualisieren:

Entschuldigung, ich wusste nicht, dass der einzige Grund, warum dies für mich nicht funktionierte, darin bestand, dass ich sie in Vorlagenbeschränkungen verwendet habe.


"Hinzufügen eines neuen Mitglieds (ereignisgeschützt), das einen Typ aus einer anderen Assembly verwendet." - IIRC, der Client muss nur auf die abhängigen Assemblys verweisen, die Basistypen der Assemblys enthalten, auf die er bereits verweist. Es muss nicht auf Assemblys verweisen, die lediglich verwendet werden (selbst wenn Typen in Methodensignaturen enthalten sind). Da bin ich mir nicht 100% sicher. Haben Sie eine Referenz für genaue Regeln dafür? Außerdem kann das Verschieben eines Typs bei Verwendung nicht unterbrochen TypeForwardedToAttributewerden.
Pavel Minaev

Dass "TypeForwardedTo" für mich neu ist, werde ich mir ansehen. Was den anderen betrifft, bin ich auch nicht zu 100% dabei ... lass mich sehen, ob Repro kann und ich werde den Beitrag aktualisieren.
csharptest.net

-WerrorErzwingen Sie also nicht das Buildsystem, das Sie mit Release-Tarballs versenden. Dieses Flag ist für den Entwickler des Codes am hilfreichsten und für den Verbraucher meistens nicht hilfreich.
Binki

@binki ausgezeichneter Punkt, Warnungen als Fehler zu behandeln sollte nur in DEBUG-Builds ausreichen.
csharptest.net

3

Hinzufügen von Überladungsmethoden, um die Verwendung von Standardparametern zu verringern

Art der Unterbrechung: Änderung der stillen Semantik auf Quellenebene

Da der Compiler Methodenaufrufe mit fehlenden Standardparameterwerten in einen expliziten Aufruf mit dem Standardwert auf der aufrufenden Seite umwandelt, ist die Kompatibilität für vorhandenen kompilierten Code gegeben. Für den gesamten zuvor kompilierten Code wird eine Methode mit der richtigen Signatur gefunden.

Auf der anderen Seite werden Aufrufe ohne Verwendung optionaler Parameter jetzt als Aufruf der neuen Methode kompiliert, bei der der optionale Parameter fehlt. Es funktioniert immer noch einwandfrei, aber wenn sich der aufgerufene Code in einer anderen Assembly befindet, hängt der neu kompilierte Code, der ihn aufruft, jetzt von der neuen Version dieser Assembly ab. Das Bereitstellen von Assemblys, die den überarbeiteten Code aufrufen, ohne auch die Assembly bereitzustellen, in der sich der überarbeitete Code befindet, führt zu Ausnahmen "Methode nicht gefunden".

API vor Änderung

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

API nach Änderung

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

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

Beispielcode, der noch funktioniert

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

Beispielcode, der jetzt beim Kompilieren von der neuen Version abhängig ist

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

1

Schnittstelle umbenennen

Ein bisschen Pause: Quelle und Binär

Betroffene Sprachen: Höchstwahrscheinlich alle, getestet in C #.

API vor Änderung:

public interface IFoo
{
    void Test();
}

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

API nach Änderung:

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

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

Beispiel für einen Clientcode, der funktioniert, aber danach fehlerhaft ist:

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

1

Überladungsmethode mit einem Parameter vom Typ nullable

Art: Pause auf Quellenebene

Betroffene Sprachen: C #, VB

API vor einer Änderung:

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

API nach der Änderung:

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

Beispiel für einen Clientcode, der vor der Änderung funktioniert und danach fehlerhaft ist:

new Foo().Bar(null);

Ausnahme: Der Aufruf ist zwischen den folgenden Methoden oder Eigenschaften nicht eindeutig.


0

Beförderung zu einer Verlängerungsmethode

Art: Unterbrechung auf Quellenebene

Betroffene Sprachen: C # v6 und höher (vielleicht andere?)

API vor Änderung:

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

API nach Änderung:

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

Beispiel für einen Clientcode, der vor der Änderung funktioniert und danach beschädigt wird:

using static Foo;

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

Weitere Informationen: https://github.com/dotnet/csharplang/issues/665

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.