I wish I knew about string.Join a long time ago
Having working on the test team for many products which involve serialization (conversion between CLR objects and various wire formats, such as XML, binary and JSON), I’ve created my share of test types, since there are many, many rules about serialization, and we needed to validate them with actual types. Things such as collections, polymorphism, visibility, decorating attributes, primitive types, classes vs. structures, nested types, enumerations, etc., all needed to be represented in our “type library” so we could write the tests for the serializers. And since we need to understand failures without running the tests under a debugger (since we test in many different platforms, the runs are done in an automated lab, and all we get at the end are logs and pass / rail result), we tend to trace a lot of information, including the actual object contents when there is a mismatch between the actual result and our expectations.
So I’ve written code such as this quite a few times, iterating over the collections in the data, using a counter to find out whether to add a separator, and then adding the items themselves. Nothing out of the ordinary, but I’ve written those 19 lines probably way too much.
- public override string ToString()
- {
- StringBuilder sb = new StringBuilder();
- sb.Append("Order{Id=");
- sb.Append(this.Id);
- sb.Append(",Items=");
- if (this.Items == null)
- {
- sb.Append("null");
- }
- else
- {
- sb.Append("[");
- for (int i = 0; i < this.Items.Length; i++)
- {
- if (i > 0)
- {
- sb.Append(",");
- }
- sb.Append(this.Items[i]);
- }
- sb.Append("]");
- }
- sb.Append("}");
- return sb.ToString();
- }
Then I saw the light in string.Join, which does exactly what I had been doing “by hand” all that time. Those lines can now be reduced in half – and less code is (almost) always better code, so that’s a gain.
- if (this.Items == null)
- {
- sb.Append("null");
- }
- else
- {
- sb.Append("[");
- sb.Append(string.Join(",", (IEnumerable<Product>)this.Items));
- sb.Append("]");
- }
And if you’re really into reduction, we can go even further, without sacrificing the readability of the code, IMO.
- sb.Append(this.Items == null ?
- "null" :
- "[" + string.Join(",", (IEnumerable<Product>)this.Items) + "]");
I know, this can also be refactored into a helper method, which is exactly what I did in many places, although having the concise option is good for new projects. With a helper method, the call becomes a single line:
- sb.Append(this.Items.ToPrettyString());
Where ToPrettyString is defined as an extension method in a helper class (updated on 2012/03/29 – added a missing “Select” statement on the original code, without which the code would stack overflow). The OfType<object> call, which seems innocuous, does the conversion between IEnumerable and IEnumerable<T>, for which the Select extension method is defined.
- public static class ToStringExtensions
- {
- public static string ToPrettyString(this object obj)
- {
- if (obj == null)
- {
- return "null";
- }
- else if (obj is IEnumerable)
- {
- return "[" + string.Join(",", ((IEnumerable)obj).OfType<object>().Select(o => o.ToPrettyString())) + "]";
- }
- else
- {
- return obj.ToString();
- }
- }
- }
Every time I look at code I wrote over a year back I see places where I could have done better. That’s always good to find out that, although some mistakes were made, at least we’re still learning…
Update: after seeing some comments about this post on reddit, I have another one with more context for those who are interested.
Comments
- Anonymous
March 28, 2012
Next step: implementpublic static string Embrace(this string str, string open, string close){ return new StringBuilder().Append(open).Append(str).Append(close).ToString();}public static string EmbraceSquare(this string str){ return str.Embrace("[", "]");}public static string EmbraceCurly(this string str){ return str.Embrace("{", "}");}