Modificare le regole per la compatibilità

Nel corso del tempo, .NET ha tentato di mantenere un elevato livello di compatibilità tra le diverse versioni e nella sua implementazione. Anche se .NET 5 (e .NET Core) e versioni successive possono essere considerate una nuova tecnologia rispetto a .NET Framework, due fattori principali limitano la capacità di tale implementazione di .NET di discostarsi da .NET Framework:

  • Numerosi sviluppatori hanno originariamente sviluppato o continuano a sviluppare applicazioni .NET Framework e si aspettano un comportamento coerente in tutte le implementazioni di .NET.

  • I progetti di libreria .NET Standard consentono agli sviluppatori di creare librerie per API comuni condivise da .NET Framework e .NET 5 (e .NET Core) e versioni successive. Gli sviluppatori si aspettano che una libreria usata in un'applicazione .NET 5 debba comportarsi in modo identico alla stessa libreria usata in un'applicazione .NET Framework.

Oltre alla compatibilità tra implementazioni .NET, gli sviluppatori prevedono un elevato livello di compatibilità tra le versioni di una determinata implementazione di .NET. In particolare, il codice scritto per una versione precedente di .NET Core dovrebbe essere eseguito senza problemi in .NET 5 o una versione successiva. In realtà, molti gli sviluppatori si aspettano che le nuove API incluse nelle nuove versioni rilasciate di .NET siano compatibili anche con le versioni non definitive in cui sono state introdotte le API.

Questo articolo illustra le modifiche che influiscono sulla compatibilità e sul modo in cui il team .NET valuta ciascun tipo di modifica. Comprendere come il team .NET si avvicina alle possibili modifiche di rilievo è particolarmente utile per gli sviluppatori che aprono richieste pull che modificano il comportamento delle API .NET esistenti.

Le sezioni seguenti descrivono le categorie delle modifiche apportate alle API di .NET e il relativo impatto sulla compatibilità delle applicazioni. Le modifiche sono consentite (✔️), non consentite (❌) o richiedono un giudizio e una valutazione del comportamento prevedibile, ovvio e coerente del comportamento precedente (❓).

Nota

  • Oltre a servire da guida per la valutazione delle modifiche alle librerie di .NET, questi criteri possono essere usati dagli sviluppatori di librerie per valutare le modifiche alle librerie destinate a più versioni e implementazioni di .NET.
  • Per informazioni sulle categorie di compatibilità, ad esempio compatibilità avanti e indietro, vedere Come le modifiche al codice possono influire sulla compatibilità.

Modifiche al contratto pubblico

Le modifiche di questa categoria interessano la superficie di attacco pubblica di un tipo. La maggior parte delle modifiche incluse in questa categoria non è consentita perché viola la compatibilità con le versioni precedenti, ovvero la capacità di un'applicazione sviluppata con una versione precedente di un'API di essere eseguita senza ricompilazione in una versione successiva.

Tipi

  • ✔️ CONSENTITA: rimozione di un'implementazione dell'interfaccia da un tipo quando l'interfaccia è già implementata da un tipo di base

  • ❓ RICHIEDE UN GIUDIZIO: aggiunta di una nuova implementazione dell'interfaccia a un tipo

    Questa è una modifica accettabile perché non ha effetti negativi sui client esistenti. Affinché la nuova implementazione rimanga accettabile, le modifiche al tipo devono rimanere entro i limiti delle modifiche accettabili definite di seguito. È necessario prestare particolare attenzione quando si aggiungono interfacce che influiscono direttamente sulla capacità di una finestra di progettazione o un serializzatore di generare codice o dati che non possono essere utilizzati nelle versioni precedenti. Un esempio è costituito dall'interfaccia ISerializable.

  • ❓ RICHIEDE UN GIUDIZIO: presentazione di una nuova classe di base

    Un tipo può essere introdotto in una gerarchia tra due tipi esistenti se non introduce nuovi membri astratti o modifica la semantica o il comportamento di tipi esistenti. Ad esempio, in .NET Framework 2.0, la classe DbConnection è diventata una nuova classe di base per SqlConnection, che in precedenza derivava direttamente da Component.

  • ✔️ CONSENTITA: spostamento di un tipo da un assembly a un altro

    L’assembly precedente deve essere contrassegnato con l'attributo TypeForwardedToAttribute che punta al nuovo assembly.

  • ✔️ CONSENTITA: modifica di un tipo struct in un tipo readonly struct

    La modifica di un tipo readonly struct in un tipo struct non è consentita.

  • ✔️ CONSENTITA: aggiunta della parola chiave sealed o abstract a un tipo quando non sono presenti costruttori accessibili (pubblici o protetti)

  • ✔️ CONSENTITA: espansione della visibilità di un tipo

  • NON CONSENTITA: modifica dello spazio dei nomi o del nome di un tipo

  • NON CONSENTITA: ridenominazione o rimozione di un tipo pubblico

    Questa operazione causa l'interruzione di tutto il codice che usa il tipo rinominato o rimosso.

    Nota

    In rari casi, .NET può rimuovere un'API pubblica. Per altre informazioni, vedere Rimozione di API in .NET. Per informazioni sui criteri di supporto di .NET, vedere Criteri di supporto di .NET.

  • NON CONSENTITA: modifica del tipo sottostante di un'enumerazione

    Si tratta di una modifica che causa un'interruzione a livello di compilazione, comportamento e codice binario che può rendere non analizzabili gli argomenti degli attributi.

  • NON CONSENTITA: chiusura (‘sealing’) di un tipo che in precedenza era non sealed

  • NON CONSENTITA: aggiunta di un'interfaccia al set di tipi di base di un'interfaccia

    Se un'interfaccia implementa un'interfaccia che in precedenza non implementava, tutti i tipi che implementavano la versione originale dell'interfaccia vengono interrotti.

  • ❓ RICHIEDE UN GIUDIZIO: rimozione di una classe da un set di classi di base o di un'interfaccia dal set di interfacce implementate

    Esiste un'unica eccezione alla regola per la rimozione di un'interfaccia. È possibile aggiungere l'implementazione di un'interfaccia che deriva dall'interfaccia rimossa. È ad esempio possibile rimuovere IDisposable se il tipo o l'interfaccia implementa ora IComponent, che implementa a sua volta l'interfaccia IDisposable.

  • NON CONSENTITA: modifica di un tipo readonly struct in un tipo struct

    La modifica di un tipo struct in un tipo readonly struct è comunque consentita.

  • NON CONSENTITA: la modifica di un tipo struct in un tipo ref struct e viceversa

  • NON CONSENTITA: limitazione della visibilità di un tipo

    L'espansione della visibilità di un tipo è tuttavia consentita.

Membri

  • ✔️ CONSENTITA: espansione della visibilità di un membro non virtuale

  • ✔️ CONSENTITA: aggiunta di un membro astratto a un tipo pubblico che non ha costruttori accessibili (pubblici o protetti) o che è sealed

    Non è tuttavia consentita l'aggiunta di un membro astratto a un tipo pubblico che ha costruttori accessibili (pubblici o protetti) e che non è sealed.

  • ✔️ CONSENTITA: limitazione della visibilità di un membro protetto quando il tipo non ha costruttori accessibili (pubblici o protetti) oppure è sealed

  • ✔️ CONSENTITA: spostamento di un membro in una classe di livello superiore nella gerarchia rispetto al tipo da cui è stato rimosso

  • ✔️ CONSENTITA: aggiunta o rimozione di un override

    Se si introduce un override, i consumer precedenti potrebbero ignorare l'override quando eseguono la chiamata a base.

  • ✔️ CONSENTITA: aggiunta di un costruttore a una classe, insieme a un costruttore (senza parametri), se in precedenza la classe era priva di costruttori

    Non è tuttavia consentita l'aggiunta di un costruttore a una classe che in precedenza era priva di costruttori senza aggiungere il costruttore senza parametri.

  • ✔️ CONSENTITA: modifica di un membro da astratto a virtuale

  • ✔️ CONSENTITA: modifica da un ref readonly a un valore restituito ref (ad eccezione dei metodi o delle interfacce virtuali)

  • ✔️ CONSENTITA: rimozione di readonly da un campo, a meno che il tipo statico del campo non sia un tipo di valore modificabile

  • ✔️ CONSENTITA: chiamata di un nuovo evento non definito in precedenza

  • ❓ RICHIEDE UN GIUDIZIO: aggiunta di un nuovo campo di istanza a un tipo

    Questa modifica ha impatto sulla serializzazione.

  • NON CONSENTITA: ridenominazione o rimozione di un parametro o un membro pubblico

    Questa operazione causa l'interruzione di tutto il codice che usa il parametro o il membro rinominato o rimosso.

    Questo include la rimozione o la ridenominazione di un getter o un setter da una proprietà, nonché la ridenominazione o la rimozione dei membri di un'enumerazione.

  • NON CONSENTITA: aggiunta di un membro a un'interfaccia

    Se si specifica un'implementazione, l'aggiunta di un nuovo membro a un'interfaccia esistente non comporta necessariamente errori di compilazione negli assembly downstream. Tuttavia, non tutti i linguaggi supportano membri di interfaccia predefiniti (DIM). In alcuni scenari, inoltre, il runtime non può decidere quale membro dell'interfaccia predefinito richiamare. Per questi motivi, l'aggiunta di un membro a un'interfaccia esistente è considerata una modifica che causa un'interruzione.

  • NON CONSENTITA: modifica del valore di una costante pubblica o del membro di un'enumerazione

  • NON CONSENTITA: modifica del tipo di una proprietà, di un campo, di un parametro o di un valore restituito

  • NON CONSENTITA: aggiunta, rimozione o modifica dell'ordine dei parametri

  • NON CONSENTITA: aggiunta o rimozione della parola chiave in, out o ref in un parametro

  • NON CONSENTITA: ridenominazione di un parametro (inclusa la modifica di maiuscole e minuscole)

    Questa viene considerata una modifica che causa un'interruzione per due motivi:

  • NON CONSENTITA: modifica di un valore restituito ref in un ref readonlyvalore restituito

  • NON CONSENTITA: modifica di un valore ref readonly in un refvalore restituito su un metodo o un'interfaccia virtuale

  • NON CONSENTITA: aggiunta o rimozione della parola chiave abstract in un membro

  • NON CONSENTITA: rimozione della parola chiave virtual da un membro

  • NON CONSENTITA: aggiunta della parola chiave virtual a un membro

    Anche se spesso questa non è una modifica che causa un'interruzione perché il compilatore C# tende a generare istruzioni callvirt in linguaggio intermedio (IL) per chiamare metodi non virtuali (diversamente da una normale chiamata, callvirt esegue un controllo null), questo comportamento non è invariabile per diversi motivi:

    • C# non è l'unico linguaggio di destinazione di .NET.

    • Il compilatore C# tenta sempre più di ottimizzare callvirt in una normale chiamata ogni volta che il metodo di destinazione non è virtuale e presumibilmente non è null (ad esempio quando si accede a un metodo tramite l'operatore di propagazione null ?.).

    Quando un metodo viene impostato come virtuale, spesso il codice consumer finisce per chiamarlo in modo non virtuale.

  • NON CONSENTITA: impostazione di un membro virtuale come astratto

    Un membro virtuale fornisce un'implementazione di metodo che può essere sottoposta a override da una classe derivata. Un membro astratto non fornisce alcuna implementazione e deve essere sottoposto a override.

  • NON CONSENTITA: aggiunta della parola chiave sealeda un membro dell'interfaccia

    L'aggiunta sealed a un membro di interfaccia predefinita lo rende non virtuale, impedendo la chiamata dell'implementazione di un tipo derivato di tale membro.

  • NON CONSENTITA: aggiunta di un membro astratto a un tipo pubblico che ha costruttori accessibili (pubblici o protetti) e che non è sealed

  • NON CONSENTITA: aggiunta o rimozione della parola chiave static in un membro

  • NON CONSENTITA: aggiunta di un overload che preclude un overload esistente e che definisce un comportamento diverso

    Questa modifica causa un'interruzione dei client esistenti associati all'overload precedente. Se, ad esempio, una classe dispone di una singola versione di un metodo che accetta uno struct UInt32, un consumer esistente viene correttamente associato a tale overload quando viene passato un valore Int32. Se tuttavia si aggiunge un overload che accetta uno struct Int32, quando viene eseguita la ricompilazione o viene usata l'associazione tardiva, il compilatore stabilisce l'associazione con il nuovo overload. In caso di comportamento diverso, si tratta di una modifica che causa un'interruzione.

  • NON CONSENTITA: aggiunta di un costruttore a una classe che in precedenza era priva di costruttori senza aggiungere il costruttore senza parametri

  • NON CONSENTITA: aggiunta di readonly a un campo

  • NON CONSENTITA: limitazione della visibilità di un membro

    Ciò include la limitazione della visibilità di un membro protetto quando non sono presenti costruttori accessibili (public o protected) e il tipo non è sealed. Se questo non avviene, la limitazione della visibilità di un membro protetto è consentita.

    L'espansione della visibilità di un membro è consentita.

  • NON CONSENTITA: modifica del tipo di membro

    Il valore restituito di un metodo o il tipo di una proprietà o di un campo non può essere modificato. Ad esempio, la firma di un metodo che restituisce un tipo Object non può essere modificata in modo da restituire un tipo String o viceversa.

  • NON CONSENTITA: aggiunta di un campo di istanza a uno struct senza campi non pubblici

    Se uno struct ha solo campi pubblici o non dispone di campi, i chiamanti possono dichiarare variabili locali di tale tipo di struct senza chiamare il costruttore dello struct o inizializzare prima di tutto l'oggetto locale in default(T), purché tutti i campi pubblici vengano impostati nello struct prima di usarlo. L'aggiunta di nuovi campi, pubblici o non pubblici, a tale struct è una modifica che causa un'interruzione di origine per questi chiamanti, perché il compilatore richiederà ora l'inizializzazione dei campi aggiuntivi.

    Inoltre, l'aggiunta di nuovi campi, pubblici o non pubblici, a uno struct senza campi o solo campi pubblici è una modifica di rilievo binaria ai chiamanti che hanno applicato [SkipLocalsInit] al codice. Poiché il compilatore non è a conoscenza di questi campi in fase di compilazione, potrebbe generare IL che non inizializza completamente lo struct, causando la creazione dello struct da dati dello stack non inizializzati.

    Se uno struct include campi non pubblici, il compilatore applica già l'inizializzazione tramite il costruttore o default(T), e l'aggiunta di nuovi campi di istanza non è una modifica che causa un'interruzione.

  • NON CONSENTITA: generazione di un evento esistente che in precedenza non veniva mai generato

Modifiche comportamentali

Assembly

  • ✔️ CONSENTITA: rendere portabile un assembly quando le stesse piattaforme sono ancora supportate

  • NON CONSENTITA: modifica del nome di un assembly

  • NON CONSENTITA: modifica della chiave pubblica di un assembly

Proprietà, campi, parametri e valori restituiti

  • ✔️ CONSENTITA: modifica del valore di una proprietà, di un campo, di un valore restituito o di un parametro out in un tipo più derivato

    Ad esempio, un metodo che restituisce un tipo Object può restituire un'istanza di String. Non è tuttavia possibile modificare la firma del metodo.

  • ✔️ CONSENTITA: aumento dell'intervallo di valori accettati per una proprietà o un parametro se il membro non è virtuale

    L'intervallo di valori che possono essere passati al metodo o che vengono restituiti dal membro può essere esteso, ma il tipo di parametro o di membro deve rimanere invariato. Ad esempio, i valori passati a un metodo possono aumentare da 0-124 a 0-255, ma il tipo di parametro non può cambiare da Byte a Int32.

  • NON CONSENTITA: aumento dell'intervallo di valori accettati per una proprietà o un parametro se il membro è virtuale

    Questa modifica interrompe i membri sottoposti a override esistenti, che non funzioneranno correttamente per l'intervallo di valori esteso.

  • NON CONSENTITA: riduzione dell'intervallo di valori accettati per una proprietà o un parametro

  • NON CONSENTITA: aumento dell'intervallo di valori restituiti per una proprietà, un campo, un valore restituito o un parametro out

  • NON CONSENTITA: modifica dei valori restituiti per una proprietà, un campo, un valore restituito da un metodo o un parametro out

  • NON CONSENTITA: modifica del valore predefinito di una proprietà, un campo o un parametro

    La modifica o la rimozione di un valore predefinito di un parametro non è un'interruzione binaria. La rimozione di un valore predefinito di un parametro è un'interruzione di origine e la modifica di un valore predefinito di un parametro può comportare un'interruzione comportamentale dopo la ricompilazione.

    Per questo motivo, la rimozione dei valori predefiniti dei parametri è accettabile nel caso specifico di "spostamento" di tali valori predefiniti in un nuovo overload del metodo al fine di eliminare l'ambiguità. Si consideri ad esempio un metodo MyMethod(int a = 1) esistente. Se si introduce un overload di MyMethod con due parametri facoltativi a e b, è possibile mantenere la compatibilità spostando il valore predefinito di a nel nuovo overload. Ora i due overload sono MyMethod(int a) e MyMethod(int a = 1, int b = 2). Questo modello consente a MyMethod() di effettuare la compilazione.

  • NON CONSENTITA: modifica della precisione di un valore numerico restituito

  • ❓RICHIEDE UN GIUDIZIO: modifica nell'analisi dell'input e nella generazione di nuove eccezioni (anche se il comportamento dell'analisi non è specificato nella documentazione

Eccezioni

  • ✔️ CONSENTITA: generazione di un'eccezione più derivata rispetto a un'eccezione esistente

    Poiché la nuova eccezione è una sottoclasse di un'eccezione esistente, il codice che gestisce l'eccezione precedente continua a gestire quella nuova. In .NET Framework 4, ad esempio, i metodi per la creazione e il recupero delle impostazioni cultura hanno iniziato a generare CultureNotFoundException anziché ArgumentException in caso di impossibilità di trovare le impostazioni cultura. Poiché CultureNotFoundException deriva da ArgumentException, questa è una modifica accettabile.

  • ✔️ CONSENTITA: generazione di un'eccezione più specifica rispetto a NotSupportedException, NotImplementedException, NullReferenceException e

  • ✔️ CONSENTITA: generazione di un'eccezione considerata irreversibile

    Le eccezioni irreversibili non devono essere intercettate, ma devono essere gestite da un gestore catch-all di alto livello. Non è quindi previsto che gli utenti abbiano codice che intercetta queste eccezioni esplicite. Le eccezioni irreversibili comprendono:

  • ✔️ CONSENTITA: generazione di una nuova eccezione in un nuovo percorso del codice

    L'eccezione deve interessare solo un nuovo percorso del codice che viene eseguito con un nuovo stato o nuovi valori di parametro e non può essere eseguito da codice esistente creato per la versione precedente.

  • ✔️ CONSENTITA: rimozione di un'eccezione per abilitare nuovi scenari o un comportamento più efficace

    Ad esempio, un metodo Divide che in precedenza gestiva solo valori positivi, generando un'eccezione ArgumentOutOfRangeException negli altri casi, può essere modificato in modo da supportare valori negativi e positivi senza generare un'eccezione.

  • ✔️ CONSENTITA: modifica del testo di un messaggio di errore

    Gli sviluppatori non devono basarsi sul testo dei messaggi di errore, che cambiano anche in base alle impostazioni cultura dell'utente.

  • NON CONSENTITA: generazione di un'eccezione in tutti gli altri casi non elencati

  • NON CONSENTITA: rimozione di un'eccezione in tutti gli altri casi non elencati

Attributi

  • ✔️ CONSENTITA: modifica del valore di un attributo che non è osservabile

  • ✔️ CONSENTITA: modifica del valore di un attributo che è osservabile

  • ❓ RICHIEDE UN GIUDIZIO: rimozione di un attributo

    Nella maggior parte dei casi, la rimozione di un attributo (ad esempio NonSerializedAttribute) è una modifica che causa un'interruzione.

Piattaforme supportate

  • ✔️CONSENTITA: supporto di un'operazione su una piattaforma che in precedenza non era supportata

  • NON CONSENTITA: mancato supporto o nuova richiesta di uno specifico Service Pack per un'operazione che in precedenza era supportata su una piattaforma

Modifiche all'implementazione interna

  • ❓RICHIEDE UN GIUDIZIO: modifica dell'area di superficie di un tipo interno

    Queste modifiche sono in genere consentite, anche se interrompono la reflection privata. È tuttavia possibile che in alcuni casi non lo siano, ad esempio quando le librerie di terze parti più diffuse o numerosi sviluppatori dipendono dalle API interne.

  • ❓RICHIEDE UN GIUDIZIO: modifica dell'implementazione interna di un membro

    Queste modifiche sono in genere consentite, anche se interrompono la reflection privata. È tuttavia possibile che in alcuni casi non lo siano, ad esempio quando il codice cliente dipende spesso dalla reflection privata o la modifica presenta effetti collaterali imprevisti.

  • ✔️ CONSENTITA: miglioramento delle prestazioni di un'operazione

    La possibilità di modificare le prestazioni di un'operazione è essenziale, ma tali modifiche possono interrompere il codice che si basa la velocità corrente di un'operazione. Questo vale in particolare per il codice che dipende dalla tempistica delle operazioni asincrone. La modifica delle prestazioni non deve avere alcun effetto su altri comportamenti dell'API in questione. In caso contrario, la modifica causerà un'interruzione.

  • ✔️ CONSENTITA: modifica indiretta (e spesso con effetti negativi) delle prestazioni di un'operazione

    La modifica in questione è accettabile se non è classificata per altri motivi come modifica che causa un'interruzione. È spesso necessario eseguire azioni che includono operazioni supplementari o che aggiungono nuove funzionalità. Questo ha quasi sempre effetto sulle prestazioni, ma può essere fondamentale per fare in modo che l'API in questione funzioni come previsto.

  • NON CONSENTITA: modifica di un'API sincrona in una asincrona (e viceversa)

Modifiche al codice

  • ✔️CONSENTITA: aggiunta di parametri a un parametro

  • NON CONSENTITA: modifica di uno struct in una classe e viceversa

  • NON CONSENTITA: aggiunta della parola chiave checked a un blocco di codice

    Per effetto di questa modifica, il codice eseguito in precedenza può generare un'eccezione OverflowException e non essere accettabile.

  • NON CONSENTITA: rimozione di paramsda un parametro

  • NON CONSENTITA: modifica dell'ordine di generazione degli eventi

    Gli sviluppatori possono ragionevolmente aspettarsi che gli eventi vengano generati nello stesso ordine e il codice degli sviluppatori spesso dipende dall'ordine di generazione degli eventi.

  • NON CONSENTITA: rimozione della generazione di un evento per effetto di una determinata azione

  • NON CONSENTITA: modifica del numero di volte in cui vengono chiamati determinati eventi

  • NON CONSENTITA: aggiunta di FlagsAttribute a un tipo di enumerazione

Vedi anche