类型自定义格式字符串
引言
String可能是使用最多的类型,ToString()则应该是大家使用得最多的方法了。然而它不应该仅仅是用来输出类型的名称,如果使用得当,它可以方便地输出我们对类型自定义的格式。本文将循序渐进地讨论ToString(),以及相关的IFormattable、IFormatProvider以及ICustomFormatter接口。
在类型内部提供自定义格式字符串的能力
继承自System.Object 基类的 ToString()
String是人们直接就可以看懂的数据类型之一,很多情况下我们都会期望能够获得类型的一个字符串输出。因此,Microsoft 在.Net Framework所有类型的基类System.Object中提供了一个虚拟的 ToString()方法,它的默认实现是返回对象的类型名称。
假设我们有这样的一个类型,它定义了“朋友”这一对象的一些信息:
namespace CustomToString public class Friend { private string familyName; // 姓 private string firstName; // 名 public Friend(string familyName, string firstName){ this.familyName = familyName; this.firstName = firstName; } public Friend(): this("张","子阳"){} public string FamilyName { get { return familyName; } } public string FirstName { get { return firstName; } } } }
当我们在Friend的实例上调用 ToString()方法时,便会返回类型的名称:CustomToString.Friend。
Friend f = new Friend(); Console.WriteLine(f.ToString()); // 输出:CustomToString.Friend
覆盖 ToString() 方法
在上面的例子中,不管类型实例(对象)所包含的数据(字段值)是什么,它总是会返回相同的结果(CustomToString.Friend)。很多时候,返回一个对象的类型名称对我们来说没有多大的意义,拿上面来说,我们可能更加期望能够返回朋友的姓名(famliyName和firstName字段的值)。这时候,我们可以简单地覆盖System.Object基类的ToString()方法,在 Friend 类中添加如下方法:
// 覆盖System.Object基类的 ToString() 方法 public override string ToString() { return String.Format("Friend: {0}{1}", familyName, firstName); }
此时,我们再次运行代码:
Friend f = new Friend(); Console.WriteLine(f.ToString()); // 输出:Friend: 张子阳 f = new Friend("王","涛"); Console.WriteLine(f.ToString()); // 输出:Friend: 王涛
可以看到对于不同的对象,ToString()根据对象的字段值返回了不同的结果,这样对我们来说会更加有意义。
重载 ToString() 方法
有时候,我们可能需要将对象按照不同的方式进行格式化。就拿Friend类型来说:西方人是名在前,姓在后;而中国人是 姓在前,名在后。所以如果运行下面的代码,虽然程序不会出错,但从英语语法角度来看却有问题:
Friend a = new Friend("Zhang", "Jimmy"); Console.WriteLine(a.ToString()); // 输出:Friend: ZhangJimmy
而我们期望输出的是:Jimmy Zhang。这个时候,大家可以想一想想 .Net Framework 解决这个问题采用的方法:重载ToString()。让ToString()方法接收一个参数,根据这个参数来进行格式化。比如 int a = 123; Console.WriteLine(a.ToString("c"));指定了字符串"c"作为参数,产生货币类型的输出:¥123.00。我们也可以使用这种方式来改进Friend类,在Friend中重载一个 ToString() 方法,使之根据一个字符参数来定义其字符串格式化:
// 根据字符串参数来定义类型的格式化 public string ToString(string format) { switch (format.ToUpper()) { case "W": // West: 西方 return String.Format("Friend : {0} {1}", firstName, familyName); case "E": // East: 东方 return this.ToString(); case "G": // General default: return base.ToString(); } }
然后我们在使用ToString()方法时可以使用重载的版本,对于英文名,我们传入"W"作为参数,这样就解决了上面的问题:
Friend f = new Friend(); Console.WriteLine(f.ToString()); // 输出:Friend: 张子阳 f = new Friend("Zhang", "Jimmy"); Console.WriteLine(f.ToString("W")); // 输出:Friend: Jimmy Zhang
这个问题更好的解决办法并非是重载ToString(),可以简单地使用属性来完成,比如这样:
public string WesternFullName{ get{ return String.Format("{0} {1}", firstName, familyName)} } public string EasternFullName{ get{ return String.Format("{0}{1}", familyName, firstName)} }
在本文中,我在这里仅仅是举一个例子来说明,所以就先不去管使用属性这种方式了,后面也是一样。
实现 IFormattable 接口
我们站在类型设计者的角度来思考一下:我们为用户提供了Friend类,虽然重载的 ToString() 可以应对 东方/西方 的文化差异,但是用户的需求总是千变万化。比如说,用户是一名Web开发者,并且期望人名总是以加粗的方式显示,为了避免每次操作时都取出属性再进行格式化,他会希望只要在类型上应用ToString()就可以达到期望的效果,这样会更省事一些,比如:
Friend f = new Friend(); label1.Text = String.Format("<b>{0}{1}</b>", f.familyName, f.firstName); // 这样会更加方便(用户期望): // label1.Text = f.ToString(***);
此时我们提供的格式化方法就没有办法实现了。对于不可预见的情况,我们希望能让用户自己来决定如何进行对象的字符串格式化。Microsoft显然想到了这一问题,并为我们提供了IFormattable接口。当你作为一名类型设计者,期望为你的用户提供自定义的格式化ToString()时,可以实现这个接口。我们现在来看一下这个接口的定义:
public interface IFormattable { string ToString(string format, IFormatProvider formatProvider); }
它仅包含一个方法 ToString():参数 format 与我们上一小节重载的ToString()方法中的 format 含义相同,用于根据参数值判断如何进行格式化;参数 formatProvider 是一个 IFormatProvider 类型,它的定义如下:
public interface IFormatProvider { object GetFormat(Type formatType); }
其中 formatType 是当前对象的类型实例(还有一种可能是ICustomFormatter,后面有说明) --Type对象。在本例中,我们是对Friend这一类型进行格式化,那么这个formatType 的值就相当于 typeof(Friend),或者 f.GetType() (f为Friend类型的实例)。GetFormat()方法返回一个Object类型的对象,由这个对象进行格式化的实际操作,这个对象实现了 ICustomFormatter 接口,它只包含一个方法,Format():
public interface ICustomFormatter{ string Format(string format, object arg, IFormatProvider formatProvider); }
其中 format 的含义与上面相同,arg 为欲进行格式化的类型实例,在这里是Friend的一个实例,formatProvider 这里通常不会用到。
看到这里你可能会感觉有点混乱,实际上,你只要记得:作为类型设计者,你只需要实现 IFormattable 接口就可以了:先通过参数provider的 IFormatProvider.GetFormat() 方法,得到一个 ICustomFormatter 对象,再进一步调用 ICustomFormatter 对象的 Format()方法,然后返回 Format() 方法的返回值:
public class Friend: IFormattable{ // 略 ... // 实现 IFormattable 接口 public string ToString(string format, IFormatProvider provider) { if (provider != null) { ICustomFormatter formatter = provider.GetFormat(this.GetType()) as ICustomFormatter; if (formatter != null) return formatter.Format(format, this, provider); } return this.ToString(format); } }
上面需要注意的地方就是 IFormatProvider.GetFormat()方法将当前的Friend对象的类型信息(通过this.GetType())传递了进去。
类型设计者的工作在这里就完结了,现在让我们看下对于这个实现了IFormattable的类型,类型的用户该如何使用自己定义的方法对对象进行字符串格式化。作为类型的用户,为了能够实现对象的自定义格式字符串,需要实现 IFormatProvider 和 ICustomFormatter接口。此时有两种策略:
- 建立一个类,比如叫 FriendFormatter,这个类实现 IFormatProvider 和 ICustomFormatter 接口。
- 建立两个类,比如叫 ObjectFormatProvider 和 FriendFormatter,分别实现 IFormatProvider 和 ICustomFormatter 接口,并且让 ObjectFormatProvider 的 GetFormat()方法返回一个 FriendFormatter 的实例。
我们先来看看第一种策略:
public class FriendFormatter : IFormatProvider, ICustomFormatter { // 实现 IFormatProvider 接口,由 Friend类的 IFormattable.ToString()方法调用 public object GetFormat(Type formatType) { if (formatType == typeof(Friend)) return this; else return null; } // 实现 ICustomFormatter 接口 public string Format(string format, object arg, IFormatProvider formatProvider) { //if (arg is IFormattable) // return ((IFormattable)arg).ToString(format, formatProvider); Friend friend = arg as Friend; if (friend == null) return arg.ToString(); switch (format.ToUpper()) { case "I": return String.Format("Friend: <i>{0}{1}<i>" ,friend.FamilyName, friend.FirstName); case "B": return String.Format("Friend: <b>{0}{1}<b>", friend.FamilyName, friend.FirstName); default: return arg.ToString(); } } }
结合上面的 ToString()方法一起来看,这里的流程非常清楚:使用这种方式时,GetFormat中的判断语句,if(formatType == typeof(Friend)) 确保 FriendFormatter 类只能应用于 Friend类型对象的格式化。随后,通过this关键字返回了当前 FriendFormatter 对象的引用。因为FriendFormatter也实现了 ICustomFormatter接口,所以在Friend类型的 IFormattable.ToString()方法中,能够将FriendFormater 转换为一个ICustomFormatter类型,接着调用了ICustomFormatter.Format()方法,返回了预期的效果。
注意上面注释掉的部分,可能是参考了MSDN的缘故吧,有些人在实现ICustomFormatt的时候,会加上那部分语句。实际上MSND范例中使用的一个Long类型,并且使用的是String.Format()的重载方法来进行自定义格式化,与这里不尽相同。当你屏蔽掉上面的注释时,很显然会形成一个无限循环。
我们现在来对上面的代码进行一下测试:
public class ObjectFormatProvider : IFormatProvider { // 实现 IFormatProvider 接口,由 Friend类的 ToString() 方法调用 public object GetFormat(Type formatType) { if (formatType == typeof(Friend)) return new FriendFormatter();//返回一个实现了ICustomFormatter的类型实例 else return null; } } // 实现ICustomFormatter接口,总是为一个特定类型(比如Friend)提供格式化服务 public class FriendFormatter : ICustomFormatter { // 实现 ICustomFormatter 接口 public string Format(string format, object arg, IFormatProvider formatProvider) { //if (arg is IFormattable) // return ((IFormattable)arg).ToString(format, formatProvider); Friend friend = arg as Friend; if (friend == null) return arg.ToString(); switch (format.ToUpper()) { case "I": return String.Format("Friend: <i>{0}{1}<i>", friend.FamilyName, friend.FirstName); case "B": return String.Format("Friend: <b>{0}{1}<b>", friend.FamilyName, friend.FirstName); default: return arg.ToString(); } } }
看上去和上面的方法几乎一样,区别不过是将一个类拆成了两个。实际上,拆分成两个类会更加的灵活:使用一个类实现两个接口的方式时,FriendFormatter 只能用来格式化 Friend类型。如果再有一个Book类,类似地,需要再创建一个 BookFormatter。
而将它拆分成两个类,只需要再创建一个类实现一遍 ICustomFormatter 接口,然后对ObjectFormatProvider做些许修改就可以了。此时Provider类可以视为一个通用类,可以为多种类型提供格式化服务。现在假设我们有一个Book类型,我们只需要这样修改一下 ObjectFormatProvider类就可以了:
public class ObjectFormatProvider : IFormatProvider { // 实现 IFormatProvider 接口,由 Friend类的 ToString() 方法调用 public object GetFormat(Type formatType) { if (formatType == typeof(Friend)) return new FriendFormatter(); if (formatType == typeof(Book)) return new BookFormatter(); // 返回一个BookFormatter对象 else return null; } } // BookFormatter 类型省略 ...
在类型外部提供自定义格式字符串的能力
现在我们站在一个类型用户的角度来思考一下:很多时候,类型的设计者并没有为类型实现IFormattable接口,此时我们该如何处理呢?我们再思考一下.Net Framework中的处理方式:
int a = 123; Console.WriteLine(a.ToString("c")); // 输出: ¥123.00 Console.WriteLine(String.Format("{0:c}", a)); // 输出: ¥123.00
实际上,String.Format()还提供了一个重载方法,可以一个接收IFormatProvider对象,这个IFormatProvider由我们自己定义,来实现我们所需要的格式化效果。根据上面的对比,我们再做一个总结:为了实现类型的自定义格式字符串,我们总是需要实现IFormatProvider接口。如果类型实现了IFormattable接口,我们可以在类型上调用ToString()方法,传递IFormatProvider对象;如果类型没有实现IFormattable接口,我们可以通过String.Format()静态方法,传递IFormatProvider对象。
现在我们就来创建实现IFormatProvider接口的类型了,与上面的方式稍稍有些不同:通过Reflector工具(不知道的可以去百度一下)可以看到,调用 String.Format() 时内部会创建一个 StringBuilder类型的对象builder,然后调用 builder.AppendFormat(provider, format, args); 在这个方法内部,最终会调用provider的GetFormat()方法:
formatter = (ICustomFormatter) provider.GetFormat(typeof(ICustomFormatter));
可以看到,provider.GetFormat()传递了一个typeof(ICustomFormatter)对象。因此,如果要判断是不是在类型外部通过String.Format()这种方式来使用 IFormatProvider,只需要判断 formatType是不是等于 typeof(ICustomFormatter) 就可以了:
public class OutFriendFormatter : IFormatProvider, ICustomFormatter { // 实现 IFormatProvider 接口,由 Friend类的 ToString() 方法调用 public object GetFormat(Type formatType) { if (formatType == typeof(ICustomFormatter)) return this; else return null; } // 实现 ICustomFormatter 略 }
我们再次对代码进行一下测试:
Friend f = new Friend(); OutFriendFormatter formatter = new OutFriendFormatter(); string output = String.Format(formatter, "{0:i}", f); Console.WriteLine(output); // Friend: <i>张子阳<i>
.Net 中实现IFormatProvider的一个例子
.Net 中使用 IFormatProvider 最常见的一个例子就是 CultureInfo 类了。很多时候,我们需要对金额进行格式化,此时我们通常都会这样:
int money = 100; Console.WriteLine(String.Format("{0:c}", money));
我们期望这个输出的结果是 ¥100.00。然而情况并非总是如此,当你将这个程序运行于中文操作系统下时,的确会如你所愿得到 ¥100.00;而在英文操作系统下,你恐怕会得到一个 $100.00。这是因为在对数字以金额方式进行显示的时候,会依据当前系统的语言环境做出判断,如果你没有显示地指定语言环境,那么就会按照默认的语言环境来进行相应的显示。在.Net中,将语言环境进行封装的类是 CultureInfo,并且它实现了IFormatProvider,当我们需要明确指定金额的显示方式时,可以借助这个类来完成:
int money = 100; IFormatProvider provider = new CultureInfo("zh-cn"); Console.WriteLine(String.Format(provider, "{0:c}", money)); // 输出:¥100.00 provider = new CultureInfo("en-us"); Console.WriteLine(String.Format(provider, "{0:c}", money)); // 输出:$100.00
总结
在这篇文章中,我较系统地讨论了如何对类型进行自定义格式化。我们通过各种方式达到了这个目的:覆盖ToString()、重载ToString()、实现 IFormatProvider接口。我们还讨论了实现IFormatProvider和ICustomFormatter的两种方式:创建一个类实现它们,或者各自实现为不同的类。
我想很多人在读这篇文章以前就会使用这些方法了,我在这里希望大家能够多进行一点思考,以一个.Net 框架设计者的角度来思考:为什么会设计出三个接口配合 String.Format()静态类来实现这一过程?这样设计提供了怎样的灵活性?从这篇文章中,我期望你收获更多的不是作为一个框架使用者如何去使用这些类型,而是作为一个框架设计者来设计出这样的类型结构。
感谢阅读,希望这篇文章能给你带来帮助!