Chciałbym zebrać jak najwięcej informacji dotyczących wersjonowania API w .NET / CLR, a konkretnie, w jaki sposób zmiany API niszczą lub nie psują aplikacji klienckich. Najpierw zdefiniujmy niektóre terminy:
Zmiana interfejsu API - zmiana publicznie widocznej definicji typu, w tym dowolnego z jego publicznych elementów. Obejmuje to zmianę typu i nazwy członka, zmianę typu podstawowego typu, dodawanie / usuwanie interfejsów z listy zaimplementowanych interfejsów typu, dodawanie / usuwanie członków (w tym przeciążeń), zmianę widoczności członka, zmianę nazwy metody i parametrów typu, dodawanie wartości domyślnych dla parametrów metody, dodawania / usuwania atrybutów typów i członków oraz dodawania / usuwania ogólnych parametrów typów dla typów i członków (czy coś przegapiłem?). Nie obejmuje to żadnych zmian w organach członkowskich ani żadnych zmian w członkach prywatnych (tj. Nie bierzemy pod uwagę Odbicia).
Przerwa na poziomie binarnym - zmiana interfejsu API, która powoduje, że zestawy klientów kompilowane na podstawie starszej wersji interfejsu API potencjalnie nie ładują się z nową wersją. Przykład: zmiana sygnatury metody, nawet jeśli pozwala na wywołanie w taki sam sposób, jak poprzednio (tj. Void, aby zwrócić typ / parametr wartości domyślne przeciążenia).
Przerwa na poziomie źródła - zmiana interfejsu API, która powoduje, że istniejący kod został napisany w celu skompilowania ze starszą wersją interfejsu API potencjalnie nie kompilując się z nową wersją. Jednak już skompilowane zestawy klienckie działają tak jak wcześniej. Przykład: dodanie nowego przeciążenia, które może powodować niejednoznaczność wywołań metod, które były jednoznaczne poprzednio.
Cicha semantyka na poziomie źródła - zmiana API, która powoduje, że istniejący kod napisany w celu kompilacji ze starszą wersją API cicho zmienia semantykę, np. Przez wywołanie innej metody. Kod powinien jednak nadal się kompilować bez ostrzeżeń / błędów, a wcześniej skompilowane zestawy powinny działać jak poprzednio. Przykład: implementacja nowego interfejsu w istniejącej klasie, która powoduje wybranie innego przeciążenia podczas rozwiązywania problemu przeciążenia.
Ostatecznym celem jest skatalogowanie jak największej liczby łamliwych i cichych semantyki zmian w interfejsie API oraz opisanie dokładnego efektu złamania oraz tego, na jakie języki nie ma on wpływu. Aby rozwinąć ten drugi: podczas gdy niektóre zmiany dotyczą wszystkich języków uniwersalnie (np. Dodanie nowego elementu do interfejsu spowoduje przerwanie implementacji tego interfejsu w dowolnym języku), niektóre wymagają bardzo specyficznej semantyki języka, aby wejść do gry, aby uzyskać przerwę. Zwykle wiąże się to z przeciążeniem metod i ogólnie wszystkim, co ma związek z konwersjami typu niejawnego. Wydaje się, że nie ma tutaj sposobu na zdefiniowanie „najmniej wspólnego mianownika” nawet dla języków zgodnych z CLS (tj. Tych, które są zgodne co najmniej z regułami „konsumenta CLS” zdefiniowanymi w specyfikacji CLI) - chociaż ja ” Będę wdzięczny, jeśli ktoś poprawi mnie, że się tutaj mylę - więc będzie musiał przejść język po języku. Najbardziej interesujące są oczywiście te, które są dostarczane z .NET po wyjęciu z pudełka: C #, VB i F #; ale inne, takie jak IronPython, IronRuby, Delphi Prism itp. są również istotne. Im bardziej jest to przypadek narożny, tym bardziej interesujące będzie - usuwanie elementów jest dość oczywiste, ale subtelne interakcje między np. Przeciążeniem metody, parametrami opcjonalnymi / domyślnymi, wnioskowaniem typu lambda i operatorami konwersji mogą być bardzo zaskakujące czasami.
Kilka przykładów na rozpoczęcie:
Dodawanie przeciążeń nowej metody
Rodzaj: przerwa na poziomie źródła
Języki, których to dotyczy: C #, VB, F #
API przed zmianą:
public class Foo
{
public void Bar(IEnumerable x);
}
API po zmianie:
public class Foo
{
public void Bar(IEnumerable x);
public void Bar(ICloneable x);
}
Przykładowy kod klienta działający przed zmianą i zepsuty po nim:
new Foo().Bar(new int[0]);
Dodanie nowych przeciążeń operatora niejawnej konwersji
Rodzaj: przerwa na poziomie źródła.
Języki, których to dotyczy: C #, VB
Nie dotyczy języków: F #
API przed zmianą:
public class Foo
{
public static implicit operator int ();
}
API po zmianie:
public class Foo
{
public static implicit operator int ();
public static implicit operator float ();
}
Przykładowy kod klienta działający przed zmianą i zepsuty po nim:
void Bar(int x);
void Bar(float x);
Bar(new Foo());
Uwagi: F # nie jest zepsuty, ponieważ nie ma żadnego wsparcia na poziomie języka dla przeciążonych operatorów, ani jawnych, ani niejawnych - oba muszą być wywoływane bezpośrednio jako op_Explicit
i op_Implicit
metody.
Dodanie nowych metod instancji
Rodzaj: cicha semantyka na poziomie źródła.
Języki, których to dotyczy: C #, VB
Nie dotyczy języków: F #
API przed zmianą:
public class Foo
{
}
API po zmianie:
public class Foo
{
public void Bar();
}
Przykładowy kod klienta, który podlega cichej zmianie semantyki:
public static class FooExtensions
{
public void Bar(this Foo foo);
}
new Foo().Bar();
Uwagi: F # nie jest zepsuty, ponieważ nie obsługuje języka ExtensionMethodAttribute
i wymaga wywoływania metod rozszerzenia CLS jako metod statycznych.