共用方式為


調試技巧和工具,幫助您編寫更好的程式碼

修復程式碼中的錯誤和錯誤可能是一項耗時且有時令人沮喪的任務。 學習如何有效調試需要時間。 像 Visual Studio 這樣強大的 IDE 可以讓您的工作變得更加輕鬆。 IDE 可以幫助您更快地修復錯誤和調試程式碼,並幫助您以更少的錯誤編寫更好的程式碼。 本文提供「錯誤修正」程式的整體檢視,因此您可以知道何時使用程式碼分析器、何時使用偵錯工具、如何修正例外狀況,以及如何針對意圖撰寫程式碼。 如果您已經知道需要使用偵錯工具,請參閱 先查看偵錯工具

在本文中,您將瞭解如何使用 IDE 來提高編碼工作階段的生產力。 我們提到幾項任務,例如:

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

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

  • 如何按意圖編碼來降低錯誤(使用 assert)

  • 使用偵錯工具的時機

為了示範這些工作,我們示範您在嘗試偵錯應用程式時可能遇到的一些最常見的錯誤和錯誤類型。 雖然範例程式碼是 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. 在搜尋方塊中,輸入 console ,然後輸入 .NET 的 其中一個 [主控台應用程式 ] 選項。

  3. 選取 下一步

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

    選擇建議的目標架構或 .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 程式碼分析器識別的錯誤和警告。 紅色波浪線是編譯階段錯誤,您必須先修正這些錯誤,才能執行程式碼。 綠色波浪線是警告。 雖然您通常可以在不修復警告的情況下運行您的應用程序,但它們可能是錯誤的來源,您通常可以通過調查它們來節省時間和麻煩。 如果您偏好清單檢視,這些警告和錯誤也會顯示在 「錯誤清單」 視窗中。

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

錯誤顯示為紅色波浪線

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

檢查燈泡!

第一個紅色波浪線代表編譯時錯誤。 將鼠標懸停在它上面,您會看到消息 The name `Encoding` does not exist in the current context

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

使用燈泡修復程式碼

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

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

修正剩餘的錯誤和警告

此程式碼中還有一些潦草線可供查看。 在這裡,您會看到常見的類型轉換錯誤。 當您將滑鼠停留在波浪線上時,您會看到程式碼正在嘗試將字串轉換為 int,但不支援該操作,需要新增明確程式碼才能進行轉換。

類型轉換錯誤

因為程式碼分析器無法猜測您的意圖,所以這次沒有燈泡可以幫助您。 要修復此錯誤,您需要知道程式碼的意圖。 在此範例中,不難看出 points 應該是數值(整數)型別,因為您嘗試將 points 加到 totalpoints

若要修正此錯誤,請從以下位置變更 points 類別的 User 成員:

[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偵錯 > 開始偵錯)或在偵錯工具列中點擊 [開始偵錯] 按鈕 Start Debugging開始偵錯。

此時,範例應用程式會 SerializationException 擲回例外狀況 (執行階段錯誤)。 也就是說,應用程式會阻塞它嘗試序列化的資料。 因為您以偵錯模式啟動應用程式 (已附加偵錯工具),所以偵錯工具的例外狀況協助程式會帶您直接前往擲回例外狀況的程式碼,並提供有用的錯誤訊息。

發生 SerializationException

錯誤訊息會指示您無法將值 4o 剖解析為整數。 因此,在此範例中,您知道資料是錯誤的: 4o 應該是 40。 但是,如果您在實際場景中無法控制數據(例如您從 Web 服務獲取數據),您會怎麼做? 你如何解決這個問題?

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

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

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

如果是前者,請修復錯誤。 (在範例應用程式中,您需要修復錯誤的資料。如果是後者,您可能需要使用區塊處理 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 區塊可以隱藏例外狀況,並可能讓您的程式碼更難偵錯,而不是更容易。

  • 在擲回例外狀況的特定函式周圍使用 try/catch 區塊 (ReadObject,在範例應用程式中)。 如果您在較大的程式碼區塊周圍使用它,您最終會隱藏錯誤的位置。 例如,不要在呼叫父函數try/catch周圍使用ReadToObject區塊,如圖所示,否則您將不知道異常發生的確切位置。

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

針對範例應用程式,在 SerializationException 方法中變更GetJsonData4o以修正40

小提示

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

使用 assert 澄清您的程式碼意圖

選取偵錯工具列中的重新啟動應用程式 按鈕 (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 函數,您可以協助指定程式碼的意圖。 在上述範例中,我們指定下列項目:

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

透過以這種方式指定意圖,您可以強制執行您的需求。 這是一種簡單方便的方法,可用於在開發過程中發現錯誤。 (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();

使用此程式碼,您可以滿足您的程式碼需求,並確保不會將擁有firstnamelastname值為null的記錄新增至資料。

在此範例中,我們在迴圈內新增了兩個 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) 的新點值才能更新使用者的記錄。

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

好的,現在您已經修復了示例應用程序的所有關鍵問題,您可以繼續進行其他重要的事情!

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

  • 您嘗試隔離程式碼中的執行階段錯誤,但無法使用先前討論的方法和工具來執行此操作。

  • 您想要驗證您的程式碼,也就是說,在程式碼執行時觀察它,以確保其按照您預期的方式運作並執行您想要的動作。

    在程式碼執行時觀察程式碼很有啟發性。 您可以透過這種方式深入了解程式碼,並且通常可以在錯誤出現任何明顯症狀之前識別它們。

若要瞭解如何使用偵錯工具的基本功能,請參閱 絕對初學者的偵錯

修正效能問題

另一種錯誤包括效率低下的程式碼,會導致應用程式執行緩慢或使用過多記憶體。 一般而言,最佳化效能是您在應用程式開發後期執行的動作。 不過,您可能會在早期遇到效能問題 (例如,您看到應用程式的某些部分執行緩慢),而且您可能需要儘早使用分析工具測試應用程式。 如需分析工具的詳細資訊,例如 CPU 使用量工具和記憶體分析器,請參閱 第一次查看分析工具

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