Effective C# 原则5:始终提供ToString()

JerryXia 发表于 , 阅读 (908)

在.Net世界里,用得最多的方法之一就是System.Object.ToStrying()了。你应该为你所有的客户写一个“通情达理”的类(译注:这里是指这个类应该对用户友好)。要么,你就迫使所用类的用户,去使用类的属性并添加一些合理的易读的说明。这个以字符串形式存在,关于你设计的类的说明,可以很容易的向你的用户显示一些关于对象的信息到:Windows
Form里,Web
Form里,控制台输出。这些字符说明可以用于调试。你写的任何一种类型,都应该合理的重写这个方法。当你设计更多的复杂的类型时,你应该实现应变能力更强的IFormattable.ToString().
承认这个:如果你不重写(override)这个常规的方法,或者只是写一个很糟糕的,你的客户将不得不为你修正它。

System.Object版的ToString()方法只返回类型的名字。这并没有太多有用的信息:“Rect”,“Point”,“Size”并不会如你所想的那样显示给你的用户。但那只是在你没有为你的类重写ToString()方法时得到的。你只用为你的类写一次,但你的客户却会使用很多次。当你设计一个类时,多添加一点小小的工作,就可以在你或者是其他人每次使用时得到回报。
(译注:废话!)

======================

这一原则就不翻译了,看的有点郁闷。就是ToString()的几个重写版本。以及一些格式化输出。我觉得本书不应该讨论这些入门级的内容,所以只是读了一遍,就没有全部翻译。

大家知道要重写它就行了,最好是提供几个重载版本。回头有时间再翻译这一原则的其它内容。

给一点个人建议,一般不会在一个类的ToString上提供很多的说明,给一个名字就已经足够了,然后加一个SDK帮助。更多时候,在后面添加成员类的说明。我就在一个第三方库的ToString上看到很严谨的结构,都是在类名后面,添加一些内容和重要属性的说明。

=========================================补译:

让我们来考虑一个简单的需求:重写System.Object.ToString()方法。你所设计的每一个类型都应该重写ToString()方法,用来为你的类型提供一些最常用的文字说明。考虑这个Customer类以及它的三个成员(fields)(译注:一般情况,类里的fields译为成员,这是面向对象设计时的概念,而在与数据库相关的地方,则是指字段):

public class Customer
{
    private string  _name;
    private decimal  _revenue;
    private string  _contactPhone;
}

默认继承自System.Object的ToString()方法会返回"Customer"。这对每个人都不会有太大的帮助。就算ToString()只是为了在调试时使用,也应该更灵活(sophisticated)一些。你重写的ToString()方法应该返回文字说明,更像是你的用户在使用这个类一样。在Customer例子中,这应该是名字:

public override string ToString()
{
    return  _name;
}

如果你不遵守这一原则里的其它意见,就按照上面的方法为你所定义的所有类型重写该方法。它会直接为每个人省下时间。

当你负责任的为Object.ToString()方法实现了重写时,这个类的对象可以更容易的被添加到Windows
Form里,Web Form里,或者打印输出。
.NET的FCL使用重载的Object.ToString()在控件中显示对象:组合框,列表框,文本框,以及其它一些控件。如果你一个Windows
Form或者Web
Form里添加一个Customer对象的链表,你将会得到它们的名字(以文本)显示出来(译注:而不是每个对象都是同样的类型名)。

Syste.Console.WriteLine()和System.String.Formate()在内部(实现的方法)是一样的。任何时候,.Net的FCL想取得一个customer的字符串说明时,你的customer类型会提供一个客户的名字。一个只有三行的简单函数,完成了所有的基本需求。

这是一个简单的方法,ToString()还可以以文字(输出的方法)满足很多用户自定义类型的需求。但有些时候,你的要求可能会更多。前面的customer类型有三个成员:名字,收入和联系电话。对System.Object.ToString()(译注:原文这里有误,掉了Object)的重写只使用了_name。你可以通过实现IFormattable(这个接口)来弥补这个不足。这是一个当你需要对外输出格式化文本时使用的接口。IFormattable包含一个重载版的ToString()方法,使用这个方法,你可以为你的类型信息指定详细的格式。这也是一个当你要产生并输出多种格式的字符串时要使用的接口。customer类就是这种情况,用户将希望产生一个报表,这个报表包含了已经表格化了的用户名和去年的收入。IFormattable.ToString()方法正合你意,它可以让用户格式化输出你的类型信息。这个方法原型的参数上一包含一个格式化字符串和一个格式化引擎:string
System.IFormattable.ToString( string format, IFormatProvider
formatProvider )

你可以为你设计的类型指定要使用的格式字符串。你也可以为你的格式字符串指定关键字符。在这个customer的例子中,你可以完全可以用n来表示名字,r表示收入以及p来表示电话。这样一来,你的用户就可以随意的组合指定信息,而你则须要为你的类型提供下面这个版本的的IFormattable.ToString():

#region IFormattable Members
// supported formats:
// substitute n for name.
// substitute r for revenue
// substitute p for contact phone.
// Combos are supported:  nr, np, npr, etc
// "G" is general.
string System.IFormattable.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 "np":
      return string.Format( "{0,20}, {1,15}",
        _name, _contactPhone );
    case "pr":
      return string.Format( "{0,15}, {1,10:C}",
        _contactPhone, _revenue );
    case "pn":
      return string.Format( "{0,15}, {1,20}",
        _contactPhone, _name );
    case "rn":
      return string.Format( "{0,10:C}, {1,20}",
        _revenue, _name );
    case "rp":
      return string.Format( "{0,10:C}, {1,20}",
        _revenue, _contactPhone );
    case "nrp":
      return string.Format( "{0,20}, {1,10:C}, {2,15}",
        _name, _revenue, _contactPhone );
    case "npr":
      return string.Format( "{0,20}, {1,15}, {2,10:C}",
        _name, _contactPhone, _revenue );
    case "pnr":
      return string.Format( "{0,15}, {1,20}, {2,10:C}",
        _contactPhone, _name, _revenue );
    case "prn":
      return string.Format( "{0,15}, {1,10:C}, {2,15}",
        _contactPhone, _revenue, _name );
    case "rpn":
      return string.Format( "{0,10:C}, {1,15}, {2,20}",
        _revenue, _contactPhone, _name );
    case "rnp":
      return string.Format( "{0,10:C}, {1,20}, {2,15}",
        _revenue, _name, _contactPhone );
    case "n":
    case "G":
    default:
      return _name;
  }
}
#endregion

(译注:上面的做法显然不合理,要是我的对象有10个成员,这样的组合是会让人疯掉的。推荐使用正则表达式来完成这样的工作,正则表达式在处理文字时的表现还是很出色的。)

添加了这样的函数后,你就让用户具有了可以这样指定customer数据的能力:IFormattable
c1 = new Customer(); Console.WriteLine( "Customer record: {0}",
c1.ToString( "nrp", null ) );

任何对IFormattable.ToString()的实现都要指明类型,但不管你在什么时候实现IFormattation接口,你都要注意处理大小写。首先,你必须支持能用格式化字符:“G”。其次,你必须支持两个空格式化字符:""和null。当你重载Object.ToString()这个方法时,这三个格式化字符应该返回同样的字符串。.Net的FCL经常用null来调用IFormattable.ToString()方法,来取代对Object.ToString()的调用,但在少数地方使用格式符"G"来格式化字符串,从而区别通用的格式。如果你添加了对IFormattable接口的支持,并不再支持标准的格式化,你将会破坏FCL里的字符串的自动(隐式)转换。

IFormattable.ToString()的第二个参数是一个实现了IFormatProvider接口的对象。这个对象为用户提供了一些你没有预先设置的格式化选项(译注:简单一点,就是你可以只实现你自己的格式化选项,其它的默认由它来完成)。如果你查看一下前面IFormattable.ToString()的实现,你就会毫不犹豫的拿出不计其数的,任何你喜欢的格式化选项,而这些都是的格式化中所没有的。支持人们容易阅读的输出是很自然的事,但不管你支持多少种格式,你的用户总有一天会想要你预先没想到的格式。这就为什么这个方法的前几行要检察实现了IFormatProvider的对象,并把ICustomFormatter的工作委托给它了。

让我们把(讨论的)焦点从类的作者转移到类的使用者上来。你发现你想要的格式化不被支持。例如,你有一个一组客户,他们的名字有的大于20个字符,并且你想修改格式化选项,让它支持50个字符长的客户名。这就是为什么IFormatProvider接口要存在。你可以设计一个实现了IFormatProvider的类,并且让它同时实现ICustomFormatter接口用于格式化输出。IFormatProvider接口定义了一个方法:GetFormat()。这个方法返回一个实现了ICustomFormatter接口的对象。由ICustomFormatter接口的指定方法来完成实际的格式化工作。下面这一对(接口)实现了对输出的修改,让它可以支持50个字符长的用户名:

// Example IFormatProvider:
public class CustomFormatter : IFormatProvider
{
  #region IFormatProvider Members
  // IFormatProvider contains one method.
  // This method returns an object that
  // formats using the requested interface.
  // Typically, only the ICustomFormatter
  // is implemented
  public object GetFormat( Type formatType )
  {
    if ( formatType == typeof( ICustomFormatter ))
      return new CustomerFormatProvider( );
    return null;
  }
  #endregion

  // Nested class to provide the
  // custom formatting for the Customer class.
  private class CustomerFormatProvider : ICustomFormatter
  {
    #region ICustomFormatter Members
    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()定义格式化字符串,因此你可以按常规指定多重格式。FormatProvider就是一个由GetFormat()方法取得的IFormatProvider对象。

为了满足用户的格式化要求,你必须用IFormatProvider对象明确的调用string.Format()方法:
Console.WriteLine(string.Format(new CustomFormatter(),  "", c1 ));

你可以设计一个类,让它实现IFormatProvider和ICustomFormatter接口,再实现或者不实现IFormattable
接口。因此,即使这个类的作者没有提供合理的ToStrying行为,你可以自己来完成。当然,从类的外面来实现,你只能访问公共属性成数据来取得字符串。实现两个接口,IFormatProvider
和 IcustomFormatter,
只做一些文字输出,并不需要很多工作。但在.Net框架里,你所实现的指定的文字输出在哪里都可以得到很好的支持。

所以,再回到类的作者上来。重写Object.ToString(),为你的类提供一些说明是件很简单的事。你每次都应该为你的类型提供这样的支持。而且这应该是对你的类型最显而易见的,最常用的说明。在一些极端情况下,你的格式化不能支持一些过于灵活的输出时,你应该借用IFormattable接口的优势。它为你的类型进行自定义格式化输出提供了标准方法。如果你放弃这些,你的用户将失去用于实现自定义格式化的工具。这些解决办法须要写更多的代码,并且因为你的用户是在类的外面的,所以他们无法检查类的里面的状态。

最后,大家注意到你的类型的信息,他们会明白输出的文字。尽可能以简单的方式的提供这样的信息吧:为你的所有类型重写ToString()方法。

添加新评论