より優れたコードを記述するためのデバッグ手法とツール

コード内のバグとエラーの修正は、時間がかかり、場合によっては面倒な作業になることがあります。 効果的にデバッグする方法の習得には時間を要します。 Visual Studio のような強力な IDE を使用すると、この仕事が大幅に簡単になります。 IDE を使用すると、エラーの修正やコードのデバッグをより迅速に行うことができます。また、少ないバグでより適切なコードを記述するためにも役立ちます。 この記事では、"バグ修正" プロセスの全体像を示します。コード アナライザーやデバッガーを使用するタイミング、例外を修正する方法、意図したようにコーディングする方法を知ることができます。 デバッガーを使用する必要があることが既にわかっている場合は、「はじめてのデバッガー」を参照してください。

この記事では、IDE を使用してコーディング セッションの生産性を高める方法について学習していきます。 ここでは、次のようないくつかのタスクについて説明します。

  • IDE のコード アナライザーを使用してデバッグ用コードを準備する

  • 例外を修正する方法 (実行時エラー)

  • 意図したコーディングでバグを最小化する方法 (アサートの使用)

  • どのようなときにデバッガーを使用するか

これらのタスクをデモンストレーションするために、アプリをデバッグするときに遭遇し得る、最も一般的な種類のエラーとバグをいくつか提示します。 ここでのサンプル コードは C# ですが、通常、概念的な情報は C++、Visual Basic、JavaScript、そして Visual Studio でサポートされているその他の言語にも適用することができます (特別な記載のない限り)。 スクリーン ショットは C# になっています。

バグやエラーが含まれるサンプル アプリを作成する

次のコードには、Visual Studio IDE を使用して修正できるいくつかのバグがあります。 このアプリケーションは、ある操作からの JSON データの取得、データのオブジェクトへの逆シリアル化、新しいデータでの簡単なリストの更新をシミュレートするシンプルなアプリです。

アプリの作成には、Visual Studio と .NET デスクトップ開発ワークロードをインストールしておく必要があります。

  • Visual Studio をまだインストールしていない場合は、Visual Studio のダウンロード ページに移動し、無料試用版をインストールしてください。

  • Visual Studio は既にインストールしていて、ワークロードだけをインストールする必要がある場合は、[ツール]>[ツールと機能を取得] の順に選択します。 Visual Studio インストーラーが起動します。 [.NET デスクトップ開発] ワークロードを選択し、[変更] を選びます。

次の手順に従いアプリケーションを作成します。

  1. Visual Studio を開きます。 [スタート ウィンドウ] で、 [新しいプロジェクトの作成] を選択します。

  2. 検索ボックスに「console」と入力した後、.NET の[コンソール アプリ] オプションの 1 つを入力 します。

  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 のコード アナライザーによって識別されたエラーと警告を表しています。 赤い波線はコンパイル時エラーであり、コードを実行する前に修正する必要があります。 緑の波線は警告です。 多くの場合、警告を修正せずにアプリを実行できますが、バグの原因となる場合があり、多くの場合、警告を調査することによって時間を節約し問題を減らすことができます。 リスト ビューの方がよければ、これらの警告とエラーは [エラー一覧] ウィンドウにも表示されます。

サンプル アプリ内には、修正が必要な何本かの赤い波線と、調査が必要な 1 本の緑の波線が表示されています。 1 つ目のエラーを次に示します。

赤い波線で示されるエラー

このエラーを修正するための IDE の別の機能が、電球アイコンによって表されているのを確認できます。

電球を確認する

最初の赤い波線は、コンパイル時のエラーを表します。 それをポイントすると、The name `Encoding` does not exist in the current context というメッセージが表示されます。

このエラーの左下に電球アイコンが表示されることに注意してください。 ドライバー アイコン ドライバーアイコン と共に、電球アイコン 電球アイコン は、コードをインラインで修正またはリファクタリングするのに役立つクイック アクションを表します。 電球は、修正する "必要がある" 問題を表します。 ドライバーは、修正することができる問題を示します。 左側の using System.Text をクリックし、最初の修正候補を使用してこのエラーを解決します。

電球を使用してコードを修正する

この項目を選択すると、Visual Studio によって using System.Text ステートメントが Program.cs ファイルの先頭に追加され、赤い波線が表示されなくなります。 (修正候補によって適用された変更が不明な場合は、 修正プログラムを適用する前に、右側にある [変更 のプレビュー] リンクを選択します。)

前述のエラーは、コードに新しい using ステートメントを追加することによって通常は修正される、一般的なエラーです。 これには、共通の類似するエラー (The type or namespace "Name" cannot be found. など) がいくつか存在します。この種のエラーは、アセンブリ参照の欠落 (プロジェクトを右クリックして [追加]>[参照] を選択)、名前のスペルミス、または追加する必要のあるライブラリの欠落 (C# の場合は、プロジェクトを右クリックして [NuGet パッケージの管理] を選択) を表している場合があります。

残りのエラーと警告を修正する

このコードには、調べる必要のある波線がさらにいくつかあります。 ここでは、一般的な型変換エラーを見ます。 波線をポイントすると、コードで string から int への変換が試みられていることがわかります。この処理は、変換を行う明示的なコードを追加しない限り、サポートされていません。

型変換エラー

コード アナライザーでは意図を推測できないため、今度は役に立つ電球は表示されません。 このエラーを修正するには、コードの意図を理解する必要があります。 この、totalpointspoints を追加しようとしている例では、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 が整数として解析されないことが示されています。 したがって、この例では、データが不適切であることがわかります。4o40 にする必要があります。 とは言え、データを制御できない実際のシナリオ (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 (Exception) {} のような空の catch ブロックは使わないようにします。これは、エラーを公開または処理するための適切なアクションを実行しません。 空の、または情報を提供しない catch ブロックは例外を隠蔽するので、コードのデバッグを容易にするのではなく、むしろ難しくします。

  • 例外をスローする特定の関数 (サンプル アプリでは ReadObject) に対して try/catch ブロックを使用します。 コードの大きなチャンクに対して使用すると、エラーの場所がわからなくなります。 たとえば、次に示すような、親関数 ReadToObject の呼び出しに対して try/catch ブロックを使用しないでください。使用すると、例外が発生した場所を正確に把握できなくなります。

    // Don't do this
    try
    {
        User[] users = ReadToObject(data);
    }
    catch (SerializationException)
    {
    }
    
  • アプリによく知らない関数、特に (Web 要求など) の外部データと対話する関数をインクルードした場合には、その関数でスローされやすい例外について、ドキュメントを確認します。 これは、適切なエラー処理とアプリのデバッグのために不可欠な情報です。

サンプル アプリの場合、4o40 に変更することで、GetJsonData メソッドの SerializationException を修正します。

ヒント

Copilot をご利用の場合は、例外をデバッグする際に AI による支援を受けられます。 [Copilot に質問する][Copilot に質問する] ボタンのスクリーンショット。 ボタンをお探しください。 詳細については、「GitHub Copilot を使ったデバッグ」をご覧ください。

アサートを使用してコードの意図を明確にする

デバッグ ツール バーの [再起動]アプリの再起動 ボタンを選択します (Ctrl + Shift + F5)。 これにより、アプリがより少い手順で再起動されます。 コンソール ウィンドウに次の出力が表示されます。

出力に null 値がある

この出力には、正しくないものが表示されます。 3 番目のレコードの namelastname の値が空白になっています。

これは、関数内で assert ステートメントを使用するという、あまり利用されていない、役に立つコーディング手法について説明するよい機会です。 次のコードを追加することで、firstnamelastnamenull ではないことを確認する実行時チェックを組み込みます。 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 コードは、デバッグ ビルドでのみアクティブになります。

再起動すると、式 users[i].firstname != nulltrue ではなく false と評価されるため、デバッガーは assert ステートメントで一時停止します。

アサートが false に解決される

assert エラーは、調べる必要がある問題があることを示しています。 assert を使用すると、必ずしも例外が表示されない多くのシナリオに対応できます。 この例では、ユーザーには例外が表示されず、レコードの一覧に firstname として null 値が追加されます。 この条件では、後に (コンソール出力に表示されるような) 問題が発生し、デバッグを困難にする場合があります。

Note

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

このコードを使用すると、コードの要件を満たしながら、firstname または lastname の値が null となっているレコードがデータに追加されないようにすることができます。

この例では、2 つの 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 は、true または false として解決される任意の種類の式で使用できます。 たとえば、次のような assert ステートメントを追加できます。

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

上記のコードは、ユーザーのレコードを更新するにはゼロ (0) より大きい新しいポイント値が必要である、という意図を指定する場合に便利です。

デバッガーでコードを検査する

サンプル アプリの重要な問題をすべて修正したので、他の重要な事柄に移ることができます。

デバッガーの例外ヘルパーを紹介しましたが、デバッガーははるかに強力なツールであり、コードのステップ実行や変数の検査など、他の操作も実行できます。 これらの強力な機能は、多くの、特に次のようなシナリオで役立ちます。

  • コード内の実行時のバグを究明しようとしているものの、これまでの説明に出てきた手法とツールを使用できない。

  • コードを検証しようとしている。つまり、実行中にコードを監視して、期待どおりに動作し、意図したことが行われていることを確認する。

    コードを実行中に監視することは有益です。 このようにするとコードに関する詳細がわかり、明らかな症状を示す前にバグを識別できることがよくあります。

デバッガーの重要な機能を使用する方法については、「入門者向けのデバッグ」を参照してください。

パフォーマンスの問題を修正

別の種類のバグには、アプリの実行速度の低下や、メモリの過剰使用の原因になる、非効率的なコードが含まれます。 一般に、パフォーマンスの最適化は、アプリ開発の最後の方で行います。 ただし、早い段階にパフォーマンスの問題が発生した (たとえば、アプリの一部分の実行が遅いことがわかった) 場合には、早期にプロファイリング ツールによるアプリのテストが必要になります。 CPU 使用率ツールやメモリ アナライザーなどのプロファイリング ツールの詳細については、プロファイリング ツールの概要に関する記事を参照してください。

この記事では、コード内の多くの一般的なバグを回避して修正する方法と、デバッガーを使用する状況について説明しました。 次に、Visual Studio のデバッガーを使用してバグを修正する方法の詳細について説明します。