前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >类型自定义格式字符串

类型自定义格式字符串

作者头像
张子阳
发布2018-09-30 10:21:09
5550
发布2018-09-30 10:21:09
举报

类型自定义格式字符串

2008-5-30 作者: 张子阳 分类: .Net 框架

引言

String可能是使用最多的类型,ToString()则应该是大家使用得最多的方法了。然而它不应该仅仅是用来输出类型的名称,如果使用得当,它可以方便地输出我们对类型自定义的格式。本文将循序渐进地讨论ToString(),以及相关的IFormattable、IFormatProvider以及ICustomFormatter接口。

在类型内部提供自定义格式字符串的能力

继承自System.Object 基类的 ToString()

String是人们直接就可以看懂的数据类型之一,很多情况下我们都会期望能够获得类型的一个字符串输出。因此,Microsoft 在.Net Framework所有类型的基类System.Object中提供了一个虚拟的 ToString()方法,它的默认实现是返回对象的类型名称。

假设我们有这样的一个类型,它定义了“朋友”这一对象的一些信息:

代码语言:txt
复制
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。

代码语言:txt
复制
Friend f = new Friend();
Console.WriteLine(f.ToString()); // 输出:CustomToString.Friend

覆盖 ToString() 方法

在上面的例子中,不管类型实例(对象)所包含的数据(字段值)是什么,它总是会返回相同的结果(CustomToString.Friend)。很多时候,返回一个对象的类型名称对我们来说没有多大的意义,拿上面来说,我们可能更加期望能够返回朋友的姓名(famliyName和firstName字段的值)。这时候,我们可以简单地覆盖System.Object基类的ToString()方法,在 Friend 类中添加如下方法:

代码语言:txt
复制
// 覆盖System.Object基类的 ToString() 方法
public override string ToString() {
   return String.Format("Friend: {0}{1}", familyName, firstName);
}

此时,我们再次运行代码:

代码语言:txt
复制
Friend f = new Friend();
Console.WriteLine(f.ToString());    // 输出:Friend: 张子阳
f = new Friend("王","涛");
Console.WriteLine(f.ToString()); // 输出:Friend: 王涛

可以看到对于不同的对象,ToString()根据对象的字段值返回了不同的结果,这样对我们来说会更加有意义。

重载 ToString() 方法

有时候,我们可能需要将对象按照不同的方式进行格式化。就拿Friend类型来说:西方人是名在前,姓在后;而中国人是 姓在前,名在后。所以如果运行下面的代码,虽然程序不会出错,但从英语语法角度来看却有问题:

代码语言:txt
复制
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() 方法,使之根据一个字符参数来定义其字符串格式化:

代码语言:txt
复制
// 根据字符串参数来定义类型的格式化
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"作为参数,这样就解决了上面的问题:

代码语言:txt
复制
Friend f = new Friend();
Console.WriteLine(f.ToString());        // 输出:Friend: 张子阳

f = new Friend("Zhang", "Jimmy");
Console.WriteLine(f.ToString("W")); // 输出:Friend: Jimmy Zhang

这个问题更好的解决办法并非是重载ToString(),可以简单地使用属性来完成,比如这样:

代码语言:txt
复制
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()就可以达到期望的效果,这样会更省事一些,比如:

代码语言:txt
复制
Friend f = new Friend();
label1.Text = String.Format("<b>{0}{1}</b>", f.familyName, f.firstName);
// 这样会更加方便(用户期望):
// label1.Text = f.ToString(***);

此时我们提供的格式化方法就没有办法实现了。对于不可预见的情况,我们希望能让用户自己来决定如何进行对象的字符串格式化。Microsoft显然想到了这一问题,并为我们提供了IFormattable接口。当你作为一名类型设计者,期望为你的用户提供自定义的格式化ToString()时,可以实现这个接口。我们现在来看一下这个接口的定义:

代码语言:txt
复制
public interface IFormattable {
    string ToString(string format, IFormatProvider formatProvider);
}

它仅包含一个方法 ToString():参数 format 与我们上一小节重载的ToString()方法中的 format 含义相同,用于根据参数值判断如何进行格式化;参数 formatProvider 是一个 IFormatProvider 类型,它的定义如下:

代码语言:txt
复制
public interface IFormatProvider {
    object GetFormat(Type formatType);
}

其中 formatType 是当前对象的类型实例(还有一种可能是ICustomFormatter,后面有说明) --Type对象。在本例中,我们是对Friend这一类型进行格式化,那么这个formatType 的值就相当于 typeof(Friend),或者 f.GetType() (f为Friend类型的实例)。GetFormat()方法返回一个Object类型的对象,由这个对象进行格式化的实际操作,这个对象实现了 ICustomFormatter 接口,它只包含一个方法,Format():

代码语言:txt
复制
public interface ICustomFormatter{
    string Format(string format, object arg, IFormatProvider formatProvider);
}

其中 format 的含义与上面相同,arg 为欲进行格式化的类型实例,在这里是Friend的一个实例,formatProvider 这里通常不会用到。

看到这里你可能会感觉有点混乱,实际上,你只要记得:作为类型设计者,你只需要实现 IFormattable 接口就可以了:先通过参数provider的 IFormatProvider.GetFormat() 方法,得到一个 ICustomFormatter 对象,再进一步调用 ICustomFormatter 对象的 Format()方法,然后返回 Format() 方法的返回值:

代码语言:txt
复制
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接口。此时有两种策略:

  1. 建立一个类,比如叫 FriendFormatter,这个类实现 IFormatProvider 和 ICustomFormatter 接口。
  2. 建立两个类,比如叫 ObjectFormatProvider 和 FriendFormatter,分别实现 IFormatProvider 和 ICustomFormatter 接口,并且让 ObjectFormatProvider 的 GetFormat()方法返回一个 FriendFormatter 的实例。

我们先来看看第一种策略:

代码语言:txt
复制
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()的重载方法来进行自定义格式化,与这里不尽相同。当你屏蔽掉上面的注释时,很显然会形成一个无限循环。

我们现在来对上面的代码进行一下测试:

代码语言:txt
复制
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类就可以了:

代码语言:txt
复制
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中的处理方式:

代码语言:txt
复制
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()方法:

代码语言:txt
复制
formatter = (ICustomFormatter) provider.GetFormat(typeof(ICustomFormatter));

可以看到,provider.GetFormat()传递了一个typeof(ICustomFormatter)对象。因此,如果要判断是不是在类型外部通过String.Format()这种方式来使用 IFormatProvider,只需要判断 formatType是不是等于 typeof(ICustomFormatter) 就可以了:

代码语言:txt
复制
public class OutFriendFormatter : IFormatProvider, ICustomFormatter
{
    // 实现 IFormatProvider 接口,由 Friend类的 ToString() 方法调用
    public object GetFormat(Type formatType)
    {
       if (formatType == typeof(ICustomFormatter))   
           return this;
       else
           return null;
    }
    
    // 实现 ICustomFormatter 略    
}

我们再次对代码进行一下测试:

代码语言:txt
复制
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 类了。很多时候,我们需要对金额进行格式化,此时我们通常都会这样:

代码语言:txt
复制
int money = 100;
Console.WriteLine(String.Format("{0:c}", money));

我们期望这个输出的结果是 ¥100.00。然而情况并非总是如此,当你将这个程序运行于中文操作系统下时,的确会如你所愿得到 ¥100.00;而在英文操作系统下,你恐怕会得到一个 $100.00。这是因为在对数字以金额方式进行显示的时候,会依据当前系统的语言环境做出判断,如果你没有显示地指定语言环境,那么就会按照默认的语言环境来进行相应的显示。在.Net中,将语言环境进行封装的类是 CultureInfo,并且它实现了IFormatProvider,当我们需要明确指定金额的显示方式时,可以借助这个类来完成:

代码语言:txt
复制
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()静态类来实现这一过程?这样设计提供了怎样的灵活性?从这篇文章中,我期望你收获更多的不是作为一个框架使用者如何去使用这些类型,而是作为一个框架设计者来设计出这样的类型结构。

感谢阅读,希望这篇文章能给你带来帮助!

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2008-5-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 类型自定义格式字符串
    • 引言
      • 在类型内部提供自定义格式字符串的能力
        • 继承自System.Object 基类的 ToString()
        • 覆盖 ToString() 方法
        • 重载 ToString() 方法
        • 实现 IFormattable 接口
      • 在类型外部提供自定义格式字符串的能力
        • .Net 中实现IFormatProvider的一个例子
      • 总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档