Help us understand the problem. What is going on with this article?

[C#] usingブロックを使ったStringBuilderのインデント処理

More than 1 year has passed since last update.

概要

ツリー構造や集約ルートモデルのデバッグプリントするとき、階層ごとにインデントを入れたいですが、コード上もわかりやすく、簡単にかけるようにならないかと考えて、ユーティリティクラスを作ってみました。

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

まあ、XXXBBBDDDの間に挿入したいなら、普通にusingブロックを分割してください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away