Search code examples
c#stringformattingglobalization

Why need to perform a formatType check when implement a ICustomFormatter interface in C#?


I am reading C#6.0 in a nutshell, in the book it says user can also write their own format provider that works in conjunction with existing types. And on page 245 there is a sample code as below:

I don't know if the check could be omitted when implement GetFormat function? Can I just use

return this 

instead of

if (formatType == typeof(ICustomFormatter)) 
    return this;
return null; 

in that implementation? As I think formatType will always be typeof ICustomFormatter as per class declaration, because it implemented the ICustomFormatter interface?

using System;
using System.Globalization;
using System.Text;

namespace Page245
{
    class Program
    {
        static void Main(string[] args)
        {
            double n = -123.45;
            IFormatProvider fp = new WordyFormatProvider();
            Console.WriteLine(string.Format(fp, "{0:C} in words is {0:W}", n));
        }
        public class WordyFormatProvider : IFormatProvider, ICustomFormatter
        {
            static readonly string[] _numberWords =
            "zero one two three four five six seven eight nine minus point".Split();
            IFormatProvider _parent;   // Allows consumers to chain format providers
            public WordyFormatProvider() : this(CultureInfo.CurrentCulture) { }
            public WordyFormatProvider(IFormatProvider parent)
            {
                _parent = parent;
            }
            public object GetFormat(Type formatType)
            {
                if (formatType == typeof(ICustomFormatter)) // Can this check be omitted?, just return this directly, as formatType will always be typeof ICustomFormatter as per class declaration, it implements the ICustomFormatter interface??
                    return this;
                return null;
            }
            public string Format(string format, object arg, IFormatProvider prov)
            {
                // If it's not our format string, defer to the parent provider:
                if (arg == null || format != "W")
                    return string.Format(_parent, "{0:" + format + "}", arg);
                StringBuilder result = new StringBuilder();
                string digitList = string.Format(CultureInfo.InvariantCulture, "{0}", arg);
                foreach (char digit in digitList)
                {
                    int i = "0123456789-.".IndexOf(digit);
                    if (i == -1) continue;
                    if (result.Length > 0) result.Append(' ');
                    result.Append(_numberWords[i]);
                }
                return result.ToString();
            }
        }

    }
}

I have changed code and the results doesn't seem to change, but I would like to ask if there is any potential downside if I modify the code like mentioned? Thanks for any comments or answer.

It writes to console as below and result doesn't change after modification -€123.45 in words is minus one two three point four five


Solution

  • There are times when IFormatProvider.GetFormat is called with formatType set to something other than your formatter type.

    Let's use a really simple test IFormatProvider:

    public class CustomFormatter : IFormatProvider
    {
        public object GetFormat(Type formatType)
        {
            Console.WriteLine("Called with " + formatType);
            return null;
        }
    }
    

    Now, let's try some things:

    string s = 3.ToString(new CustomFormatter());
    

    Here, GetFormat is called with typeof(NumberFormatInfo).

    string s = DateTime.Now.ToString(new CustomFormatter());
    

    Here, GetFormat is called with typeof(DateTimeFormatInfo).

    string s = string.Format(new CustomFormatter(), "{0}", 3);
    

    Here, GetFormat is first called with typeof(ICustomFormatter), and then with typeof(NumberFormatInfo).


    Now it's true that in the current implementation of .NET, if you pass your own IFormatProvider to string.Format, it will first ask it for an ICustomFormatter, and if that returns null, for an NumberFormatInfo / DateTimeFormatInfo (if appropriate).

    However, you cannot rely on this. Someone might use your IFormatProvider somewhere else (e.g. passing to an object which implements IFormattable), in which case your IFormatProvider might be asked for a different formatType. It might be that the implementation of string.Format changes in the future, and it asks for a NumberFormatInfo before or as well as an ICustomFormatter.

    The docs for IFormatProvider.GetFormat say:

    Returns

    An instance of the object specified by formatType, if the IFormatProvider implementation can supply that type of object; otherwise, null.

    For everything to work, now and in the future, you need to follow this. If your IFormatProvider can supply an instance of the requested type, it should do so. Otherwise, it should return null.


    As for why string.Format first calls our CustomFormatter with typeof(ICustomFormatter), and then with typeof(NumberFormatInfo), see this doc:

    How arguments are formatted

    Format items are processed sequentially from the beginning of the string. Each format item has an index that corresponds to an object in the method's argument list. The Format method retrieves the argument and derives its string representation as follows:

    • If the argument is null, the method inserts String.Empty into the result string. You don't have to be concerned with handling a NullReferenceException for null arguments.

    • If you call the Format(IFormatProvider, String, Object[]) overload and the provider object's IFormatProvider.GetFormat implementation returns a non-null ICustomFormatter implementation, the argument is passed to its ICustomFormatter.Format(String, Object, IFormatProvider) method. If the format item includes a formatString argument, it is passed as the first argument to the method. If the ICustomFormatter implementation is available and produces a non-null string, that string is returned as the string representation of the argument; otherwise, the next step executes.

    • If the argument implements the IFormattable interface, its IFormattable.ToString implementation is called.

    • The argument's parameterless ToString method, which either overrides or inherits from a base class implementation, is called.

    The argument is not null, so we skip the first bullet.

    We called the overload of string.Format which takes an IFormatProvider, but our IFormatProvider.GetFormat implementation returned null, so we skip over the second bullet.

    Our argument, an Int32, does implement IFormattable, so its IFormattable.ToString implementation is called (and our IFormatProvider is passed in). Int32.ToString(IFormatProvider provider) calls IFormatProvider.GetFormat and passes in typeof(NumberFormatInfo).