トップ «前の日記(2011-01-28) 最新 次の日記(2011-01-31)» 編集

日々の破片

著作一覧

2011-01-29

_ (続).NET FrameworkのException.StackTrace

追記:matarilloさんからのツッコミを受けて、Backyardへ修正版を作成。というわけで、以下でInnerExceptionとアプリケーションログの関係について記述しているところは正しくはありません。

昨日は、yfakariyaさんやmatarilloさんからいろいろ指摘を受けたので、あらためてMSDNを読んでExceptionクラスが返すものをチェックしてみた。

結論から言うと、アプリケーションとライブラリでは同じExceptionオブジェクトでも(少なくとも後からの調査用のログという観点からは)見るべきプロパティを変えるべきだ。

以下のテストプログラムを用意した。

Fooメソッドは呼び出し先メソッド(当然ライブラリを想定する――アセンブリの違いというのは全くの誤解釈だとわかったので同一クラスでも良い)を信用していない。したがって、このメソッドが採取すべきログは呼び出し先の振る舞いを調べるための資料となるものだ。

Bazメソッドは呼び出し元メソッド(当然アプリケーションを想定する)を信用していない。したがって、このメソッドが採取すべきログは呼び出し元の呼び出し条件を調べるための資料となるものだ。

using System;
using System.Text.RegularExpressions;
public class Exp
{
    static readonly Regex KEY = new Regex("^(\\d\\d):.+$");
    bool withE;
    public Exp(bool e)
    {
        withE = e;
    }
    public void Foo()
    {
        try
        {
            Bar();
        }
        catch (Exception e)
        {
            Console.WriteLine("in Foo:");
            PrintException(e);
        }
    }
    void Bar()
    {
        Console.WriteLine(string.Format("key={0}", Baz(null)));
    }
    public int Baz(string s)
    {
        try
        {
            var m = KEY.Match(s);
            if (m.Success)
            {
                return int.Parse(m.Groups[1].Value);
            }
            else
            {
                return -1;
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("in Baz:");            
            PrintException(e);
            if (withE)
            {
                throw e;
            }
            else
            {
                throw;
            }   
        }
    }
    void PrintException(Exception e)
    {
        Console.WriteLine("ToString: " + e.ToString());
        foreach (var p in e.GetType().GetProperties())
        {
            Console.WriteLine(string.Format("{0}: {1}", p.Name, p.GetValue(e, null)));
        }
    }
    public static void Main()
    {
        var e = new Exp(false);
        e.Foo();
        Console.WriteLine("---------------------------------");
        e = new Exp(true);
        e.Foo();        
    }
}

最初の呼び出しと次の呼び出しの差は、ライブラリが例外をスローする場合に、暗黙の再スローか明示した再スローかだ。というか、昨日までは暗黙再スローの存在はまったく知らなかった(10年前には言語仕様を読んでいるから忘れたのだと思うが、覚えていなければ知らないのと同じ)。

出力は以下となる。サーバーサイドではないのでPDBはデプロイ対象としていない状態を想定する。

c:\Users\arton\Documents\test>Exp
in Baz:
ToString: System.ArgumentNullException: 値を Null にすることはできません。
パラメーター名: input
   場所 System.Text.RegularExpressions.Regex.Match(String input)
   場所 Exp.Baz(String s)
Message: 値を Null にすることはできません。
パラメーター名: input
ParamName: input
Data: System.Collections.ListDictionaryInternal
InnerException: 
TargetSite: System.Text.RegularExpressions.Match Match(System.String)
StackTrace:    場所 System.Text.RegularExpressions.Regex.Match(String input)
   場所 Exp.Baz(String s)
HelpLink: 
Source: System
in Foo:
ToString: System.ArgumentNullException: 値を Null にすることはできません。
パラメーター名: input
   場所 System.Text.RegularExpressions.Regex.Match(String input)
   場所 Exp.Baz(String s)
   場所 Exp.Bar()
   場所 Exp.Foo()
Message: 値を Null にすることはできません。
パラメーター名: input
ParamName: input
Data: System.Collections.ListDictionaryInternal
InnerException: 
TargetSite: System.Text.RegularExpressions.Match Match(System.String)
StackTrace:    場所 System.Text.RegularExpressions.Regex.Match(String input)
   場所 Exp.Baz(String s)
   場所 Exp.Bar()
   場所 Exp.Foo()
HelpLink: 
Source: System
---------------------------------
in Baz:
ToString: System.ArgumentNullException: 値を Null にすることはできません。
パラメーター名: input
   場所 System.Text.RegularExpressions.Regex.Match(String input)
   場所 Exp.Baz(String s)
Message: 値を Null にすることはできません。
パラメーター名: input
ParamName: input
Data: System.Collections.ListDictionaryInternal
InnerException: 
TargetSite: System.Text.RegularExpressions.Match Match(System.String)
StackTrace:    場所 System.Text.RegularExpressions.Regex.Match(String input)
   場所 Exp.Baz(String s)
HelpLink: 
Source: System
in Foo:
ToString: System.ArgumentNullException: 値を Null にすることはできません。
パラメーター名: input
   場所 Exp.Baz(String s)
   場所 Exp.Bar()
   場所 Exp.Foo()
Message: 値を Null にすることはできません。
パラメーター名: input
ParamName: input
Data: System.Collections.ListDictionaryInternal
InnerException: 
TargetSite: System.Text.RegularExpressions.Match Match(System.String)
StackTrace:    場所 Exp.Baz(String s)
   場所 Exp.Bar()
   場所 Exp.Foo()
HelpLink: 
Source: System

以上からわかることは、catchしたExceptionオブジェクトから得られるスタックトレースは、StackTraceプロパティのものであろうがToStringメソッドのものであろうが、併設したtryブロックから先だけだということだ。

これはアプリケーション(ライブラリを信用していない)にとっては十分な情報量だ。しかも最低限に必要と考えられる情報(Messageプロパティの値とStackTraceプロパティの値)はすべて単一のToStringメソッド呼び出しで得られる。

つまり、アプリケーションが例外をログするのであれば、ToStringの結果をログすれば良い(InnerExceptionチェインがnullになるまでToStringすればもっと良い)。

それに対して、アプリケーションを信用しないライブラリにとっては話が異なる。

信用していないのだから、アプリケーションプログラマに対して、このメソッドの呼び出し(またはそのメソッドを呼び出したメソッド……)はおかしい(引数がおかしいのか、状態がおかしいのか、いずれにしろ事前条件相当のものは例外とは別にログする必要はある)と指摘できなければならない。そのためには、別途スタックトレースを入手する必要がある(Diagnostics.StackTraceとか)。

それだけではなく、アプリケーションに例外をスローする場合には次のいずれかを利用すべきということも言える。

1: 独自にArgumentException(事前条件のうち引数違反)やInvalidOperationException(事前条件のうち状態違反)を作成し、InnerExceptionにキャッチしたExceptionオブジェクトを設定する。ただし、アプリケーションを信用していないのだからInnerExceptionがアプリケーション側でログされることは期待できない。したがって2:のほうが良い。

2: 暗黙の再スロー(無引数throw)。キャッチしたオブジェクトを指定した再スローでは真の原因(上の例ではRegexが検出した引数null)の通知元がTargetSiteプロパティにしか残らない。すると、上で示したようにアプリケーションはToStringの結果だけをログするとした場合に、真の原因が不明となりライブラリ側の調査が難しくなる可能性がある。

本日のツッコミ(全2件) [ツッコミを入れる]
_ matarillo (2011-01-29 21:40)

実のところ、ExceptionのToString()を呼ぶとInnerExceptionの情報も再帰的に出力されます。(上のコード、throw e;のところをthrow new ArgumentException("bad argument.", e);などに変えてみるとわかります)<br>なので、選択肢1もアリかと思います。(アプリケーションも、ToString()ぐらいは呼んでくれるだろう)

_ arton (2011-01-29 22:04)

おお、どうもありがとう。そいつを3番目にして修正かけます。


2003|06|07|08|09|10|11|12|
2004|01|02|03|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|

ジェズイットを見習え