C# Canonical Forms—Formattable Object
Is the Object Formattable?
When you create a new object, or an instance of a value type for that matter, it inherits a method
from System.Object called ToString(). This method accepts no parameters and simply returns
a string representation of the object. In all cases, if it makes sense to call ToString() on your object,
you’ll need to override this method. The default implementation provided by System.Object merely
returns a string representation of the object’s type name, which of course is not useful for an object
requiring a string representation based upon its internal state. You should always consider overriding
Object.ToString() for all of your types, even if only for the convenience of logging the object
state to a debug output log.
Object.ToString() is useful for getting a quick string representation of an object; however, it’s
sometimes not useful enough. For example, consider the previous ComplexNumber example. Suppose
you want to provide a ToString() override for that class. An obvious implementation would output
the complex number as an ordered pair within a pair of parentheses such as "(1, 2)" for example.
However, the real and imaginary components of ComplexNumber are of type double. Also, floating-point
numbers don’t always appear the same across all cultures. Americans use a period to separate the
fractional element of a floating-point number, whereas most Europeans use a comma. This problem
is solved easily if you utilize the default culture information attached to the thread. By accessing
the System.Threading.Thread. CurrentThread.CurrentCulture property, you can get references to the
default cultural information detailing how to represent numerical values, including monetary amounts,
as well as information on how to represent time and date values.
By default, the CurrentCulture property gives you access to System.Globalization.
DateTimeFormatInfo and System.Globalization.NumberFormatInfo. Using the information provided
by these objects, you can output the ComplexNumber in a form that is appropriate for the default culture
of the machine the application is running on.
That solution seems easy enough. However, you must realize that there are times where using
the default culture is not sufficient, and a user of your objects may need to specify which culture to
use. Not only that, the user may want to specify the exact formatting of the output. For example, a user
may prefer to say that the real and imaginary portions of a ComplexNumber instance should be displayed
with only five significant digits while using the German cultural information. If you develop software for servers, you know that you need this capability. A company that runs a financial services server
in the United States and services requests from Japan will want to display Japanese currency in the
format customary for the Japanese culture. You need to specify how to format an object when it is
converted to a string via ToString() without having to change the CurrentCulture on the thread
beforehand.
In fact, the Standard Library provides an interface for doing just that. When a class or struct
needs the capability to respond to such requests, it implements the IFormattable interface. The
following code shows the simple-looking IFormattable interface. However, don’t be fooled by its
simplistic looks, because depending on the complexity of your object, it may be tricky to implement:
public interface IFormattable { string ToString( string format, IFormatProvider formatProvider ); }
Let’s consider the second parameter first. If the client passes null for formatProvider, you should
default to using the culture information attached to the current thread as previously described.
However, if formatProvider is not null, you’ll need to acquire the formatting information from the
provider via the IFormatProvider.GetFormat method. IFormatProvider looks like this:
public interface IFormatProvider { object GetFormat( Type formatType ); }
In an effort to be as generic as possible, the designers of the Standard Library designed GetFormat()
to accept an object of type System.Type. Thus, it is extensible as to what types the object that implements
IFormatProvider may support. This flexibility is handy if you intend to develop custom format
providers that need to return as-of-yet-undefined formatting information.
The Standard Library provides a System.Globalization.CultureInfo type that will most likely
suffice for all of your needs. The CultureInfo object implements the IFormatProvider interface, and
you can pass instances of it as the second parameter to IFormattable.ToString(). Soon, I’ll show an
example of its usage when I make modifications to the ComplexNumber example, but first, let’s look at
the first parameter to ToString().
The format parameter of ToString() allows you to specify how to format a specific number.
The format provider can describe how to display a date or how to display currency based upon cultural
preferences, but you still need to know how to format the object in the first place. All of the types
within the Standard Library, such as Int32, support the standard format specifiers as described
under" Standard Numeric Format Strings" in the MSDN. In a nutshell, the format string consists of
a single letter specifying the format, and then an optional number between 0 and 99 that declares
the precision. For example, you can specify that a double be output as a 5 significant digit floatingpoint
number with F5. Not all types are required to support all formats except for one—the G format—
which stands for "general." In fact, the G format is what you get when you call the parameterless
Object.ToString() on most objects in the Standard Library. Some types will ignore the format specification
in special circumstances. For example, a System.Double can contain special values that
represent NaN (Not a Number), PositiveInfinity, or NegativeInfinity. In such cases, System.Double
ignores the format specification and displays a symbol appropriate for the culture as provided by
NumberFormatInfo.
The format specifier may also consist of a custom format string. Custom format strings allow the user to specify the exact layout of numbers as well as mixed-in string literals and so on by using the syntax described under "Custom Numeric Format String" in the MSDN. The client can specify one format for negative numbers, another for positive numbers, and a third for zero values. I won’t spend any time detailing these various formatting capabilities. Instead, I encourage you to reference the MSDN material for detailed information regarding them.
As you can see, implementing IFormattable.ToString() can be quite a tedious experience,
especially since your format string could be highly customized. However, in many cases—and the
ComplexNumber example is one of those cases—you can rely upon the IFormattable implementations
of standard types. Since ComplexNumber uses System.Double to represent its real and imaginary parts,
you can defer most of your work to the implementation of IFormattable on System.Double. Let’s
look at modifications to the ComplexNumber example to support IFormattable. Assume that the
ComplexNumber type will accept a format string exactly the same way that System.Double does and that
each component of the complex number will be output using this same format. Of course, a better
implementation may provide more capabilities such as allowing you to specify whether the output
should be in Cartesian or polar format, but I’ll leave that to you as an exercise:
using System; using System.Globalization; public sealed class ComplexNumber : IFormattable { public ComplexNumber( double real, double imaginary ) { this.real = real; this.imaginary = imaginary; } public override string ToString() { return ToString( "G", null ); } // IFormattable implementation public string ToString( string format, IFormatProvider formatProvider ) { string result = "(" + real.ToString(format, formatProvider) + " " + real.ToString(format, formatProvider) + ")"; return result; } // Other methods removed for clarity private readonly double real; private readonly double imaginary; } public sealed class EntryPoint { static void Main() { ComplexNumber num1 = new ComplexNumber( 1.12345678, 2.12345678 ); Console.WriteLine( "US format: {0}", num1.ToString( "F5", new CultureInfo("en-US") ) ); Console.WriteLine( "DE format: {0}", num1.ToString( "F5", new CultureInfo("de-DE") ) ); Console.WriteLine( "Object.ToString(): {0}", num1.ToString() ); } }
Here’s the output from running the previous example:
US format: (1.12346 1.12346) DE format: (1,12346 1,12346) Object.ToString(): (1.12345678 1.12345678)
In Main(), notice the creation and use of two different CultureInfo instances. First, the
ComplexNumber is output using American cultural formatting, and second, using German cultural
formatting. In both cases, I specify to output the string using only five significant digits. You will see
that System.Double’s implementation of IFormattable.ToString() even rounds the result as expected.
Finally, you can see that the Object.ToString() override is implemented to defer to the IFormattable.ToString method using the G (general) format.
IFormattable provides the clients of your objects with powerful capabilities when they have
specific formatting needs for your objects. However, that power comes at an implementation cost.
Implementing IFormattable.ToString() can be a very detail-oriented task that takes a lot of time
and attentiveness.
|

