String.Format类实用程序的正则表达式

我正在编写一个名为StringTemplate的类,它允许使用String.Format格式化对象,但使用名称而不是占位符的索引。 这是一个例子:

 string s = StringTemplate.Format("Hello {Name}. Today is {Date:D}, and it is {Date:T}.", new { Name = "World", Date = DateTime.Now }); 

为了实现这个结果,我寻找占位符并用索引替换它们。 然后我将生成的格式字符串传递给String.Format

这种方法很好,除非有双重括号,这是一个转义序列。 所需的行为(与String.Format相同)如下所述:

  • “Hello {Name}”应格式化为“Hello World”
  • “Hello {{Name}}”应格式化为“Hello {Name}”
  • “Hello {{{Name}}}”应格式化为“Hello {World}”
  • “Hello {{{{Name}}}}”应格式化为“Hello {{Name}}”

等等…

但是我当前的正则表达式没有检测到转义序列,并且始终将括号中的子字符串视为占位符,因此我得到类似“Hello {0}”的内容

这是我当前的正则表达式:

 private static Regex _regex = new Regex(@"{(?\w+)(?:[^}]+)?}", RegexOptions.Compiled); 

如何修改此正则表达式以忽略转义大括号? 看起来真的很难的是我应该根据括号的数量是奇数还是偶数来检测占位符……我想不出用正则表达式做一个简单的方法,它甚至可能吗?


为了完整性,这是StringTemplate类的完整代码:

 public class StringTemplate { private string _template; private static Regex _regex = new Regex(@"{(?\w+)(?:[^}]+)?}", RegexOptions.Compiled); public StringTemplate(string template) { if (template == null) throw new ArgumentNullException("template"); this._template = template; } public static implicit operator StringTemplate(string s) { return new StringTemplate(s); } public override string ToString() { return _template; } public string Format(IDictionary values) { if (values == null) { throw new ArgumentNullException("values"); } Dictionary indexes = new Dictionary(); object[] array = new object[values.Count]; int i = 0; foreach (string key in values.Keys) { array[i] = values[key]; indexes.Add(key, i++); } MatchEvaluator evaluator = (m) => { if (m.Success) { string key = m.Groups["key"].Value; string format = m.Groups["format"].Value; int index = -1; if (indexes.TryGetValue(key, out index)) { return string.Format("{{{0}{1}}}", index, format); } } return string.Format("{{{0}}}", m.Value); }; string templateWithIndexes = _regex.Replace(_template, evaluator); return string.Format(templateWithIndexes, array); } private static IDictionary MakeDictionary(object obj) { Dictionary dict = new Dictionary(); foreach (var prop in obj.GetType().GetProperties()) { dict.Add(prop.Name, prop.GetValue(obj, null)); } return dict; } public string Format(object values) { return Format(MakeDictionary(values)); } public static string Format(string template, IDictionary values) { return new StringTemplate(template).Format(values); } public static string Format(string template, object values) { return new StringTemplate(template).Format(values); } } 

您可以使用正则表达式匹配平衡对,然后找出如何处理大括号。 请记住,.NET正则表达式不是“常规”。

 class Program { static void Main(string[] args) { var d = new Dictionary { { "Name", "World" } }; var t = new Test(); Console.WriteLine(t.Replace("Hello {Name}", d)); Console.WriteLine(t.Replace("Hello {{Name}}", d)); Console.WriteLine(t.Replace("Hello {{{Name}}}", d)); Console.WriteLine(t.Replace("Hello {{{{Name}}}}", d)); Console.ReadKey(); } } class Test { private Regex MatchNested = new Regex( @"\{ (?> ([^{}]+) | \{ (?) | \} (?<-D>) )* (?(D)(?!)) \}", RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled | RegexOptions.Singleline); public string Replace(string input, Dictionary vars) { Matcher matcher = new Matcher(vars); return MatchNested.Replace(input, matcher.Replace); } private class Matcher { private Dictionary Vars; public Matcher(Dictionary vars) { Vars = vars; } public string Replace(Match m) { string name = m.Groups[1].Value; int length = (m.Groups[0].Length - name.Length) / 2; string inner = (length % 2) == 0 ? name : Vars[name]; return MakeString(inner, length / 2); } private string MakeString(string inner, int braceCount) { StringBuilder sb = new StringBuilder(inner.Length + (braceCount * 2)); sb.Append('{', braceCount); sb.Append(inner); sb.Append('}', braceCount); return sb.ToString(); } } } 

使用正则表达式很可能 – 但我完全不相信它是最容易维护的解决方案。 鉴于你真的对这里的大括号和冒号感兴趣(我认为),我个人会避免使用正则表达式。

我将构造一系列标记,每个标记都是文字或格式字符串。 通过沿着弦走,并注意开口和闭合括号来构造它。 然后评估序列只是连接令牌的问题,在适当的时候格式化每个令牌。

然后我再也不是很喜欢正则表达式 – 只是偶尔他们很精彩,但很多时候他们觉得有点过分。 在这种情况下,也许有一些聪明的方法让他们做你想做的事……

顺便说一句,你需要定义在大括号不匹配的情况下你想要发生什么,例如

 {{Name} foo 

使用正则表达式通常很容易决定奇偶校验。 例如,这是一个表达式,匹配任何具有偶数A的字符串,但不匹配奇数:

 (AA)* 

所以你需要做的就是找到只匹配奇数{ s和} s的表达式。

 {({{)* }(}})* 

(尽管逃避了角色)。 所以将这个想法添加到你当前的表达式会产生类似的东西

 {({{)*(?\w+)(?:[^}]+)?}(}})* 

但是,这与双方大括号的基数不符。 换句话说, {{{将匹配} ,因为它们都是奇怪的。 正则表达式无法计算内容,因此您无法找到与您想要的基数匹配的表达式。

实际上,您应该做的是使用自定义解析器解析字符串,该解析器读取字符串并计算{但不是实例{{以便将它们与}实例匹配但不是}}实例。 我想你会发现这就是.NET中的String格式化程序无论如何都在幕后工作,因为正则表达式不适合解析任何类型的嵌套结构。

或者您可以同时使用两种想法:将潜在的令牌与正则表达式匹配,然后使用快速检查结果匹配来validation其大括号的平衡。 但这可能最终会让人感到困惑和间接。 你通常最好为这种场景编写自己的解析器。

我最终使用了一种类似于加文建议的技术。

我更改了正则表达式,以便它匹配占位符周围的所有大括号:

 private static Regex _regex = new Regex(@"(?{+)(?\w+)(?:[^}]+)?(?}+)", RegexOptions.Compiled); 

我更改了MatchEvaluator的逻辑,以便正确处理转义大括号:

  MatchEvaluator evaluator = (m) => { if (m.Success) { string open = m.Groups["open"].Value; string close = m.Groups["close"].Value; string key = m.Groups["key"].Value; string format = m.Groups["format"].Value; if (open.Length % 2 == 0) return m.Value; open = RemoveLastChar(open); close = RemoveLastChar(close); int index = -1; if (indexes.TryGetValue(key, out index)) { return string.Format("{0}{{{1}{2}}}{3}", open, index, format, close); } else { return string.Format("{0}{{{{{1}}}{2}}}{3}", open, key, format, close); } } return m.Value; }; 

如果需要,我依赖String.Format抛出FormatException 。 我做了一些unit testing,到目前为止看起来工作得很好……

谢谢大家的帮助!