Ostateczny przewodnik po przełamujących API zmianach w .NET

227

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_Expliciti op_Implicitmetody.

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 ExtensionMethodAttributei wymaga wywoływania metod rozszerzenia CLS jako metod statycznych.

Pavel Minaev
źródło
Na pewno Microsoft już to omawia ... msdn.microsoft.com/en-us/netframework/aa570326.aspx
Robert Harvey
1
@Robert: twój link dotyczy czegoś zupełnie innego - opisuje konkretne przełomowe zmiany w samym .NET Framework . To szersze pytanie opisuje ogólne wzorce, które mogą wprowadzać przełomowe zmiany we własnych interfejsach API (jako autor biblioteki / frameworka). Nie znam żadnego takiego dokumentu od MS, który byłby kompletny, chociaż wszelkie linki do takich, nawet jeśli niekompletne, są zdecydowanie mile widziane.
Pavel Minaev
Czy w którejkolwiek z tych kategorii „przerwy” występuje problem, który ujawni się dopiero w czasie wykonywania?
Rohit
1
Tak, kategoria „przerwa binarna”. W takim przypadku masz już zestaw innej firmy skompilowany dla wszystkich jego wersji. Jeśli upuścisz nową wersję zestawu na miejscu, zespół innych producentów przestanie działać - albo po prostu nie ładuje się w czasie wykonywania, albo działa niepoprawnie.
Pavel Minaev
3
Dodałbym je w poście i komentarzach blogs.msdn.com/b/ericlippert/archive/2012/01/09/…
Łukasz Madon

Odpowiedzi:

42

Zmiana podpisu metody

Rodzaj: Break na poziomie binarnym

Języki, których to dotyczy: C # (VB i F # najprawdopodobniej, ale niesprawdzone)

API przed zmianą

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

API po zmianie

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

Przykładowy kod klienta działający przed zmianą

Foo.bar(13);
Justin Drury
źródło
15
W rzeczywistości może to być przerwa na poziomie źródła, jeśli ktoś spróbuje utworzyć delegata dla bar.
Pavel Minaev
To też prawda. Znalazłem ten szczególny problem, kiedy wprowadziłem pewne zmiany w narzędziach drukujących w aplikacji mojej firmy. Kiedy aktualizacja została wydana, nie wszystkie biblioteki DLL, które odwoływały się do tych narzędzi, zostały ponownie skompilowane i wydane, więc wygenerowano wyjątek od nieznanej metody.
Justin Drury
1
Wraca to do faktu, że typy zwracane nie liczą się do podpisu metody. Nie można również przeciążać dwóch funkcji opartych wyłącznie na typie zwrotu. Taki sam problem.
Jason Short,
1
pytanie do tej odpowiedzi: czy ktoś zna konsekwencje dodania wartości domyślnej dotnet4 „public static void bar (int i = 0);” czy zmieniasz tę wartość domyślną z jednej wartości na inną?
k3b
1
Dla tych, którzy zamierzają wylądować na tej stronie , myślę dla C # (i „Myślę, że” większość innych języków OOP), Typy Zwrotu nie przyczyniają się do podpisu metody. Tak, odpowiedź jest słuszna, że ​​zmiany podpisu przyczyniają się do zmiany poziomu binarnego. ALE ten przykład nie wydaje się poprawny IMHO poprawny przykład, który mogę myśleć, jest PRZED publiczną sumą dziesiętną (int a, int b) Po publicznej sumie dziesiętnej (dziesiętna a, dziesiętna b) Prosimy odnieść się do tego łącza MSDN 3.6 Sygnatury i przeciążenie
Bhanu Chhabra
40

Dodanie parametru o wartości domyślnej.

Kind of Break: przerwa na poziomie binarnym

Nawet jeśli wywołujący kod źródłowy nie musi się zmieniać, nadal wymaga ponownej kompilacji (podobnie jak w przypadku dodawania zwykłego parametru).

Jest tak, ponieważ C # kompiluje wartości domyślne parametrów bezpośrednio do zestawu wywołującego. Oznacza to, że jeśli nie dokonasz ponownej kompilacji, otrzymasz MissingMethodException, ponieważ stary zestaw próbuje wywołać metodę z mniejszą liczbą argumentów.

Interfejs API przed zmianą

public void Foo(int a) { }

API po zmianie

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

Przykładowy kod klienta, który jest następnie łamany

Foo(5);

Kod klienta musi zostać ponownie skompilowany na Foo(5, null)poziomie kodu bajtowego. Wywoływany zestaw będzie zawierał tylko Foo(int, string), a nie Foo(int). Jest tak, ponieważ domyślne wartości parametrów są wyłącznie funkcją językową, środowisko wykonawcze .Net nic o nich nie wie. (To wyjaśnia również, dlaczego wartości domyślne muszą być stałymi w czasie kompilacji w C #).

Eldritch Conundrum
źródło
2
jest to przełomowa zmiana nawet na poziomie kodu źródłowego: Func<int> f = Foo;// to się nie powiedzie ze zmienionym podpisem
Vagaus
26

Ten był bardzo nieoczywisty, kiedy go odkryłem, zwłaszcza w świetle różnicy w tej samej sytuacji dla interfejsów. To wcale nie jest przerwa, ale zaskakujące jest to, że zdecydowałem się dołączyć:

Przekształcenie członków klasy w klasę podstawową

Rodzaj: bez przerwy!

Języki, których dotyczy problem: brak (tzn. Żaden nie jest uszkodzony)

API przed zmianą:

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

API po zmianie:

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

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

Przykładowy kod, który działa przez całą zmianę (mimo że spodziewałem się, że się zepsuje):

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

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

Uwagi:

C ++ / CLI jest jedynym językiem .NET, który ma konstrukcję analogiczną do implementacji interfejsu jawnego dla członków wirtualnej klasy podstawowej - „jawne zastąpienie”. W pełni spodziewałem się, że spowoduje to taki sam rodzaj zepsucia, jak przy przenoszeniu elementów interfejsu do interfejsu podstawowego (ponieważ IL wygenerowana dla jawnego zastąpienia jest taka sama jak dla jawnej implementacji). Ku mojemu zdziwieniu tak nie jest - mimo że wygenerowana IL nadal określa, że BarOverridezastępuje, Foo::Bara FooBase::Barmoduł ładujący jest wystarczająco inteligentny, aby zastąpić się nawzajem poprawnie bez żadnych skarg - najwyraźniej Footo, co robi różnicę , jest klasą. Domyśl...

Pavel Minaev
źródło
3
Tak długo, jak klasa podstawowa znajduje się w tym samym zestawie. W przeciwnym razie jest to zmiana binarna.
Jeremy,
@Jeremy, jaki rodzaj kodu psuje się w takim przypadku? Czy korzystanie z Baz () przez dowolnego dzwoniącego zewnętrznego zostanie zerwane, czy jest to tylko problem z ludźmi, którzy próbują rozszerzyć Foo i zastąpić Baz ()?
ChaseMedallion
@ChaseMedallion łamie się, jeśli jesteś użytkownikiem używanym. Na przykład skompilowana biblioteka DLL odwołuje się do starszej wersji Foo, a ty odwołujesz się do skompilowanej biblioteki DLL, ale także używasz nowszej wersji biblioteki Foo DLL. Łamie się z dziwnym błędem, a przynajmniej zrobił to dla mnie w bibliotekach, które wcześniej opracowałem.
Jeremy,
19

Ten jest być może nie tak oczywistym szczególnym przypadkiem „dodawania / usuwania elementów interfejsu”, i pomyślałem, że zasługuje na własny wpis w świetle innego przypadku, który zamierzam opublikować w następnej kolejności. Więc:

Refaktoryzacja elementów interfejsu do interfejsu podstawowego

Rodzaj: zrywa na poziomie źródłowym i binarnym

Języki, których dotyczy problem: C #, VB, C ++ / CLI, F # (dla przerwania źródła; binarny naturalnie wpływa na dowolny język)

API przed zmianą:

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

API po zmianie:

interface IFooBase 
{
    void Bar();
}

interface IFoo : IFooBase
{
    void Baz();
}

Przykładowy kod klienta, który jest uszkodzony przez zmianę na poziomie źródła:

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

Przykładowy kod klienta, który jest uszkodzony przez zmianę na poziomie binarnym;

(new Foo()).Bar();

Uwagi:

W przypadku podziału poziomu źródła problem polega na tym, że C #, VB i C ++ / CLI wymagają dokładnej nazwy interfejsu w deklaracji implementacji elementu interfejsu; dlatego jeśli element zostanie przeniesiony do interfejsu podstawowego, kod nie będzie się kompilował.

Przerwanie binarne wynika z faktu, że metody interfejsu są w pełni kwalifikowane w wygenerowanej IL do jawnych implementacji, a nazwa interfejsu musi być również dokładna.

Implikowana implementacja, jeśli jest dostępna (tj. C # i C ++ / CLI, ale nie VB) będzie działać dobrze zarówno na poziomie źródłowym, jak i binarnym. Wywołania metod też nie psują.

Pavel Minaev
źródło
Nie dotyczy to wszystkich języków. Dla VB nie jest to zrywająca zmiana kodu źródłowego. Dla C # to jest.
Jeremy,
Czy więc Implements IFoo.Barbędzie się odwoływać w przejrzysty sposób IFooBase.Bar?
Pavel Minaev,
Tak, faktycznie, możesz odwoływać się do członka bezpośrednio lub pośrednio poprzez interfejs dziedziczenia podczas jego implementacji. Jest to jednak zawsze przełomowa zmiana binarna.
Jeremy,
15

Zmiana kolejności wyliczonych wartości

Rodzaj przerwy: cicha semantyka na poziomie źródła / na poziomie binarnym

Dotyczy języków: wszystkie

Zmiana kolejności wyliczonych wartości zachowa zgodność na poziomie źródła, ponieważ literały mają tę samą nazwę, ale ich indeksy porządkowe zostaną zaktualizowane, co może powodować niektóre rodzaje cichych przerw na poziomie źródła.

Jeszcze gorzej jest cichy podział na poziomie binarnym, który można wprowadzić, jeśli kod klienta nie zostanie ponownie skompilowany z nową wersją API. Wartości wyliczane są stałymi czasami kompilacji i jako takie wszelkie ich zastosowania są zapisywane w IL zestawu klienta. Ten przypadek może być czasami szczególnie trudny do wykrycia.

Interfejs API przed zmianą

public enum Foo
{
   Bar,
   Baz
}

API po zmianie

public enum Foo
{
   Baz,
   Bar
}

Przykładowy kod klienta, który działa, ale później jest uszkodzony:

Foo.Bar < Foo.Baz
glopes
źródło
12

To jest naprawdę bardzo rzadka rzecz w praktyce, ale mimo to zaskakująca, kiedy to się dzieje.

Dodawanie nowych nieobciążonych członków

Rodzaj: przerwa na poziomie źródła lub cicha zmiana semantyki.

Języki, których to dotyczy: C #, VB

Nie dotyczy języków: F #, C ++ / CLI

API przed zmianą:

public class Foo
{
}

API po zmianie:

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

Przykładowy kod klienta, który jest uszkodzony przez zmianę:

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

Uwagi:

Problem jest spowodowany wnioskowaniem typu lambda w C # i VB w obecności rozdzielczości przeciążenia. Stosuje się tutaj ograniczoną formę pisania kaczki, aby zerwać więzi, w których pasuje więcej niż jeden typ, sprawdzając, czy ciało lambda ma sens dla danego typu - jeśli tylko jeden typ daje kompilowalną treść, to ten jest wybierany.

Niebezpieczeństwo polega na tym, że kod klienta może mieć przeciążoną grupę metod, w której niektóre metody przyjmują argumenty własnych typów, a inne przyjmują argumenty typów ujawnione przez bibliotekę. Jeśli któryś z jego kodów opiera się następnie na algorytmie wnioskowania typu, aby ustalić poprawną metodę opartą wyłącznie na obecności lub nieobecności członków, wówczas dodanie nowego członka do jednego z typów o tej samej nazwie, co w jednym z typów klienta, może potencjalnie wyłączone, co powoduje niejednoznaczność podczas usuwania przeciążenia.

Zauważ, że typy Fooi Barw tym przykładzie nie są w żaden sposób powiązane, ani przez dziedziczenie, ani w żaden inny sposób. Wystarczy ich użycie w jednej grupie metod, aby to wyzwolić, a jeśli zdarzy się to w kodzie klienta, nie masz nad tym kontroli.

Przykładowy kod powyżej pokazuje prostszą sytuację, w której jest to przerwa na poziomie źródła (tj. Wyniki błędów kompilatora). Może to jednak być również cicha zmiana semantyki, jeśli przeciążenie wybrane za pomocą wnioskowania zawiera inne argumenty, które w przeciwnym razie spowodowałyby, że zostanie on uszeregowany poniżej (np. Argumenty opcjonalne z wartościami domyślnymi lub niedopasowanie typu między argumentem zadeklarowanym a rzeczywistym wymagającym niejawnego argumentu konwersja). W takim scenariuszu rozdzielczość przeciążenia już nie zawiedzie, ale kompilator po cichu wybierze inne przeciążenie. W praktyce jednak bardzo trudno jest napotkać ten przypadek bez starannego tworzenia sygnatur metod, aby celowo go spowodować.

Pavel Minaev
źródło
9

Przekształć implementację interfejsu niejawnego na jawny.

Kind of Break: Source and Binary

Języki, których dotyczy problem: wszystkie

Jest to tak naprawdę tylko wariant zmiany dostępności metody - jest tylko trochę bardziej subtelny, ponieważ łatwo przeoczyć fakt, że nie każdy dostęp do metod interfejsu jest koniecznie przez odniesienie do typu interfejsu.

Interfejs API przed zmianą:

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

Interfejs API po zmianie:

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

Przykładowy kod klienta, który działa przed zmianą, a następnie ulega uszkodzeniu:

new Foo().GetEnumerator(); // fails because GetEnumerator() is no longer public
LBushkin
źródło
7

Konwertuj jawną implementację interfejsu na domyślną.

Kind of Break: Źródło

Języki, których dotyczy problem: wszystkie

Refaktoryzacja implementacji interfejsu jawnego na domyślną jest bardziej subtelna w tym, w jaki sposób może ona złamać interfejs API. Na pozór wydaje się, że powinno to być względnie bezpieczne, jednak w połączeniu z dziedziczeniem może powodować problemy.

Interfejs API przed zmianą:

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

Interfejs API po zmianie:

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

Przykładowy kod klienta, który działa przed zmianą, a następnie ulega uszkodzeniu:

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"
LBushkin
źródło
Niestety, nie do końca podążam - z pewnością przykładowy kod przed zmianą interfejsu API w ogóle się nie skompiluje, ponieważ wcześniej zmiana Foonie miała nazwy publicznej GetEnumerator, a metoda wywoływana jest za pomocą odwołania typu Foo.. ,
Pavel Minaev
Rzeczywiście, starałem się uprościć przykład z pamięci i skończyło się to „foobar” (wybacz pun). Zaktualizowałem przykład, aby poprawnie zademonstrować przypadek (i być kompatybilnym).
LBushkin
W moim przykładzie problem jest spowodowany czymś więcej niż tylko przejściem metody interfejsu z niejawnej na jawną. Zależy to od sposobu, w jaki kompilator C # określa, którą metodę wywołać w pętli foreach. Biorąc pod uwagę reguły dotyczące rozdzielczości kompilatora, przełącza się z wersji w klasie pochodnej na wersję w klasie bazowej.
LBushkin
Zapomniałeś yield return "Bar":) ale tak, widzę, dokąd to zmierza - foreachzawsze wywołuje publiczną metodę o nazwie GetEnumerator, nawet jeśli nie jest to prawdziwa implementacja IEnumerable.GetEnumerator. Wydaje się, że ma to jeszcze jeden kąt: nawet jeśli masz tylko jedną klasę i implementuje się ją IEnumerablejawnie, oznacza to, że dodanie zmiany nazwy metody publicznej, GetEnumeratorktóra ją foreachwywoła , jest przełomową zmianą źródła , ponieważ teraz będzie ona używać tej metody zamiast implementacji interfejsu. Ten sam problem dotyczy również IEnumeratorwdrażania ...
Pavel Minaev
6

Zmiana pola na właściwość

Kind of Break: API

Języki, których dotyczy problem: Visual Basic i C # *

Informacja: Kiedy zmienisz normalne pole lub zmienną na właściwość w języku Visual Basic, każdy kod zewnętrzny odnoszący się do tego elementu w jakikolwiek sposób będzie musiał zostać ponownie skompilowany.

Interfejs API przed zmianą:

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

Interfejs API po zmianie:

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    

Przykładowy kod klienta, który działa, ale później jest uszkodzony:

Foo.Bar = "foobar"
Hagelt18
źródło
2
W rzeczywistości spowodowałoby to również uszkodzenie w języku C #, ponieważ nie można używać właściwości outi refargumentów metod, w przeciwieństwie do pól, i nie może być celem &operatora jednoargumentowego .
Pavel Minaev,
5

Dodawanie przestrzeni nazw

Przerwa na poziomie źródła / cicha semantyka na poziomie źródła

Ze względu na sposób, w jaki działa rozpoznawanie przestrzeni nazw w vb.Net, dodanie przestrzeni nazw do biblioteki może spowodować, że kod Visual Basic skompilowany z poprzednią wersją interfejsu API nie skompiluje się z nową wersją.

Przykładowy kod klienta:

Imports System
Imports Api.SomeNamespace

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

Jeśli nowa wersja interfejsu API doda przestrzeń nazw Api.SomeNamespace.Data, powyższy kod nie zostanie skompilowany.

Staje się to bardziej skomplikowane przy imporcie przestrzeni nazw na poziomie projektu. Jeśli Imports Systemzostanie pominięty w powyższym kodzie, ale Systemprzestrzeń nazw zostanie zaimportowana na poziomie projektu, kod może nadal powodować błąd.

Jeśli jednak interfejs API zawiera klasę DataRoww Api.SomeNamespace.Dataprzestrzeni nazw, kod zostanie skompilowany, ale drbędzie to przypadek, System.Data.DataRowgdy zostanie skompilowany ze starą wersją interfejsu API i Api.SomeNamespace.Data.DataRowpo skompilowaniu z nową wersją interfejsu API.

Zmiana nazwy argumentu

Przerwa na poziomie źródła

Zmiana nazw argumentów jest przełomową zmianą w vb.net od wersji 7 (?) (.Net wersja 1?) I c # .net od wersji 4 (.Net wersja 4).

API przed zmianą:

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

API po zmianie:

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

Przykładowy kod klienta:

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

Parametry ref

Przerwa na poziomie źródła

Dodanie zastąpienia metody z tą samą sygnaturą, z tym wyjątkiem, że jeden parametr jest przekazywany przez referencję zamiast przez wartość spowoduje, że źródło vb, które odwołuje się do interfejsu API, nie będzie w stanie rozwiązać funkcji. Visual Basic nie ma możliwości (?) Odróżnienia tych metod w punkcie wywołania, chyba że mają one różne nazwy argumentów, więc taka zmiana może spowodować, że oba elementy nie będą nadawać się do użycia z kodu VB.

API przed zmianą:

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

API po zmianie:

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

Przykładowy kod klienta:

Api.SomeNamespace.Foo.Bar(str)

Zmiana pola na właściwość

Przerwa na poziomie binarnym / Przerwa na poziomie źródła

Oprócz oczywistej przerwy na poziomie binarnym może to powodować przerwanie na poziomie źródła, jeśli element członkowski zostanie przekazany do metody przez odwołanie.

API przed zmianą:

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

API po zmianie:

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

Przykładowy kod klienta:

FooBar(ref Api.SomeNamespace.Foo.Bar);
jswolf19
źródło
4

Zmiana interfejsu API:

  1. Dodanie atrybutu [Przestarzałe] (w pewnym sensie opisałeś atrybuty, ale może to być przełomowa zmiana w przypadku używania ostrzeżenia jako błędu).

Przerwa na poziomie binarnym:

  1. Przenoszenie typu z jednego zestawu do drugiego
  2. Zmiana przestrzeni nazw typu
  3. Dodanie typu klasy bazowej z innego zestawu.
  4. Dodanie nowego elementu (chronionego przed zdarzeniem), który wykorzystuje typ z innego zestawu (Klasa 2) jako ograniczenie argumentu szablonu.

    protected void Something<T>() where T : Class2 { }
  5. Zmiana klasy potomnej (Class3), aby wywodziła się z typu w innym zestawie, gdy klasa jest używana jako argument szablonu dla tej klasy.

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

Ciche zmiany semantyki na poziomie źródła:

  1. Dodawanie / usuwanie / zmiana przesłonięć funkcji Equals (), GetHashCode () lub ToString ()

(nie jestem pewien, gdzie one pasują)

Zmiany we wdrożeniu:

  1. Dodawanie / usuwanie zależności / referencji
  2. Aktualizowanie zależności do nowszych wersji
  3. Zmiana „platformy docelowej” między x86, Itanium, x64 lub anycpu
  4. Budowanie / testowanie na innej instalacji frameworka (tj. Instalacja 3.5 na .Net 2.0 box pozwala na wywołania API, które następnie wymagają .Net 2.0 SP2)

Zmiany w bootstrapie / konfiguracji:

  1. Dodawanie / usuwanie / zmiana niestandardowych opcji konfiguracji (tj. Ustawienia App.config)
  2. Przy częstym stosowaniu IoC / DI w dzisiejszych aplikacjach konieczne jest ponowne skonfigurowanie i / lub zmiana kodu ładowania początkowego dla kodu zależnego od DI.

Aktualizacja:

Przepraszam, nie zdawałem sobie sprawy, że jedynym powodem, dla którego mi to przeszkadzało, było użycie ich w ograniczeniach szablonów.

csharptest.net
źródło
„Dodanie nowego elementu (chronionego przed zdarzeniem), który używa typu z innego zestawu.” - IIRC, klient musi jedynie odwoływać się do zestawów zależnych, które zawierają podstawowe typy zestawów, do których już się odwołuje; nie musi odwoływać się do zestawów, które są po prostu używane (nawet jeśli typy są w sygnaturach metod); Nie jestem tego w 100% pewien. Czy masz odniesienie do szczegółowych zasad w tym zakresie? Również przeniesienie typu może być niełamliwe, jeśli TypeForwardedToAttributezostanie użyte.
Pavel Minaev
To „TypeForwardedTo” to dla mnie wiadomość, sprawdzę to. Jeśli chodzi o inne, nie jestem też w 100% na tym ... pokażę, czy mogę to zrobić, a ja zaktualizuję post.
csharptest.net,
Więc nie -Werrornaciskaj w swoim systemie kompilacji, który dostarczasz z wydanymi archiwami. Ta flaga jest najbardziej pomocna dla twórcy kodu i najczęściej nieprzydatna dla konsumenta.
binki
@binki doskonały punkt, traktowanie ostrzeżeń jako błędów powinno wystarczyć tylko w kompilacjach DEBUG.
csharptest.net
3

Dodanie metod przeciążania w celu zmniejszenia użycia domyślnych parametrów

Rodzaj przerwy: cicha semantyka na poziomie źródła zmienia się

Ponieważ kompilator przekształca wywołania metod z brakującymi wartościami parametrów domyślnych w jawne wywołanie z wartością domyślną po stronie wywołującej, zapewniona jest kompatybilność z istniejącym skompilowanym kodem; dla całego wcześniej skompilowanego kodu zostanie znaleziona metoda z poprawnym podpisem.

Z drugiej strony wywołania bez użycia parametrów opcjonalnych są teraz kompilowane jako wywołanie nowej metody, w której brakuje parametru opcjonalnego. Wszystko nadal działa poprawnie, ale jeśli wywoływany kod znajduje się w innym zestawie, nowo skompilowany kod wywołujący go jest teraz zależny od nowej wersji tego zestawu. Wdrażanie zestawów wywołujących refaktoryzowany kod bez wdrażania pakietu, w którym znajduje się refaktoryzowany kod, powoduje wyjątki „nie znaleziono metody”.

API przed zmianą

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

API po zmianie

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

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

Przykładowy kod, który nadal będzie działał

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

Przykładowy kod, który jest teraz zależny od nowej wersji podczas kompilacji

  public int CodeDependentToNewVersion()
  {
     return MyMethod(5); 
  }
Nie wiesz
źródło
1

Zmiana nazwy interfejsu

Kinda of Break: Source and Binary

Języki, których dotyczy problem: najprawdopodobniej wszystkie przetestowane w C #.

Interfejs API przed zmianą:

public interface IFoo
{
    void Test();
}

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

Interfejs API po zmianie:

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

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

Przykładowy kod klienta, który działa, ale później jest uszkodzony:

new Bar().GetFoo().Test(); // Binary only break
IFoo foo = new Bar().GetFoo(); // Source and binary break
Aidiakapi
źródło
1

Metoda przeciążenia z parametrem typu zerowalnego

Rodzaj: Przerwa na poziomie źródła

Języki, których to dotyczy: C #, VB

API przed zmianą:

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

API po zmianie:

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

Przykładowy kod klienta działający przed zmianą i zepsuty po nim:

new Foo().Bar(null);

Wyjątek: wywołanie jest niejednoznaczne między następującymi metodami lub właściwościami.

Bohdan Spilnyi
źródło
0

Awans na metodę przedłużenia

Rodzaj: przerwa na poziomie źródła

Języki, których dotyczy problem: C # v6 i wyższe (może inne?)

API przed zmianą:

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

API po zmianie:

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

Przykładowy kod klienta działający przed zmianą i zepsuty po nim:

using static Foo;

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

Więcej informacji: https://github.com/dotnet/csharplang/issues/665

rory.ap
źródło