共用方式為


C# 中的版本控制

在本教學課程中,您會了解版本控制在 .NET 中的意義。 您也會了解進行程式庫版本控制以及升級成新版程式庫時要考量的因素。

語言版本

C# 編譯器是 .NET SDK 的一部分。 根據預設,編譯器會選擇符合您專案所選 TFM 的 C# 語言版本。 如果 SDK 版本高於您所選擇的架構,編譯器可以使用更高的語言版本。 您可以藉由在專案中設定 LangVersion 元素來變更預設值。 您可以在編譯器選項的相關文章中了解如何操作。

警告

不建議將 LangVersion 元素設定為 latest。 此 latest 設定表示已安裝的編譯器會使用其最新版本。 這個版本可能會因電腦而異,導致組建不可靠。 此外,它所啟用的語言功能可能需要目前 SDK 中未包含的執行階段或程式庫功能。

撰寫程式庫

身為已建立公用用途 .NET 程式庫的開發人員,您很可能經歷過必須推出新更新的情況。 如何執行此程序影響重大,因為您需要確保現有程式碼能夠順暢轉換到新版文件庫。 以下是建立新版本時要考量的幾件事︰

語意化版本控制系統

語意版本控制 (簡稱為 SemVer) 是文件庫各版本套用的命名慣例,表示特定的重大事件。 理想情況是,您提供給文件庫的版本資訊應該能幫助開發人員判斷其與使用同一文件庫舊版本的專案是否相容。

SemVer 最基本的方法是 3 元件格式 MAJOR.MINOR.PATCH,其中:

  • 當您進行不相容的 API 變更時,MAJOR 會遞增
  • 當您以向後相容的方式新增功能時,MINOR 會增加。
  • 當您進行回溯相容的錯誤修正時,PATCH 將會遞增

透過範例瞭解版本遞增

為了協助釐清何時遞增每個版本號碼,以下是具體範例:

主要版本遞增(不相容的 API 變更)

這些變更會要求使用者修改其程式代碼以使用新版本:

  • 移除公開方法或屬性:

    // 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!
    }
    
  • 變更方法簽章:

    // 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
    
  • 以意想不到的方式改變現有方法的行為以至於打破預期:

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

小版本更新(向後兼容功能)

這些變更會新增新功能,而不會中斷現有的程序代碼:

  • 新增新的公用方法或屬性:

    // 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
    }
    
  • 新增多載:

    // 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
    
  • 將選擇性參數新增至現有的方法:

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

    注意

    這是 來源相容變更,但 二進位中斷性變更。 此程式庫的使用者必須重新編譯才能正常運作。 許多函式庫只會在 主要 版本變更時考慮這種情況,而不會在 次要 版本變更時考慮。

PATCH 版本遞增(向後兼容錯誤修正)

這些變更會修正問題,而不需要新增新功能或中斷現有的功能:

  • 修復現有方法實作中的錯誤:

    // 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
    }
    
  • 效能改進:不會改變 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;
    }
    

主要原則是:如果現有的程式碼可以不需變更就使用新版本,那麼這是一次小版本更新或修補更新。 如果需要修改現有的程式代碼以使用您的新版本,這是主要更新。

將版本資訊套用至您的 .NET 程式庫時,還有其他方式可以指定其他狀況,例如發行前版本等等。

回溯相容性

當您發行新版本的程式庫時,與舊版本的向下相容性將很可能成為您的主要考量之一。 如果相依於前一版的程式碼在重新編譯時,能在新版正常運作,那麼您的程式庫新舊兩個版本就是源碼相容。 如果相依於舊版本的應用程式能在不重新編譯的情況下正常運作,那麼新版的程式庫與舊版就是二進位相容。

以下是嘗試維持與舊版文件庫的相容性時要考慮的一些事項︰

  • 虛擬方法:當您在新版本中將虛擬方法改為非虛擬方法時,意味著覆寫該方法的專案必須進行更新。 這是一項極重大的變更,強烈建議您不要這麼做。
  • 方法簽章︰如果更新方法行為時需要變更其簽章,您應該改建立重載,這樣呼叫該方法的程式碼仍然可以正常運作。 您可以調整舊的方法簽章以呼叫新的方法簽章,使實作保持一致。
  • Obsolete 屬性︰您可以在程式碼中使用這個屬性,指定未來版本中要取代及可能移除的類別或類別成員。 這可確保使用您文件庫的開發人員在面對重大變更時有更完善的準備。
  • 選擇性的方法引數︰當您將先前的選擇性方法引數變為強制,或是變更其預設值後,所有不提供這些引數的程式碼都需要更新。

注意

將強制引數變成選擇性應該沒什麼效果,特別是如果它不變更方法行為時。

使用者升級至新版文件庫的過程愈簡單,他們就愈可能快速升級。

應用程式組態檔

身為 .NET 開發人員,您在大多數的專案類型中,有很大的機會遇到app.config 檔案。 這個簡單的組態檔可以大幅改善新更新的推出流程。 文件庫的設計通常應該是將可能定期變更的資訊儲存在 app.config 檔案中,因此當這類資訊更新時,只要將舊版的組態檔替換成新版即可,不需要重新編譯文件庫。

使用程式庫

身為使用其他開發人員所組建的 .NET 文件庫的開發人員,您很清楚新版文件庫可能無法與您的專案完全相容,而您可能必須經常更新自己的程式碼,才能使用這些變更。

幸而 C# 和 .NET 生態系統隨附的功能和技巧讓我們能輕鬆更新應用程式,使用可能會推出重大變更的新版程式庫。

組件繫結重新導向

您可以使用 app.config 檔案更新應用程式使用的文件庫版本。 透過新增所謂的 繫結重新導向,您可以使用新的程式庫版本,而不必重新編譯應用程式。 下例示範如何更新應用程式的 app.config 檔案,使用 1.0.1ReferencedLibrary 修補版本,不使用原先編譯的 1.0.0 版。

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

注意

新版 ReferencedLibrary 必須與您的應用程式二進位相容,此方法才能有效。 如需決定相容性時需要留意的變更,請參閱前文的回溯相容性一節。

新的

您使用 new 修飾詞隱藏繼承的基底類別成員。 這是衍生類別可以回應基底類別更新的一種方式。

以下列範例為例:

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

輸出

A base method
A derived method

您在上例中可以看到 DerivedClass 如何隱藏出現在 MyMethod 中的 BaseClass 方法。 這表示,當新版文件庫中的基底類別新增您衍生類別中已有的成員時,您只要對衍生類別成員使用 new 修飾詞,就可以隱藏基底類別成員。

未指定任何 new 修飾詞時,衍生類別預設會隱藏基底類別中發生衝突的成員,雖然會產生編譯器警告,但程式碼仍會編譯。 這表示,只要將新成員新增至現有的類別,即可讓新版的程式庫與依賴它的程式碼在來源和二進位上相容。

覆寫

override 修飾詞表示衍生的實作會擴充基底類別成員的實作,而非隱藏它。 基底類別成員需要套用 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()}");
}

輸出

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

override 修飾詞會在編譯期間評估,如果編譯器找不到要覆寫的虛擬成員,它會擲回錯誤。

您對前述技巧的掌握以及對其使用時機的理解,將大大促進程式庫版本之間的過渡。