ToString IFormattable 自定义格式化

实现 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 FriendIFormattable{
   // 略 ...

    // 实现 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 的实例。

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

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()方法,返回了预期的效果。

NOTE:注意上面注释掉的部分,可能是参考了MSDN的缘故吧,有些人在实现ICustomFormatt的时候,会加上那部分语句。实际上MSND范例中使用的一个Long类型,并且使用的是String.Format()的重载方法来进行自定义格式化,与这里不尽相同。当你屏蔽掉上面的注释时,很显然会形成一个无限循环。

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

Friend f = new Friend();
FriendFormatter formatter = new FriendFormatter();
Console.WriteLine(f.ToString("b", formatter));    // 输出:Friend: <b>张子阳</b>

接下来我们看下第二种方式,将 IFormatProvider 和 ICustomFormatter 交由不同的类来实现:

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值