日常写代码的过程中,我们经常会使用string.Format来返回一段字符串:
var name = "nestor";
var msg = string.Format("Hello, I am {0:XXX}, today is {1:yyyy-MM-dd}.", name, DateTime.Now);
运行结果:Hello, I am nestor, today is 2019-07-08.
在.Net Framework中,{}已经被定义为了特殊的标记,如果我们想输出{},可以这样:
var msg2 = string.Format("Hello {{}}, I am {0}.", name);
运行结果:Hello {}, I am nestor.
我们先看一下string.Format方法签名:
static string Format(string format, params object[] args);
调用这个方法的规则是,根据参数format里面定义的序号,使用后面的args参数填充。
format里面定义的序号的格式分两种:
第一种是简单格式符:{0}这样的不带有特殊格式符的。
第二种是特殊格式符:{1:yyyy-MM-dd}带有特殊格式符的则继续分解,将冒号后面的特殊格式符分解出来,并且会作为参数去调用IFormattable里面的ToString()。
接口IFormattable的ToString方法签名
string ToString(string format, IFormatProvider formatProvider);
所以,我们想要支持带有特殊格式符的序号的话,后面传入的填充对象必须要实现IFormattable接口。
定义Person类,并实现IFormattable接口:
public class Person : IFormattable
{
public string Name { get; set; }
public override string ToString()
{
return Name;
}
public string ToString(string format, IFormatProvider formatProvider)
{
if (string.IsNullOrEmpty(format))
return this.ToString();
switch (format)
{
case "UPP":
return Name.ToUpper();
case "LOW":
return Name.ToLower();
default:
return this.ToString();
}
}
}
我们使用Person对象参数传入:
var p = new Person() { Name = "nestor" };
var msg3 = string.Format("Hellos, I am {0:UPP}, today is {1:yyyy-MM-dd}."
, p, DateTime.Now);
运行的结果:Hellos, I am NESTOR, today is 2019-07-08.
我们上面的结果,我们可以看到,我们使用了特殊格式符的序号,并且生效了。针对IFormattable接口,我们总结一下:
对于两种序号,不管是哪种格式的序号:
1. 如果填充的参数没有实现IFormattable接口,特殊格式符和简单格式符一样,都调用填充对象自己的ToString()方法。
2. 如果填充的参数实现IFormattable接口,即使是简单格式符也和特殊格式符一样,会调用填充对象实现的IFormattable接口的ToString方法。
.Net Framework中的源码逻辑:
IFormattable formattable = item as IFormattable;
if (formattable != null)
{
if (str == null && stringBuilder != null)
{
str = stringBuilder.ToString();
}
empty = formattable.ToString(str, provider);
}
else if (item != null)
{
empty = item.ToString();
}
目前为止,想要实现序号带有特殊格式符来控制输出的内容的话,必须要求填充的参数类型实现IFormattable接口才可以做到。
但是如果填充的参数类型是.net自带的类,或是第三方dll提供的类,我们并不能修改这些类型,那怎么办?
我们使用string.Format重载方法:
static string Format(IFormatProvider provider, string format, params object[] args);
我们可以通过传入IFormatProvider接口来控制输出格式,.Net Framework中的源码逻辑:
if (provider != null)
{
customFormatter = (ICustomFormatter)provider.GetFormat(typeof(ICustomFormatter));
}
string empty = null;
if (customFormatter != null)
{
empty = customFormatter.Format(str, item, provider);
}
这里涉及到IFormatProvider接口和ICustomFormatter接口两个接口。
IFormatProvider接口签名:
object GetFormat(Type formatType);
ICustomFormatter接口签名:
string Format(string format, object arg, IFormatProvider formatProvider);
对于IFormatProvider接口里面的GetFormat方法返回值类型是object,其实是不准确的,严格来说应该返回ICustomFormatter接口类型,如果返回的对象没有实现ICustomFormatter接口,会报异常。
如果返回null, .Net Framework会认为提供的IFormatProvider provider参数无效,接下来会当成调用下面的方法来处理。
static string Format(string format, params object[] args);
.Net Framework的源码大致如下:
if (customFormatter != null)
{
if (stringBuilder != null)
{
str = stringBuilder.ToString();
}
empty = customFormatter.Format(str, item, provider);
}
if (empty == null)
{
IFormattable formattable = item as IFormattable;
if (formattable != null)
{
if (str == null && stringBuilder != null)
{
str = stringBuilder.ToString();
}
empty = formattable.ToString(str, provider);
}
else if (item != null)
{
empty = item.ToString();
}
}
好,到这里我们知道,如果我们想要准确调用:
static string Format(IFormatProvider provider, string format, params object[] args);
我们需要实现IFormatProvider接口和ICustomFormatter接口
定义MyFormatProvider类实现IFormatProvider接口:
public class MyFormatProvider : IFormatProvider
{
public object GetFormat(Type formatType)
{
return new MyCustomFormatter();
}
}
定义MyCustomFormatter类实现ICustomFormatter接口:
public class MyCustomFormatter : ICustomFormatter
{
public string Format(string format, object arg, IFormatProvider formatProvider)
{
var str = arg.ToString();
switch (format)
{
case "UPP":
str = str.ToUpper();
break;
case "LOW":
str = str.ToLower();
break;
case "APPEND":
str = str + "~";
break;
default:
break;
}
return "Custom " + str;
}
}
显式提供MyFormatProvider对象,并出入string.Format:
var msg4 = string.Format(new MyFormatProvider(), "{0:APPEND}", new Person() { Name = "nestor" });
运行的结果:Custom nestor~
经过一番研究,虽然这两个接口定义的有点不太友好,使用起来也很别扭,但是至少是可以工作的,也是.Net Framework提供的一种方案。
最后对这篇文章进行总结:
.Net Framework提供的string.Format()方法可以控制填入的参数最后字符串返回的格式,并提供了两种形式的控制:简单格式符和特殊格式符。
1. 重写填入的参数object.ToString()方法,但是只能满足简单格式符应用场景。
2. 填入的参数实现IFormattable接口,可以满足特殊格式符应用场景,但是必须要修改定义参数的源码来完成。
3. 实现IFormatProvider接口和ICustomFormatter接口,并显式传入string.Format()方法,实现方式复杂,但是好处是不需要修改填入的参数的源码。