《C#高级编程》第6版190页。
AppendFormat()实际上会在调用Console.WriteLine()时调用,它负责确定所有像{0:D}的格式化表达式应使用什么表达式替代。
为了说明如何格式化字符串,看看执行下面的语句会得到什么结果:
Console.WriteLine("The double is {0,10:E} and the int contains {1}", d, i);
Console.WriteLine只是把参数的完整列表传送给静态方法String.Format(),如果要在字符串中以其他方式格式化这些值,例如显示在一个文本框中,也可以调用这个方法。带有3个参数的WriteLine()重载方法如下:
public void WriteLine(string format, object arg0, object arg1)
{
Console.WriteLine(String.Format(format, arg0, arg1));
}
上面的代码依次调用了带有1个参数的重载方法WriteLine(),仅显示了传递过来的字符串的内容,没有对它进行进一步的格式化。
String.Format()现在需要用对应对象的合适字符串表示来替换每个格式说明符,构造最终的字符串。但是,如前所述,对于这个建立字符串的过程,需要StringBuilder实例,而不是String实例。在这个示例中,StringBuilder实例是用字符串的第一部分(即文本“The double is”)创建和初始化的。然后调用StringBuilder.AppendFormat()方法,传递第一个格式说明符“{0,10:E}”和相应的对象double,把这个对象的字符串表示添加到构造好的字符串中,这个过程会继续重复调用StringBuilder.Append()和StringBuilder.AppendFormat()方法,直到得到了全部格式化好的字符串为止。
下面的内容比较有趣。StringBuilder.AppendFormat()需要指出如何格式化对象,它首先检查对象,确定它是否执行System命名空间中的接口IFormattable。只要试着把这个对象转换为接口,看看转换是否成功即可,或者使用C#关键字is,也能实现此测试。如果测试失败,AppendFormat()只会调用对象的ToString()方法,所有的对象都从System.Object继承了这个方法或重写了该方法。在前面给出的编写各种类和结构的示例中,执行过程都是这样,因为我们编写的类都没有执行这个接口。这就是前面的章节中,Object.ToString()的重写方法允许在Console.WriteLine()语句中显示类和结构如Vector的原因。
但是,所有预定义的基本数字类型都执行这个接口,对于这些类型,特别是这个示例中的double和int,就不会调用继承自System.Object的基本ToString()方法。为了理解这个过程,需要了解IFormattable接口。
IFormattable只定义了一个方法,该方法也叫作ToString(),它带有两个参数,这与System.Object版本的ToString()不同,它不带参数。下面是IFormattable的定义:
interface IFormattable
{
string ToString(string format, IFormatProvider formatProvider);
}
这个ToString()重载方法的第一个参数是一个字符串,它指定要求的格式。换言之,它是字符串的说明符部分,放在字符串的{}中,该参数最初传递给Console.WriteLine()或String.Format()。例如,在本例中,最初的语句如下:
Console.WriteLine("The double is {0,10:E} and the int contains {1}", d, i);
在计算第一个说明符{0,10:E}时,在double变量d上调用这个重载方法,传递给它的第一个参数是E。StringBuilder.AppendFormat()传递的总是显示在原始字符串的合适格式说明符内冒号后面的文本。
本书不讨论ToString()的第2个参数,它是执行接口IFormatProvider的对象引用。这个接口提供了ToString()在格式化对象时需要考虑的更多信息——一般包括文化背景信息(.NET文化背景类似于Windows时区,如果格式化货币或日期,就需要这些信息)。如果直接从源代码中调用这个ToString()重载方法,就需要提供这样一个对象。但StringBuilder.AppendFormat()为这个参数传递一个空值。如果formatProvider为空,ToString()就要使用系统设置中指定的文化背景信息。
现在回过头来看看本例。第一个要格式化的项是double,对此要求使用指数计数法,格式说明符为E。如前所述,StringBuilder.AppendFormat()方法会建立执行IFormattable接口的对象double,因此要调用带有两个参数的ToString()重载方法,其第一个参数是字符串“E”,第二个参数为空。现在double的这个方法在执行时,会考虑要求的格式和当前的文化背景,以合适的格式返回double的字符串表示。StringBuilder.AppendFormat()则按照需要在返回的字符串中添加前导空格,使之共有10个字符。
下一个要格式化的对象是int,它不需要任何特殊的格式(格式说明符是{1})。由于没有格式要求,StringBuilder.AppendFormat()会给该格式字符串传递一个空引用,并适当地响应带有两个参数的int.ToString()重载方法。由于没有特殊的格式要求,所以也可以调用不带参数的ToString()方法。
整个字符串格式化过程如图8-2所示。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Racer graham = new Racer("Graham", "Hill", "UK", 14);
Racer emerson = new Racer("Emerson", "Fittipaldi", "Brazil", 14);
Racer mario = new Racer("Mario", "Andretti", "USA", 12);
List<Racer> racers = new List<Racer>(20) {graham, emerson, mario};
racers.Add(new Racer("Michael", "Schumacher", "Germany", 91));
racers.Add(new Racer("Mika", "Hakkinen", "Finland", 20));
racers.AddRange(new Racer[] {
new Racer("Niki", "Lauda", "Austria", 25),
new Racer("Alian", "Prost", "France", 51)});
racers.Insert(3, new Racer("Phil", "Hill", "USA", 3));
racers.ForEach(r => Console.WriteLine("{0:A}", r));
}
}
class Racer : IComparable<Racer>, IFormattable
{
public Racer() : this(String.Empty, String.Empty, String.Empty) {}
public Racer(string firstname, string lastname, string country) : this(firstname, lastname, country, 0) {}
public Racer(string firstname, string lastname, string country, int wins)
{
this.Firstname = firstname;
this.Lastname = lastname;
this.Country = country;
this.Wins = wins;
}
public string Firstname { get; set; }
public string Lastname { get; set; }
public string Country { get; set; }
public int Wins { get; set; }
public override string ToString()
{
return String.Format("{0} {1}", Firstname, Lastname);
}
public string ToString(string format, IFormatProvider formatProvider)
{
if (format != null)
format = format.ToUpper();
switch (format)
{
case null:
case "N": // Name
return ToString();
case "F": // FirstName
return Firstname;
case "L": // LastName
return Lastname;
case "W": // Wine
return String.Format("{0}, Wins: {1}", ToString(), Wins);
case "C": // Country
return String.Format("{0}, Country: {1}", ToString(), Country);
case "A": // All
return String.Format("{0}, {1} Wins: {2}", ToString(), Country, Wins);
default:
throw new FormatException(String.Format(formatProvider, "Format {0} is not supported", format));
}
}
public string ToString(string format)
{
return ToString(format, null);
}
public int CompareTo(Racer other)
{
int compare = this.Lastname.CompareTo(other.Lastname);
if (compare == 0)
return this.Firstname.CompareTo(other.Firstname);
return compare;
}
}