Domanda

Vorrei raccogliere quante più informazioni possibili sul controllo delle versioni dell'API in .NET/CLR e in particolare su come le modifiche dell'API interrompono o meno le applicazioni client.Per prima cosa definiamo alcuni termini:

Modifica dell'API - una modifica nella definizione pubblicamente visibile di un tipo, inclusi i suoi membri pubblici.Ciò include la modifica dei nomi dei tipi e 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 (inclusi gli sovraccarichi), la modifica della visibilità dei membri, la ridenominazione di parametri di metodo e tipo, l'aggiunta di valori predefiniti per i parametri del metodo, aggiunta/rimozione di attributi su tipi e membri e aggiunta/rimozione di parametri di tipo generico su tipi e membri (mi sono perso qualcosa?).Ciò non include eventuali modifiche agli organi membri o eventuali modifiche ai membri privati ​​(ad es.non prendiamo in considerazione la Riflessione).

Rottura a livello binario - una modifica dell'API che comporta il mancato caricamento degli assembly client compilati rispetto alla versione precedente dell'API con la nuova versione.Esempio:cambiando la firma del metodo, anche se permette di essere chiamato nello stesso modo di prima (cioè:void per restituire sovraccarichi di valori predefiniti di tipo/parametro).

Interruzione a livello di origine - una modifica dell'API che fa sì che il codice esistente scritto per essere compilato con la versione precedente dell'API potrebbe non essere compilato 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 ai metodi che in precedenza non erano ambigue.

Modifica della semantica silenziosa a livello di origine - una modifica dell'API che comporta un codice esistente scritto per essere compilato con una versione precedente dell'API che ne modifica silenziosamente la semantica, ad es.chiamando un metodo diverso.Il codice dovrebbe tuttavia continuare a essere compilato senza avvisi/errori e gli assembly precedentemente compilati dovrebbero funzionare come prima.Esempio:implementare una nuova interfaccia su una classe esistente che comporta la scelta di un sovraccarico diverso durante la risoluzione dell'overload.

L'obiettivo finale è catalogare il maggior numero possibile di modifiche API semantiche di rottura e silenziose e descrivere l'effetto esatto della rottura e quali lingue ne sono interessate e quali non ne sono interessate.Per approfondire quest'ultimo:mentre alcuni cambiamenti riguardano universalmente tutte le lingue (ad es.l'aggiunta di un nuovo membro a un'interfaccia interromperà le implementazioni di quell'interfaccia in qualsiasi linguaggio), alcuni richiedono che una semantica linguistica molto specifica entri in gioco per ottenere una pausa.Ciò in genere comporta l'overload del metodo e, in generale, qualsiasi cosa abbia a che fare con le conversioni di tipo implicite.Non sembra esserci alcun modo per definire il "minimo comune denominatore" qui anche per i linguaggi conformi a CLS (cioèquelli conformi almeno alle regole del "consumatore CLS" come definito nelle specifiche CLI) - anche se apprezzerò se qualcuno mi corregge perché ho torto qui - quindi questo dovrà andare lingua per lingua.Quelli di maggior interesse sono naturalmente quelli forniti immediatamente con .NET:Do#, Verb e Fa#;ma sono rilevanti anche altri, come IronPython, IronRuby, Delphi Prism ecc.Più è un caso limite, più sarà interessante: cose come la rimozione di membri sono abbastanza evidenti, ma le sottili interazioni tra ad es.l'overload del metodo, i parametri opzionali/predefiniti, l'inferenza del tipo lambda e gli operatori di conversione a volte possono essere molto sorprendenti.

Alcuni esempi per avviare questo processo:

Aggiunta di nuovi sovraccarichi di metodo

Tipo:interruzione a livello di sorgente

Lingue interessate:Do#, Verb, Fa#

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

Esempio di codice client 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#, Verb

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

Esempio di codice client funzionante prima della modifica e interrotto dopo:

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

Appunti:F# non è danneggiato, perché non ha alcun supporto a livello di linguaggio per gli operatori sovraccarichi, né espliciti né impliciti: entrambi devono essere chiamati direttamente come op_Explicit E op_Implicit metodi.

Aggiunta di nuovi metodi di istanza

Tipo:modifica della semantica silenziosa a livello di sorgente.

Lingue interessate:C#, Verb

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 silenziosa della semantica:

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

new Foo().Bar();

Appunti:F# non è danneggiato perché non dispone del supporto a livello di linguaggio per ExtensionMethodAttribute, e richiede che i metodi di estensione CLS vengano chiamati come metodi statici.

È stato utile?

Soluzione

Modifica della firma di un metodo

Tipo:Rottura a livello binario

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

API prima della modifica

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

API dopo la modifica

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

Esempio di codice client funzionante prima della modifica

Foo.bar(13);

Altri suggerimenti

Aggiunta di un parametro con un valore predefinito.

Tipo di pausa:Rottura a livello binario

Anche se il codice sorgente chiamante non necessita di modifiche, 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 ricompili, otterrai una MissingMethodException perché il vecchio assembly tenta di chiamare un metodo con meno argomenti.

API prima della modifica

public void Foo(int a) { }

API dopo la modifica

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

Codice client di esempio che viene successivamente danneggiato

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 caratteristica del linguaggio, il runtime .Net non ne sa nulla.(Questo spiega anche perché i valori predefiniti devono essere costanti in fase di compilazione in C#).

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

Refactoring dei membri della classe in una classe base

Tipo:non una pausa!

Lingue interessate:nessuno (cioènessuno è rotto)

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 rompesse):

// 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 dispone di un costrutto analogo all'implementazione esplicita dell'interfaccia per i membri della classe base virtuale: "override esplicito".Mi aspettavo pienamente che ciò provocasse lo stesso tipo di rottura di quando si spostano i membri dell'interfaccia su un'interfaccia di base (poiché IL generato per l'override esplicito è lo stesso dell'implementazione esplicita).Con mia sorpresa, non è così, anche se IL generato lo specifica ancora BarOverride sovrascrive Foo::Bar piuttosto che FooBase::Bar, il caricatore di assemblaggi è abbastanza intelligente da sostituirne correttamente uno con l'altro senza alcuna lamentela - a quanto pare, il fatto che Foo è una classe è ciò che fa la differenza.Vai a capire...

Questo è un caso speciale forse non così ovvio di "aggiunta/rimozione di membri dell'interfaccia", e ho pensato che meritasse una voce a parte alla luce di un altro caso che pubblicherò di seguito.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;quello binario influisce naturalmente su qualsiasi lingua)

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 interrotto da una modifica a livello di origine:

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

Codice client di esempio interrotto da una modifica a livello binario;

(new Foo()).Bar();

Appunti:

Per l'interruzione del livello di origine, il problema è che C#, VB e C++/CLI richiedono tutti esatto nome dell'interfaccia nella dichiarazione di implementazione del membro dell'interfaccia;quindi, 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 completamente qualificati nell'IL generato per implementazioni esplicite e anche il nome dell'interfaccia deve essere esatto.

Implementazione implicita ove disponibile (ad es.C# e C++/CLI, ma non VB) funzioneranno correttamente sia a livello sorgente che binario.Anche le chiamate ai metodi non si interrompono.

Riordinamento dei valori enumerati

Tipo di pausa: Modifica della semantica silenziosa a livello di origine/binario

Lingue interessate:Tutto

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

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

API prima della modifica

public enum Foo
{
   Bar,
   Baz
}

API dopo la modifica

public enum Foo
{
   Baz,
   Bar
}

Codice client di esempio che funziona ma successivamente si rompe:

Foo.Bar < Foo.Baz

Questa è davvero una cosa molto rara nella pratica, ma comunque sorprendente quando accade.

Aggiunta di nuovi membri non sovraccaricati

Tipo:interruzione del livello di origine o cambiamento silenzioso della semantica.

Lingue interessate:C#, Verb

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 danneggiato 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 della risoluzione dell'overload.Qui viene impiegata una forma limitata di tipizzazione duck per rompere i legami in cui più di un tipo corrisponde, controllando se il corpo della lambda ha senso per un dato 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 qualsiasi dei suoi codici si basa quindi sull'algoritmo di inferenza del tipo per determinare il metodo corretto basato esclusivamente sulla presenza o 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 un'inferenza disattivato, con conseguente ambiguità durante la risoluzione dell'overload.

Nota che tipi Foo E Bar in questo esempio non sono imparentati in alcun modo, né per eredità né altro.Il semplice utilizzo di essi in un singolo gruppo di metodi è sufficiente per attivare ciò e, se ciò si verifica nel codice client, non ne hai alcun controllo.

Il codice di esempio riportato sopra dimostra una situazione più semplice in cui si tratta di un'interruzione a livello di origine (ad esempiorisultati degli errori del compilatore).Tuttavia, questo può anche trattarsi di un cambiamento semantico silenzioso, se l'overload scelto tramite inferenza avesse altri argomenti che altrimenti lo avrebbero classificato di seguito (ad es.argomenti facoltativi con valori predefiniti o mancata corrispondenza del tipo tra l'argomento dichiarato e quello effettivo che richiede una conversione implicita).In tale scenario, la risoluzione dell'overload non fallirà più, ma un diverso sovraccarico verrà selezionato silenziosamente dal compilatore.In pratica, tuttavia, è molto difficile imbattersi in questo caso senza costruire attentamente le firme del metodo per provocarlo deliberatamente.

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

Tipo di pausa:Sorgente e binario

Lingue interessate:Tutto

Questa è in realtà solo una variante 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 avvengono necessariamente tramite 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 interrotto in seguito:

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

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

Tipo di pausa:Fonte

Lingue interessate:Tutto

Il refactoring di un'implementazione di interfaccia esplicita in una implicita è più sottile nel modo in cui può interrompere un'API.A prima vista sembrerebbe che questo dovrebbe essere relativamente sicuro, tuttavia, se combinato con l’ereditarietà, 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 interrotto in seguito:

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"

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, qualsiasi codice esterno che faccia riferimento in qualche modo a quel membro dovrà essere ricompilato.

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 successivamente si rompe:

Foo.Bar = "foobar"

Aggiunta dello spazio dei nomi

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

A causa del modo in cui funziona la risoluzione dello spazio dei nomi in vb.Net, l'aggiunta di uno spazio dei nomi a una libreria può causare la mancata compilazione del codice Visual Basic compilato con una versione precedente dell'API con una nuova versione.

Codice cliente 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 precedente non verrà compilato.

Diventa più complicato con le importazioni di spazi dei nomi a livello di progetto.Se Imports System è omesso dal codice precedente, ma il file System namespace viene importato a livello di progetto, il codice potrebbe comunque generare un errore.

Tuttavia, se l'Api include una classe DataRow nel suo Api.SomeNamespace.Data namespace, il codice verrà compilato ma dr sarà un esempio di System.Data.DataRow quando compilato con la vecchia versione dell'API e Api.SomeNamespace.Data.DataRow quando compilato con la nuova versione dell'API.

Rinominazione degli argomenti

Interruzione a livello di origine

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 cliente di esempio:

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

Parametri di rif

Interruzione a livello di origine

L'aggiunta di un override del metodo con la stessa firma, tranne per il fatto che un parametro viene passato per riferimento anziché per valore, farà sì che l'origine vb che fa riferimento all'API non sia in grado 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 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 cliente di esempio:

Api.SomeNamespace.Foo.Bar(str)

Campo per la modifica della proprietà

Rottura a livello binario/Rottura a livello di origine

Oltre all'ovvia 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 cliente di esempio:

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

Modifica dell'API:

  1. Aggiunta dell'attributo [Obsoleto] (lo hai più o meno coperto menzionando gli attributi;tuttavia, questa può essere una modifica sostanziale quando si utilizza l'avviso come errore.)

Rottura a livello binario:

  1. Spostamento di un tipo da un assembly 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 di un altro assembly (Class2) come vincolo di argomento del 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 adattino)

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 un'installazione di framework diversa (ad es.l'installazione di 3.5 su un box .Net 2.0 consente chiamate API che richiedono quindi .Net 2.0 SP2)

Modifiche al bootstrap/configurazione:

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

Aggiornamento:

Scusa, non avevo capito che l'unico motivo per cui questo non funzionava per me era che li usavo nei vincoli del modello.

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

Tipo di pausa: Modifica della semantica silenziosa a livello di sorgente

Poiché il compilatore trasforma le chiamate al 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.

D'altro canto, le chiamate senza l'utilizzo di parametri facoltativi vengono ora compilate come una chiamata al nuovo metodo a cui manca il parametro facoltativo.Tutto funziona ancora correttamente, ma se il codice chiamato risiede in un altro assembly, il codice appena compilato che lo chiama ora dipende dalla nuova versione di questo assembly.La distribuzione di assembly che chiamano il codice sottoposto a refactoring senza distribuire anche l'assembly in cui risiede il codice sottoposto a refactoring genera eccezioni "metodo non trovato".

API prima della modifica

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

Rinominare un'interfaccia

Tipo di pausa:Fonte e Binario

Lingue interessate:Molto probabilmente tutti, testati 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 successivamente si rompe:

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

Metodo di overload con un parametro di tipo nullable

Tipo: Interruzione a livello di origine

Lingue interessate: C#, Verb

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

Esempio di codice client funzionante prima della modifica e interrotto dopo:

new Foo().Bar(null);

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

Promozione a Metodo di Estensione

Tipo:interruzione a livello di sorgente

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

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

Esempio di codice client 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

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top