Click here to Skip to main content
15,861,172 members
Articles / Programming Languages / C#

More implementations of extended string.Format()

Rate me:
Please Sign up or sign in to vote.
4.67/5 (3 votes)
6 Sep 2013CPOL4 min read 15.8K   107   6   2
Here I describe several implementations of extended string.Format() methods.

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

C#
var text = string.Format("{0}: {1}", GetId(), GetName());

something like this 

C#
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 

C#
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: 

C#
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:

  1. Your class must implement IEnumerable interface. Compiler need it for unknown reason because it does not use it.
  2. 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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:  

CaseAverage time of one call, ms
Reflection0.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

  1. Initial revision.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Finstek
China China
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHere's my try at it.... Pin
James Curran6-Sep-13 6:25
James Curran6-Sep-13 6:25 
AnswerRe: Here's my try at it.... Pin
Ivan Yakimov10-Sep-13 23:06
professionalIvan Yakimov10-Sep-13 23:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.