导航

条款5:总是提供ToString()方法

Posted on 2007-03-21 01:02  taoeternal  阅读(592)  评论(0编辑  收藏  举报

条款5:总是提供ToString()方法

System.Object.ToString()恐怕是.NET中最常用的方法了。应该为我们的类的所有客户代码提供一个合理的版本,否则这些代码就只能使用我们的类的一些属性来自己定制可读的表示了。类型的字符串表示非常有用,可以在很多地方向用户显示对象的有关信息,例如在Windows Forms上、Web Forms上、控制台输出窗口中,以及调试环境中。为此,我们创建的每一个类型都应该重写Object类的ToString()方法。如果创建的是更复杂的类型,则应该实现Iformattable.ToString()方法。如果我们没有重写该方法,或者写得不够好,那么使用它们的客户代码就要自己想办法修补了。

System.Object默认提供的ToString()方法会返回类型的名称。这样的信息一般没有什么用处,像"Rect"、"Point"、"Size"这样的字符串大多都不是我们希望显示给用户的。但如果我们不重写Object的ToString()方法,用户看到的就将是这些。我们只需要写一次,但是客户将享用无数次。一点点付出,就可以让很多人(包括我们自己)受益。

让我们来看看重写System.Object.ToString()这个最简单的需求。该方法主要的功能就是为类型提供一个最常用的文本表示。例如,考虑下面这个具有三个字段的Customer类:

public class Customer

{

  private string   _name;

  private decimal  _revenue;

  private string   _contactPhone;

}

如果不提供重写的版本,Customer将继承Object类的ToString()方法,也就是返回一个"Customer"字符串。这个字符串实在没有什么用处。即使ToString()方法只应用于调试的目的,它也应该输出一个更有意义的字符串。我们重写的时候应该尽量考虑客户所希望的表示。就Customer类来说,返回_name是一个不错的选择:

public override string ToString()

{

  return _name;

}

即使大家不遵循本条款中的其他建议,也要遵循这里所展示的实践。它可以节省很多人的时间。在我们为Customer类重写了ToString()方法之后,该类的对象将可以更容易地添加到Windows Forms控件、Web Forms控件或者控制台上。.NET FCL在将对象显示到各个控件(如Combo Box、List Box、Text Box等)上时,使用的就是Object.ToString()的重写版本。如果我们在Windows Forms或者Web Forms上创建了一个Customer对象的列表,其文本显示将为Customer的名称(_name)。System.Console.WriteLine()方法、System.String.Format()方法等内部也都调用到了ToString()方法。只要当.NET FCL需要获取Customer的字符串表示时,我们的Customer类型都将以其名称(_name)来响应。仅仅提供一个具有三行代码的方法,就可以处理所有这些基本的需求。

虽然简单的ToString()方法很多时候已经可以满足我们的需求,但有时候,我们还需要功能更强的方法。上述Customer类型有三个字段:_name、_revenue和_contactPhone,而我们仅使用了_name一个字段。我们可以通过实现IFormattable接口来解决这个问题。IFormattable接口包含了一个重载的ToString()方法,它允许我们为类型指定某种格式信息。当我们需要为类型创建不同形式的字符串输出时,这个接口非常有用。Customer类型就是一个例子。比如,有些用户可能希望创建一个报表,在其中以表格的形式包含客户的名称和上一年的收入。IFormattable.ToString()方法允许用户为我们的类型指定某种格式的字符串输出。其签名如下:

string System.IFormattable.ToString( string format,

  IFormatProvider formatProvider )

我们可以使用格式字符串来为我们的类型指定自己的格式。比如,使用特定的字符来表示某种格式信息。在Customer类型的例子中,我们可以使用n来表示name,使用r来表示revenue,使用p来表示phone。另外,还可以指定这些字符的组合形式。下面的代码展示了一种可能的做法:

#region IFormattable Members

// 所支持的格式:

// 用n 表示name。

// 用r 表示revenue。

// 用p 表示contact phone。

// 同时支持组合格式: nr、np、npr等。

// "G" 表示通用格式。

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

添加该函数使得Customer类型的客户可以定制Customer类型的表示:

IFormattable c1 = new Customer();

Console.WriteLine( "Customer record: {0}",

  c1.ToString( "nrp", null ) );

IFormattable.ToString()的实现一般来说是依类型而异的,但有些工作是每一个类型中我们都需要处理的。首先,我们必须支持表示“通用格式”的"G"。其次,我们必须支持两种形式的“空格式”,即""和null。这三种格式返回的字符串必须与Object.ToString()的重写版本返回的字符串相同。.NET FCL对每一个实现了IFormattable接口的类型,会调用IFormattable.ToString(),而非Object.ToString()。.NET FCL通常会用一个null的格式字符串来调用IFormattable.ToString(),只是在一小部分场合会使用"G"来表示通用格式。如果我们的类型支持IFormattable接口,但又不支持这些标准格式,那么我们就打破了FCL中的自动字符串转换规则。

IFormattable.ToString()方法的第二个参数为一个实现了IFormatProvider接口的对象。该对象允许客户程序提供一些我们不能预料的格式化选项。如果看前面IFormattable.ToString()的实现,总会有一些我们期望、但实际上却没有提供的格式化选项。如果我们希望提供的输出容易为人所读懂,这种情况便不可避免。不管我们支持多少种格式化选项,用户总有一天会期望某种我们无法预料的格式。这就是上面的代码示例中最开始的几行所做的工作:寻找实现了IFormatProvider接口的对象,然后将格式化任务交给其中的ICustomFormatter来完成。

下面,将我们的视角从类的作者转到类的使用者上来。假设我们期望的某种格式没有获得支持,例如某些customer的name字符数要大于20,这时候我们希望提供字符数为50的name。这就是IFormatProvider接口的用武之地了。我们需要创建两个类:一个实现IFormatProvider接口,另一个实现ICustomFormatter接口——该类用于创建自定义的输出格式。IFormatProvider接口中定义有一个方法:GetFormat(),该方法会返回一个实现了ICustomFormatter接口的对象。ICustomFormatter接口中包含了实际执行格式化的方法。下面的代码实现了提供字符数为50的name输出:

// IFormatProvider示例:

public class CustomFormatter : IFormatProvider

{

  #region IFormatProvider Members

  // IFormatProvider 仅包含一个方法。

  // 该方法返回一个使用指定接口格式的对象。

  // 一般情况下,只有ICustomFormatter被实现。

  public object GetFormat( Type formatType )

  {

    if ( formatType == typeof( ICustomFormatter ))

      return new CustomerFormatProvider( );

    return null;

  }

  #endregion

  // 一个嵌套类,为Customer类提供定制格式。

  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()方法定义format参数,以便指定多种格式化选项。参数formatProvider则是用于调用GetFormat()方法的一个IFormatProvider对象。

要指定我们自己定制的格式,需要显式调用string.Format()方法,并传递一个IFormatProvider对象:

Console.WriteLine( string.Format( new CustomFormatter(),

  "", c1 ));

不管一个类是否实现了IFormattable接口,我们都可以为其创建IformatProvider和ICustomFormatter的实现类。因此即使一个类的原作者没有提供合理的ToString()行为,我们仍然可以为其提供格式化支持。当然,作为一个类的外部访问者,我们只能通过访问其中的公有属性和数据成员来构造字符串。虽然编写两个类(分别实现IFormatProvider和ICustomFormatter)需要很多工作,且其目的仅仅是为了得到一个字符串。但是,一旦使用了这种方式来实现我们自己定义的字符串输出,它们将在.NET框架的各个地方得到支持。

现在,再让我们回到类作者这一角色上来。重写Object.ToString()是为类提供字符串表示的最简单方式。每当我们创建一个类型时,都要提供该方法。它应该是我们的类型最明显、最常用的一种表示。只有在一些比较少的情况下,当我们期望为类型提供更复杂的输出格式时,才应该实现IFormattable接口。它为“类型的用户定制类型的字符串输出”提供了一种标准的方式。如果我们没有做这些工作,用户就要自己来实现自定义格式化器。那样的做法需要更多的代码,因为用户处于类外,无法访问对象的内部状态。

人们总有获取类型信息的需求,而字符串对于人来说是最容易理解的。我们应该积极地去做这件事,而重写所有类型中的ToString()方法可能是所有做法中用最简单的。