Back to Basics: Streamlining the StringBuilder

To get back in the habit of blogging I thought I’d start by trying to breathe some new life into one of the oldest pieces of the .NET Framework – the StringBuilder. A few years ago I wrote about an aspect of the StringBuilder class that’s often overlooked – it’s fluent interface. Back then I speculated that one reason the fluent interface is so rarely used is that virtually every StringBuilder example, including those on MSDN, fail to highlight it by using a more imperative style.

For all its usefulness, the fluent interface is far from perfect. It excels at simple string construction but quickly breaks down for anything more involved, namely, conditionally appending text or appending items from a collection. Consider the following where we attempt to construct a formatted date string using the fluent interface:

var date = DateTime.Now;

var sb = new StringBuilder();
if(date.Month < 10)
  sb.Append(0);
sb.AppendFormat("{0}/", date.Month);
if(date.Day < 10)
  sb.Append(0);
sb
  .AppendFormat("{0}/", date.Day)
  .Append(date.Year);

var dateStr = sb.ToString();
Console.WriteLine(dateStr);

In the preceding snippet, we continuously enter and exit the confines of the fluent interface. This forces us to manage the StringBuilder instance and breaks the flow of the code. Wouldn’t it be cleaner if we could somehow embed the conditional blocks within the fluent interface and avoid the context shifts?

StringBuilder predates many modern language features so addressing the fluent interface’s shortcomings hasn’t always been easy. Traditionally, the only viable option was to wrap a StringBuilder within a another class (a decorator) that exposed the existing methods as well as any additional functionality. Fortunately, C# has sufficiently evolved to the extent that defining some higher-order extension methods makes extending StringBuilder’s fluent interface trivial. Let’s begin by defining one such method, AppendWhen, that conditionally appends a value.

public static class StringBuilderExtensions
{
  public static StringBuilder AppendWhen<T>(
    this StringBuilder @this,
    Func predicate,
    T value)
  {
    return
      predicate()
        ? @this.Append(value)
        : @this;
  }
}

The AppendWhen method isn’t particularly complicated. It merely accepts a Func which we invoke to determine whether or not to append the supplied value. Whatever the result, we return the StringBuilder instance so we can continue chaining methods. With AppendWhen in place, we can revise the date string construction to this:

var date = DateTime.Now;

var dateStr =
  new StringBuilder()
    .AppendWhen(() => date.Month < 10, 0)
    .AppendFormat("{0}/", date.Month)
    .AppendWhen(() => date.Day < 10, 0)
    .AppendFormat("{0}/", date.Day)
    .Append(date.Year)
    .ToString();

Console.WriteLine(dateStr);

Now, rather than jumping in and out of the fluent method chain, we have an easy to follow method chain that allows us to stay focused on string construction. It’s easy to apply this same pattern to some of the other Append methods. For instance, here’s a possible implementation for conditionally appending a formatted string:

public static class StringBuilderExtensions
{
  // ...Snip...

  public static StringBuilder AppendFormatWhen(
    this StringBuilder @this,
    Func<bool> predicate,
    string format,
    params object[] args)
  {
    return
      predicate()
        ? @this.AppendFormat(format, args)
        : @this;
  }
}

Another common pattern with the StringBuilder is appending each item from a sequence. Again, this scenario requires us to leave the confines of the fluent interface and enter a looping construct. How many times have you written something like this?

var companions =
  new []
  { 
    new { First = "Amy", Last = "Pond" },
    new { First = "Rory", Last = "Williams" },
    new { First = "Donna", Last = "Noble" },
    new { First = "Rose", Last = "Tyler" },
    new { First = "Jack", Last = "Harkness" }
  };
  
var sb =
  new StringBuilder()
    .AppendLine("Companions:");

foreach(var c in companions.OrderBy(c => c.Last))
{
  sb
    .AppendFormat("\t{1}, {0}", c.First, c.Last)
    .AppendLine();
}

var companionStr = sb.ToString();
Console.WriteLine(companionStr);

Wouldn’t it also be cleaner to embed that loop within the fluent interface? With another extension method, we can.

public static class StringBuilderExtensions
{
  // ...Snip...

  public static StringBuilder AppendSequence<T>(
    this StringBuilder @this,
    IEnumerable<T> sequence,
    Func<StringBuilder, T, StringBuilder> appender)
  {
    return sequence.Aggregate (@this, appender);
  }
}

The call to Aggregate within AppendSequence makes it trivial to apply the supplied function to each item in the supplied sequence. This lets us revise the code for constructing the companion string to this:

var companions =
  new []
  { 
    new { First = "Amy", Last = "Pond" },
    new { First = "Rory", Last = "Williams" },
    new { First = "Donna", Last = "Noble" },
    new { First = "Rose", Last = "Tyler" },
    new { First = "Jack", Last = "Harkness" }
  };

var companionStr =
  new StringBuilder()
    .AppendLine("Companions:")
    .AppendSequence(
      companions.OrderBy(c => c.Last),
      (sb, c) => sb.AppendFormat("\t{1}, {0}", c.First, c.Last).AppendLine())
    .ToString();

Console.WriteLine(companionStr);

These are but a few of the ways extension methods can be applied to StringBuilder to improve its interface. Some other methods I’ve found useful include AppendFormattedLine, which appends a formatted string and a new line, and AppendLines, which appends multiple blanks lines. These are both simple methods so I’ll leave their implementation to your imagination. What I won’t do though, is finish this post without including at least a little F# so here’s the StringBuilderExtensions class with the AppendWhen, AppendFormatWhen, and AppendSequence methods.

type StringBuilder with
  member sb.AppendWhen<'T> (predicate : unit -> bool, value : 'T) =
    if predicate() then sb.Append(value)
    else sb
  member sb.AppendFormatWhen (predicate : unit -> bool, format, args : obj seq) =
    if predicate() then sb.AppendFormat(format, args |> Seq.toArray)
    else sb
  member sb.AppendSequence<'T> (sequence : 'T seq, appender : StringBuilder -> 'T -> StringBuilder) =
    Seq.fold appender sb sequence

As it’s implemented here, the F# version won’t work across language boundaries but you can easily allow that by importing the System.Runtime.CompilerServices namespace and decorating the class and each of its methods with ExtensionAttribute.

Advertisement

2 comments

    1. Thanks, me too.

      Re: Disqus – This blog is hosted WordPress so I don’t really have control over that. If I ever migrate to another platform I’ll probably look for a better option.

Comments are closed.