梁某人

发展才是硬道理

导航

String.Format 摘录

Formatting string output is difficult to avoid in many applications, even with modern graphical user interfaces. It's inevitable that at some point you'll need to format data in a way that's easy to understand. Practically every runtime has its set of string formatting procedures and the .NET Framework is no exception. Understanding how the culture-aware and extensible string formatting works in the .NET Framework, though, can help you create better ways of formatting data.

This article is not so much about the format providers present in the .NET Framework Class Library - though they will be discussed briefly - but about how to write custom formatting routines in such a way that they can easily be used anywhere they're required. Basic string formatting will be discussed, as well as format providers in the .NET Framework, extending existing types with custom format providers, and extending your types with custom formatting.

String Formatting Made Simple

There are countless ways to format strings in any language. It could be as simple as converting all data to strings and concatenating the strings, or using inline expressions with possible format specifiers.

Take, for example, Perl. This simple example uses a local string variable in its print function, as well as a real number that would use the appropriate decimal separator based on the LC_CTYPE environment variable.

use locale;
my $var1 = "Perl";
my $var2 = 5.6;
print("Hello, from $var1 $var2!");

The print function simply evaluates the variables in the expression and writes it to STDOUT (the standard output handle). The current locale changes the format very little without having to pass the variables into other functions first. These inline expressions also do not easily allow the developer to use different expressions while evaluating the same variables.

The ANSI C printf function solves this problem by separating the string expression from the variables to be formatted using a variable parameter list and indexed format specifiers.

char* var1 = "ANSI C";
float var2 = 7.1;
printf("Hello, from %s %g!", var1, var2);

The printf function is also subject to the same locale environment variables as Perl. This ANSI C example shows how a separate string can be used to format the variable list of arguments. This facilitates loading different format strings, including format strings in a different language. The format specifiers additionally gives you a little more control over how the variables are evaluated, such as printing integers in hexadecimal notation, though hese format specifications are pretty inflexible.

In both examples above, string formatting is rather inflexible using the standard runtime libraries. Other solutions exist that use third party libraries or user-defined functions to format arguments before they are evaluated, but it would be nice to have an extensible formatting solution provided by the runtime without having to support third-party code or calling user-defined functions before formatting your arguments.

In the .NET Framework, there are several ways to format inline string expressions, including String.Format[^], StringBuilder.AppendFormat[^], and TextWriter.Write[^] (and WriteLine[^]), which the Console[^] class inherits. These methods not only allow you to load different string expressions to format, but allow you to control the format of a variable list of arguments using a variety of format specifiers provided in the Framework Class Library (FCL) as well as custom format specifiers, which will be covered later. The following example uses the CultureInfo from Thread.CurrentCulture to format relevant variables.

string var1 = ".NET";
float var2 = 1.1;
string.Format("Hello, from {0} {1}!", var1, var2);

Also notice that the format specifiers are also indexed, meaning that you can evaluate parameters regardless of the order in the variable parameter list. You could even re-use the same variable repeatedly in an expression. This also looks much nicer and is more extensible - such that string format expressions could be loaded from a resource - than another common approach that was mentioned earlier in this article:

string var1 = ".NET";
float var2 = 1.1;
string s = "Hello, from " + var1 + " " + var2.ToString() + "!";

I'm sure you'll agree that the string format expression is much easier to read and change at runtime. The true power of formatting strings in .NET doesn't stop there, however. Additional format specifications can be used to control the output of numeric types in endless ways.

Format Specifications in .NET

When using methods like String.Format, indexed placeholders are evaluated and formatted using format specifications in the string format expression. This is known as Composite Formatting[^]. Each format specifier can take the form of {index[,alignment][:formatString]}. These format strings can be just about anything and can define additional options for a specific format. You can also pad and align the formatted output string to fit a certain number of spaces or tabs.

These format specifiers are passed by the StringBuilder internally to implementations of IFormattable, or ICustomFormatter when an IFormatProvider is passed as the first argument (for methods in the FCL). This will be covered shortly.

Format Providers in the Framework Class Library

Format providers - as the name implies - provide formatting capabilities to types. The two format providers in the FCL are the DateTimeFormatInfo[^] and NumberFormatInfo[^] classes. Both implement the IFormatProvider interface, which is an interface that must be supported for format providers when used with the string formatting methods described earlier. This defines a contract by which all format providers must abide so that a common implementation can be established. This interface will be covered in greater detail later.

When formatting numeric types and the DateTime structure, if no IFormatProvider implementation is provided, the two format provider classes above are used automatically to provide formatting for the related types. A simple example using a DateTime follows.

DateTime now = DateTime.Now;
string.Format("Short date:         {0:d}", now);
string.Format("Long date:          {0:D}", now);
string.Format("Sortable date/time: {0:s}", now);
string.Format("Custom date:        {0:ddd, MMM dd, yyyy}", now);

Using format specifiers, you could also load the string format expression from an embedded resource file using the ResourceManager[^]. While the current CultureInfo is used to format the actual dates, times, and numbers (or a specific culture's DateTimeFormatInfo or NumberFormatInfo is passed as the IFormatProvider parameter), you could use this approach to load localized strings - including strings that use a right-to-left reading order.

For instance, if you want to format a date using another culture's formatting information without changing the executing thread's CurrentCulture, you could simply get the DateTimeFormatInfo for that culture:

CultureInfo culture = new CultureInfo("de-DE");
DateTime dt = DateTime.Now;
string.Format(culture.DateTimeFormat, "Großdatum:          {0:D}", now);

The date would use the German culture information for dates and times to format the value, including localized day and month names and the order of the various elements of the string.

To further extend this as mentioned previously, you could get the string format expression from an embedded resource. For simplicity, the Thread.CurrentUICulture is assumed to be set appropriately.

ResourceManager resources = new ResourceManager("Strings.resources",
  GetType().Assembly);
string format = resources.GetString("CommonFormat");
DateTime dt = DateTime.Now;
string.Format(format, now);

The value keyed as "CommonFormat" would contain the string format expression, like "Long date: {0:D}". These are commonly stored in ResX files, which is beyond the scope of this article.

Extending Existing Types with Custom Format Providers

Numeric types and the DateTime structure already have associated format providers that provide a lot of different formats. The DateTimeFormatInfo even lets you use custom format specifiers. But what if you want to format any type and either can't extend the type or don't want to just for the same of formatting?

You can implement the ICustomFormatter interface and expose the implementation through a custom IFormatProvider, which you can pass as the first parameter to methods like String.Format. Two custom format providers are included with the sample project for this article: a custom NumberFormatInfo class which can convert numbers to any radix (from the .NET Framework SDK, included because its very handy) and to Roman numerals up to 3,999,999; and a StringFormatInfo class which can convert strings to Morse Code (includes support for current characters, including "@" which was just added early in 2004). In both samples, both the ICustomFormatter and IFormatProvider interfaces are implemented to make things easy, and there's absolutely no reason why you couldn't do the same in production code. The declaration of the StringFormatInfo class follows.

public sealed class StringFormatInfo : IFormatProvider, ICustomFormatter
{
}

As you can see, the class implements IFormatProvider so that we can simply pass it into call like String.Format:

StringFormatInfo fmtinfo = new StringFormatInfo();
string.Format(fmtinfo, @"The morse code for ""{0}"":\n{0:m}", "Hello, world");

IFormatProvider[^] declares a single method, GetFormat(Type)[^]. To implement this method, see if the argument is of type ICustomFormatter, not your class type. This has to do with the internal implementation of formatting methods.

public object GetFormat(Type formatType)
{
  if (typeof(ICustomFormatter).Equals(formatType)) return this;
  return null;
}

ICustomFormatter[^] also declares a single method, Format(String, Object, IFormatProvider)[^]. This is where the real work is done. You should first make sure that a object to be formatted is not null (Nothing in Visual Basic), unless you want to handle null values specially with your custom format provider. Then check the format string - the first parameter declared in the method. You can choose whether or not to compare the strings in a case-sensitive manner. The DateTimeFormatInfo provided in the FCL, for example, differentiates between "d" and "D", and several other characters. For the StringFormatInfo example, a case-insensitive comparison is performed.

public string Format(string format, object arg, IFormatProvider formatProvider)
{
  if (arg == null) throw new ArgumentNullException("arg");
 
  if (format != null && arg is string)
  {
    string s = format.Trim().ToLower();
    if (s.StartsWith("m"))
      return FormatMorseCode(arg as string, format);
  }
 
  if (arg is IFormattable)
    return ((IFormattable)arg).ToString(format, formatProvider);
  else return arg.ToString();
}

In this example, I simply trim the format and convert it to lower case. I could've just as easily used String.Compare, but if you supported multiple format specifiers you don't want the overhead of a case-insensitive string comparison in each condition. After that, I check if the format specifiers starts with an "m". If so, I pass certain arguments to my FormatMorseCode method, for which you can see the implementation in the sample project.

One important thing to remember is that if you don't handle the object type or the format specifier is not valid for your IFormatProvider implementation, you should handle the object appropriately by determining if it supports the IFormattable interface (which I'll cover shortly); if it does, call the IFormattable.ToString(String, IFormatProvider) method; otherwise, just call ToString which is declared by the Object class and is thus inherited by every class, some of which override the default implementation which simply prints the fully-qualified type of the object.

Format providers may also define properties that control the formatting in specific ways. The DateTimeFormatInfo, for example, defines many properties to get and set the Calendar to use, the localized names of days, and much more. The sample StringFormatInfo provides a LineWidth property which helps ensure that dots and dashes are kept together for for clarity. If you were to separate your IFormatProvider and ICustomFormatter implementations, you should typically define these properties on your IFormatProvider implementation since it is passed to the ICustomFormatter implementation, as well as the IFormattable implementation which I'll discuss shortly. In this case, make sure you are dealing with the right type and get the properties you need by casting to your type. When possible, exposing these properties as format specifier options may also be advantageous, though you probably shouldn't make it too complicated and simply expose what would be easy to represent and parse.

Custom format providers give you the ability to use a simple class or classes that provide custom format specifiers and options to pre-existing types. This is quite a bit of unnecessary work, though, when you define your own types.

Extending Your Types with Custom Formatting

When defining your own class and structure types, you can provide custom formatting easily enough through any means you want. If you want to support the standard formatting routines in .NET, however, you must implement IFormattable. You could also simply decide to override the ToString() method inherited from the Object class if you want to display a custom string for a type and don't need to support more than one type. Point is one example with only overrides ToString() and returns a string in the form of "{X=X, Y=Y}".

IFormattable[^] declares just one method again, ToString(String, IFormatProvider)[^]. There again is the IFormatProvider interface, which is another good reason to define format properties on your IFormatProvider implementation. You could always check for different types you support, then cast them and get the properties you need to help control the format of your output. In this simple example, I do just that:

char c = divisor;
// ...
if (formatProvider is NumberFormatInfo)
  if (((NumberFormatInfo)formatProvider).UseDiacritic)
    c = diacritic;

Implementing IFormattable.ToString is pretty easy, but you should also consider overloading ToString to provide a single parameter for each of type. These can simply call the implementation method, which uses the format specifier and optionally information from the IFormatProvider argument to format your type as a string.

public override string ToString()
{
  return ToString("g", null); // Always support "g" as default format.
}
 
public string ToString(string format)
{
  return ToString(format, null);
}
 
public string ToString(IFormatProvider formatProvider)
{
  return ToString(null, formatProvider);
}
 
public string ToString(string format, IFormatProvider formatProvider)
{
  if (format == null) format = "g"; // Set default format, which is always "g".
  // Continue formatting by checking format specifiers and options.
}

See the Rational struct example - a mostly full-featured fractional number structure - for more details of how IFormattable.ToString is implemented in the sample project. It isn't much different from examples above for custom format providers for existing types.

Summary

Formatting strings using the .NET Framework is a very flexible system when you implement the right interfaces. You can provide both custom formatters for existing types and implement custom formatting in your own types. The sample project for this article demonstrates both way of providing custom formatting and includes a simple test application to try out the different combinations.

Using custom format providers and formatters can help streamline your code so that you can load custom format expressions from different sources like databases or resource files, and provide custom formatting options for indexed arguments, even reusing the same arguments throughout the format expression. With a little extra code, you should have no problem providing just about any string format when your application requires it.

posted on 2005-04-01 04:05  涛仔28  阅读(1791)  评论(0编辑  收藏  举报