Introduction
Some time ago I published an article about writing some extension of string.Format()
method that allows using more convenient syntax of format string. The article described how to create a method which allows to write instead of
var text = string.Format("{0}: {1}", GetId(), GetName());
something like this
var text = StringEx.Format("{ID}: {Name}", new { ID = GetId(), Name = GetName() });
If you are interested in reasons why I whould prefer the second syntax, please read my first article.
I got several replies saying that my implementation of StringEx.Format
method was rather slow because it used reflection to extract values of properties from the object passed as the second parameter. So I was thinking if there are ways to solve the same problem differently. And in fact there are.
Dictionary
If you take a closer look at the new format string "{ID}: {Name}"
it is clear that we need some values for ID
and Name
placeholders. Very simple way to store these values is a dictionary. I can write something like
var text = StringEx.Format("{ID}: {Name}", new Dictionary<string, object> {
{ "ID", GetId() },
{ "Name", GetName() } });
But can we do it better? This record is rather clumsy and verbose. I'd like to have something shorter. Finally I came to the following code:
var text = new StringFormatDictionary("{ID}: {Name}") { { "ID", GetId() }, { "Name", GetName() } }.ToString();
It is not very good, but still better than it was. Lets consider the implementation.
To be able to use syntax of collection initializers you need 2 things:
- Your class must implement
IEnumerable
interface. Compiler need it for unknown reason because it does not use it. - Your class must have a method with name
Add
. This method can have some parameters. Values for these parameters will be listed inside curly brackets in collection initializer.
So my class looked like this:
public class StringFormatDictionary : IEnumerable
{
private readonly Dictionary<string, object> _values = new Dictionary<string,object>();
public void Add(string key, object value)
{
_values[key] = value;
}
public IEnumerator GetEnumerator()
{
return _values.GetEnumerator();
}
...
}
Code for conversion of new format string into the old one is the same as it was described in the previous article:
var convertedFormat = new StringExFormatConverter().Convert(format);
var values = GetValues(_convertedFormat.PlaceholderNames);
return string.Format(formatProvider, convertedFormat.Format, values);
Here convertedFormat
is a simple object containing format string in the old form in the Format
property and an array of names for each placeholder in the PlaceholderNames
property. For example for new format string "{ID}: {Name}"
convertedFormat.Format
will contain "{0}: {1}"
and convertedFormat.PlaceholderNames
will contain [ "ID", "Name" ]
.
Code for getting array of objects for the second parameter of string.Format
is also very simple and fast:
private object[] GetValues(string[] names)
{
var values = new LinkedList<object>();
foreach (var name in names)
{
object value;
if (_values.TryGetValue(name, out value) == false)
{
throw new InvalidOperationException(string.Format("There is no member with name '{0}'", name));
}
values.AddLast(value);
}
return values.ToArray();
}
This implementation is good but its syntax is very verbose for me. So I looked for another solution.
Dynamics
I'd really like to write something like this:
var text = formatter.Format("{id}: {name}", id: GetId(), name: GetName());
The latest versions of .NET Framework give me ability to do it. This ability is dynamics. I implemented my class of formatter inheriting it from DynamicObject
:
public class DynamicStringFormatter : DynamicObject
{
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
result = null;
if (binder.Name.Equals("Format") == false)
{ return false; }
if(args.Length == 0)
{ return false; }
if(binder.CallInfo.ArgumentCount - binder.CallInfo.ArgumentNames.Count != 1)
{ return false; }
if((args[0] is string) == false)
{ return false; }
var format = (string) args[0];
var convertedFormat = new StringExFormatConverter().Convert(format);
var values = GetValues(convertedFormat.PlaceholderNames, binder, args);
result = string.Format(null, convertedFormat.Format, values);
return true;
}
...
}
Its method TryInvokeMember
will be called when any unknown to compiler method is called on the object of this type. Array args
will contain all parameters passed to this method. And binder.CallInfo
contains some information about these parameters (including their names if they have been specified).
Now it is easy to get the array of objects for string.Format
call:
private object[] GetValues(string[] names, InvokeMemberBinder binder, object[] args)
{
var values = new LinkedList<object>();
foreach (var name in names)
{
var argIndex = binder.CallInfo.ArgumentNames.IndexOf(name);
if (argIndex == -1)
{
throw new InvalidOperationException(string.Format("There is no argument with name '{0}'", name));
}
values.AddLast(args[argIndex + 1]);
}
return values.ToArray();
}
This syntax I like more. But there are limitations for this approach too. Lets compare all proposed implementations.
Comparison
Implementation using Reflection
- The syntax is close to
string.Format
but more verbose. - Implementation is rather slow due to usage of Reflection.
Implementation using Dictionary
- The syntax is very verbose.
- You must create an instance of the formatter.
- This is the fastest implementation.
Implementation using Dynamics
- The most succinct syntax of all implementations.
- Slower then Dictionary.
- You must create instance of the formatter.
- Only for latest versions of .NET Framework where dynamics are supported.
- One need to add reference to Microsoft.CSharp assembly to their projects.
I made also some performance comparison. You can find the code I used for this in the downloadable archive. Here are the results of the comparison:
Case | Average time of one call, ms |
Reflection | 0.018 |
Dictionary | 0.015 |
Dynamics (one formatter for all calls) | 0.016 |
Dynamics (new formatter for each call) | 0.016 |
Results varied from launch to launch but the tendency is the same. Dictionary is the fastest, Dynamics are slightly worse and Reflection is the worst.
Points of Interest
I'm not completely sure about my method of measuring performance. To measure average time of one call I execute Format
method many times but each time with the same parameters. In this case Reflection and Dynamics can use internal caching to improve their results. So they may be inaccurate. But I don't know how to make such a test with different set of parameters for each call. Do you know?
History
- Initial revision.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.