现在的位置: 首页 > 综合 > 正文

Effective C# 第二版 中文 之05

2015年07月12日 ⁄ 综合 ⁄ 共 7828字 ⁄ 字号 评论关闭

Item5:为类型提供ToString()方法

说在前面:之前一直将Item翻译成原则,其实越看到后面,就越觉得这个翻译不妥。有些与其说是原则,不如说是建议。更有些只是单纯地介绍一些东西。因为一时间是在想不出什么更好地翻译,而且大家都看得懂Item的意思,索性,我就直接用Item吧。

 

System.Object.ToString()是.NET环境中最常用的方法之一。你应该为类型的所有使用者提供一个合理的ToString版本,否则使用者就不得不用你的类的一些属性来自行构造出一个人类可读的描述。这个类的合理的文本描述可以很容易地向用户显示出类的信息:在WPF控件、Silverlight控件、WebForm或控制台输出中显示。这个文本表示也可以在调试的时候使用。你所创建的任何类型都应该提供一个合理的ToStirng()方法的重写。当你创建更加复杂的类型,你就应该实现更加完备的Iformattable.ToString()方法。如果你没有重写这个方法,或者写得不够好,你的用户就不得不来替你收拾烂摊子。

         System.Object提供的默认的ToString()方法返回类型的完整名称。这些信息通常都没什么用,比如:"System.Drawing.Rect","MyNamespace.Point","SomeSample.Size"。这些都不是你想要展示给用户的。但是,如果你不重写ToString()方法的话,这就是你调用此方法所得到的信息。你创建类型一次,但你的用户却要使用很多次。你用一点时间(重写此方法)换来的是节省了你自己或其它用户的许多倍的时间。

         让我们来考虑一下最简单的需求:重写System.Object.ToString()。你所创建的每个类都应当重写ToString()方法,以便提供此类的更多常见的文本描述。考虑如下一个类,它含有三个public属性:

public class  Customer

{

         public  string Name

{

         get;set;

}

public  string Revenue

{

         get;set;

}

 

public  string ContactPhone

{

         get;set;

}

public  override string  ToString()

{

         return Name;

}

 

}

         继承自Object类的ToString()方法将直接返回一个“Customer”。这对于任何人来说都没什么用。即使这个ToString()只是用来调试用,它也应该返回一些更有意义的信息。在重写时,应该尽量返回使用者希望看到的信息。在customer这个例子中,返回其Name属性是个不错的选择。

         即使你不想遵循本Item中的其它建议,也请遵循上面提到的这一条。以为它可以立竿见影的节省所有人的时间。当你提供了一个合理的Object.ToString()的重写版本后,该类的对象即可更加容易地添加到WPF控件、Sliverlight控件、WebForms控件或者被打印输出。.NET  BCL将在对象显示到如组合框、列表框、文本框或其它空间上时使用Object.ToString()的重写版本。如果我们在Windows  Form或者Web  Form 上创建了一个Customer对象的列表,其文本显示将为Customer的名称,因为System.Console.WirteLine()方法、System.String.Format()方法在内部都调用了ToString()方法。只用.NET
BCL需要获取Customer的字符串表示,你的Customer类型都会给出其名称。不过提供一个具有三行代码的方法,即可照顾到所有的这些基本需求。

         在C#3.0中,编译器会为所有匿名类型创建一个默认的ToString()方法。该默认的oString()方法将显示对象中的每个属性值。其中,表示序列的属性是LINQ查询结果,将显示出序列中类型的信息,而不是其中的每个值。参见如下代码段:

int[] list = newint[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var test = new {Name = "Me",

Numbers = from lin list select l };

Console.WriteLine(test);

将显示:

{ Name = Me,Numbers =

System.Linq.Enumerable+WhereSelectArrayIterator`2

[System.Int32,System.Int32]}

         除非你为类型给出自定义的ToString()重写,否则就连编译器生成的匿名类型都会显示出与用户自定义类型相比的更友好的输出。显然,你应该为该类型的使用者提供足够的支持。

至少不能输给编译器为某个方法作用范围内的某个临时类型提供的表示。

         虽然简单的ToString()方法很多时候已经可以满足为用户定义的类型显示文本信息的需求,但有时还会需要功能更强大的方法。上述Customer类型有三个字段:名称、收入和联系电话,前面的System.ToString重写仅使用了名称这个字段。我们可以通过实现Iformattable接口来解决这个不足。Iformattable接口包含了一个重载的ToString()方法, 它允许我们为类型提供特定格式的信息。但你需要为类型输出不同形式的字符串时,这个接口即可大显身手。Customer类就是一个例子,比如说你可能想要创建一个报表,在其中以表格的形式列出客户的名称和上一年的收入。Iformattable.
ToString()方法允许用户为该类型指定特定格式的字符串输出,其签名包含了一个格式化字符串和一个格式提供器,如下所示:

stringSystem.IFormattable.ToString(string format,

IFormatProviderformatProvider)

         这里可以使用格式字符串来为类型指定自己的格式。例如使用特定的字符表示某种格式信息。在Customer类的例子中,我们可以用n来表示名称,用r来表示收入,用p来表示联系电话。不仅如此,还可以指定这些字符的组合形式,例如如下版本的Iformattable. ToString():

// 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(stringformat,

IFormatProvider formatProvider)

{

    if(formatProvider != null)

    {

        ICustomFormatter fmt =formatProvider.GetFormat(

        this.GetType())

        asICustomFormatter;

        if(fmt != null)

        returnfmt.Format(format, this, formatProvider);

    }

    switch(format)

    {

        Item 5: Always ProvideToString() ❘ 31

        From the Library of Wow!eBook

        case"r":

            returnRevenue.ToString();

        case"p":

            returnContactPhone;

        case"nr":

            returnstring.Format("{0,20},{1,10:C}",

            Name, Revenue);

        case"np":

            returnstring.Format("{0,20},{1,15}",

            Name, ContactPhone);

        case"pr":

            returnstring.Format("{0,15},{1,10:C}",

            ContactPhone, Revenue);

        case"pn":

            returnstring.Format("{0,15},{1,20}",

            ContactPhone, Name);

        case"rn":

            returnstring.Format("{0,10:C},{1,20}",

            Revenue, Name);

        case"rp":

            returnstring.Format("{0,10:C},{1,20}",

            Revenue, ContactPhone);

        case"nrp":

            returnstring.Format("{0,20},{1,10:C}, {2,15}",

            Name, Revenue,ContactPhone);

        case"npr":

            returnstring.Format("{0,20},{1,15}, {2,10:C}",

            Name, ContactPhone,Revenue);

        case"pnr":

            returnstring.Format("{0,15},{1,20}, {2,10:C}",

            ContactPhone, Name,Revenue);

        case"prn":

            returnstring.Format("{0,15},{1,10:C}, {2,15}",

            ContactPhone, Revenue, Name);

        case"rpn":

        returnstring.Format("{0,10:C},{1,15}, {2,20}",

            Revenue, ContactPhone,Name);

        case"rnp":

            returnstring.Format("{0,10:C},{1,20}, {2,15}",

            Revenue, Name,ContactPhone);

            32 ❘ Chapter 1 C# Language Idioms

            From the Library of Wow!eBook

        case"n":

        case"G":

        default:

            returnName;

    }

}

         这样customer的使用者就可以自行定义其想要的输出格式:

         IFormattable c1 = new Customer();

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

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

         一般来说,Iformattable. ToString()的实现会根据具体类型的不同而有所差别,但有些格式化工作无论是哪个类型都需要处理。首先,我们必须支持表示通用格式的“G“。其次,我们必须支持两种形式的空格式,即””和null。这三种格式返回的字符串都必须与Object. ToString()的的重写版本返回的字符串相同。对于每一个实现了IFormattable接口的类型,.NETBCL都会调用Iformattable. ToString()而非Object. ToString(),只是在一小部分场合会使用”G”来表示通用格式。如果你的类型支持IFormattable接口,但却不支持这些标准格式,那就破坏了BCL中的字符串自动转换规则。支持IFormattable接口是个庞大的工作,你会发现新的需求会很快接踵而来。你永远都无法预料到所有的可能需要的各种格式。因此,应该给出一些最有可能会需要的格式。其它的需求就留给使用者自己去实现吧。

         Iformattable.ToString()方法的第二个参数是一个实现IformatProvider接口的对象。该对象允许客户程序提供一些我们无法事先预料的格式化选项。在前面Iformattable.ToString()方法的实现中,总会有一些用户期望但实际上却没有提供的格式化选项。这也是提供便于人类阅读的输出过程文本中普遍存在的一个问题。不管你支持多少种格式化选项,总有一些需求是你没有预料到的。这就是上面代码中开始的几行所作的工作,即找到实现IformatProvider接口的对象,然后将格式化任务交给其中的IcustomFormatter来完成。

         接下来,我们把视角从类的创建者转移到类的使用者身上。假设期望的某种格式没有得到支持,例如某些Customer的名称字符数要大于20,这时候我们希望提供字符宽度为50的Customer名称输出。这正是IformatProvider接口大显身手之处。这里我们需要创建两个类,一个实现IformatProvider接口,另一个实现IcustomFormatter接口,用于创建自定义的输出格式。IformatProvider接口中定义有一个方法GetFormat(),将返回一个实现IcustomFormatter接口的对象。IcustomFormatter接口中包含了实际执行格式化操作的方法。下面的代码实现了提供字符宽度为50的name输出:

// 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

    // isimplemented

    publicobject GetFormat(TypeformatType)

    {

        if(formatType == typeof(ICustomFormatter))

            return new CustomerFormatProvider();

        returnnull;

    }

    #endregion

    //Nested class to provide the

    //custom formatting for the Customer class.

    privateclass CustomerFormatProvider: ICustomFormatter

    {

        #region ICustomFormatter Members

            publicstring Format(stringformat, object arg,IFormatProviderformatProvider)

            {

                Customer c = arg as Customer;

                if (c == null)

                    return arg.ToString();

                returnstring.Format("{0,50},{1,15}, {2,10:C}",c.Name, c.ContactPhone, c.Revenue);

            }

        #endregion

    }

}

这个GetFormat()方法创建了一个实现IcustomFormatter接口的对象。IcustomFormatter.Format()方法则按照所需要的方式执行实际的格式化输出工作,将对象转换为字符串格式。我们可以为IcustomFormatter.Format()方法定义格式字符串,从而同时在一个例程中指定多种格式化选项。参数FormatProvider则是来自GetFormat()方法的一个IformatProvider对象。

若想指定自己的定制格式,需要显示调用string.Format()方法,并传入一个IformatProvider对象:

Console.Writeline(string.Format(nerCustomFormatter(),””,c1));

        

         无论一个类是否实现了Iformattable接口,我们都可以为其创建IformatProvider和IcustomFormatter的实现。因此,即使一个类的原作者没有提供合适的ToString(),你仍然可以自行构建并为其提供格式化支持。当然,作为类的外部访问者,我们只能通过访问其中的公有属性和数据成员来构造字符串。虽然编写实现IformatProvider和IcustomFormatter这两个类需要不少工作,且其目的仅仅是为了得到文本输出,但是用这种方式来实现自己定义的字符串输出就能保证其会在.NET
Framework的各个地方得到支持。

         现在,让我们再次回到类的创建者角色上来。重写Object.ToString()是为类提供字符串表示的最简单方式。每创建一个类型时,你都要提供该字符串表示。因为该字符串表示应该是我们类型的最明显、最常用的一种表示。其输出也不应太过冗长,一般用在控件、HTML页面或其它将显示给用户查看的地方。只有在一些较少的情况下,即希望为类型提供更复杂的输出格式时,才有必要实现Iformattable接口。它以一种标准的方式允许类的用户来定制类的输出字符串。如果我们没有做这些工作,用户就要自行编写自定义的格式化器。这会需要更多的代码,因为用户处于类的外部,所以无法访问到类的内部状态。不过也不要为此太过担心,毕竟发布者无法预料到所有格式。

         人们总要通过某种方式获取到类型的信息,而字符串的表示则是最通俗易懂的。因此,我们应该重写所有类型ToString()方法,让其简单明了地输出对象的摘要信息。

 

小结:原翻译整本书计划看来是要搁浅了。事情实在是太多了。看来自能翻译到这里了。哎,对不起自己啊。

抱歉!评论已关闭.