Item 5: Always Provide ToString()
在.net开发环境中System.Object.ToString()是最常用到的方法之一。我们应当为每个类的客户程序提供恰当的格式化输出信息。对于用户程序来说,通过这个格式化的string可以在Windows Form、web或者控制台中很方便的显示其类型信息,同时还便于调试。我们每创建一个类都应当为它重写符合需求的ToString()方法。当创建复杂的格式化类型时,我们应当使用IFormattable.ToString()。我们面对的问题就是:如果没有重写,或者重写的不恰当,那么你就必须在这些类的客户程序中弥补这些错误。
System.Object中的ToString()版本返回当前对象的类型的名称。对于类的用户来说这个信息是没有什么用处的,如果我们不重写ToString()方法,我们只能得到这种结果,客户程序得不到它们需要的信息。我们只要在类中重写特定格式化方式的ToString()方法,就能免去在客户程序中提取所需信息的多次重复代码。在编写类时多付出一点点工作,就能减少以后在调用时所需的大量工作。
让我们来看一个需要重写ToString()的简单例子。有一个类和其中的三个私有成员如下例所示:
public class Customer
{
private string _name;
private decimal _revenue;
private string _contactPhone;
}
如果不做其他工作的话,上例中类的实例从System.Object继承ToString(),返回的信息为其类型名称,即“Customer”。我们不需要这种信息。如果用于调试目的的话,我们希望它能返回更加详细的信息。对于客户程序来说,我们希望它能返回客户程序最需要得到的信息。举例来说现在我们希望它能返回类中的_name的内容,通过重写ToString()我们可以实现:
public override string ToString()
{
return _name;
}
我们应当在每个自定义类型中提供这样的方法,这样做可以节省大量的时间。为类重写恰当的ToString()方法,可以让这些类的对象在使用时会变得更加容易,不论是在Windows Form、Web Form还是控制台应用程序中。我们可以非常简单的将对象的信息显示出来。简单的三行代码就解决了客户程序中的基本需求。
上例中通过重写ToString()可以满足显示指定信息的需求。但是有时候我们还会有其他的要求。例如有客户填写了name、revenue和contact phone的内容,但现在我们的ToString()只能返回name。我们可以通过在类中实现IFormattable接口来解决这个问题。这个接口包含了ToString()方法的重载,使得你可以使用特殊的格式代码来输出不同的格式化信息。当我们需要输出不同的格式的输出时就要用到这个接口。就上例而言,有的客户程序就需要返回name和revenue的列表信息,这就可以通过IFormattable接口实现。IFormattable. ToString()方法包含两个参数,一个是指定要使用的格式的String,一个是formatProvider,提供用于检索控制格式化的对象的机制。
string System.IFormattable.ToString(string format, IFormatProvider formatProvider)
我们可以使用它来创建不同的格式类型。在上例中,我们可以定义format参数等于n的含义为显示name,参数等于r为显示revenue,参数等于p为显示phone。我们创建下面的IFormattable. ToString()方法:
public class Customer : IFormattable
{
private string _name;
private decimal _revenue;
private string _contactPhone;
IFormattable 成员#region IFormattable 成员
public string ToString(string format, IFormatProvider formatProvider)
{
if(formatProvider != null)
{
ICustomFormatter fmt = formatProvider.GetFormat(this.GetType()) as ICustomFormatter;
if(fmt != null)
{
return fmt.Format(format,this,formatProvider);
}
}
switch(format)
{
case "r":
return _revenue.ToString();
case "p":
return _contactPhone;
case "nr":
return string.Format("{0,20},{1,10:C}",_name,_revenue);
case "rpn":
return string.Format("{0,10:C},{1,15},{2,20}",_revenue,_contactPhone,_name);
case "n":
case "G":
default:
return _name;
}
}
#endregion
}
添加这个函数后,客户程序就可以通过使用不同的参数来获得特定数据。
IFormattable c1 = new Customer();
Console.WriteLine("Customer record: {0}", c1.ToString("rpn", null));
当我们实现IFormattable. ToString()时有几点必须要注意。首先,我们必须支持ToString()的一般格式,其format参数为“G”(常规格式化),其次必须支持format为empty和null的情况。这三种情况下都应当返回同System.Object.ToString()一样的结果。凡是实现了Iformattable的类型,.net FCL (Framework Class Library) 在调用它们的时候都会使用IFormattable.ToString()来代替Object.ToString()。在调用IFormattable. ToString()时一般使用null作为format的值,但是有时也会使用“G”。如果添加的自定义IFormattable.ToString()不支持这些参数,那就会破坏FCL中的字符自动转变机制。
IFormattable.ToString()中的另一个参数是一个实现了IFormatProvider接口的对象。这个对象使得客户程序可以以一些我们设计时没有预期到的格式进行输出。仔细看一下前面的例子就会发现,虽然我们提供了很多种格式,但是总有一天客户程序会需要一个我们当时没有想到的格式。这也是程序开头时我们检查formatProvider是否为空并实现ICustomFormatter接口的原因。
现在我们将注意力转移到客户程序端。举例来说我们现在需要使用一种原本不支持的format格式,比如将name的最大长度由20变为50。我们需要实现IFormatProvider接口的一个类和实现了ICustomFormatter的另一个类。IFormatProvider接口中实现了一个方法:GetFormat()。这个方法返回了一个实现了ICustomFormatter接口的对象。ICustomFormatter接口中定义了特定的返回结果格式。下面的程序中就完成了将name的输出由20增至50的修改。
public class CustomFormatter : IFormatProvider
{
IFormatProvider 成员#region IFormatProvider 成员
public object GetFormat(Type formatType)
{
if(formatType == typeof(ICustomFormatter))
{
return new CustomerFormatProvider()
}
return null;
}
#endregion
}
public class CustomerFormatProvider : ICustomFormatter
{
ICustomFormatter 成员#region ICustomFormatter 成员
public string Format(string format, object arg, IFormatProvider formatProvider)
{
Customer c = arg as Customer;
if(c == null)
{
return arg.ToString();
}
return string.Format("{0,50},{1,15},{2,10:C}",c.Name,c.ContactPhone,c.Revenue);
}
#endregion
}
GetFormat()方法创建了一个实现了ICustomFormatter接口的对象。ICustomFormatter.Format()方法定义了用户需要的特定的格式化方案。我们也可以为ICustomFormatter.Format()定义特定的格式化参数。
为了进行自定义的格式化,我们需要调用string.Format()方法并提供IFormatProvider对象作为参数。
Console.WriteLine(string.Format(new CustomFormatter(),"",c1));
不论类是否实现了IFormattable接口,我们都能为其创建IFormatProvider和ICustomFormatter的实现。这就意味着即便原来的类中没有提供特定的ToString()方法,我们也能自己解决格式化问题。当然这样我们就只能格式化输出那些公有属性和成员。我们围绕IFormatProvider和ICustomFormatter做了大量的工作,其目的就是格式化输出。我们可以在.net Framework中的任何地方使用这些格式化方式。
重写Object.ToString()是最简单的提供特定格式化字符串的方法。我们应当在每个类型中都提供这些方法,这将使得它们变得更加清晰且容易使用。在一些特殊情况下我们需要多种格式化方式,那么我们就使用Iformattable接口,这样就能为客户程序提供标准的格式化方式。如果你没做这些,那么客户程序只能自己实现格式化方法了,而这需要付出大量的代码。而且作为客户程序来说,一些内部的参数就无法进行检测了。
为所有的类型重写合适的ToString()方法是非常简便和实用的方法,因为这些输出的信息对客户程序来说是十分重要的。
译自 Effective C#:50 Specific Ways to Improve Your C# Bill Wagner著
当频繁使用类内部信息的时候,重写ToString的确是非常恰当的方法,既能方便调试,又可以使客户程序的代码简洁,减少了大量的字符串操作代码。虽然开始的时候麻烦点,但是也算是一劳永逸了。