Condividi tramite


Controllo delle versioni in C#

In questa esercitazione si apprenderà il significato del controllo delle versioni in .NET. Verranno anche presentati i fattori da considerare durante il controllo delle versioni della libreria e verrà descritto l'aggiornamento a una nuova versione di una libreria.

Versione della lingua

Il compilatore C# fa parte di .NET SDK. Per impostazione predefinita, il compilatore sceglie la versione del linguaggio C# corrispondente al TFM scelto per il progetto. Se la versione dell'SDK è successiva a quella del framework scelto, il compilatore potrebbe usare una versione del linguaggio successiva. È possibile modificare l'impostazione predefinita impostando l'elemento LangVersion nel progetto. Per altre informazioni, vedere l'articolo sulle opzioni del compilatore.

Avviso

Si sconsiglia di impostare l'elemento LangVersion su latest. L'impostazione latest indica che il compilatore installato utilizza la versione più recente. Questo può cambiare da un computer a un altro, rendendo le compilazioni inaffidabili. Inoltre, abilita le funzionalità del linguaggio che potrebbero richiedere funzionalità del runtime o della libreria non incluse nell'SDK corrente.

Creazione di librerie

Gli sviluppatori che hanno creato le librerie .NET per uso pubblico probabilmente si sono trovati in situazioni in cui è necessario distribuire i nuovi aggiornamenti. Le modalità di gestione di questo processo sono molto importanti poiché è necessario garantire che non si verifichino problemi durante la transizione del codice esistente alla nuova versione della libreria. Ecco alcuni aspetti da considerare quando si crea una nuova versione:

Versionamento Semantico

Versionamento Semantico (SemVer per brevità) è una convenzione di denominazione applicata alle versioni della tua libreria per indicare eventi di rilascio specifici. In teoria, le informazioni sulla versione applicate alla libreria consentono agli sviluppatori di determinare la compatibilità con i progetti che usano versioni precedenti di quella libreria.

L'approccio di base a SemVer è il formato a 3 componenti MAJOR.MINOR.PATCH, dove:

  • MAJOR viene incrementato quando si apportano modifiche incompatibili all'API
  • MINOR viene incrementato quando si aggiungono funzionalità in modo retrocompatibile
  • PATCH viene incrementato quando si apportano correzioni di bug retrocompatibili

Informazioni sugli incrementi di versione con esempi

Per chiarire quando incrementare ogni numero di versione, ecco esempi concreti:

Incrementi di versione MAJOR (modifiche API incompatibili)

Queste modifiche richiedono agli utenti di modificare il codice in modo che funzioni con la nuova versione:

  • Rimozione di un metodo o una proprietà pubblica:

    // Version 1.0.0
    public class Calculator
    {
        public int Add(int a, int b) => a + b;
        public int Subtract(int a, int b) => a - b; // This method exists
    }
    
    // Version 2.0.0 - MAJOR increment required
    public class Calculator
    {
        public int Add(int a, int b) => a + b;
        // Subtract method removed - breaking change!
    }
    
  • Modifica delle firme dei metodi:

    // Version 1.0.0
    public void SaveFile(string filename) { }
    
    // Version 2.0.0 - MAJOR increment required
    public void SaveFile(string filename, bool overwrite) { } // Added required parameter
    
  • Modifica del comportamento dei metodi esistenti in modi che interrompono le aspettative:

    // Version 1.0.0 - returns null when file not found
    public string ReadFile(string path) => File.Exists(path) ? File.ReadAllText(path) : null;
    
    // Version 2.0.0 - MAJOR increment required
    public string ReadFile(string path) => File.ReadAllText(path); // Now throws exception when file not found
    

Incrementi di versione minore (funzionalità compatibili con le versioni precedenti)

Queste modifiche aggiungono nuove funzionalità senza interrompere il codice esistente:

  • Aggiunta di nuovi metodi o proprietà pubblici:

    // Version 1.0.0
    public class Calculator
    {
        public int Add(int a, int b) => a + b;
    }
    
    // Version 1.1.0 - MINOR increment
    public class Calculator
    {
        public int Add(int a, int b) => a + b;
        public int Multiply(int a, int b) => a * b; // New method added
    }
    
  • Aggiunta di nuovi sovraccarichi:

    // Version 1.0.0
    public void Log(string message) { }
    
    // Version 1.1.0 - MINOR increment
    public void Log(string message) { } // Original method unchanged
    public void Log(string message, LogLevel level) { } // New overload added
    
  • Aggiunta di parametri facoltativi ai metodi esistenti:

    // Version 1.0.0
    public void SaveFile(string filename) { }
    
    // Version 1.1.0 - MINOR increment
    public void SaveFile(string filename, bool overwrite = false) { } // Optional parameter
    

    Nota

    Si tratta di una modifica compatibile con l'origine, ma una modifica binaria che causa un'interruzione. Gli utenti di questa libreria devono ricompilare affinché funzionino correttamente. Molte librerie considererebbero questa opzione solo nelle modifiche di versione principali , non nelle modifiche di versione secondarie .

Incrementi della versione PATCH (correzioni di bug compatibili con le versioni precedenti)

Queste modifiche consentono di risolvere i problemi senza aggiungere nuove funzionalità o interrompere le funzionalità esistenti:

  • Correzione di un bug nell'implementazione di un metodo esistente:

    // Version 1.0.0 - has a bug
    public int Divide(int a, int b)
    {
        return a / b; // Bug: doesn't handle division by zero
    }
    
    // Version 1.0.1 - PATCH increment
    public int Divide(int a, int b)
    {
        if (b == 0) throw new ArgumentException("Cannot divide by zero");
        return a / b; // Bug fixed, behavior improved but API unchanged
    }
    
  • Miglioramenti delle prestazioni che non modificano l'API:

    // Version 1.0.0
    public List<int> SortNumbers(List<int> numbers)
    {
        return numbers.OrderBy(x => x).ToList(); // Slower implementation
    }
    
    // Version 1.0.1 - PATCH increment
    public List<int> SortNumbers(List<int> numbers)
    {
        var result = new List<int>(numbers);
        result.Sort(); // Faster implementation, same API
        return result;
    }
    

Il principio chiave è: se il codice esistente può usare la nuova versione senza modifiche, si tratta di un aggiornamento SECONDARIO o PATCH. Se il codice esistente deve essere modificato per funzionare con la nuova versione, si tratta di un aggiornamento PRINCIPALE.

Esistono anche modi per specificare altri scenari, ad esempio le versioni non definitive, quando si applicano le informazioni sulla versione alla libreria .NET.

Compatibilità con le versioni precedenti

Quando si rilasciano nuove versioni della libreria, la compatibilità con le versioni precedenti probabilmente è una delle principali preoccupazioni. Una nuova versione della libreria è compatibile a livello di codice sorgente con una versione precedente se il codice che dipende dalla versione precedente, quando viene ricompilato, funziona con la nuova versione. Una nuova versione della libreria è compatibile a livello binario se un'applicazione che dipende dalla versione precedente funziona, senza ricompilazione, con la nuova versione.

Di seguito sono riportati alcuni aspetti da considerare quando si tenta di gestire la compatibilità della libreria con le versioni precedenti:

  • Metodi virtuali: quando si trasforma un metodo virtuale in non virtuale nella nuova versione, sarà necessario aggiornare i progetti che eseguono l'override di tale metodo. Questa è una modifica sostanziale di grande impatto ed è fortemente sconsigliata.
  • Firme del metodo: quando l'aggiornamento del comportamento di un metodo richiede anche la modifica della firma, è necessario creare un overload in modo tale che il codice che chiama il metodo continui a funzionare. È sempre possibile modificare la firma del metodo precedente per chiamare la firma del nuovo metodo in modo che l'implementazione resti coerente.
  • Attributo Obsolete: è possibile usare questo attributo nel codice per specificare le classi o i membri di classe deprecati che potrebbero essere rimossi nelle versioni future. Questo garantisce che gli sviluppatori che utilizzano la tua libreria siano meglio preparati per i cambiamenti critici.
  • Argomenti di metodo facoltativi: se si rendono obbligatori argomenti di metodo che in precedenza erano facoltativi o si modifica il valore predefinito degli argomenti, tutto il codice che non specifica tali argomenti dovrà essere aggiornato.

Nota

Rendere obbligatori gli argomenti facoltativi ha un impatto molto limitato, soprattutto se non modifica il comportamento del metodo.

Più è facile per gli utenti eseguire l'aggiornamento alla nuova versione della libreria, più rapido sarà l'aggiornamento.

File di configurazione dell'applicazione

In qualità di sviluppatore .NET, è molto probabile che tu abbia incontrato il file app.config presente nella maggior parte dei tipi di progetto. Questo semplice file di configurazione è in grado di ottimizzare la distribuzione dei nuovi aggiornamenti. In genere è consigliabile progettare le librerie in modo che le informazioni che probabilmente cambieranno regolarmente vengano memorizzate nel file app.config. Quando le informazioni vengono aggiornate è sufficiente sostituire il file di configurazione delle versioni precedenti con il nuovo file senza dover ricompilare la libreria.

Utilizzo delle librerie

Uno sviluppatore che elabora librerie .NET compilate da altri sviluppatori probabilmente sa che una nuova versione di una libreria potrebbe non essere completamente compatibile con il progetto e che spesso è necessario aggiornare il codice perché funzioni con le modifiche.

Fortunatamente per te, sia C# che l'ecosistema .NET offrono funzionalità e tecniche che ci permettono di aggiornare facilmente la nostra app per farla funzionare con le nuove versioni delle librerie che potrebbero introdurre modifiche disruptive.

Reindirizzamento del collegamento di assembly

È possibile usare il file config.app per aggiornare la versione di una libreria usata dall'applicazione. Aggiungendo un reindirizzamento di binding è possibile usare la nuova versione della libreria senza dover ricompilare l'applicazione. Nell'esempio seguente viene illustrato come aggiornare il file dell’applicazione config.app per usare la versione di patch 1.0.1 di ReferencedLibrary anziché la versione 1.0.0 con cui è stata compilata in origine.

<dependentAssembly>
    <assemblyIdentity name="ReferencedLibrary" publicKeyToken="32ab4ba45e0a69a1" culture="en-us" />
    <bindingRedirect oldVersion="1.0.0" newVersion="1.0.1" />
</dependentAssembly>

Nota

Questo approccio funziona solo se la nuova versione di ReferencedLibrary è compatibile a livello binario con l'applicazione. Consultare la sezione Compatibilità con le versioni precedenti sopra per le modifiche da considerare quando si determina la compatibilità.

Nuovo…

Usare il modificatore new per nascondere i membri ereditati di una classe di base. In questo modo le classi derivate possono rispondere agli aggiornamenti nelle classi di base.

Ad esempio:

public class BaseClass
{
    public void MyMethod()
    {
        Console.WriteLine("A base method");
    }
}

public class DerivedClass : BaseClass
{
    public new void MyMethod()
    {
        Console.WriteLine("A derived method");
    }
}

public static void Main()
{
    BaseClass b = new BaseClass();
    DerivedClass d = new DerivedClass();

    b.MyMethod();
    d.MyMethod();
}

Risultato

A base method
A derived method

Nell'esempio precedente si può vedere come DerivedClass nasconde il metodo MyMethod presente in BaseClass. Ciò significa che quando una classe di base nella nuova versione di una libreria aggiunge un membro già esistente nella classe derivata, è sufficiente usare il modificatore new per il membro della classe derivata per nascondere il membro della classe di base.

Se non si specifica alcun modificatore new, una classe derivata nasconderà per impostazione predefinita i membri in conflitto in una classe di base e il codice verrà comunque compilato anche se viene generato un avviso del compilatore. Ciò significa che la semplice aggiunta di nuovi membri a una classe esistente rende la nuova versione della libreria compatibile sia a livello di codice sorgente che a livello binario con il codice che dipende da essa.

sovrascrivere

Il modificatore override indica che un'implementazione derivata estende l'implementazione di un membro della classe di base anziché nasconderlo. È necessario che al membro della classe di base sia applicato il modificatore virtual.

public class MyBaseClass
{
    public virtual string MethodOne()
    {
        return "Method One";
    }
}

public class MyDerivedClass : MyBaseClass
{
    public override string MethodOne()
    {
        return "Derived Method One";
    }
}

public static void Main()
{
    MyBaseClass b = new MyBaseClass();
    MyDerivedClass d = new MyDerivedClass();

    Console.WriteLine($"Base Method One: {b.MethodOne()}");
    Console.WriteLine($"Derived Method One: {d.MethodOne()}");
}

Risultato

Base Method One: Method One
Derived Method One: Derived Method One

Il modificatore override viene valutato in fase di compilazione e il compilatore genera un errore se non trova un membro virtuale di cui eseguire l'override.

La tua conoscenza delle tecniche discusse e la tua comprensione delle situazioni in cui usarle contribuiranno notevolmente a rendere più agevole la transizione tra diverse versioni di una libreria.