こんにちは、トミセン(@tomisenblog)です。
便利な機能がたくさんあるC#ですが、
「C#って、データを簡単にダンプできる方法ないよね?」
「C#って、ToString()して変数の値を一覧で出力できないの?」
「Dumpして、ログに吐き出したいんだけど?」
かゆいところに手が届くことが多いC#ですが、Dumpの方法は備わっていません。
簡単にできそうなサンプルで実行してみても、オブジェクト自身がList型だったり、中にList構造をもっていると上手くいかなかったりしていませんか。
今回はそんなお悩みを解決します。ずばり!
「C# Dump 変数の詳細を表示【List型も対応】する方法」についてご紹介します。
それでは、今日は飛ばしていきますよ。
C# Dump 変数の詳細を表示する方法
ここからはC# Dump 変数の詳細を表示するための具体的な方法について解説していきます。サンプルコードを使って解説していきますので、一緒にみていきましょう。
変数の中身を見る方法
実は、変数の中身を見るだけなら方法はいくつかあります。「ウォッチの追加」してウォッチウインドウで見たり、Console.WriteLine()で出力ウインドウに出力したり。でもウォッチウインドウは実行中にしか見られませんし、出力ウインドウは、List型の場合は中身まで表示してくれません。
そこで今回はいわゆるヘルパークラス・ユーティリティクラスと言われる方法でList型も一覧化しつつ、ログに出力して、実行後にも確認できる方法を解説していきます。
ヘルパークラス・ユーティリティクラスを使う方法
ヘルパークラス・ユーティリティクラスを使ってDumpしてみよう!
では、早速ソースコードです。
ソースコード
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
public class Test
{
public void Main()
{
// データ作成
var teams = new List<Team>();
var team1 = CreateTeam("AAA", "チーム1");
team1.Members.Add(CreateMember(111, "鈴木"));
team1.Members.Add(CreateMember(222, "佐藤"));
team1.Stadiums[0] = "東京";
team1.Stadiums[1] = "千葉";
team1.Records.Add("春大会", "優勝");
team1.Records.Add("夏大会", "2位");
teams.Add(team1);
var team2 = CreateTeam("BBB", "チーム2");
team2.Members.Add(CreateMember(333, "池田"));
team2.Members.Add(CreateMember(444, "田中"));
team2.Stadiums[0] = "神奈川";
team2.Stadiums[1] = "埼玉";
team2.Records.Add("秋大会", "3位");
team2.Records.Add("冬大会", "4位");
teams.Add(team2);
// Dump
var dump = DumpHelper.Dump(teams);
// 実行結果出力
Console.WriteLine(dump);
}
/// <summary>
/// チームクラス作成
/// </summary>
/// <param name="id"></param>
/// <param name="name"></param>
/// <param name="members"></param>
/// <returns></returns>
public Team CreateTeam(string id, string name)
{
return new Team
{
Id = id,
Name = name,
};
}
/// <summary>
/// チームクラス作成
/// </summary>
/// <param name="id"></param>
/// <param name="name"></param>
/// <returns></returns>
public Member CreateMember(int id, string name)
{
return new Member
{
Id = id,
Name = name,
};
}
}
/// <summary>
/// データ格納クラス
/// </summary>
public class Team
{
/// <summary>
/// チームID
/// </summary>
public string Id { get; set; }
/// <summary>
/// チーム名
/// </summary>
public string Name { get; set; }
/// <summary>
/// メンバー
/// </summary>
public List<Member> Members { get; set; } = new List<Member>();
/// <summary>
/// 球場
/// </summary>
public string[] Stadiums { get; set; } = new string[2];
/// <summary>
/// 記録
/// </summary>
public Dictionary<string, string> Records { get; set; } = new Dictionary<string, string>();
}
/// <summary>
/// データ格納クラス
/// </summary>
public class Member
{
/// <summary>
/// メンバーID
/// </summary>
public int Id { get; set; }
/// <summary>
/// メンバー名
/// </summary>
public string Name { get; set; }
}
/// <summary>
/// オブジェクトダンプ支援クラス
/// </summary>
public class DumpHelper
{
/// <summary>
/// 階層レベル
/// </summary>
private int _level;
/// <summary>
/// インデントサイズ
/// </summary>
private readonly int _indentSize;
/// <summary>
///
/// </summary>
private readonly StringBuilder _stringBuilder;
/// <summary>
///
/// </summary>
private readonly List<int> _hashListOfFoundElements;
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="indentSize"></param>
private DumpHelper(int indentSize)
{
_indentSize = indentSize;
_stringBuilder = new StringBuilder();
_hashListOfFoundElements = new List<int>();
}
/// <summary>
/// Dump実行
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
public static string Dump(object element)
{
return Dump(element, 2);
}
/// <summary>
/// Dump実行
/// </summary>
/// <param name="element"></param>
/// <param name="indentSize"></param>
/// <returns></returns>
public static string Dump(object element, int indentSize)
{
var instance = new DumpHelper(indentSize);
return instance.DumpElement(element);
}
/// <summary>
/// Dump処理
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
private string DumpElement(object element)
{
if (element == null || element is ValueType || element is string)
{
Write(FormatValue(element));
}
else
{
var objectType = element.GetType();
if (!typeof(IEnumerable).IsAssignableFrom(objectType))
{
Write("{{{0}}}", objectType.FullName);
_hashListOfFoundElements.Add(element.GetHashCode());
_level++;
}
var enumerableElement = element as IEnumerable;
if (enumerableElement != null)
{
foreach (object item in enumerableElement)
{
if (item is IEnumerable && !(item is string))
{
_level++;
DumpElement(item);
_level--;
}
else
{
if (!AlreadyTouched(item))
DumpElement(item);
else
Write("{{{0}}} <-- bidirectional reference found", item.GetType().FullName);
}
}
}
else
{
MemberInfo[] members = element.GetType().GetMembers(BindingFlags.Public | BindingFlags.Instance);
foreach (var memberInfo in members)
{
var fieldInfo = memberInfo as FieldInfo;
var propertyInfo = memberInfo as PropertyInfo;
if (fieldInfo == null && propertyInfo == null)
continue;
var type = fieldInfo != null ? fieldInfo.FieldType : propertyInfo.PropertyType;
object value = fieldInfo != null
? fieldInfo.GetValue(element)
: propertyInfo.GetValue(element, null);
if (type.IsValueType || type == typeof(string))
{
Write("{0}: {1}", memberInfo.Name, FormatValue(value));
}
else
{
var isEnumerable = typeof(IEnumerable).IsAssignableFrom(type);
Write("{0}: {1}", memberInfo.Name, isEnumerable ? "..." : "{ }");
var alreadyTouched = !isEnumerable && AlreadyTouched(value);
_level++;
if (!alreadyTouched)
DumpElement(value);
else
Write("{{{0}}} <-- bidirectional reference found", value.GetType().FullName);
_level--;
}
}
}
if (!typeof(IEnumerable).IsAssignableFrom(objectType))
{
_level--;
}
}
return _stringBuilder.ToString();
}
/// <summary>
/// 処理済み判定
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private bool AlreadyTouched(object value)
{
if (value == null)
return false;
var hash = value.GetHashCode();
for (var i = 0; i < _hashListOfFoundElements.Count; i++)
{
if (_hashListOfFoundElements[i] == hash)
return true;
}
return false;
}
/// <summary>
/// 書き込み
/// </summary>
/// <param name="value"></param>
/// <param name="args"></param>
private void Write(string value, params object[] args)
{
var space = new string(' ', _level * _indentSize);
if (args != null)
value = string.Format(value, args);
_stringBuilder.AppendLine(space + value);
}
/// <summary>
/// 値整形
/// </summary>
/// <param name="o"></param>
/// <returns></returns>
private string FormatValue(object o)
{
if (o == null)
return ("null");
if (o is DateTime)
return (((DateTime)o).ToShortDateString());
if (o is string)
return string.Format("\"{0}\"", o);
if (o is char && (char)o == '\0')
return string.Empty;
if (o is ValueType)
return (o.ToString());
if (o is IEnumerable)
return ("...");
return ("{ }");
}
}
// 【実行結果】
// {Team}
// Id: "AAA"
// Name: "チーム1"
// Members: ...
// {Member}
// Id: 111
// Name: "鈴木"
// {Member}
// Id: 222
// Name: "佐藤"
// Stadiums: ...
// "東京"
// "千葉"
// Records: ...
// [春大会, 優勝]
// [夏大会, 2位]
// {Team}
// Id: "BBB"
// Name: "チーム2"
// Members: ...
// {Member}
// Id: 333
// Name: "池田"
// {Member}
// Id: 444
// Name: "田中"
// Stadiums: ...
// "神奈川"
// "埼玉"
// Records: ...
// [秋大会, 3位]
// [冬大会, 4位]
ソースコードだけでは分かりにくい部分があるので、いくつかのポイントをコードを見ながら解説していきます!
まずはデータ構造です。
/// <summary>
/// データ格納クラス
/// </summary>
public class Team
{
/// <summary>
/// チームID
/// </summary>
public string Id { get; set; }
/// <summary>
/// チーム名
/// </summary>
public string Name { get; set; }
/// <summary>
/// メンバー
/// </summary>
public List<Member> Members { get; set; } = new List<Member>();
/// <summary>
/// 球場
/// </summary>
public string[] Stadiums { get; set; } = new string[2];
/// <summary>
/// 記録
/// </summary>
public Dictionary<string, string> Records { get; set; } = new Dictionary<string, string>();
}
/// <summary>
/// データ格納クラス
/// </summary>
public class Member
{
/// <summary>
/// メンバーID
/// </summary>
public int Id { get; set; }
/// <summary>
/// メンバー名
/// </summary>
public string Name { get; set; }
}
Teamクラス内にList<Member>、string[]、Dictionary<string, string>のメンバを持っています。
階層化されたデータ構造の確認です。
次にデータを作ります。
// データ作成
var teams = new List<Team>();
var team1 = CreateTeam("AAA", "チーム1");
team1.Members.Add(CreateMember(111, "鈴木"));
team1.Members.Add(CreateMember(222, "佐藤"));
team1.Stadiums[0] = "東京";
team1.Stadiums[1] = "千葉";
team1.Records.Add("春大会", "優勝");
team1.Records.Add("夏大会", "2位");
teams.Add(team1);
var team2 = CreateTeam("BBB", "チーム2");
team2.Members.Add(CreateMember(333, "池田"));
team2.Members.Add(CreateMember(444, "田中"));
team2.Stadiums[0] = "神奈川";
team2.Stadiums[1] = "埼玉";
team2.Records.Add("秋大会", "3位");
team2.Records.Add("冬大会", "4位");
teams.Add(team2);
Teamクラス自身もList<Team>にしています。これで自身がList型の場合も確認ができます。
データの準備出来ので、使い方見ていきます。
// Dump
var dump = DumpHelper.Dump(teams);
めちゃくちゃ簡単です。
DumpHelper.Dump(teams);だけです。DumpHelper.Dump(teams, 6);と第二引数を指定すればインデントを調整できます。
使う準備出来ので、早速結果を見ていきます。
// 【実行結果】
// {Team}
// Id: "AAA"
// Name: "チーム1"
// Members: ...
// {Member}
// Id: 111
// Name: "鈴木"
// {Member}
// Id: 222
// Name: "佐藤"
// Stadiums: ...
// "東京"
// "千葉"
// Records: ...
// [春大会, 優勝]
// [夏大会, 2位]
// {Team}
// Id: "BBB"
// Name: "チーム2"
// Members: ...
// {Member}
// Id: 333
// Name: "池田"
// {Member}
// Id: 444
// Name: "田中"
// Stadiums: ...
// "神奈川"
// "埼玉"
// Records: ...
// [秋大会, 3位]
// [冬大会, 4位]
結果はこんな感じになります。
自身のList<Team>は、2・18行目に{Team}という形で2件分出力されています。{Team}がクラスを指しています。
Teamクラス内のList<Member>は、Members: …と変数: …に続く形で表示されます。2件分きちんと出力されています。{Member}でクラスが表示されるのはList<Team>の場合と同じです。
string[]、Dictionary<string, string>についても変数: …の形は同じですね。
以上で「C# Dump 変数の詳細を表示【List型も対応】する」ことができました!
編集後記
ほぼ同じことができるものがGitHubにあるようです。
thomasgalliker / ObjectDumper |GitHub
みんな困っている事は一緒だから、そういうこともありますよね。笑
まとめ
C# Dump 変数の詳細を表示【List型も対応】する方法について紹介しました。
少しでも、C#のコーディングが楽になると嬉しいです。
それでは、また。