彻底学通string.Format以及IFormattable,IFormatProvider,ICustomFormatter
[2] 彻底学通string.Format以及IFormattable,IFormatProvider,ICustomFormatter
[3] 彻底学通string.Format以及IFormattable,IFormatProvider,ICustomFormatter
自从使用.net以来就一直都在使用string.Format方法,一直没有空或者其他原因都没有深入去了解,主要还是因为项目上似乎没有这么高的要求,也没必要去深入了解,就算碰到了自定义的格式化内容也是写几个通用的方法而已。今天空下来仔细去理解了一下,在这里和大家分享一下,也希望大家一起交流。
string.Format方法是string类提供的静态方法,一般最多使用的是其两个参数的重载,例如:
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内部实际是数组而已,实质还是两个参数的方法重载。
你也可以不使用这种方法,将字符串相加即可:
上面两种方法的结果是一样的。
之前普遍使用第一种方法的原因是相比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}"⑦ "."
分解的原则是按照{}配对的数量进行的,{}是微软定义好的标记而已,你自己也可以去实现个用 []表示都无所谓。既然{}已经被定义为了特殊的标记,所以如果是自己需要在字符串中包含大括号的话就必须进行转义,这个转义也和我们平时使用的"/"转义表示法不同,需要使用两个大括号进行转义如 {{ 或者 }}。 如:
将{}分解出来之后根据中间的序号来对应第二个参数,如果第二个参数的实际个数小于需要的数量,则会出现运行错误(编译时不会报错), 如果参数个数大于序号的数量,则其后的忽略不计。
参数个数小于序号的实际数量,错误。
参数个数大于序号的实际数量,多出的参数忽略不计。
序号的顺序不一定必须是0,1,2,3,4可以任意排列,但是序号永远和第二个参数(实质是数组)的索引一致。
序号还能跳跃,但是中间跳跃过的序号参数里必须有。
上面讲了一下用法,接下来继续。
分解完毕之后使用 StringBuilder的Append方法将各个部分添加进去,最后再用ToString方法转成string,其实现原理非常类似于下面的代码:
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 string Name { get; set; }
}
如果写如下代码:
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 string Name { get; set; }
public override string ToString()
{
return Name;
}
}
编写如下代码:
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类型时不会出现 输出类全名的情况。