Una guida definitiva alle modifiche di API in .NET


227

Vorrei raccogliere quante più informazioni possibili sul controllo delle versioni delle API in .NET / CLR e in particolare su come le modifiche alle API fanno o non rompono le applicazioni client. Innanzitutto, definiamo alcuni termini:

Modifica API : una modifica della definizione pubblicamente visibile di un tipo, inclusi i suoi membri pubblici. Ciò include la modifica del tipo e dei nomi dei membri, la modifica del tipo di base di un tipo, l'aggiunta / rimozione di interfacce dall'elenco delle interfacce implementate di un tipo, l'aggiunta / rimozione di membri (compresi i sovraccarichi), la modifica della visibilità dei membri, la ridenominazione del metodo e dei parametri del tipo, l'aggiunta di valori predefiniti per i parametri del metodo, l'aggiunta / rimozione di attributi su tipi e membri e l'aggiunta / rimozione di parametri di tipo generico su tipi e membri (mi sono perso qualcosa?). Ciò non include eventuali modifiche negli organi membri o eventuali modifiche ai membri privati ​​(ovvero non prendiamo in considerazione Reflection).

Interruzione a livello binario : una modifica dell'API che determina l'assemblaggio di assembly client rispetto alla versione precedente dell'API che potenzialmente non si carica con la nuova versione. Esempio: modifica della firma del metodo, anche se consente di essere richiamato come prima (ovvero: void per restituire sovraccarichi di valori predefiniti di tipo / parametro).

Interruzione a livello di origine : una modifica dell'API che comporta la scrittura del codice esistente per la compilazione con la versione precedente dell'API che potenzialmente non si sta compilando con la nuova versione. Tuttavia, gli assembly client già compilati funzionano come prima. Esempio: aggiunta di un nuovo sovraccarico che può provocare ambiguità nelle chiamate di metodo precedenti non ambigue.

Modifica della semantica silenziosa a livello di origine : una modifica dell'API che si traduce in codice esistente scritto per la compilazione con una versione precedente dell'API modifica silenziosamente la sua semantica, ad esempio chiamando un metodo diverso. Tuttavia, il codice dovrebbe continuare a essere compilato senza avvisi / errori e gli assembly precedentemente compilati dovrebbero funzionare come prima. Esempio: implementazione di una nuova interfaccia su una classe esistente che determina la scelta di un sovraccarico diverso durante la risoluzione del sovraccarico.

L'obiettivo finale è quello di catalogare il maggior numero possibile di modifiche API semantiche di interruzione e di quiete e descrivere l'effetto esatto della rottura e quali lingue sono e non sono interessate da essa. Espandersi su quest'ultimo: mentre alcune modifiche influenzano universalmente tutte le lingue (ad esempio l'aggiunta di un nuovo membro a un'interfaccia interromperà le implementazioni di tale interfaccia in qualsiasi lingua), alcune richiedono una semantica linguistica molto specifica per entrare in gioco per ottenere una pausa. Questo in genere comporta un sovraccarico del metodo e, in generale, qualsiasi cosa abbia a che fare con conversioni di tipo implicite. Non sembra esserci alcun modo per definire qui il "minimo comune denominatore" anche per i linguaggi conformi a CLS (ovvero quelli conformi almeno alle regole del "consumatore CLS" come definito nelle specifiche CLI) - sebbene io ' Apprezzerò se qualcuno mi corregge come sbagliato qui - quindi questo dovrà andare lingua per lingua. Quelli di maggior interesse sono naturalmente quelli forniti con .NET out of the box: C #, VB e F #; ma anche altri, come IronPython, IronRuby, Delphi Prism ecc. sono rilevanti. Più è un caso angolare, più interessante sarà - cose come la rimozione dei membri sono abbastanza evidenti, ma le interazioni sottili tra ad esempio il sovraccarico del metodo, i parametri opzionali / predefiniti, l'inferenza del tipo lambda e gli operatori di conversione possono essere molto sorprendenti a volte.

Alcuni esempi per iniziare questo:

Aggiunta di nuovi sovraccarichi di metodo

Tipo: interruzione a livello di sorgente

Lingue interessate: C #, VB, F #

API prima della modifica:

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

API dopo la modifica:

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

Codice client di esempio funzionante prima della modifica e interrotto dopo:

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

Aggiunta di nuovi sovraccarichi dell'operatore di conversione implicita

Tipo: interruzione a livello di sorgente.

Lingue interessate: C #, VB

Lingue non interessate: F #

API prima della modifica:

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

API dopo la modifica:

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

Codice client di esempio funzionante prima della modifica e interrotto dopo:

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

Note: F # non viene interrotto, perché non ha alcun supporto a livello di lingua per operatori sovraccarichi, né espliciti né impliciti - entrambi devono essere chiamati direttamente come op_Explicite op_Implicitmetodi.

Aggiunta di nuovi metodi di istanza

Tipo: cambiamento di semantica silenziosa a livello di sorgente.

Lingue interessate: C #, VB

Lingue non interessate: F #

API prima della modifica:

public class Foo
{
}

API dopo la modifica:

public class Foo
{
    public void Bar();
}

Codice client di esempio che subisce una modifica semantica silenziosa:

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

new Foo().Bar();

Note: F # non viene interrotto, perché non ha supporto a livello di lingua per ExtensionMethodAttributee richiede che i metodi di estensione CLS siano chiamati come metodi statici.


Sicuramente Microsoft già copre questo ... msdn.microsoft.com/en-us/netframework/aa570326.aspx
Robert Harvey,

1
@Robert: il tuo link riguarda qualcosa di molto diverso: descrive specifiche modifiche di rottura in .NET Framework stesso. Questa è una domanda più ampia che descrive schemi generici che possono introdurre cambiamenti radicali nelle proprie API (come autore di librerie / framework). Non sono a conoscenza di alcun documento di questo tipo che sarebbe completo, anche se qualsiasi link a tale, anche se incompleto, è sicuramente il benvenuto.
Pavel Minaev,

In una di queste categorie di "interruzione", c'è qualche in cui il problema diventerà evidente solo in fase di esecuzione?
Rohit,

1
Sì, categoria "interruzione binaria". In tal caso, hai già un assembly di terze parti compilato per tutte le versioni dell'assembly. Se si rilascia una nuova versione dell'assembly sul posto, l'assemblaggio di terze parti smette di funzionare - o semplicemente non si carica in fase di esecuzione o funziona in modo errato.
Pavel Minaev,

3
Vorrei aggiungere quelli nel post e nei commenti blogs.msdn.com/b/ericlippert/archive/2012/01/09/…
Lukasz Madon

Risposte:


42

Modifica della firma di un metodo

Tipo: interruzione a livello binario

Lingue interessate: C # (VB e F # molto probabilmente, ma non testato)

API prima del cambiamento

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

API dopo la modifica

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

Codice client di esempio funzionante prima della modifica

Foo.bar(13);

15
In effetti, può anche essere una rottura a livello di fonte, se qualcuno cerca di creare un delegato per bar.
Pavel Minaev,

Anche questo è vero. Ho riscontrato questo particolare problema quando ho apportato alcune modifiche alle utility di stampa nell'applicazione della mia azienda. Quando è stato rilasciato l'aggiornamento, non tutte le DLL che facevano riferimento a queste utility sono state ricompilate e rilasciate in modo da generare un'eccezione non trovata sul metodo.
Justin Drury,

1
Ciò risale al fatto che i tipi restituiti non contano per la firma del metodo. Non è possibile sovraccaricare due funzioni basandosi esclusivamente sul tipo di ritorno. Stesso problema.
Jason Short,

1
domanda secondaria a questa risposta: qualcuno conosce le implicazioni dell'aggiunta di un valore predefinito dotnet4 'barra statica vuota statica (int i = 0);' o cambiando quel valore predefinito da un valore all'altro?
k3b,

1
Per coloro che stanno per atterrare su questa pagina penso per C # (e "penso" la maggior parte degli altri linguaggi OOP), i tipi di ritorno non contribuiscono alla firma del metodo. Sì, la risposta è corretta che le modifiche alla firma contribuiscono alla modifica del livello binario. MA l'esempio non sembra corretto IMHO l'esempio corretto che posso pensare è PRIMA della somma decimale pubblica (int a, int b) Dopo la somma decimale pubblica (decimale a, decimale b) Si prega di fare riferimento a questo collegamento MSDN 3.6 Firme e sovraccarico
Bhanu Chhabra,

40

Aggiunta di un parametro con un valore predefinito.

Tipo di interruzione: interruzione a livello binario

Anche se il codice sorgente chiamante non deve essere modificato, deve comunque essere ricompilato (proprio come quando si aggiunge un parametro normale).

Questo perché C # compila i valori predefiniti dei parametri direttamente nell'assembly chiamante. Significa che se non si ricompila, si otterrà un MissingMethodException perché il vecchio assembly tenta di chiamare un metodo con meno argomenti.

API prima del cambiamento

public void Foo(int a) { }

API dopo la modifica

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

Codice client di esempio che viene successivamente interrotto

Foo(5);

Il codice client deve essere ricompilato Foo(5, null)a livello di bytecode. L'assembly chiamato conterrà solo Foo(int, string), non Foo(int). Questo perché i valori dei parametri predefiniti sono puramente una funzione del linguaggio, il runtime .Net non ne sa nulla. (Questo spiega anche perché i valori predefiniti devono essere costanti di compilazione in C #).


2
questo è un cambiamento Func<int> f = Foo;
decisivo

26

Questo è stato molto ovvio quando l'ho scoperto, soprattutto alla luce della differenza con la stessa situazione per le interfacce. Non è affatto una pausa, ma è abbastanza sorprendente che ho deciso di includerlo:

Rifattorizzare i membri della classe in una classe base

Tipo: non una pausa!

Lingue interessate: nessuna (ovvero nessuna è rotta)

API prima della modifica:

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

API dopo la modifica:

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

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

Codice di esempio che continua a funzionare durante la modifica (anche se mi aspettavo che si interrompesse):

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

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

Appunti:

C ++ / CLI è l'unico linguaggio .NET che ha un costrutto analogo all'implementazione esplicita dell'interfaccia per i membri della classe di base virtuale: "esplicito override". Mi aspettavo che ciò causasse lo stesso tipo di rottura di quando si spostavano i membri dell'interfaccia su un'interfaccia di base (poiché IL generato per l'override esplicito è lo stesso che per l'implementazione esplicita). Con mia sorpresa, questo non è il caso - anche se il IL generato specifica ancora che BarOverridesostituisce Foo::Barpiuttosto che il FooBase::Barcaricatore di assemblaggio è abbastanza intelligente da sostituirne uno correttamente senza alcun reclamo - apparentemente, il fatto che Foosia una classe è ciò che fa la differenza. Vai a capire...


3
Finché la classe base è nello stesso assembly. Altrimenti si tratta di un cambiamento di rottura binaria.
Jeremy,

@Jeremy che tipo di codice si rompe in quel caso? L'uso di Baz () da parte di un chiamante esterno si interromperà o è solo un problema con le persone che cercano di estendere Foo e ignorare Baz ()?
ChaseMedallion,

@ChaseMedallion si sta rompendo se sei un utente di seconda mano. Ad esempio, la DLL compilata fa riferimento a una versione precedente di Foo e l'utente fa riferimento a quella DLL compilata, ma utilizza anche una versione più recente della DLL Foo. Si rompe con uno strano errore, o almeno lo ha fatto per me nelle biblioteche che ho sviluppato prima.
Jeremy,

19

Questo è un caso speciale forse non così ovvio di "aggiunta / rimozione di membri di interfaccia", e ho pensato che meriti il ​​suo ingresso alla luce di un altro caso che pubblicherò dopo. Così:

Refactoring dei membri dell'interfaccia in un'interfaccia di base

Tipo: interruzioni sia a livello sorgente che binario

Lingue interessate: C #, VB, C ++ / CLI, F # (per interruzione del codice sorgente; uno binario influisce naturalmente su qualsiasi linguaggio)

API prima della modifica:

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

API dopo la modifica:

interface IFooBase 
{
    void Bar();
}

interface IFoo : IFooBase
{
    void Baz();
}

Codice client di esempio che viene interrotto dalla modifica a livello di origine:

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

Codice client di esempio che viene interrotto dalla modifica a livello binario;

(new Foo()).Bar();

Appunti:

Per l'interruzione a livello di sorgente, il problema è che C #, VB e C ++ / CLI richiedono tutti il nome esatto dell'interfaccia nella dichiarazione di implementazione dei membri dell'interfaccia; pertanto, se il membro viene spostato su un'interfaccia di base, il codice non verrà più compilato.

L'interruzione binaria è dovuta al fatto che i metodi di interfaccia sono pienamente qualificati in IL generato per implementazioni esplicite e il nome dell'interfaccia deve anche essere esatto.

L'implementazione implicita ove disponibile (ad es. C # e C ++ / CLI, ma non VB) funzionerà perfettamente sia a livello sorgente che binario. Le chiamate al metodo non si interrompono neanche.


Questo non è vero per tutte le lingue. Per VB non si tratta di una rottura del codice sorgente che si interrompe. Per C # lo è.
Jeremy,

Quindi Implements IFoo.Barfarà riferimento in modo trasparente IFooBase.Bar?
Pavel Minaev,

Sì, effettivamente, è possibile fare riferimento a un membro direttamente o indirettamente tramite l'interfaccia di ereditarietà quando lo si implementa. Tuttavia, questa è sempre una rottura del cambiamento binario.
Jeremy,

15

Riordinare i valori enumerati

Tipo di interruzione: cambiamento semantico silenzioso a livello di sorgente / binario

Lingue interessate: tutte

Il riordino dei valori enumerati manterrà la compatibilità a livello di sorgente poiché i letterali hanno lo stesso nome, ma i loro indici ordinali verranno aggiornati, il che può causare alcuni tipi di interruzioni silenziose a livello di sorgente.

Ancora peggio sono le interruzioni silenziose a livello binario che possono essere introdotte se il codice client non viene ricompilato con la nuova versione dell'API. I valori di Enum sono costanti in fase di compilazione e pertanto qualsiasi loro utilizzo viene inserito nell'IL dell'assembly client. Questo caso può essere particolarmente difficile da individuare a volte.

API prima del cambiamento

public enum Foo
{
   Bar,
   Baz
}

API dopo la modifica

public enum Foo
{
   Baz,
   Bar
}

Codice client di esempio che funziona ma viene successivamente interrotto:

Foo.Bar < Foo.Baz

12

Questo è davvero una cosa molto rara nella pratica, ma comunque sorprendente quando succede.

Aggiunta di nuovi membri non sovraccaricati

Tipo: interruzione a livello di sorgente o modifica semantica silenziosa.

Lingue interessate: C #, VB

Lingue non interessate: F #, C ++ / CLI

API prima della modifica:

public class Foo
{
}

API dopo la modifica:

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

Codice client di esempio che viene interrotto dalla modifica:

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

Appunti:

Il problema qui è causato dall'inferenza del tipo lambda in C # e VB in presenza di risoluzione di sovraccarico. Una forma limitata di tipizzazione anatra viene utilizzata qui per rompere i legami in cui più di un tipo corrisponde, controllando se il corpo del lambda ha senso per un determinato tipo - se solo un tipo risulta in un corpo compilabile, quello viene scelto.

Il pericolo qui è che il codice client possa avere un gruppo di metodi sovraccarico in cui alcuni metodi accettano argomenti dei propri tipi e altri accettano argomenti dei tipi esposti dalla libreria. Se uno dei suoi codici si basa quindi sull'algoritmo di inferenza del tipo per determinare il metodo corretto basato esclusivamente sulla presenza o l'assenza di membri, l'aggiunta di un nuovo membro a uno dei tuoi tipi con lo stesso nome di uno dei tipi del client può potenzialmente generare inferenza spento, con conseguente ambiguità durante la risoluzione del sovraccarico.

Si noti che i tipi Fooe Barin questo esempio non sono correlati in alcun modo, né per eredità né in altro modo. Il solo uso di essi in un singolo gruppo di metodi è sufficiente per innescare questo, e se ciò si verifica nel codice client, non si ha alcun controllo su di esso.

Il codice di esempio sopra mostra una situazione più semplice in cui si tratta di un'interruzione a livello di origine (ovvero risultati dell'errore del compilatore). Tuttavia, questo può anche essere un cambiamento di semantica silenziosa, se il sovraccarico che è stato scelto tramite inferenza aveva altri argomenti che altrimenti avrebbero causato la sua classificazione di seguito (ad esempio argomenti opzionali con valori predefiniti o tipo non corrispondente tra argomento dichiarato e reale che richiede un implicito conversione). In tale scenario, la risoluzione del sovraccarico non fallirà più, ma un compilatore diverso verrà tranquillamente selezionato dal compilatore. In pratica, tuttavia, è molto difficile imbattersi in questo caso senza costruire con cura le firme dei metodi per causarlo deliberatamente.


9

Converti un'implementazione dell'interfaccia implicita in esplicita.

Kind of Break: Source e Binary

Lingue interessate: tutte

Questa è in realtà solo una variazione della modifica dell'accessibilità di un metodo - è solo un po 'più sottile poiché è facile trascurare il fatto che non tutti gli accessi ai metodi di un'interfaccia sono necessariamente attraverso un riferimento al tipo di interfaccia.

API prima della modifica:

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

API dopo la modifica:

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

Codice client di esempio che funziona prima della modifica e viene successivamente interrotto:

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

7

Converti un'implementazione esplicita dell'interfaccia in una implicita.

Kind of Break: Source

Lingue interessate: tutte

Il refactoring di un'implementazione esplicita dell'interfaccia in una implicita è più sottile nel modo in cui può rompere un'API. In superficie, sembrerebbe che questo dovrebbe essere relativamente sicuro, tuttavia, se combinato con l'eredità può causare problemi.

API prima della modifica:

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

API dopo la modifica:

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

Codice client di esempio che funziona prima della modifica e viene successivamente interrotto:

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"

Siamo spiacenti, non seguo del tutto: sicuramente il codice di esempio prima che la modifica dell'API non si compili affatto, poiché prima la modifica Foonon aveva un metodo pubblico chiamato GetEnumeratore stai chiamando il metodo tramite un riferimento di tipo Foo.. .
Pavel Minaev

In effetti, ho cercato di semplificare un esempio dalla memoria e alla fine è diventato "foobar" (scusa il gioco di parole). Ho aggiornato l'esempio per dimostrare correttamente il caso (ed essere compilabile).
LBushkin,

Nel mio esempio, il problema è causato da qualcosa di più della semplice transizione di un metodo di interfaccia dall'essere implicito all'essere pubblico. Dipende dal modo in cui il compilatore C # determina quale metodo chiamare in un ciclo foreach. Date le regole di risoluzione del compilatore, passa dalla versione nella classe derivata alla versione nella classe base.
LBushkin,

Hai dimenticato yield return "Bar":) ma sì, vedo dove sta andando ora - foreachchiama sempre il metodo pubblico denominato GetEnumerator, anche se non è la vera implementazione IEnumerable.GetEnumerator. Questo sembra avere un altro punto di vista: anche se hai solo una classe, e implementa in modo IEnumerableesplicito, ciò significa che è una modifica che interrompe l'origine per aggiungere un metodo pubblico GetEnumeratorad esso denominato , perché ora foreachutilizzerà quel metodo sull'implementazione dell'interfaccia. Inoltre, lo stesso problema è applicabile IEnumeratorall'implementazione ...
Pavel Minaev,

6

Modifica di un campo in una proprietà

Tipo di pausa: API

Lingue interessate: Visual Basic e C # *

Informazioni: quando si modifica un campo normale o una variabile in una proprietà in Visual Basic, sarà necessario ricompilare qualsiasi codice esterno che fa riferimento a quel membro in qualsiasi modo.

API prima della modifica:

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

API dopo la modifica:

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    

Codice client di esempio che funziona ma viene successivamente interrotto:

Foo.Bar = "foobar"

2
In realtà ciò spezzerebbe anche le cose in C #, poiché le proprietà non possono essere usate oute refargomenti di metodi, diversamente dai campi, e non possono essere la destinazione &dell'operatore unario .
Pavel Minaev il

5

Aggiunta spazio dei nomi

Interruzione a livello di sorgente / Modifica semantica silenziosa a livello di origine

A causa del modo in cui la risoluzione dello spazio dei nomi funziona in vb.Net, l'aggiunta di uno spazio dei nomi a una libreria può far sì che il codice Visual Basic compilato con una versione precedente dell'API non venga compilato con una nuova versione.

Codice client di esempio:

Imports System
Imports Api.SomeNamespace

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

Se una nuova versione dell'API aggiunge lo spazio dei nomi Api.SomeNamespace.Data, il codice sopra riportato non verrà compilato.

Diventa più complicato con le importazioni dello spazio dei nomi a livello di progetto. Se Imports Systemviene omesso dal codice precedente, ma lo Systemspazio dei nomi viene importato a livello di progetto, il codice potrebbe comunque causare un errore.

Tuttavia, se l'API include una classe DataRownel suo Api.SomeNamespace.Dataspazio dei nomi, il codice verrà compilato ma drsarà un'istanza di System.Data.DataRowquando viene compilato con la vecchia versione dell'API e Api.SomeNamespace.Data.DataRowquando viene compilato con la nuova versione dell'API.

Argomento Rinomina

Interruzione a livello di sorgente

La modifica dei nomi degli argomenti è una modifica sostanziale in vb.net dalla versione 7 (?) (.Net versione 1?) E c # .net dalla versione 4 (.Net versione 4).

API prima della modifica:

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

API dopo la modifica:

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

Codice client di esempio:

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

Parametri di riferimento

Interruzione a livello di sorgente

L'aggiunta di un metodo override con la stessa firma, tranne per il fatto che un parametro viene passato per riferimento anziché per valore, impedirà all'origine vb che fa riferimento all'API di risolvere la funzione. Visual Basic non ha modo (?) Di differenziare questi metodi nel punto di chiamata a meno che non abbiano nomi di argomenti diversi, quindi una tale modifica potrebbe rendere inutilizzabili entrambi i membri dal codice vb.

API prima della modifica:

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

API dopo la modifica:

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

Codice client di esempio:

Api.SomeNamespace.Foo.Bar(str)

Cambio da campo a proprietà

Interruzione a livello binario / Interruzione a livello di sorgente

Oltre all'evidente interruzione a livello binario, ciò può causare un'interruzione a livello di origine se il membro viene passato a un metodo per riferimento.

API prima della modifica:

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

API dopo la modifica:

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

Codice client di esempio:

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

4

Cambio API:

  1. Aggiunta dell'attributo [Obsoleto] (si è trattato di questo con la menzione di attributi; tuttavia, questo può essere un cambiamento di rottura quando si utilizza l'avviso-come-errore.)

Pausa a livello binario:

  1. Spostamento di un tipo da un assieme a un altro
  2. Modifica dello spazio dei nomi di un tipo
  3. Aggiunta di un tipo di classe base da un altro assembly.
  4. Aggiunta di un nuovo membro (protetto da eventi) che utilizza un tipo da un altro assembly (Class2) come vincolo argomento modello.

    protected void Something<T>() where T : Class2 { }
  5. Modifica di una classe figlio (Class3) per derivare da un tipo in un altro assembly quando la classe viene utilizzata come argomento modello per questa classe.

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

Modifica della semantica silenziosa a livello di origine:

  1. Aggiunta / rimozione / modifica delle sostituzioni di Equals (), GetHashCode () o ToString ()

(non sono sicuro di dove si adattano)

Modifiche alla distribuzione:

  1. Aggiunta / rimozione di dipendenze / riferimenti
  2. Aggiornamento delle dipendenze alle versioni più recenti
  3. Modifica della "piattaforma di destinazione" tra x86, Itanium, x64 o anycpu
  4. Creazione / test su una diversa installazione del framework (ovvero l'installazione di 3.5 su una scatola .Net 2.0 consente chiamate API che richiedono quindi .Net 2.0 SP2)

Bootstrap / Modifiche alla configurazione:

  1. Aggiunta / rimozione / modifica delle opzioni di configurazione personalizzate (ad es. Impostazioni di Configurazione app)
  2. Con il pesante uso di IoC / DI nelle applicazioni odierne, è necessario riconfigurare e / o modificare il codice di bootstrap per il codice dipendente da DIO.

Aggiornare:

Mi dispiace, non mi ero reso conto che l'unica ragione per cui questo mi stava rompendo era che li avevo usati nei vincoli del modello.


"Aggiunta di un nuovo membro (protetto da eventi) che utilizza un tipo da un altro assembly." - IIRC, il cliente deve solo fare riferimento agli assembly dipendenti che contengono tipi di base degli assembly a cui fa già riferimento; non deve fare riferimento a assembly che vengono semplicemente utilizzati (anche se i tipi sono nelle firme del metodo); Non ne sono sicuro al 100%. Hai un riferimento per regole precise per questo? Inoltre, se TypeForwardedToAttributesi utilizza uno spostamento di un tipo può non essere interrotto .
Pavel Minaev,

Che "TypeForwardedTo" sia una novità per me, lo controllerò. Per quanto riguarda l'altro, non ci sono nemmeno al 100% ... fammi vedere se riesco a riprogrammare e aggiornerò il post.
csharptest.net,

Quindi, non forzare -Werrornel tuo sistema di compilazione fornito con i tarball di rilascio. Questo flag è molto utile per lo sviluppatore del codice e molto spesso inutile per il consumatore.
binki,

@binki ottimo punto, trattando gli avvisi come errori dovrebbero essere sufficienti solo nelle build DEBUG.
csharptest.net,

3

Aggiunta di metodi di sovraccarico per eliminare l'utilizzo dei parametri predefiniti

Tipo di interruzione: cambiamento della semantica silenziosa a livello di sorgente

Poiché il compilatore trasforma le chiamate di metodo con valori di parametro predefiniti mancanti in una chiamata esplicita con il valore predefinito sul lato chiamante, viene fornita la compatibilità per il codice compilato esistente; verrà trovato un metodo con la firma corretta per tutto il codice precedentemente compilato.

Dall'altro lato, le chiamate senza utilizzo di parametri opzionali vengono ora compilate come una chiamata al nuovo metodo in cui manca il parametro opzionale. Tutto funziona ancora bene, ma se il codice chiamato si trova in un altro assembly, il codice appena compilato che lo chiama ora dipende dalla nuova versione di questo assembly. La distribuzione di assembly che chiama il codice refactored senza distribuire anche l'assembly in cui risiede il codice refactored comporta eccezioni "metodo non trovato".

API prima del cambiamento

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

API dopo la modifica

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

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

Codice di esempio che funzionerà ancora

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

Codice di esempio che ora dipende dalla nuova versione durante la compilazione

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

1

Rinominare un'interfaccia

Kinda of Break: Source e Binary

Lingue interessate: molto probabilmente tutte, testate in C #.

API prima della modifica:

public interface IFoo
{
    void Test();
}

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

API dopo la modifica:

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

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

Codice client di esempio che funziona ma viene successivamente interrotto:

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

1

Metodo di sovraccarico con un parametro di tipo nullable

Tipo: interruzione a livello di sorgente

Lingue interessate: C #, VB

API prima di una modifica:

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

API dopo la modifica:

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

Codice client di esempio funzionante prima della modifica e interrotto dopo di essa:

new Foo().Bar(null);

Eccezione: la chiamata è ambigua tra i seguenti metodi o proprietà.


0

Promozione a un metodo di estensione

Tipo: interruzione a livello di sorgente

Lingue interessate: C # v6 e successive (forse altre?)

API prima della modifica:

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

API dopo la modifica:

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

Codice client di esempio funzionante prima della modifica e interrotto dopo:

using static Foo;

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

Ulteriori informazioni: https://github.com/dotnet/csharplang/issues/665

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.