最近在学习正则表达式,看了deerchao的正则表达式30分钟入门教程,感觉收获不少,于是跃跃欲试的想要尝试一下,于是就想到了用正则表达式实现一个简单的string.Format方法。
我们知道,在.net中,string.Format是一个很强大的方法,可以把字符串转换成很多你想要的格式,今天就来尝试一下实现一个简单的根据大括号的参数顺序进行字符串替换的方法吧,比如我们使用
string s = string.Format("I'm {0}, a {1} years old man, {2}", "Boyd", 26, "Hello Regular Expression");
这句话最终的输出结果是:
I'm Boyd, a 26 years old man, Hello Regular Expression
我们今天就来试着实现一个Format方法,接受一个string类型的pattern,然后后面跟一个参数列表,传入替换的参数,最后返回一个替换过的string,方法的签名看起来像这样:
string Format(string pattern, params object[] args)
大体思路是利用正则表达式匹配出pattern中的大括号,然后把大括号中的数字拿出来,当做索引,在args数组里找到对应的参数,然后替换pattern对应的大括号(不考虑异常情况)。
匹配大括号的正则应该是这样的:
\{\d\} (匹配前面是一个前大括号,中间是一个数字,后面是一个后大括号的位置)
然后匹配出大括号中间的数字,可以直接用\d,为了保险起见,我用了零宽断言:
(?<=\{)\d(?=\}) (匹配前面是前大括号但不包括前大括号,中间是数字,后面是后大括号但不包括后大括号的位置)
下面是实现:
static string Format(string pattern, params object[] args)
{
string indexPattern = @"(?<=\{)\d(?=\})";
string positionPattern = @"\{\d\}";
Regex positionRegex = new Regex(positionPattern);
MatchCollection matches = positionRegex.Matches(pattern); \\找出所有匹配
if (matches != null && matches.Count > 0)
{
Regex indexRegex = new Regex(indexPattern);
for (int i = 0; i < matches.Count; i++) \\遍历匹配结果
{
string position = matches[i].Value;
int index = int.Parse(indexRegex.Match(position).Value); \\匹配出参数索引
pattern = pattern.Replace(position, args[index].ToString()); \\把pattern中的字符串进行替换,并赋值给pattern
}
}
return pattern;
}
然后我们写个测试方法测试一下:
static void Main(string[] args)
{
string pattern = "I'm {0}, {2} years old man, {1}, {2}";
object[] param = new object[] { "Boyd", "Hello World", 26 };
string target = string.Format(pattern, param);
string actual = Format(pattern, param);
Debug.Assert(string.Equals(target, actual), "Assert Failed");
}
顺利通过,OY!
好像到这就应该结束了,不过,我一时头脑发热,又加了一个测试case,用大括号去替换大括号:
static void Main(string[] args)
{
string pattern = "I'm {0}, {2} years old man, {1}, {2}";
object[] param = new object[] { "Boyd", "Hello World", 26 };
string target = string.Format(pattern, param);
string actual = Format(pattern, param);
Debug.Assert(string.Equals(target, actual), "Assert Failed");
param = new object[] { "{2}", "{1}", "{3}" };
target = string.Format(pattern, param);
actual = Format(pattern, param);
Debug.Assert(string.Equals(target, actual), "Assert Failed");
}
然后它就失败了!为什么呢?因为大括号被替换了之后,在for循环的时候又被后面匹配的字符串又替换了一次,这个悲剧啊,我冥思苦想之后,发现了一个偷梁换柱的办法,先把所有字符串里的大括号用一个特殊字符替换掉,把正则也做相应的修改,这样被替换掉的字符串就不会被二次替换了,实现如下:
static string Format(string pattern, params object[] args)
{
pattern = pattern.Replace("{", @"\{"); \\先把所有前括号替换成\{
pattern = pattern.Replace("}", @"\}"); \\再把所有后括号替换成\}
string indexPattern = @"(?<=\\\{)\d(?=\\\})"; \\正则做相应修改
string positionPattern = @"\\\{\d\\\}"; \\正则做相应修改
Regex positionRegex = new Regex(positionPattern);
MatchCollection matches = positionRegex.Matches(pattern);
Regex indexRegex = new Regex(indexPattern);
if (matches != null && matches.Count > 0)
{
for (int i = 0; i < matches.Count; i++)
{
string position = matches[i].Value;
int index = int.Parse(indexRegex.Match(position).Value);
pattern = pattern.Replace(position, args[index].ToString());
}
}
哇哈哈,我真是太聪明了,看到这,你一定会说,我擦,你这方法也太low了吧,好吧好吧,我知道你会这么说,然后我就从同事那偷学了一个高大上的方法,要用到的不再是string.Replace方法了,而是更高端的Regex.Replace,它可以接受一个委托用于处理要替换的字符串,这样就不会出现被重复替换的结果了,来看一下实现吧:
static string Format(string pattern, params object[] args)
{
string indexPattern = @"\d";
string positionPattern = @"\{\d\}";
Regex positionRegex = new Regex(positionPattern);
pattern = positionRegex.Replace(pattern, (match) =>
{
string position = match.Value;
int index = int.Parse(Regex.Match(position, indexPattern).Value);
return args[index].ToString();
});
return pattern;
}
关于Regex.Replace的详细说明请参考MSDN。
源码地址:https://github.com/boydwang/LearningNote/tree/master/Regex