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_Explicit
und op_Implicit
Methoden 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 ExtensionMethodAttribute
bietet und erfordert, dass CLS-Erweiterungsmethoden als statische Methoden aufgerufen werden.
quelle
Antworten:
Ändern einer Methodensignatur
Art: Binäre Pause
Betroffene Sprachen: C # (VB und F # höchstwahrscheinlich, aber nicht getestet)
API vor Änderung
API nach Änderung
Beispiel für einen Clientcode, der vor der Änderung funktioniert
quelle
bar
.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
API nach Änderung
Beispiel für einen Clientcode, der anschließend beschädigt wird
Der Client-Code muss auf
Foo(5, null)
Bytecode-Ebene neu kompiliert werden . Die aufgerufene Assembly enthält nurFoo(int, string)
, nichtFoo(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).quelle
Func<int> f = Foo;
// Dies wird mit der geänderten SignaturDieser 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:
API nach Änderung:
Beispielcode, der während der Änderung weiterhin funktioniert (obwohl ich damit gerechnet habe, dass er nicht funktioniert):
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
ÜberschreibungenFoo::Bar
und nicht derFooBase::Bar
Assembly Loader intelligent genug sind, um sich ohne Beschwerden korrekt durch einen anderen zu ersetzen -, macht anscheinend die Tatsache, dassFoo
es sich um eine Klasse handelt, den Unterschied aus. Stelle dir das vor...quelle
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:
API nach Änderung:
Beispiel für einen Clientcode, der durch Änderungen auf Quellenebene beschädigt wird:
Beispiel für einen Clientcode, der durch Änderung auf Binärebene beschädigt wird;
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.
quelle
Implements IFoo.Bar
wird transparent ReferenzIFooBase.Bar
?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
API nach Änderung
Beispiel für einen Clientcode, der funktioniert, aber danach fehlerhaft ist:
quelle
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:
API nach Änderung:
Beispiel für einen Clientcode, der durch Änderung beschädigt wird:
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
Foo
undBar
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.
quelle
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:
API nach Änderung:
Beispiel für einen Client-Code, der vor der Änderung funktioniert und danach fehlerhaft ist:
quelle
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:
API nach Änderung:
Beispiel für einen Client-Code, der vor der Änderung funktioniert und danach fehlerhaft ist:
quelle
Foo
keine öffentliche Methode namens benanntGetEnumerator
wurde und Sie die Methode über eine Referenz vom Typ aufrufenFoo
. .yield return "Bar"
:) aber ja, ich sehe, wohin das jetzt führt -foreach
ruft immer die öffentliche Methode namens aufGetEnumerator
, auch wenn es nicht die eigentliche Implementierung für istIEnumerable.GetEnumerator
. Dies scheint einen weiteren Blickwinkel zu haben: Selbst wenn Sie nur eine Klasse haben und dieseIEnumerable
explizit implementiert wird , bedeutet dies, dass es sich um eine Änderung handelt, die eine Quelle bricht, wenn eine öffentliche Methode mit dem Namen hinzugefügt wirdGetEnumerator
, daforeach
diese Methode jetzt über die Schnittstellenimplementierung verwendet wird. Das gleiche Problem gilt auch für dieIEnumerator
Implementierung ...Ä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:
API nach Änderung:
Beispiel für einen Clientcode, der funktioniert, aber danach fehlerhaft ist:
quelle
out
undref
Argumente von Methoden im Gegensatz zu Feldern nicht verwendet werden können und nicht das Ziel des unären&
Operators sein können.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:
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 System
der obige Code nicht enthalten ist, derSystem
Namespace jedoch auf Projektebene importiert wird, kann der Code dennoch zu einem Fehler führen.Wenn die API jedoch eine Klasse
DataRow
in ihrenApi.SomeNamespace.Data
Namespace einschließt , wird der Code kompiliert,dr
ist jedoch eine Instanz dessen,System.Data.DataRow
wenn er mit der alten Version der API undApi.SomeNamespace.Data.DataRow
mit 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:
API nach Änderung:
Beispiel für einen Clientcode:
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:
API nach Änderung:
Beispiel für einen Clientcode:
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:
API nach Änderung:
Beispiel für einen Clientcode:
quelle
API-Änderung:
Pause auf Binärebene:
Hinzufügen eines neuen Mitglieds (ereignisgeschützt), das einen Typ aus einer anderen Assembly (Klasse 2) als Einschränkung für Vorlagenargumente verwendet.
Ä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.
Änderung der stillen Semantik auf Quellenebene:
(nicht sicher, wo diese passen)
Bereitstellungsänderungen:
Bootstrap / Konfigurationsänderungen:
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.
quelle
TypeForwardedToAttribute
werden.-Werror
Erzwingen 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.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
API nach Änderung
Beispielcode, der noch funktioniert
Beispielcode, der jetzt beim Kompilieren von der neuen Version abhängig ist
quelle
Schnittstelle umbenennen
Ein bisschen Pause: Quelle und Binär
Betroffene Sprachen: Höchstwahrscheinlich alle, getestet in C #.
API vor Änderung:
API nach Änderung:
Beispiel für einen Clientcode, der funktioniert, aber danach fehlerhaft ist:
quelle
Überladungsmethode mit einem Parameter vom Typ nullable
Art: Pause auf Quellenebene
Betroffene Sprachen: C #, VB
API vor einer Änderung:
API nach der Änderung:
Beispiel für einen Clientcode, der vor der Änderung funktioniert und danach fehlerhaft ist:
Ausnahme: Der Aufruf ist zwischen den folgenden Methoden oder Eigenschaften nicht eindeutig.
quelle
Beförderung zu einer Verlängerungsmethode
Art: Unterbrechung auf Quellenebene
Betroffene Sprachen: C # v6 und höher (vielleicht andere?)
API vor Änderung:
API nach Änderung:
Beispiel für einen Clientcode, der vor der Änderung funktioniert und danach beschädigt wird:
Weitere Informationen: https://github.com/dotnet/csharplang/issues/665
quelle