【原文地址】Fun With Named Formats, String Parsing, and Edge Cases
【原文发表日期】 Jan 04, 2009
最近有一次我需要通过named format而非通常的位置参数来格式化一个字符串。我们暂且不讨论这件事本身是否是个好主意,相信我,我的选择是很谨慎的。
现有的String.Format方法可以通过位置参数来格式化其值,比如:
string s = string.Format("{0} first, {1} second", 3.14, DateTime.Now);
但我想做的是通过可命名的属性及其值来实现格式化,就像这样:
var someObj = new {pi = 3.14, date = DateTime.Now};
string s = NamedFormat("{pi} first, {date} second", someObj);
在网上搜了一下,我很快在StackOverflow上的一个问题解答中找到了三种实现方式:
- 一个用扩展方法实现的更智能(或可说“很叛逆”)的ToString (Scott Hanselman提供)
- C#: String.Inject() - 通过变量键值名格式化字符串 (Oskar提供)
- FormatWith 2.0版 - 通过可命名变量格式化字符串(James Newton-King提供)
这三种方法本质上有很相似的地方,他们都通过正则表达式来处理字符串。Hanselman的思路是给object类写一个扩展方法(extension method)(请注意这在VB.NET中行不同,因为VB.NET不允许继承object类)。而James和Oskar给string类写了扩展方法。James在实现上可能更进了一步,他利用了DataBinder.Eval来处理键值,这样,你就可以定义诸如{foo.bar.baz}的格式化串(这里bar是foo的属性,而baz是bar的属性)。James的实现的这个功能是我所需要的,也是其他两者所没有的。
此外,James也很好地应用了Regex.Replace方法里的MatchEvaluator这个委托(delegate) 参数。这可能是Regex类中最没能被充分利用却又特别强大的功能之一。这样的处理使他实现的代码显得十分简洁明快。
处理大括号转义
我最近和Eilon关于这类型的字符串格式化有过交流。他提到很多开发人员经常会忽视或者将escaping彻底搞错。所以我也想测试一下以上三种实现是否能正确地处理一个brace escaping的特例。
下面的代码:
Console.WriteLine(String.Format("{{{0}}}", 123));
应该打印出 {123} 这个结果。
所以我希望上面实现的方法在处理:
Console.WriteLine(NamedFormat("{{{foo}}}", new {foo = 123}));
的时候,也能返回一样的 {123} 这个结果。然而,只有James的方法通过了这个测试。但是,当我再进一步测试这个模式:{{{{{foo}}}}}的时候,所有三个方法都没能通过。正确的输出结果应该是 {{123}}。
当然,这并不是什么大不了的事,因为这实在是一个很极端的特例,但你永远无法知道在某个时候特例会让你猝不及防,就像2008年年末那天Zune的用户们所遭遇的(译者注:Zune是微软出品的一个媒体播放器,在2008年12月30日,一个有趣但又令微软尴尬的“闰年虫”问题发生了)。更重要的是,这向我们提出了一个有趣的问题:究竟应该如何正确处理括号转义的情况?我想这会是个有趣的尝试。
我们可以通过正则表达式来处理,但那可能不太容易。你不但要面对正则表达式里令人头疼的balanced matching的情况,更麻烦的是模式的匹配结果是和连续的括号数是奇数个还是偶数个相关的。
比如,{0}} 这个模式是无效的,因为最右边括号被转义了。然而 {0}}} 是有效的。这个表达式相当于{0} 后接着}} (或者任意偶数个括号)。
效率
我早先提过,只有James的方法通过DataBinder.Eval可以来处理子属性,当然,也有人批评到Eval的调用会使整个实现的效率惨不忍睹。
个人认为,除非我亲自测试过,我很怀疑执行效率是否确实会是一个实际问题,毕竟,这里还有许多其他我们要担心的方面。不过我想何妨一试呢,我写了个命令行程序,将每个实现方法循环1000次,然后取运行时间的平均值。结果如下:

可以看到James的方法比Hanselman的要慢43倍。但即使如此,它也不过需要4.4毫秒。所以只要你复杂的逻辑循环中大量使用,问题应该不大,当然,这里确实有提高空间。
我的实现
写到这里,我想我自己通过手工字符串处理而非正则表达式来实现一下这个方法应该是个有趣的尝试。我也担心我正则表达式的功力无法正确处理我前面所提到的困难。在完成我的实现后,我再次测试了一下运行效率:

很好!去除了正则表达式的额外开销后,我的实现显然更有效率,即使,我在其中也使用了DataBinder.Eval。当然,希望我的实现是正确无误的,因为“快但是错”比“对但是慢”要糟糕得多。
放弃正则表达式的一个缺点是我的实现方法显得比较冗长。我在这里贴出了全部的代码。这里也有压缩过的解决方案,里面包括了所有的单元测试案例和其他三个实现方法。你可以自己尝试一下,看看哪些测试案例他们通过了,哪些没有。
代码的核心分为两部分。第一部分是一个私有(private)函数,是用来拆解字符串的,并将所有的结果存在一个实现了ITextExpression接口的枚举集合中。第二部分是用户调用的公用方法,用来将所有分拆开的字段重新连接起来,并对表达式进行求值,然后返回最终结果。
我想我们还可以通过合并这些操作来进一步优化,但我还是比较喜欢将字符串分解和后面的重新合并分开实现,因为这样逻辑显得更清晰,也有助于我自己理解。开始的时候,我曾打算将分解后的字符串缓存起来然后重用,因为字符串string这个类是“不可变的”(immutable)。但我实际测试后发现这样做并不能带来明显的效率上的提升。
public static class HaackFormatter
{
public static string HaackFormat(this string format, object source)
{
if (format == null) {
throw new ArgumentNullException("format");
}
var formattedStrings = (from expression in SplitFormat(format)
select expression.Eval(source)).ToArray();
return String.Join("", formattedStrings);
}
private static IEnumerable<ITextExpression> SplitFormat(string format)
{
int exprEndIndex = -1;
int expStartIndex;
do
{
expStartIndex = format.IndexOfExpressionStart(exprEndIndex + 1);
if (expStartIndex < 0)
{
//everything after last end brace index.
if (exprEndIndex + 1 < format.Length)
{
yield return new LiteralFormat(
format.Substring(exprEndIndex + 1));
}
break;
}
if (expStartIndex - exprEndIndex - 1 > 0)
{
//everything up to next start brace index
yield return new LiteralFormat(format.Substring(exprEndIndex + 1
, expStartIndex - exprEndIndex - 1));
}
int endBraceIndex = format.IndexOfExpressionEnd(expStartIndex + 1);
if (endBraceIndex < 0)
{
//rest of string, no end brace (could be invalid expression)
yield return new FormatExpression(format.Substring(expStartIndex));
}
else
{
exprEndIndex = endBraceIndex;
//everything from start to end brace.
yield return new FormatExpression(format.Substring(expStartIndex
, endBraceIndex - expStartIndex + 1));
}
} while (expStartIndex > -1);
}
static int IndexOfExpressionStart(this string format, int startIndex) {
int index = format.IndexOf('{', startIndex);
if (index == -1) {
return index;
}
//peek ahead.
if (index + 1 < format.Length) {
char nextChar = format[index + 1];
if (nextChar == '{') {
return IndexOfExpressionStart(format, index + 2);
}
}
return index;
}
static int IndexOfExpressionEnd(this string format, int startIndex)
{
int endBraceIndex = format.IndexOf('}', startIndex);
if (endBraceIndex == -1) {
return endBraceIndex;
}
//start peeking ahead until there are no more braces...
// }}}}
int braceCount = 0;
for (int i = endBraceIndex + 1; i < format.Length; i++) {
if (format[i] == '}') {
braceCount++;
}
else {
break;
}
}
if (braceCount % 2 == 1) {
return IndexOfExpressionEnd(format, endBraceIndex + braceCount + 1);
}
return endBraceIndex;
}
}
以下是辅助类的代码:
public class FormatExpression : ITextExpression
{
bool _invalidExpression = false;
public FormatExpression(string expression) {
if (!expression.StartsWith("{") || !expression.EndsWith("}")) {
_invalidExpression = true;
Expression = expression;
return;
}
string expressionWithoutBraces = expression.Substring(1
, expression.Length - 2);
int colonIndex = expressionWithoutBraces.IndexOf(':');
if (colonIndex < 0) {
Expression = expressionWithoutBraces;
}
else {
Expression = expressionWithoutBraces.Substring(0, colonIndex);
Format = expressionWithoutBraces.Substring(colonIndex + 1);
}
}
public string Expression {
get;
private set;
}
public string Format
{
get;
private set;
}
public string Eval(object o) {
if (_invalidExpression) {
throw new FormatException("Invalid expression");
}
try
{
if (String.IsNullOrEmpty(Format))
{
return (DataBinder.Eval(o, Expression) ?? string.Empty).ToString();
}
return (DataBinder.Eval(o, Expression, "{0:" + Format + "}") ??
string.Empty).ToString();
}
catch (ArgumentException) {
throw new FormatException();
}
catch (HttpException) {
throw new FormatException();
}
}
}
public class LiteralFormat : ITextExpression
{
public LiteralFormat(string literalText) {
LiteralText = literalText;
}
public string LiteralText {
get;
private set;
}
public string Eval(object o) {
string literalText = LiteralText
.Replace("{{", "{")
.Replace("}}", "}");
return literalText;
}
}
做这件事主要是为了好玩,虽然我也打算将这个方法用在Subtext(译者注:Subtext是Phil Haack领导的一个开源的blog引擎)。
如果你发现了任何我的实现方法无法正确处理的特例,请告诉我。我也会在将这个方法加入Subtext的过程中增加更多的测试案例。就我所知,它可以正确处理所有常见的格式化和brace escaping。
打印 | 张贴于 2009-01-08 17:22:33 | Tag:暂无标签
留言反馈
多谢,已据此更新,: )
同意Neilchen的意见
Handling Brace Escaping 就是 “处理大括号转义”.
"escape" 中文通常翻译为“转义”.
@demonfox: 这个的确是一篇好文章,我就是昨天看到这篇文章后,才感觉应该继续翻译Phil Haacked的文章,而且我把他这篇文章中的实现用于我的@Me功能新的模板了:)