-
Notifications
You must be signed in to change notification settings - Fork 16
Text
Totem features the Text
type, a unified medium for working with strings.
.NET has several options for building text, each with its own quirks:
StringBuilder
- Helpfully returns itself from
Append*
calls - Uses
Append
where other APIs useWrite
- Uses
AppendFormat
where other APIs use overloads TextWriter
- Not-so-helpfully returns
void
fromWrite
calls - Just-different-enough write signatures from
StringBuilder
to be annoying Stream
- Direct API is sparse - requires
StreamWriter
(with the same drawbacks asTextWriter
) - Requires thinking about seek positioning
This is a decent amount of friction to navigate each time we want to build some text. We often fall back to procedural code, full of concatenation and String.Format
, to avoid the overhead (and the benefits) of the tools at our disposal.
Beneath this, though, lies a more subtle and nefarious truth: all of these techniques are eager. They represent the result of building text; the work happens immediately when the call is made.
The alternative is a deferred approach, one that represents the ability to build text only when required. This is the purpose of the Text
type - represent an algorithm for producing text.
- We can see the same approach to deferred execution throughout LINQ
The classic example scenario benefiting from this approach is writing to a log:
Log.Info("Something happened: " + expensiveObject.ToString());
If the log level is too low, we calculate that string just to ignore it. A common workaround is to check the level first:
if(Log.CanWriteInfo)
{
Log.Info("Something happened: " + expensiveObject.ToString());
}
We avoid the expensive string, but at the cost of readability; who wants to see that block everywhere?
Next, we may try to defer evaluation by using a function:
Log.Info(() => "Something happened: " + expensiveObject.ToString());
This works and is reasonably readable; it does not, however, alleviate the friction of the other techniques:
Log.Info(() =>
{
var builder = new StringBuilder();
builder.Append("Something happened: ").AppendLine(expensiveObject);
if(foo != null)
{
builder.AppendFormat(" [foo: {0}]", foo);
}
builder.AppendLine().AppendLine().AppendFormat(" [bar: {0}]", bar).AppendLine();
return builder.ToString();
}
The log write looks much nicer:
Log.Info(Text
.Of("Something happened: ")
.WriteLine(expensiveObject)
.WriteIf(foo != null, Text.Of(" [foo: {0}]", foo))
.WriteTwoLines()
.WriteLine(" [bar: {0}]", bar));
If Log.Info
never calls .ToString()
on the Text
instance it receives, the writes never happen, and we avoid the overhead of the expensive string and multiple format calls. We also get a much nicer API based on extension methods!
See the test scenarios for a comprehensive look into the capabilities of the Text
type.