Kokudoriing

技術系与太話ブログ

デバッガを騙す

C# は良くも悪くも IDE べったりな言語。
言語が特定ツールに依存することは賛否両論ですが、少なくとも C# はそういう言語だからシカタナイ。
そして IDE と言えば IntelliSense とデバッガですね。
今回はそのデバッガのお話。

よく C# でクラスや構造体を作ると ToString をオーバーライドしろと言われます。
何故なのか。Object#ToString が全然嬉しくない文字列を寄越しやがるからですね。

using System;
using System.Diagnostics;

namespace ConsoleApplication
{
	class Kokudori
	{
		public string Name { get { return "Kokudori"; } }
		public int Age { get { return 20; } }
	}

	class Program
	{
		static void Main(string[] args)
		{
			var kokudori = new Kokudori();
			Console.WriteLine(kokudori); // "ConsoleApplication.Kokudori"
		}
	}
}

なるほど全く嬉しくない。
なのでこんなことをしたりする。

using System;
using System.Diagnostics;

namespace ConsoleApplication
{
	class Kokudori
	{
		public string Name { get { return "Kokudori"; } }
		public int Age { get { return 20; } }
		public override string ToString()
		{
			// フォーマット文字列の { } のエスケープは {{ }}
			return String.Format("Kokudori {{ Name = {0}, Age = {1} }}", Name, Age);
		}
	}

	class Program
	{
		static void Main(string[] args)
		{
			var kokudori = new Kokudori();
			Console.WriteLine(kokudori); // "Kokudori { Name = Kokudori, Age = 20 }"
		}
	}
}

これでプリントデバッグがはかどりますね。
でもプリントデバッグなんてそうそうしない。
IDE にはデバッガがあるし、イミディエイトウィンドウでより柔軟にプリントデバッグが出来るから。

しかし ToString をオーバーライドするとデバッガウィンドウでも変化が。

これは非常に嬉しい。
今の例だとプロパティの一覧を文字列出力してるだけだから微妙だけども。
でも、"ConsoleApplication.Kokudori" なんて横の型カラムで出てる情報をわざわざ値で出す意味なんて全くない。

ただデバッガは開発中に開発者が見るためのもの。
対して ToString は public に公開されたメソッド。
いつも同じ内容を出力したいわけじゃないだろうし、そうあるべきでもない。
例えば今回の場合は "Kokudori { Name = Kokudori, Age = 20 }" なんてしてるけども、
Kokudori 型かどうかは先程も言った通り横の型カラムでも見れるから情報が重複していてアレ。

そこでようやく本題。
良くも悪くも IDE べったりなので IDE に対してきめ細かい制御も出来てしまう。
DebuggerDisplay属性はデバッガウィンドウ向け ToString メソッドみたいなもの。(本当はもっと高機能)

これで ToString とデバッガ向け出力を変えることが出来た。

using System;
using System.Diagnostics;

namespace ConsoleApplication
{
	[DebuggerDisplay("Name = {Name}, Age = {Age}")]
	class Kokudori
	{
		public string Name { get { return "Kokudori"; } }
		public int Age { get { return 20; } }
		public override string ToString()
		{
			return String.Format("Kokudori {{ Name = {0}, Age = {1} }}", Name, Age);
		}
	}

	class Program
	{
		static void Main(string[] args)
		{
			var kokudori = new Kokudori();
			Console.WriteLine(kokudori); // "Kokudori { Name = Kokudori, Age = 20 }"
			Debugger.Break();
		}
	}
}

ちなみに "{Name}" のように { メンバ名 } とすると自動でメンバの内容の ToSTring されたものに置き換えてくれる。

また、DebuggerDisplay は名前付き引数を入れてやるとよりきめ細やかな制御が可能。

[DebuggerDisplay("Name = {Name}, Age = {Age}")]
class Kokudori
{
	public string Name { get { return "Kokudori"; } }
	[DebuggerDisplay("ひ・み・つ", Name="年齢")]
	public int Age { get { return 20; } }
}


このように非常にイラッとするデバッグ表示ができるようになりました。

似たような属性として DebuggerBrowsable 属性があります。
これは値の表示の方法を制御します。

DebuggerBrowsable 属性には DebuggerBrowsableState 列挙体を1つ渡します。
DebuggerBrowsableState.Collapsed は既定値です。
DebuggerBrowsableState.Never はそのメンバをデバッグウィンドウに表示しません。
DebuggerBrowsableState.RootHidden は要素がコレクションである場合、ルート要素を表示しません。

DebuggerBrowsable は中々使いドコロが難しいです。
例えば、コレクションっぽいものを独自実装したとします。
DebuggerBrowsableState.RootHidden を内部コレクションに、
DebuggerBrowsableState.Never を Count に適用し、
DebuggerDisplay("Count = {Count}") を型に適用するとあたかも BCL のコレクションのように見えます。
(もちろんこの例は作為的です。大抵の場合コレクションを独自実装する必要はありません。)

using System.Collections.Generic;
using System.Diagnostics;

namespace ConsoleApplication
{
	[DebuggerDisplay("Count = {Count}")]
	class OriginList<T> : IEnumerable<T>
	{
		[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
		private List<T> list = new List<T>();

		public void Add(T item)
		{
			list.Add(item);
		}

		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		public int Count { get { return list.Count; } }

		public IEnumerator<T> GetEnumerator()
		{
			return list.GetEnumerator();
		}

		System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
		{
			return list.GetEnumerator();
		}
	}

	class Program
	{
		static void Main(string[] args)
		{
			var originList = new OriginList<string>()
			{
				"aaa", "bbb", "ccc"
			};
			var list = new List<string>()
			{
				"aaa", "bbb", "ccc"
			};
			Debugger.Break();
		}
	}
}

Visual Studio素敵!というお話でしたー。