[转载]彻底学通string.Format以及IFormattable,IFormatProvider,ICustomFormatter

[转载]彻底学通string.Format以及IFormattable,IFormatProvider,ICustomFormatter – 喆 喆 – 博客园.

自从使用.net以来就一直都在使用string.Format方法,一直没有空或者其他原因都没有深入去了解,主要还是因为项目上似乎没有这 么高的要求,也没必要去深入了解,就算碰到了自定义的格式化内容也是写几个通用的方法而已。今天空下来仔细去理解了一下,在这里和大家分享一下,也希望大 家一起交流。

string.Format方法是string类提供的静态方法,一般最多使用的是其两个参数的重载,例如:

var name = Zhezhe;
var msg
= string.Format(Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}., name, DateTime.Now, DateTime.Now.DayOfWeek);
Console.WriteLine(msg);

后面一个参数是.net语法简写的可变参数,在.net内部实际是数组而已,实质还是两个参数的方法重载。

你也可以不使用这种方法,将字符串相加即可

var msg1 = Hello Cnblogs, I am + name + ,Today is + DateTime.Now.ToString(yyyy-MM-dd) + + DateTime.Now.DayOfWeek + .;

上面两种方法的结果是一样的。

之前普遍使用第一种方法的原因是相比string的多个加号相加在性能上有一定优势,因为其内部是使用StringBuilder类的,还有一个原因是代码的可读性比起+这样的方式更好一些。

分析一下第一种方法的实现原理:

1.Format方法的内部解析方式和原理

Format方法在取到第一个参数Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}.之后便将其分解成多个部分

① Hello Cnblogs, I am “{0}”,Today is ““{1:yyyy-MM-dd}” {2}”.”

分解的原则是按照{}配对的数量进行的,{}是微软定义好的标记而已,你自己也可以去实现个用 []表示都无所谓。既然{}已经被定义为了特殊的标记,所以如果是自己需要在字符串中包含大括号的话就必须进行转义,这个转义也和我们平时使用的”/”转 义表示法不同,需要使用两个大括号进行转义如 {{ 或者 }}。 如:

var msg2 = string.Format(Hello {{}},I am {0}, name);

将{}分解出来之后根据中间的序号来对应第二个参数,如果第二个参数的实际个数小于需要的数量,则会出现运行错误(编译时不会报错), 如果参数个数大于序号的数量,则其后的忽略不计。

参数个数小于序号的实际数量,错误

var msg4 = string.Format(Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}., name, DateTime.Now);

参数个数大于序号的实际数量,多出的参数忽略不计

var msg4 = string.Format(Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd}., name, DateTime.Now,DateTime.Now.DayOfWeek);

序号的顺序不一定必须是0,1,2,3,4可以任意排列,但是序号永远和第二个参数(实质是数组)的索引一致。

var msg4 = string.Format(Hello Cnblogs, I am {2},Today is {0:yyyy-MM-dd} {1}., DateTime.Now, DateTime.Now.DayOfWeek, name);

序号还能跳跃,但是中间跳跃过的序号参数里必须有

var msg5 = string.Format(Hello Cnblogs, I am {0},Today is {2:yyyy-MM-dd} {3}., name, test, DateTime.Now, DateTime.Now.DayOfWeek);

上面讲了一下用法,接下来继续

分解完毕之后使用 StringBuilder的Append方法将各个部分添加进去,最后再用ToString方法转成string,其实现原理非常类似于下面的代码

var s = new StringBuilder();
s.Append(
Hello Cnblogs, I am );
s.Append(name);
s.Append(
,Today is );
s.Append(DateTime.Now.ToString(
yyyy-MM-dd));
s.Append(
);
s.Append(DateTime.Now.DayOfWeek);
s.Append(
.);
var msg3
= s.ToString();

顺便解释一下string和StringBuilder:string虽然也是引用类型,但是该类型.net内部进行了特殊处理,让其表现出和值类 型相似的特征,特别是在每次变动之后就会重新分配内存空间,而StringBuilder就不会,所以如果有很多个字符串相加拼接,则string性能较 低。

在用 Append方法进行添加的时候会有两种情况:

一种是{0},{1}这样的不带有特殊格式化的则直接会调用该对象的ToString方法,比如上面的 s.Append(DateTime.Now.DayOfWeek);其实就是 s.Append(DateTime.Now.DayOfWeek.ToString());在.net中,如果是自己定义的类,并且没有重写ToString方法,则会输出类的全名,下面会详细讨论。

另一种是{0:yyyy-MM-dd}带有特殊格式化的则继续分解,将冒号后面的内容分解出来,并且在调用ToString时作为参数传入,上面的s.Append(DateTime.Now.ToString(yyyy-MM-dd));就体现了这一点。所以这些其实都没什么奥妙可言,冒号也是一个预定义好的标记而已,如果微软让你去实现这个,你也可以用其他符号。

2.ToString方法的深入理解

通过第一步的分析如果纯粹从分析Format这个方法来说已经足够了,大括号的特殊标记作用以及和后面参数的对应关系也已经解释清楚了。但是这里还是需要深入了解一下ToString方法。

上面1中提到如果一个自己定义的类不去重写ToString方法的话则会 输出类的全名,例如

public class Person
{
public string Name { get; set; }
}

如果写如下代码

var msg6 = string.Format(Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}.,
new Person() {Name = Zhezhe}, DateTime.Now, DateTime.Now.DayOfWeek);
Console.WriteLine(msg6);

则会输出:

这里再次强调一下,如果某个对象需要转换成ToString,并且没有手动调用该方法,程序会自动调用该方法,上面的new Person() {Name = Zhezhe}没有手工调用,程序会自动调用方法(new Person() {Name = Zhezhe}).ToString(); 这个是微软让你少些代码而已,好的习惯是始终写上 .ToString();

.net中的任何对象都具有该方法,因为该方法在object对象中定义,任何类或者结构都会继承object,所以不用担心一个对象没有ToString方法。

接下来定义带有ToString重载方法的类

public class PersonWithToString
{
public string Name { get; set; }

public override string ToString()
{
return Name;
}
}

编写如下代码:

//使用自己定义类,但是重写了ToString方法
var msg7 = string.Format(Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}., new PersonWithToString(){ Name = Zhezhe }, DateTime.Now, DateTime.Now.DayOfWeek);
Console.WriteLine(msg7);

输入结果为 输出就正常了,自己重写的方法起作用了。

总结:对自己定义的类始终重写 ToString方法。 这样在 string.Format 中或者其他需要程序自动转换成string类型时不会出现 输出类全名的情况。

3.ToString带有自定义格式化参数的理解

上面讲到的ToString都是不带格式化参数的,像 {1:yyyy-MM-dd} 这样的情况是没法处理的,也许有人会说像 DateTime.Now.ToString(yyyy-MM-dd) 这样的情况自己去重载一个ToString方法就可以了,果真如此吗? 下面就测试一下

public class PersonWithToString
{
public string Name { get; set; }

public override string ToString()
{
return Name;
}

public string ToString(string format)
{
switch (format)
{
case UPP:
return Name.ToUpper();
case LOW:
return Name.ToLower();
default:
return Name;
}
}
}

var msg9 = string.Format(Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}.,
new PersonWithToString() { Name = Zhezhe }.ToString(UPP), DateTime.Now, DateTime.Now.DayOfWeek);
Console.WriteLine(msg9);

msg9的实际输出为 Hello Cnblogs, I am ZHEZHE,Today is 2010-07-30 Friday.  这个正是我们需要的,当然,这个肯定是对的,要不然就是.net的bug了

接下来再看看下面的

var msg8 = string.Format(Hello Cnblogs, I am {0:UPP},Today is {1:yyyy-MM-dd} {2}.,
new PersonWithToString() { Name = Zhezhe }, DateTime.Now, DateTime.Now.DayOfWeek);

实际输出是: Hello Cnblogs, I am Zhezhe,Today is 2010-07-30 Friday.  并不是我们所期望的。实际上上面的代码是调用了PersonWithToString类的不带参数的ToString()方法。言外之意就是  {0:UPP}这样的格式实际上内部处理的是和 {0}

一样的效果了。在1中提到了分解的原理用了类似两个字,实际情况并不是这么简单。

{0:UPP} 真正调用的方法签名是    string ToString(string format,IFormatProvider formatProvider)

而且也不是直接调用该对象的此方法。而是通过 IFormattable 接口实现的方法

现在定义实现了该接口的 Person2类

Person2

public class Person2 : IFormattable
{
public string Name { get; set; }

public override string ToString()
{
return Name;
}

#region IFormattable Members

public string ToString(string format, IFormatProvider formatProvider)
{
if (string.IsNullOrEmpty(format))
return ToString();

switch (format)
{
case UPP:
return Name.ToUpper();
case LOW:
return Name.ToLower();
default:
return Name;
}
}

#endregion
}

运行一下代码得到预期的结果

//使用实现了IFormattable接口的Person2对象
var msg10 = string.Format(Hello Cnblogs, I am {0:UPP},Today is {1:yyyy-MM-dd} {2}.,
new Person2() { Name = ZhezheToUpper }, DateTime.Now, DateTime.Now.DayOfWeek);
Console.WriteLine(msg10);

ZhezheToUpper已经输出成全部大写形式了。

既然{0:UPP}会调用接口定义的ToString方法,那么{0}呢? 如果该类没有实现IFormattable接口,上面已经说了,会调用重载的或者是基类的ToString()方法。但是如果该类已经实现了IFormattable接口,那么{0}也不会去调用重载的或者是基类的ToString()方法了,它始终是去调用 接口定义的 ToString方法。下面具体印证一下

Person3

public class Person3 : IFormattable
{
public string Name { get; set; }

public override string ToString()
{
return Name;
}

#region IFormattable Members

public string ToString(string format, IFormatProvider formatProvider)
{
if (string.IsNullOrEmpty(format))
return Name + IFormattable Method;

switch (format)
{
case UPP:
return Name.ToUpper();
case LOW:
return Name.ToLower();
default:
return Name + IFormattable Method;
}
}

#endregion
}

运行下面的测试代码

var msg11 = string.Format(Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}.,
new Person3() { Name = ZhezheToUpper }, DateTime.Now, DateTime.Now.DayOfWeek);
Console.WriteLine(msg11);

输出为: Hello Cnblogs, I am ZhezheToUpper IFormattable Method,Today is 2010-07-30 Friday.

证明了确实是调用了接口定义的方法,而不是重载的ToString方法,否则是输出ZhezheToUpper

再来看一下Person2中实现的ToString方法,

if (string.IsNullOrEmpty(format))
return ToString();
如果是刚才的{0}不带格式化参数的调用,则format参数传过来的是null值,这里需要自己判断,如果是null值,一般情况下是手工去调用重载的ToString()方法。
所以Person2的做法是好的,而Person3中的做法是不好的,Person3只是为了测试分辨出调用的是哪个方法才这么设计的。

总结:一.对于实现IFormattable 接口时,如果format参数为null(即不带格式化参数的情况,如{0})则应该调用重载的 ToString()方法,而不应该自己去另外写代码。

二.如果找不到相应的格式化参数,例如{0:AAA},在Person2的switch中并无匹配的AAA,这种情况一般也应该去调用重载的 ToString()方法。

否则就会出现

//以下两个输出结果不一样,是不合理的
var msg12 = string.Format(Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}.,new Person3() { Name = ZhezheToUpper }, DateTime.Now, DateTime.Now.DayOfWeek);
Console.WriteLine(msg12);

var msg13 = string.Format(Hello Cnblogs, I am {0},Today is {1:yyyy-MM-dd} {2}.,new Person3() { Name = ZhezheToUpper }.ToString(), DateTime.Now, DateTime.Now.DayOfWeek);
Console.WriteLine(msg13);

不同的结果的情况

上面的输出结果不同:

这是不好的设计

4.继续了解 IFormatProvider 和 ICustomFormatter 接口

到这里为止,应该说灵活应用string.Format()已经没什么多大的问题了,但是也还是存在一些问题,比如我们必须得为每个类单独去实现IFormattable接口才能实现自定义的格式化参数。在一些场后还是觉得不太方便或者说代码冗余。

.net的string.Format静态方法还提供了重载方法,具体签名如下:public static string Format(IFormatProvider provider,string format,params Object[] args)

这个方法比起原来使用的方法最前面增加了 IFormatProvider类型参数。使用此方法的优点是不需要为后面的参数对象实现 IFormattable 接口就可以使用自定义的格式化参数。既然这样的话也就解决了第4部分开头提到的问题了。

还是用例子说话吧

下面是正方形类

Square类

public class Square
{
public string Name { get; set; }

/// <summary>
/// 边长
/// </summary>
public double Side { get; set; }

public override string ToString()
{
return string.Format({0}(Side:{1}),Name, Side);
}
}

下面是长方形类

Rectangle类

public class Rectangle
{
public string Name { get; set; }

/// <summary>
///
/// </summary>
public double Width { get; set; }

/// <summary>
///
/// </summary>
public double Height { get; set; }

public override string ToString()
{
return string.Format({0}(Width:{1},Height:{2}),Name, Width, Height);
}
}

两个类都重写了ToString方法

定义MyHelloFormatProvider类,该类从名称上就可以看出是格式化的提供者

public class MyHelloFormatProvider : IFormatProvider
{
#region IFormatProvider Members

public object GetFormat(Type formatType)
{
return new MyHelloFormatter();
}

#endregion
}

该类实现了 IFormatProvider 接口,接口只有一个唯一的方法需要实现,GetFormat返回的是真正进行格式化操作的类,这里很像是工厂模式。

返回 MyHelloFormatter 对象之后,在MyHelloFormatter 中具体进行格式化操作。

public class MyHelloFormatter : ICustomFormatter
{
#region ICustomFormatter Members

public string Format(string format, object arg, IFormatProvider formatProvider)
{
var t
= Hello ;
switch (format)
{
case UPP:
t
= t.ToUpper();
break;
case LOW:
t
= t.ToLower();
break;
default:
break;
}

return t + arg.ToString();
}

#endregion
}

MyHelloFormatter 实现了ICustomFormatter接口,该接口也只有一个唯一的方法,即实际执行格式化的方法

如果不使用格式化参数或者格式化参数不匹配,情况会怎么样?

代码

var msg15 = string.Format(new MyHelloFormatProvider(), {0}  {1}, new Rectangle() { Name = MyRectangle, Width = 14.3, Height = 10 }, new Square() { Name = MySquare, Side = 24.2 });
Console.WriteLine(msg15);

var msg16 = string.Format(new MyHelloFormatProvider(), {0}  {1}, new Rectangle() { Name = MyRectangle, Width = 14.3, Height = 10 }.ToString(), new Square() { Name = MySquare, Side = 24.2 }.ToString());
Console.WriteLine(msg16);

var msg17 = string.Format(new MyHelloFormatProvider(), {0:AAA}  {1:BBB}, new Rectangle() { Name = MyRectangle, Width = 14.3, Height = 10 }, new Square() { Name = MySquare, Side = 24.2 });
Console.WriteLine(msg17);

以上输出都是一样的: Hello MyRectangle(Width:14.3,Height:10)  Hello MySquare(Side:24.2)

上面的运行结果表明,如果提供了new MyHelloFormatProvider() ,那么执行过程过是: 根据MyHelloFormatProvider 对象得到 MyHelloFormatter 对象,利用MyHelloFormatter 对象的Format方法进行格式化

这里还有一个问题,如果 MyHelloFormatProvider 的 GetFormat返回的不是一个实现了 ICustomFormatter 接口的对象又会是什么情况呢?

答案是会报异常。 那么如果返回的是 null 呢? 答案是直接调用了对象的ToString()方法了。如果返回null,则运行结果如下:

MyRectangle(Width:14.3,Height:10)  MySquare(Side:24.2)

带上格式化参数的运行结果

var msg18 = string.Format(new MyHelloFormatProvider(), {0:UPP}  {1:LOW}, new Rectangle() { Name = MyRectangle, Width = 14.3, Height = 10 }, new Square() { Name = MySquare, Side = 24.2 });
Console.WriteLine(msg18);

HELLO MyRectangle(Width:14.3,Height:10)  hello MySquare(Side:24.2)
通过上面的例子我们知道如果我们需要定义一种通用的格式化方式的 话,不需要让类实现 IFormattable 接口,可以通过定义实现 IFormatProvider,ICustomFormatter接口的类去做,上面的无论是正方形还是长方形类都需要在前面加上 Hello 进行格式化,可以是普通的,小写的,大写的等等,不需要两个类单独去实现了,就选以后增加了圆形,三角形等等,也都能用我们已经定义好的 MyHelloFormatProvider 和 MyHelloFormatter  去进行格式化。

使用这种方式还能解决另外一个问题,假如我们已经为圆形类实现了 IFormattable  接口,并且已经实现了{0:UPP}格式化参数,但是实现的方法中没有加{0:LOW}格式化参数,而且这个类我们又不能更改(可能是.net自带的类, 可能是第三方dll提供的类等等),那该怎么办呢? 显然已经不可能靠IFormattable  接口来解决了

使用这节讲的方法就可以实现我们要求了。以下是具体实现

圆形类

public class Circle : IFormattable
{
public string Name { get; set; }

/// <summary>
/// 半径
/// </summary>
public double Radius { get; set; }

public override string ToString()
{
return string.Format({0}(Radius:{1}), Name, Radius);
}

#region IFormattable Members

public string ToString(string format, IFormatProvider formatProvider)
{
if (string.IsNullOrEmpty(format))
return ToString();

var t = Hello ;
switch (format)
{
case UPP:
t
= t.ToUpper();
break;
default:
break;
}

return t + Name;
}

#endregion
}

该类可以实现UPP格式化参数的格式化。

var msg19 = string.Format(Test: {0}, new Circle() { Name = MyCircle, Radius = 10 });
Console.WriteLine(msg19);

var msg20 = string.Format(Test: {0:UPP}, new Circle() {Name = MyCircle, Radius = 10});
Console.WriteLine(msg20);

运行上面的代码得到:

Test: MyCircle(Radius:10)
Test: HELLO MyCircle

第一个无格式化参数,实际调用ToString()方法得到,由代码 if (string.IsNullOrEmpty(format))决定

第二个带UPP格式化参数,也得到了预期的结果。

现在需要实现LOW的格式化参数

var msg21 = string.Format(new MyHelloFormatProvider(),Test: {0:LOW}, new Circle() { Name = MyCircle, Radius = 10 });
Console.WriteLine(msg21);

在不修改Circle类并且不重新定义其他类的情况下就可以达到我们的要求了

显示结果如下: Test: hello MyCircle(Radius:10)      hello已经是全部小写了。

写了这么多,感觉有些乱七八糟了,发现还有很多没有提到,很多都讲重复了。本人也难得写博客,文字水平表达能力欠佳,还望阅者理解。

以下是全部代码

/Files/szp1118/ConsoleApplication3.rar

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏