偵錯技術和工具可協助您撰寫更好的程式碼

修正程式碼中的 BUG 和錯誤會是耗時且有時令人沮喪的工作。 要了解如何有效地偵錯需要時間。 Visual Studio 等功能強大的 IDE 可讓您的工作變得容易許多。 IDE 可協助您更快速地修正錯誤並偵錯程式碼,並協助您撰寫 BUG 較少的更佳程式碼。 本文可讓您全面檢視「BUG 修正」程序,讓您知道何時使用程式碼分析器、何時使用偵錯工具、如何修正例外狀況,以及如何根據意圖來撰寫程式碼。 如果您已經知道需要使用偵錯工具,請參閱初探偵錯工具

在本文中,您將了解如何使用 IDE 來提升程式碼撰寫工作階段的生產力。 我們會接觸數項工作,例如:

  • 使用 IDE 的程式碼分析器來準備程式碼以進行偵錯

  • 如何修正例外狀況 (執行階段錯誤)

  • 如何藉由撰寫意圖程式碼來將錯誤降到最低 (使用判斷提示)

  • 使用偵錯工具的時機

為了示範這些工作,我們會示範一些您在嘗試偵錯應用程式時可能會遇到的最常見錯誤和 BUG 類型。 雖然範例程式碼是 C#,但是這些概念資訊通常適用於 C++、Visual Basic、JavaScript 以及 Visual Studio 所支援的其他語言 (除非另外註明)。 螢幕擷取畫面則使用 C# 表示。

建立範例應用程式 (內含一些 Bug 和錯誤)

下列程式碼有一些您可以使用 Visual Studio IDE 修正的錯誤。 這個應用程式是一個簡單的應用程式,模擬從某些作業取得 JSON 資料、將資料還原序列化至物件,以及使用新資料更新簡單清單。

若要建立應用程式,您必須安裝 Visual Studio 和 .NET 桌面開發工作負載。

  • 如果您尚未安裝 Visual Studio,請前往 Visual Studio 下載頁面免費進行安裝。

  • 如果您需要安裝工作負載,但已擁有 Visual Studio,請選取 [工具]> [取得工具與功能]。 Visual Studio 安裝程式即會啟動。 選擇 [.NET 桌面開發] 工作負載,然後選擇 [修改]

請遵循下列步驟來建立應用程式:

  1. 開啟 Visual Studio。 在 [開始] 視窗上,選取 [建立新專案]

  2. 在搜尋方塊中輸入主控台,然後輸入 .NET 的其中一個主控台應用程式選項。

  3. 選取 [下一步] 。

  4. 輸入專案名稱 (例如 Console_Parse_JSON),然後視情況選取 [下一步] 或 [建立]

    選擇建議的目標 Framework 或 .NET 8,然後選擇 [建立]

    如果您未看到 .NET 的 [主控台應用程式] 專案範本,請前往 [工具]> [取得工具與功能],以開啟 Visual Studio 安裝程式。 選擇 [.NET 桌面開發] 工作負載,然後選擇 [修改]

    Visual Studio 隨即建立主控台專案,並出現在右窗格的 [方案總管] 中。

當專案準備就緒時,請將專案的 Program.cs 檔案中的預設程式碼取代為下列範例程式碼:

using System;
using System.Collections.Generic;
using System.Runtime.Serialization.Json;
using System.Runtime.Serialization;
using System.IO;

namespace Console_Parse_JSON
{
    class Program
    {
        static void Main(string[] args)
        {
            var localDB = LoadRecords();
            string data = GetJsonData();

            User[] users = ReadToObject(data);

            UpdateRecords(localDB, users);

            for (int i = 0; i < users.Length; i++)
            {
                List<User> result = localDB.FindAll(delegate (User u) {
                    return u.lastname == users[i].lastname;
                    });
                foreach (var item in result)
                {
                    Console.WriteLine($"Matching Record, got name={item.firstname}, lastname={item.lastname}, age={item.totalpoints}");
                }
            }

            Console.ReadKey();
        }

        // Deserialize a JSON stream to a User object.
        public static User[] ReadToObject(string json)
        {
            User deserializedUser = new User();
            User[] users = { };
            MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(json));
            DataContractJsonSerializer ser = new DataContractJsonSerializer(users.GetType());

            users = ser.ReadObject(ms) as User[];

            ms.Close();
            return users;
        }

        // Simulated operation that returns JSON data.
        public static string GetJsonData()
        {
            string str = "[{ \"points\":4o,\"firstname\":\"Fred\",\"lastname\":\"Smith\"},{\"lastName\":\"Jackson\"}]";
            return str;
        }

        public static List<User> LoadRecords()
        {
            var db = new List<User> { };
            User user1 = new User();
            user1.firstname = "Joe";
            user1.lastname = "Smith";
            user1.totalpoints = 41;

            db.Add(user1);

            User user2 = new User();
            user2.firstname = "Pete";
            user2.lastname = "Peterson";
            user2.totalpoints = 30;

            db.Add(user2);

            return db;
        }
        public static void UpdateRecords(List<User> db, User[] users)
        {
            bool existingUser = false;

            for (int i = 0; i < users.Length; i++)
            {
                foreach (var item in db)
                {
                    if (item.lastname == users[i].lastname && item.firstname == users[i].firstname)
                    {
                        existingUser = true;
                        item.totalpoints += users[i].points;

                    }
                }
                if (existingUser == false)
                {
                    User user = new User();
                    user.firstname = users[i].firstname;
                    user.lastname = users[i].lastname;
                    user.totalpoints = users[i].points;

                    db.Add(user);
                }
            }
        }
    }

    [DataContract]
    internal class User
    {
        [DataMember]
        internal string firstname;

        [DataMember]
        internal string lastname;

        [DataMember]
        // internal double points;
        internal string points;

        [DataMember]
        internal int totalpoints;
    }
}

尋找紅色和綠色波浪線!

在您嘗試啟動範例應用程式並執行偵錯工具之前,請先檢查程式碼編輯器中的程式碼是否有紅色和綠色波浪線。 這些代表 IDE 程式碼分析器所識別的錯誤和警告。 紅色波浪線是編譯時期錯誤,您必須修正才能執行程式碼。 綠色波浪線是警告。 雖然您通常可以在不修正警告的情況下執行應用程式,但它們可能是 Bug 的來源,而且您通常會藉由調查它們來節省時間和麻煩。 如果您偏好清單檢視,這些警告和錯誤也會顯示在 [錯誤清單] 視窗中。

在範例應用程式中,您會看到數個需要修正的紅色波浪線,以及一個需要調查的綠色波浪線。 以下是第一個錯誤。

顯示為紅色波浪線的錯誤

若要修正此錯誤,您可以查看以燈泡圖示表示之 IDE 的另一個功能。

檢查燈泡!

第一個紅色波浪線代表編譯時期錯誤。 將滑鼠停留在上方,您會看到訊息 The name `Encoding` does not exist in the current context

請注意,此錯誤會在左下方顯示燈泡圖示。 與螺絲起子圖示 螺絲起子圖示 一起,燈泡圖示 燈泡圖示 代表快速動作,可協助您修正或重構內嵌程式碼。 燈泡代表您應該修正的問題。 螺絲起子適用於您可以選擇修正的問題。 使用第一個建議的修正,按一下左側的 using System.Text 來解決此錯誤。

使用燈泡來修正程式嗎

當您選取此項目時,Visual Studio 會在 Program.cs 檔案頂端新增 using System.Text 陳述式,而且紅色波浪線會消失。 (當您不確定所建議的修正會套用什麼變更時,請在套用修正之前,選擇右側的 [預覽變更] 連結。)

上述錯誤是您在程式碼中新增 using 陳述式來修正的常見錯誤。 有數個常見的類似錯誤,例如The type or namespace "Name" cannot be found.這些類型的錯誤可能表示遺漏組件參考 (以滑鼠右鍵按一下專案、選擇 [新增]> [參考])、名稱拼字錯誤,或您需要新增的遺漏程式庫 (適用於 C#,以滑鼠右鍵按一下專案,然後選擇 [管理 NuGet 套件])。

修正其餘的錯誤和警告

在此程式碼中還有一些要查看的波浪線。 在這裡,您會看到常見的類型轉換錯誤。 當您將滑鼠停留在波浪線上方時,您會看到程式碼正在嘗試將字串轉換成 int,除非您新增明確的程式碼來進行轉換,否則不支援此字串。

類型轉換錯誤

因為程式碼分析器無法猜測您的意圖,因此目前沒有燈泡可協助您進行。 若要修正此錯誤,您必須知道程式碼的意圖。 在此範例中,由於您嘗試將 points 新增至 totalpoints,所以查看 points 應該是數值 (整數) 值不會太難。

若要修正此錯誤,請從下列項目變更 User 類別的 points 成員:

[DataMember]
internal string points;

轉換為:

[DataMember]
internal int points;

程式碼編輯器中的紅色波浪線會消失。

接下來,將滑鼠停留在 points 資料成員宣告中的綠色波浪線上。 程式碼分析器會告訴您變數永遠不會獲指派值。

未指派變數的警告訊息

這通常代表需要修正的問題。 不過,在範例應用程式中,您實際上是在還原序列化程序中將資料儲存在 points 變數中,然後將該值新增至 totalpoints 資料成員。 在此範例中,您知道程式碼的意圖,而且可以安全地忽略警告。 不過,如果您想要排除警告,您可以取代下列程式碼:

item.totalpoints = users[i].points;

取代為這個:

item.points = users[i].points;
item.totalpoints += users[i].points;

綠色波浪線消失了。

修正例外狀況

當您已修正所有紅色波浪線並且已解決或至少已調查所有綠色波浪線時,您已準備好啟動偵錯工具並執行應用程式。

F5 ( [偵錯] > [開始偵錯]) 或 [偵錯] 工具列中的 [開始偵錯] 按鈕[偵錯]

此時,範例應用程式會擲回 SerializationException 例外狀況 (執行階段錯誤)。 也就是說,應用程式會阻礙嘗試序列化的資料。 由於您已在偵錯模式中啟動應用程式 (已連結偵錯工具),偵錯工具的例外狀況協助程式會帶您前往擲回例外狀況的程式碼,並提供實用的錯誤訊息。

發生 SerializationException

錯誤訊息會指示您 4o 值無法剖析為整數。 因此,在此範例中,您知道資料不正確:4o 應該為 40。 不過,如果您不是在真實案例中控制資料 (假設您從 Web 服務取得資料),該怎麼辦? 您該如何修正這個問題?

當您遇到例外狀況時,您需要詢問 (並回答) 幾個問題:

  • 此例外狀況是否只是您可以修正的錯誤? 或者,

  • 您的使用者是否可能會遇到此例外狀況?

如果是前者,請修正 Bug。 (在範例應用程式中,您接著需要修正不正確的資料。)如果是後者,您可能需要使用 try/catch 區塊來處理程式碼中的例外狀況 (我們將會在下一節中查看其他可能的策略)。 在範例應用程式中,取代下列程式碼:

users = ser.ReadObject(ms) as User[];

使用此程式碼取代:

try
{
    users = ser.ReadObject(ms) as User[];
}
catch (SerializationException)
{
    Console.WriteLine("Give user some info or instructions, if necessary");
    // Take appropriate action for your app
}

try/catch 區塊有一些效能成本,因此您只想在真正需要時使用它們,也就是 (a) 可能在應用程式的發行版本中發生,以及 (b) 方法的文件指出您應該檢查例外狀況 (假設文件已完成!)。 在許多情況下,您可以適當地處理例外狀況,而且使用者永遠不需要知道例外狀況。

以下是例外狀況處理的幾個重要祕訣:

  • 避免使用空的 catch 區塊,例如 catch (Exception) {},這不會採取適當的動作來公開或處理錯誤。 空白或非資訊 catch 區塊可以隱藏例外狀況,並讓您的程式碼更難以偵錯,而不是更容易。

  • 在擲回例外狀況 (在範例應用程式中為 ReadObject) 的特定函式周圍使用 try/catch 區塊。 如果您在較大的程式碼區塊周圍使用它,最後就會隱藏錯誤的位置。 例如,請勿在對父代函式 try/catch 的呼叫周圍使用 ReadToObject 區塊,如這裡所示,否則您不知道發生例外狀況的確切位置。

    // Don't do this
    try
    {
        User[] users = ReadToObject(data);
    }
    catch (SerializationException)
    {
    }
    
  • 針對您在應用程式中包含的不熟悉函式,特別是與外部資料互動的函式 (例如 Web 要求),請檢查文件以查看函式可能擲回的例外狀況。 這可以是正確錯誤處理和偵錯應用程式的重要資訊。

針對範例應用程式,將 4o 變更為 40,以修正 GetJsonData 方法中的 SerializationException

提示

如果您具有 Copilot,可以在偵錯例外狀況時取得 AI 協助。 只要尋找 [詢問 Copilot][詢問 Copilot] 按鈕的螢幕擷取畫面。 按鈕。 如需詳細資訊,請參閱使用 Copilot 進行偵錯

使用判斷提示來釐清程式碼意圖

選取 [偵錯工具列] 中的 [重新啟動]重新啟動應用程式 按鈕 (Ctrl + Shift + F5)。 這會以較少的步驟重新啟動應用程式。 您會在主控台視窗中看到以下輸出。

輸出中的 Null 值

您可以在此輸出中看到某些內容並不正確。 第三筆記錄的 namelastname 值空白!

這是討論實用程式碼撰寫練習的好時機,通常使用量過低,在函式中使用 assert 陳述式。 藉由新增下列程式碼,您可以包含執行階段檢查,以確保 firstnamelastname 不是 null。 取代 UpdateRecords 方法中的下列程式碼:

if (existingUser == false)
{
    User user = new User();
    user.firstname = users[i].firstname;
    user.lastname = users[i].lastname;

取代為這個:

// Also, add a using statement for System.Diagnostics at the start of the file.
Debug.Assert(users[i].firstname != null);
Debug.Assert(users[i].lastname != null);
if (existingUser == false)
{
    User user = new User();
    user.firstname = users[i].firstname;
    user.lastname = users[i].lastname;

藉由在開發過程中將類似這個項目的 assert 陳述式新增至函式,您可以協助指定程式碼的意圖。 在上述範例中,我們會指定下列項目:

  • 名字需要有效的字串
  • 姓氏需要有效的字串

藉由以這種方式指定意圖,您可以強制執行需求。 這是一種簡單且方便的方法,可用來在開發期間呈現 Bug。 (assert 陳述式也會用來作為單元測試中的主要元素。)

選取 [偵錯工具列] 中的 [重新啟動]重新啟動應用程式 按鈕 (Ctrl + Shift + F5)。

注意

assert 程式碼只在偵錯組建中為使用中。

當您重新啟動時,偵錯工具會在 assert 陳述式上暫停,因為運算式 users[i].firstname != null 評估為 false,而不是 true

判斷提示解析為 false

assert 錯誤會告訴您有需要調查的問題。 assert 可以涵蓋許多您不一定會看到例外狀況的案例。 在此範例中,使用者不會看到例外狀況,且 null 值會在記錄清單中新增為 firstname。 這個情況可能會導致稍後的問題 (例如您在主控台輸出中看到的問題),而且可能更難偵錯。

注意

在您於 null 值上呼叫方法、產生 NullReferenceException 結果的案例中。 您通常想要避免針對一般例外狀況使用 try/catch 區塊,也就是未繫結至特定程式庫函式的例外狀況。 任何物件都可以擲回 NullReferenceException。 如果您不確定,請檢查程式庫函式的文件。

在偵錯程序期間,最好保留特定 assert 陳述式,直到您知道您需要將其取代為實際的程式碼修正。 假設您決定使用者可能會在應用程式的發行組建中遇到例外狀況。 在此情況下,您必須重構程式碼,以確保您的應用程式不會擲回嚴重例外狀況,或導致其他錯誤。 因此,若要修正此程式碼,請取代下列程式碼:

if (existingUser == false)
{
    User user = new User();

使用此程式碼取代:

if (existingUser == false && users[i].firstname != null && users[i].lastname != null)
{
    User user = new User();

藉由使用此程式碼,您可以滿足程式碼需求,並確定具有 nullfirstnamelastname 值的記錄不會新增至資料。

在此範例中,我們在迴圈內新增了兩個 assert 陳述式。 一般而言,使用 assert 時,最好是在函式或方法的進入點 (開頭) 新增 assert 陳述式。 您目前正在查看範例應用程式中的 UpdateRecords 方法。 在此方法中,如果任一方法引數為 null,您就知道發生問題,因此請在函式的進入點使用 assert 陳述式來檢查兩者。

public static void UpdateRecords(List<User> db, User[] users)
{
    Debug.Assert(db != null);
    Debug.Assert(users != null);

針對上述陳述式,您的意圖是在更新任何項目之前,先載入現有的資料 (db),然後擷取新資料 (users)。

您可以使用 assert 搭配任何解析為 truefalse 的運算式。 因此,例如您可以新增 assert 陳述式,如下所示。

Debug.Assert(users[0].points > 0);

如果您想要指定下列意圖,上述程式碼會很有用:需要大於零 (0) 的新點值,才能更新使用者的記錄。

在偵錯工具中檢查您的程式碼

既然您已修正範例應用程式發生錯誤的所有內容,您可以移至其他重要內容!

我們向您顯示偵錯工具的例外狀況協助程式,但偵錯工具是一個更為強大的工具,也可讓您執行其他動作,例如逐步執行程式碼並檢查其變數。 這些更強大的功能在許多案例中很有用,尤其是下列案例:

  • 您正在嘗試在程式碼中隔離執行階段 BUG,但是無法使用先前討論的方法和工具來執行。

  • 您想要驗證程式碼,也就是說,在程式碼執行時加以監看,以確保其行為符合預期,並且執行您想要的內容。

    執行程式碼時監看程式碼很有意義。 您可以透過這種方式深入了解您的程式碼,而且通常會在顯現任何明顯的徵兆之前識別錯誤。

若要了解如何使用偵錯工具的基本功能,請參閱適合完全初學者的偵錯

修正效能問題

另一種 Bug 包含效率不佳的程式碼,導致您的應用程式執行速度緩慢或使用太多記憶體。 一般而言,最佳化效能是稍後在應用程式開發中執行的動作。 不過,您會提早遇到效能問題 (例如,您看到應用程式的某些部分執行速度緩慢),您可能需要儘早使用程式碼剖析工具來測試您的應用程式。 如需分析工具的詳細資訊,例如 CPU 使用量工具和記憶體分析器,請參閱先查看分析工具

在本文中,您已了解如何避免及修正程式碼中的許多常見錯誤,以及何時使用偵錯工具。 接下來,深入了解如何使用 Visual Studio 偵錯工具來修正 Bug。