概要
ツリー構造や集約ルートモデルのデバッグプリントするとき、階層ごとにインデントを入れたいですが、コード上もわかりやすく、簡単にかけるようにならないかと考えて、ユーティリティクラスを作ってみました。
IDisposableにして、 usingブロックを使って書けたらブロックのインデントが出力のインデントに一致してわかりやすい(当社比) かなという思い付きです。
コード(ユーティリティクラス)
コメント欄に改良版がありますので、そちらをご覧ください。取り込もうと思ったけど単なるコピペになりそうなので、自分の元ソースはそのままにしておきます。
using System;
using System.Text;
using System.Text.RegularExpressions;
public class IndentedStringBuilder : IDisposable
{
private const string INDENT = " ";
private const string REPLACEMENT = "$1" + INDENT;
private StringBuilder _parent;
private StringBuilder _internal;
public IndentedStringBuilder(StringBuilder parent)
{
_parent = parent;
_internal = new StringBuilder();
}
public StringBuilder Builder { get { return _internal; } }
/// <summary>
/// 親のStringBuilderが改行で終わってなかったら改行を追加する
/// </summary>
private void AdjustParentEndLine()
{
if (_parent.Length == 0 ) return;
var lastChar = _parent[_parent.Length - 1];
if(lastChar == '\r' || lastChar == '\n') return;
_parent.AppendLine();
}
public void Dispose()
{
AdjustParentEndLine();
var lines = Regex.Split(_internal.ToString(), "\r\n|\r|\n");
var count = lines.Length;
// 元の文字列の末尾が改行だと、空の行が一個増えるのを防ぐ
if (count > 0 && lines[count - 1].Length == 0) count--;
lines.Take(count).Foreach(x =>
{
_parent.Append(INDENT);
_parent.AppendLine(x);
});
}
}
使い方
こんな風にusingブロックで囲んで使います。ネストして多段インデントもできます。
インデントの前後で必要に応じて改行を追加する機能もありますので、下記の例の場合 AppendLine から Append に変えても同じ出力結果になります。
(当然ですが、同じインデント階層に連続で書き出す場合は Append で改行はしません。)
サンプルコード
var sb = new StringBuilder();
sb.AppendLine("AAA");
using (var child = new IndentedStringBuilder(sb))
{
child.Builder.AppendLine("BBB");
using (var child2 = new IndentedStringBuilder(child.Builder))
{
child2.Builder.AppendLine("CCC");
}
child.Builder.AppendLine("DDD");
}
sb.AppendLine("EEE");
return sb.ToString();
出力
AAA
BBB
CCC
DDD
EEE
#解説
IDisposableをusingブロックで使うと、ブロックを抜けるときにdisposeされるという仕様を使って、dispose呼ばれたときにインデント付与処理をしています。
IndentedStringBuilder 自体が StringBuilder を継承できればよかったんですが、sealed クラスなのでできません。なのでやむなく Builder プロパティー経由で StringBuilder にアクセスするようにしました。
お好みで IndentedStringBuilder に Append() など StringBuilder と同じメソッド生やしてもいいかもしれません。
言うほど使いやすくも見やすくもねーよ、と言われたらごめんなさいと言うしかありません。そこは主観の問題なのでご容赦ください。
#注意点
usingブロックの中で普通に元のStringBuilderを編集できてしまうけれど、(おそらく)書いた人の期待とは違う結果になります。
var sb = new StringBuilder();
sb.AppendLine("AAA");
using (var child = new IndentedStringBuilder(sb))
{
child.Builder.AppendLine("BBB");
sb.AppendLine("XXX"); // <--これ
child.Builder.AppendLine("DDD");
}
sb.AppendLine("EEE");
return sb.ToString();
出力
AAA
XXX
BBB
DDD
EEE
まあ、XXX
をBBB
とDDD
の間に挿入したいなら、普通にusingブロックを分割してください。