Dela via


Versionshantering i C#

I den här självstudien får du lära dig vad versionshantering innebär i .NET. Du lär dig också vilka faktorer du bör tänka på när du versionshanterar ditt bibliotek och uppgraderar till en ny version av ett bibliotek.

Språkversion

C#-kompilatorn är en del av .NET SDK. Som standard väljer kompilatorn den C#-språkversion som matchar den valda TFM för projektet. Om SDK-versionen är större än ditt valda ramverk kan kompilatorn använda en högre språkversion. Du kan ändra standardinställningen genom att ange elementet LangVersion i projektet. Du kan lära dig hur du i vår artikel om kompilatoralternativ.

Varning

Det rekommenderas inte att ange elementet LangVersion till latest . Inställningen latest innebär att den installerade kompilatorn använder sin senaste version. Det kan ändras från dator till dator, vilket gör byggen otillförlitliga. Dessutom möjliggör den språkfunktioner som kan kräva körnings- eller biblioteksfunktioner som inte ingår i den aktuella SDK:t.

Författarbibliotek

Som utvecklare som har skapat .NET-bibliotek för offentligt bruk har du förmodligen varit i situationer där du måste distribuera nya uppdateringar. Hur du går till väga för den här processen är viktigt eftersom du behöver se till att det sker en sömlös övergång av befintlig kod till den nya versionen av biblioteket. Här är flera saker att tänka på när du skapar en ny version:

Semantisk versionshantering

Semantisk versionshantering (SemVer för kort) är en namngivningskonvention som tillämpas på versioner av ditt bibliotek för att beteckna specifika milstolpehändelser. Helst bör versionsinformationen som du ger ditt bibliotek hjälpa utvecklare att fastställa kompatibiliteten med sina projekt som använder äldre versioner av samma bibliotek.

Den mest grundläggande metoden för SemVer är 3-komponentformatet MAJOR.MINOR.PATCH, där:

  • MAJOR ökas när du gör inkompatibla API-ändringar
  • MINOR ökas när du lägger till funktioner på ett bakåtkompatibelt sätt
  • PATCH ökas när du gör bakåtkompatibla felkorrigeringar

Förstå versionssteg med exempel

Här är några konkreta exempel för att klargöra när varje versionsnummer ska ökas:

Större versionssteg (inkompatibla API-ändringar)

Dessa ändringar kräver att användarna ändrar sin kod för att fungera med den nya versionen:

  • Tar bort en offentlig metod eller egenskap:

    // 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!
    }
    
  • Ändra metodsignaturer:

    // 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
    
  • Ändra beteendet för befintliga metoder på ett sätt som bryter förväntningarna:

    // 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
    

MINOR versionsuppdateringar (bakåtkompatibla funktioner)

Dessa ändringar lägger till nya funktioner utan att den befintliga koden bryts:

  • Lägga till nya offentliga metoder eller egenskaper:

    // 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
    }
    
  • Lägga till nya överlagringar:

    // 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
    
  • Lägga till valfria parametrar i befintliga metoder:

    // 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
    

    Kommentar

    Det här är en källkompatibel förändring, men en binärt brytande förändring. Användare av det här biblioteket måste kompilera om för att det ska fungera korrekt. Många bibliotek skulle endast överväga detta i större versionsändringar, inte delversionsändringar .

Versionsökningar för PATCH (felkorrigeringar som är bakåtkompatibla)

Dessa ändringar åtgärdar problem utan att lägga till nya funktioner eller bryta befintliga funktioner:

  • Åtgärda en bugg i implementeringen av en befintlig metod:

    // 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
    }
    
  • Prestandaförbättringar som inte ändrar API:et:

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

Huvudprincipen är: om befintlig kod kan använda din nya version utan ändringar är det en MINOR- eller PATCH-uppdatering. Om befintlig kod behöver ändras för att fungera med din nya version är det en MAJOR-uppdatering.

Det finns också sätt att ange andra scenarier, till exempel förhandsversioner, när du tillämpar versionsinformation på .NET-biblioteket.

Bakåtkompatibilitet

När du släpper nya versioner av biblioteket är bakåtkompatibilitet med tidigare versioner förmodligen ett av de viktigaste problemen. En ny version av biblioteket är källkompatibel med en tidigare version om kod som är beroende av den tidigare versionen kan, när den kompileras om, fungera med den nya versionen. En ny version av biblioteket är binärkompatibel om ett program som är beroende av den gamla versionen, utan omkompilering, kan arbeta med den nya versionen.

Här följer några saker att tänka på när du försöker upprätthålla bakåtkompatibilitet med äldre versioner av biblioteket:

  • Virtuella metoder: När du gör en virtuell metod icke-virtuell i din nya version innebär det att projekt som åsidosätter den metoden måste uppdateras. Detta är en enormt genomgripande förändring och avråds starkt.
  • Metodsignaturer: När du uppdaterar ett metodbeteende måste du också ändra dess signatur, du bör i stället skapa en överlagring så att kodanrop till den metoden fortfarande fungerar. Du kan alltid ändra den gamla metodsignaturen för att anropa den nya metodsignaturen så att implementeringen förblir konsekvent.
  • Föråldrat attribut: Du kan använda det här attributet i koden för att ange klasser eller klassmedlemmar som är inaktuella och som sannolikt kommer att tas bort i framtida versioner. Detta säkerställer att utvecklare som använder ditt bibliotek är bättre förberedda för icke-bakåtkompatibla ändringar.
  • Valfria metodargument: När du gör tidigare valfria metodargument obligatoriska eller ändrar deras standardvärde måste all kod som inte anger dessa argument uppdateras.

Kommentar

Att göra obligatoriska argument valfria bör ha mycket liten effekt, särskilt om det inte ändrar metodens beteende.

Ju enklare du gör det för användarna att uppgradera till den nya versionen av biblioteket, desto mer troligt är det att de uppgraderar tidigare.

Konfigurationsfil för program

Som .NET-utvecklare finns det en mycket stor chans att du har stött på app.config filen som finns i de flesta projekttyper. Den här enkla konfigurationsfilen kan gå långt för att förbättra distributionen av nya uppdateringar. Du bör vanligtvis utforma dina bibliotek på ett sådant sätt att information som sannolikt kommer att ändras regelbundet lagras i app.config filen, på det här sättet när sådan information uppdateras, behöver konfigurationsfilen för äldre versioner bara ersättas med den nya utan att biblioteket behöver kompileras om.

Använda bibliotek

Som utvecklare som använder .NET-bibliotek som skapats av andra utvecklare är du förmodligen medveten om att en ny version av ett bibliotek kanske inte är helt kompatibel med ditt projekt och du kanske ofta måste uppdatera koden för att arbeta med dessa ändringar.

Som tur är har C# och .NET-ekosystemet funktioner och tekniker som gör det enkelt för oss att uppdatera appen så att den fungerar med nya versioner av bibliotek som kan medföra brytande ändringar.

Omdirigering av sammansättningsbindning

Du kan använda filen app.config för att uppdatera versionen av ett bibliotek som appen använder. Genom att lägga till vad som kallas en bindningsomdirigering kan du använda den nya biblioteksversionen utan att behöva kompilera om din app. I följande exempel visas hur du uppdaterar appens app.config-fil för att använda 1.0.1 korrigeringsversionen ReferencedLibrary i stället för den version som den 1.0.0 ursprungligen kompilerades med.

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

Kommentar

Den här metoden fungerar bara om den nya versionen av ReferencedLibrary är binärkompatibel med din app. Se avsnittet Bakåtkompatibilitet ovan för ändringar att hålla utkik efter när du fastställer kompatibilitet.

Ny

Du använder new modifieraren för att dölja ärvda medlemmar i en basklass. Det här är ett sätt som härledda klasser kan svara på uppdateringar i basklasser.

Se följande exempel:

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

Resultat

A base method
A derived method

I exemplet ovan kan du se hur DerivedClass döljer metoden MyMethod som finns i BaseClass. Det innebär att när en basklass i den nya versionen av ett bibliotek lägger till en medlem som redan finns i din härledda klass kan du helt enkelt använda new modifieraren på din härledda klassmedlem för att dölja basklassmedlemmen.

När ingen new modifierare har angetts döljer en härledd klass som standard motstridiga medlemmar i en basklass, även om en kompilatorvarning genereras kommer koden fortfarande att kompileras. Det innebär att om du bara lägger till nya medlemmar i en befintlig klass blir den nya versionen av biblioteket både käll- och binärkompatibel med kod som är beroende av den.

åsidosätta

Modifieraren override innebär att en härledd implementering utökar implementeringen av en basklassmedlem i stället för att dölja den. Basklassmedlemmen måste ha virtual modifieraren tillämpad på den.

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

Resultat

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

Modifieraren override utvärderas vid kompileringstillfället och kompilatorn utlöser ett fel om den inte hittar någon virtuell medlem att åsidosätta.

Dina kunskaper om de diskuterade teknikerna och din förståelse för de situationer där du kan använda dem kommer att underlätta övergången mellan versioner av ett bibliotek.