曾经一度为格式化输出而困惑,看着满天遍野的结构,都不敢去轻易触动。只能使用最安全,但是 低能的object.ToString()方法。终于忍受不了这种窘困的处境,下力气研究一番,也算是有点心得,希望和大家交流一下。
鉴于该格式化输出的结构过于繁琐,我不希望文章陷入条款的解释,于是,我从一个实际问题入手,一步一步地介绍格式化输出的概念,三个接口的意义,以及使用接口的一种模式(Pattern)。
1. PhoneNumber 类
假设我们有如下的一个类,用于存储我们的手机号码。该类提供了简单的ToString()实现,即直接返回含有短线(-)的电话号码。
class
PhoneNumber

...
{
private string _number = "139-0814-2314";


public PhoneNumber() ...{ }
public PhoneNumber(string number)

...{
_number = number;
}
public string Number

...{

get ...{ return _number; }

set ...{ _number = value; }
}

public override string ToString()

...{
return _number.ToString();
}
}
2. 提供去掉短线的输出格式 (IFormattable)
现在,我们希望能输出不含有短线的电话号码。一种最简单的办法,就是提供一个属性或者方法来实现。然而,我们希望能够像以货币格式输出整数一样,能够用类似的方法,以不含有短线的方式,输出电话号码。即:
//
我们通过格式字符串(formatString),可以以货币格式输出整数。
int
d
=
100
;
string
val
=
string
.Format(
"
{0:D}
"
,d);
Console.WriteLine(val);
//
结果是(结果会因为区域设置有不同,但结构是类似的,即在数字前会出现货币符号)
//
¥100

//
我们希望能够通过如下的方式,输出不含短线的电话号码。
PhoneNumber pn
=
new
PhoneNumber();
string
val
=
string
.Format(
"
{0:R}
"
,pn);
Console.WriteLine(val);
//
期望的结果是:
//
13908142314
我们需要作的,就是实现IFormattable接口:
class
PhoneNumber : IFormattable

...
{
//同上
public override string ToString()

...{
return ToString(null, null);

}


IFormattable Members#region IFormattable Members
public string ToString(string format, IFormatProvider formatProvider)

...{
string _val = string.Empty;

switch (format)

...{
case "R":
_val = _number.Replace("-", "");
break;
default:
_val = _number.ToString(formatProvider);
break;
}
return _val;

}
#endregion
}
现在,一切工作的非常良好,我们的PhoneNumber类也非常顺利的发布出去了。一切看起来都非常不错!
3. 提供隐藏部分号码的输出格式 (IFormatProvider, ICustomFormatter)
在使用中,用户打电话问讯我们,能否提供隐藏部分号码的输出格式,即13908142314输出为139****2314。“这个容易,我们马上更新”,放下电话,我们在switch中加入了一项,就能解决问题。可是,这样做,一个潜在的问题是,每次我们得到新的需求,都需要修改我们的PhoneNumber类,这样做,每次用户都需要重新编译PhoneNumber类,以及所有和它相关的类,这样是一个高风险,违背软件设计原则的解决办法。于是,我们应该采用如下的模式来实现扩展的需求:
首先,实现一个单独的HideFormat类,实现两个接口。这个类就是我们自己定义的格式规则。
class
HideFormat : IFormatProvider, ICustomFormatter

...
{

IFormatProvider Members#region IFormatProvider Members
public object GetFormat(Type formatType)

...{
if (typeof(ICustomFormatter) == formatType)

...{
return this;
}
return null;
}
#endregion


ICustomFormatter Members#region ICustomFormatter Members
public string Format(string format, object arg, IFormatProvider formatProvider)

...{
// Now the formatProvider must have a ICustomFormatter

if (arg == null)

...{
throw new ArgumentException("The object should not be null");
}

// Describe the formatter rules here
string _val;
switch (format)

...{
case "H":
_val = arg.ToString().Replace("-", "");
_val = _val.Substring(0, 3) + "****" + _val.Substring(7, 4);
break;
default:
_val = arg.ToString();
break;
}
return _val;

}
#endregion
}
然后,修改我们的PhoneNumber类(只需要修改一次)。
class
PhoneNumber : IFormattable

...
{
//同上


IFormattable Members#region IFormattable Members
public string ToString(string format, IFormatProvider formatProvider)

...{
string _val = string.Empty;

//如果传入的是我们自己定义的格式,则使用这个格式去处理我们的格式化请求。
if (formatProvider != null)

...{
ICustomFormatter formatter = formatProvider.GetFormat(typeof(ICustomFormatter))
as ICustomFormatter;
if (null != formatter)

...{
return formatter.Format(format, this, formatProvider);
}
}
//如果传入的格式为空,或者是系统默认的格式,则按照系统默认的方式去处理我们的格式化请求。
switch (format)

...{
case "R":
_val = _number.Replace("-", "");
break;
default:
_val = _number.ToString(formatProvider);
break;
}
return _val;

}
#endregion
}
然后,为了输出隐藏部分号码的格式。我们只需要做如下的调用:
PhoneNumber pn
=
new
PhoneNumber();
SecureFormat sf
=
new
SecureFormat();

string
_val;
_val
=
string
.Format(sf,
"
{0:H}
"
, pn);
Console.WriteLine(_val);
//
输出结果是139****2314
_val
=
pn.ToString(
"
H
"
,sf);
Console.WriteLine(_val);
//
输出结果是139****2314
自此,我们完成了所有的任务和更新。最后,让我们看看扩展性。如果以后我们接到了新的格式申请,我们需要作的,就是实现一个类似SecureFormat的自定义的格式类,然后交给用户使用。不需要我们修改PhoneNumber的任何实现。这个解决方案,很好的体现了开放-闭合(Open-Close Principle)的软件设计原则。
作者:Shu Liu(本人系.NET菜鸟,希望大家给予意见和建议)
Mail: shuliu@microsoft.com