C# Canonical Forms—Formattable Object


Jump to: navigation, search
CSharp-Online.NET:Articles
C# Articles

C# Canonical Forms

© 2006 Weldon W. Nash, III

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.



Previous_Page_.gif Next_Page_.gif


Personal tools