我怎样才能创build一个更加用户友好的string.format语法?

我需要在程序中创build一个非常长的string,并使用String.Format。 我面临的问题是跟踪所有的数字,当你有超过8-10参数。

是否可以创build某种forms的重载,接受类似于这个的语法?

String.Format("You are {age} years old and your last name is {name} ", {age = "18", name = "Foo"}); 

那么下面这个如何处理匿名types(下面的例子)或常规types(域实体等):

 static void Main() { string s = Format("You are {age} years old and your last name is {name} ", new {age = 18, name = "Foo"}); } 

使用:

 static readonly Regex rePattern = new Regex( @"(\{+)([^\}]+)(\}+)", RegexOptions.Compiled); static string Format(string pattern, object template) { if (template == null) throw new ArgumentNullException(); Type type = template.GetType(); var cache = new Dictionary<string, string>(); return rePattern.Replace(pattern, match => { int lCount = match.Groups[1].Value.Length, rCount = match.Groups[3].Value.Length; if ((lCount % 2) != (rCount % 2)) throw new InvalidOperationException("Unbalanced braces"); string lBrace = lCount == 1 ? "" : new string('{', lCount / 2), rBrace = rCount == 1 ? "" : new string('}', rCount / 2); string key = match.Groups[2].Value, value; if(lCount % 2 == 0) { value = key; } else { if (!cache.TryGetValue(key, out value)) { var prop = type.GetProperty(key); if (prop == null) { throw new ArgumentException("Not found: " + key, "pattern"); } value = Convert.ToString(prop.GetValue(template, null)); cache.Add(key, value); } } return lBrace + value + rBrace; }); } 

不完全相同,但有点欺骗它…使用扩展方法,字典和一个小代码:

像这样的东西…

  public static class Extensions { public static string FormatX(this string format, params KeyValuePair<string, object> [] values) { string res = format; foreach (KeyValuePair<string, object> kvp in values) { res = res.Replace(string.Format("{0}", kvp.Key), kvp.Value.ToString()); } return res; } } 

如果年龄/名称是你的应用程序中的一个variables呢? 所以你需要一个sorting语法来使它几乎像{age_1}一样独一无二?

如果你有8-10个参数有问题:为什么不用

 "You are " + age + " years old and your last name is " + name + " 

原始实现:

 public static class StringUtility { public static string Format(string pattern, IDictionary<string, object> args) { StringBuilder builder = new StringBuilder(pattern); foreach (var arg in args) { builder.Replace("{" + arg.Key + "}", arg.Value.ToString()); } return builder.ToString(); } } 

用法:

 StringUtility.Format("You are {age} years old and your last name is {name} ", new Dictionary<string, object>() {{"age" = 18, "name" = "Foo"}}); 

你也可以使用一个匿名类,但是由于你需要reflection,所以速度要慢一些。

对于一个真正的实现,你应该使用正则expression式来

  • 允许转义{
  • 检查是否有占位符,哪里没有被replace,这是最有可能的编程错误。

从C#6开始,这种string插值现在可以使用新的string插值语法:

 var formatted = $"You are {age} years old and your last name is {name}"; 

尽pipeC#6.0现在可以通过string插值来实现这一点,但是有时候需要在运行时使用dynamic格式string来完成。 我一直无法使用其他需要DataBinder.Eval的方法,因为它们在.NET Core中不可用,并且对Regex解决scheme的性能不满意。

考虑到这一点,下面是我写的一个基于状态机的正则expression式parsing器。 它处理{{{escaping}}}无限级别,并在input包含不平衡大括号和/或其他错误时抛出FormatException 。 尽pipemain方法需要一个Dictionary<string, object> ,但helper方法也可以通过reflection来获取object并使用它的参数。

 public static class StringExtension { /// <summary> /// Extension method that replaces keys in a string with the values of matching object properties. /// </summary> /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param> /// <param name="injectionObject">The object whose properties should be injected in the string</param> /// <returns>A version of the formatString string with keys replaced by (formatted) key values.</returns> public static string FormatWith(this string formatString, object injectionObject) { return formatString.FormatWith(GetPropertiesDictionary(injectionObject)); } /// <summary> /// Extension method that replaces keys in a string with the values of matching dictionary entries. /// </summary> /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param> /// <param name="dictionary">An <see cref="IDictionary"/> with keys and values to inject into the string</param> /// <returns>A version of the formatString string with dictionary keys replaced by (formatted) key values.</returns> public static string FormatWith(this string formatString, IDictionary<string, object> dictionary) { char openBraceChar = '{'; char closeBraceChar = '}'; return FormatWith(formatString, dictionary, openBraceChar, closeBraceChar); } /// <summary> /// Extension method that replaces keys in a string with the values of matching dictionary entries. /// </summary> /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param> /// <param name="dictionary">An <see cref="IDictionary"/> with keys and values to inject into the string</param> /// <returns>A version of the formatString string with dictionary keys replaced by (formatted) key values.</returns> public static string FormatWith(this string formatString, IDictionary<string, object> dictionary, char openBraceChar, char closeBraceChar) { string result = formatString; if (dictionary == null || formatString == null) return result; // start the state machine! // ballpark output string as two times the length of the input string for performance (avoids reallocating the buffer as often). StringBuilder outputString = new StringBuilder(formatString.Length * 2); StringBuilder currentKey = new StringBuilder(); bool insideBraces = false; int index = 0; while (index < formatString.Length) { if (!insideBraces) { // currently not inside a pair of braces in the format string if (formatString[index] == openBraceChar) { // check if the brace is escaped if (index < formatString.Length - 1 && formatString[index + 1] == openBraceChar) { // add a brace to the output string outputString.Append(openBraceChar); // skip over braces index += 2; continue; } else { // not an escaped brace, set state to inside brace insideBraces = true; index++; continue; } } else if (formatString[index] == closeBraceChar) { // handle case where closing brace is encountered outside braces if (index < formatString.Length - 1 && formatString[index + 1] == closeBraceChar) { // this is an escaped closing brace, this is okay // add a closing brace to the output string outputString.Append(closeBraceChar); // skip over braces index += 2; continue; } else { // this is an unescaped closing brace outside of braces. // throw a format exception throw new FormatException($"Unmatched closing brace at position {index}"); } } else { // the character has no special meaning, add it to the output string outputString.Append(formatString[index]); // move onto next character index++; continue; } } else { // currently inside a pair of braces in the format string // found an opening brace if (formatString[index] == openBraceChar) { // check if the brace is escaped if (index < formatString.Length - 1 && formatString[index + 1] == openBraceChar) { // there are escaped braces within the key // this is illegal, throw a format exception throw new FormatException($"Illegal escaped opening braces within a parameter - index: {index}"); } else { // not an escaped brace, we have an unexpected opening brace within a pair of braces throw new FormatException($"Unexpected opening brace inside a parameter - index: {index}"); } } else if (formatString[index] == closeBraceChar) { // handle case where closing brace is encountered inside braces // don't attempt to check for escaped braces here - always assume the first brace closes the braces // since we cannot have escaped braces within parameters. // set the state to be outside of any braces insideBraces = false; // jump over brace index++; // at this stage, a key is stored in current key that represents the text between the two braces // do a lookup on this key string key = currentKey.ToString(); // clear the stringbuilder for the key currentKey.Clear(); object outObject; if (!dictionary.TryGetValue(key, out outObject)) { // the key was not found as a possible replacement, throw exception throw new FormatException($"The parameter \"{key}\" was not present in the lookup dictionary"); } // we now have the replacement value, add the value to the output string outputString.Append(outObject); // jump to next state continue; } // if } else { // character has no special meaning, add it to the current key currentKey.Append(formatString[index]); // move onto next character index++; continue; } // else } // if inside brace } // while // after the loop, if all braces were balanced, we should be outside all braces // if we're not, the input string was misformatted. if (insideBraces) { throw new FormatException("The format string ended before the parameter was closed."); } return outputString.ToString(); } /// <summary> /// Creates a Dictionary from an objects properties, with the Key being the property's /// name and the Value being the properties value (of type object) /// </summary> /// <param name="properties">An object who's properties will be used</param> /// <returns>A <see cref="Dictionary"/> of property values </returns> private static Dictionary<string, object> GetPropertiesDictionary(object properties) { Dictionary<string, object> values = null; if (properties != null) { values = new Dictionary<string, object>(); PropertyDescriptorCollection props = TypeDescriptor.GetProperties(properties); foreach (PropertyDescriptor prop in props) { values.Add(prop.Name, prop.GetValue(properties)); } } return values; } } 

最终,所有的逻辑归结为10个主要状态 – 当状态机在括号外,同样在括号内时,下一个字符是开放大括号,转义开放大括号,封闭大括号,转义的大括号,或一个普通的字符。 这些条件中的每一个都是在循环过程中单独处理的,将字符添加到输出StringBuffer或键StringBuffer 。 当一个参数被closures时,关键字StringBuffer的值被用来查找字典中的参数值,然后被压入到输出StringBuffer

编辑:

我已经把它变成了一个完整的项目在https://github.com/crozone/FormatWith