環境
Windows10 Pro 64bit 22H2
Visual Studio 2022 Community Edition
.NET Framework 4.6(Windows10にプレインストールされているランタイムで動作するようにするため)
背景
元々はアップデータを作ろうとしていたのですが、アプリの機能と比較するとアップデータは補助的な役割になるので、「MessageBox.Show」で表示することがふさわしくない部位が所々に存在することが想定されます。しかしエラーメッセージがユーザーから見えないからと言って、エラーの情報を全く残さないわけにはいかない。なのでログを残そうとしました。
とはいえ無理に自作して、自作したログ保存関数の部分でエラーが発生してしまっては本末転倒なので、.NETにログ出力に相当する機能が初めから実装されていればそちらを使いたいと思った最中での調査だった。調べたところ、「TextWriterTraceListener」リスナーを併用した「System.Diagnostics.TraceSource」クラスを用いる方法に行き着いた。(2023年4月7日時点でログシステムはまだ検討段階)
それで何をログに残そうかと考えた。
取得した例外のException.ToString()で表示される情報は複数行になる可能性があるのに対して、
TraceSourceクラスによるログ出力では複数行を記録できるような設定にするのは非常に大変なように思われる。なので、ログに出力する情報はException.Messageとエラー行に関する情報だけに絞ろうと考えた。
なのでエラーの行を特定しようと考えた。そのエラーの行を特定しようとする段階で、「System.Diagnostics.StackTrace」の「GetFrames」メソッドを用いる方法に行き着いた。
しかしこの「GetFrames」で返されるオブジェクトは配列で、どのインデックスに自分の欲しい情報が格納されているのかは自分で調べる必要があるようだった。私のググり方が悪かったのか、スタックトレースからエラー行のスタックフレームを特定する方法には行き着かなかった。
なので、私自身が実験して試してみようということになった。
コード
- データグリッドビュー:dataGridView1
空の列を一列追加しただけです。このデータグリッドビューに実験結果を表示します。 - ボタン:Button1
「System.Diagnostics.StackTrace」のコンストラクタは複数ありますが、このボタンではコンストラクタの引数にExceptionを引数に付けない状態で「StackTrace」クラスを初期化した。 - ボタン:Button2
このボタンではコンストラクタの引数にExceptionを引数に付けた状態で「StackTrace」クラスを初期化した。
using System;
using System.Windows.Forms;
namespace StackTraceTest {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e) {
try {
dataGridView1.Rows.Clear();
throw (new Exception("Exceptionあり"));
}catch(Exception ex) {
System.Diagnostics.StackTrace stackTrace = new System.Diagnostics.StackTrace(true);
System.Diagnostics.StackFrame[] stackFrames = stackTrace.GetFrames();
int slen = stackFrames.Length;
for(int i=0;i<slen;++i) {
dataGridView1.Rows.Add(stackFrames[i].ToString());
}
}
}
private void button2_Click(object sender, EventArgs e) {
try {
dataGridView1.Rows.Clear();
throw (new Exception("Exceptionなし"));
} catch (Exception ex) {
System.Diagnostics.StackTrace stackTrace = new System.Diagnostics.StackTrace(ex,true);
System.Diagnostics.StackFrame[] stackFrames = stackTrace.GetFrames();
int slen = stackFrames.Length;
for (int i = 0; i < slen; ++i) {
dataGridView1.Rows.Add(stackFrames[i].ToString());
}
}
}
}
}
今回は「StackTrace(Boolean)」と「StackTrace(Exception, Boolean)」の比較ですが、ファイル名・行番号・列番号を取得するためには、どちらのBoolean型引数にもtrueを指定する必要があります。
fNeedFileInfo Boolean
ファイル名、行番号、および列番号をキャプチャする場合は true。それ以外の場合は false。引用元:MSDNの「StackTrace(Boolean) 」と「StackTrace(Exception, Boolean) 」の引数「fNeedFileInfo 」
出力結果(Exceptionなし)
Exceptionを引数に取らなかった場合は、
(パスに関する情報は個人情報を隠すために変えています。)
添え字 | スタックトレースの中身 |
---|---|
0 | button1_Click at offset 200 in file:line:column C:\Users\xxxx\source\repos\StackTraceTest\StackTraceTest\Form1.cs:13:5 |
1 | OnClick at offset 135 in file:line:column :0:0 |
2 | OnMouseUp at offset 337 in file:line:column :0:0 |
3 | WmMouseUp at offset 733 in file:line:column :0:0 |
4 | WndProc at offset 1583 in file:line:column :0:0 |
5 | WndProc at offset 136 in file:line:column :0:0 |
6 | WndProc at offset 37 in file:line:column :0:0 |
7 | DebuggableCallback at offset 134 in file:line:column :0:0 |
8 | DispatchMessageW at offset 0 in file:line:column :0:0 |
9 | DispatchMessageW at offset 0 in file:line:column :0:0 |
10 | System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop at offset 833 in file:line:column :0:0 |
11 | RunMessageLoopInner at offset 455 in file:line:column :0:0 |
12 | RunMessageLoop at offset 82 in file:line:column :0:0 |
13 | Main at offset 86 in file:line:column C:\Users\xxxx\source\repos\StackTraceTest\StackTraceTest\Program.cs:17:3 |
インデックスの0番目に例外を発生させた13行目の情報が取得されていますが、表示結果は全体的に情報量が多いように思います。
MSDNの注釈には
注釈
StackTraceは、呼び出し元の現在のスレッドで作成されます。引用元:MSDNの「StackTrace(Boolean) 」
とあるので、恐らく例外を投げたスレッドで扱われる全てのスタックフレームを出力していると考えられます。
出力結果(Exceptionあり)
では引数にExceptionを入れると
(パスに関する情報は個人情報を隠すために変えています。)
添え字 | スタックトレースの中身 |
---|---|
0 | button2_Click at offset 200 in file:line:column C:\Users\xxxx\source\repos\StackTraceTest\StackTraceTest\Form1.cs:27:5 |
無駄な情報が省かれた形で例外を発生させた27行目に関する情報が取得されている。
MSDNの注釈にも
注釈
結果のスタック トレースでは、例外時のスタックが記述されます。引用元:MSDNの「StackTrace(Exception, Boolean) 」
と書かれてあるので、例外を投げた行のスタックフレームだけが出力されていると考えられます。
実験結果を踏まえて
エラー行の情報だけを取得したい場合は、System.Diagnostics.TraceSourceクラスのExceptionがついているコンストラクタ「StackTrace(Exception, Boolean) 」で初期化すると、エラー行に関係する部分のスタックフレームだけが出力されるので、その状態で0番目のスタックフレームを調べると取得できると思われる。(エラー行以外の付加的な情報が欲しい場合は他のスタックフレームを調べるものと思われる。)
System.Diagnostics.TraceSourceクラスのExceptionがついているコンストラクタ「StackTrace(Exception, Boolean) 」で初期化すると、ある程度情報は絞られることは分かった。
この実験では人為的にエラーを投げているので、この実験ではなく実際のエラー処理で確かめてみたところ、必要な情報が0番目のスタックフレームにあるとは限らないことが判明したため、結局全てのスタックフレームを確認することにした。ログを記録する際も全てのスタックフレームを記録するように修正した。