【问题标题】:How can I create a more user-friendly string.format syntax?如何创建对用户更友好的 string.format 语法?
【发布时间】:2009-08-24 12:21:24
【问题描述】:

我需要在程序中创建一个很长的字符串,并且一直在使用 String.Format。我面临的问题是当您有超过 8-10 个参数时跟踪所有数字。

是否可以创建某种形式的重载来接受与此类似的语法?

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

【问题讨论】:

  • 每个赞后都有一个收藏。

标签: c# string


【解决方案1】:

以下内容如何,​​既适用于匿名类型(下例),也适用于常规类型(域实体等):

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;
    });
}

【讨论】:

  • 另外它适用于域实体,即Format("Dear {Title} {Forename},...", person)
  • @Preet - C# 3.0,所以是 VS2008 或 .NET 3.5 编译器,但可以很好地针对 .NET 2.0
  • @Jason - 这是一个循环论证;如果您有选择格式化(并因此输出)敏感数据的代码,那么您使用的什么方法并不重要...
  • 这段代码有一个错误,它不处理转义大括号。尝试使用这种格式,例如:“{{age}} = {age}, {{name}} = {name}”。我有一个关于如何解决这个问题的问题:stackoverflow.com/questions/1445571/…。我最终找到了解决方案,但我没有发布它,因为我对此并不满意......
  • 类似的东西应该在 .NET 框架中(并且 DataBinder.Eval 可用于按名称访问属性)
【解决方案2】:

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

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

【讨论】:

    【解决方案3】:

    不太一样,但有点欺骗它...使用扩展方法、字典和一些代码:

    这样的……

      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;
            }
    
        }
    

    【讨论】:

    • 扩展方法不起作用,String.Format 是静态的。但是你可以创建一个新的静态方法。
    【解决方案4】:

    如果年龄/姓名是您的应用程序中的一个变量怎么办。所以你需要一种排序语法来使它像 {age_1} 一样几乎是独一无二的?

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

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

    【讨论】:

    • +1 的简单性和愿意在原始问题中反对 string.Format 要求。虽然我确实喜欢 Marc Gravell 的解决方案。
    • 在我的简单示例中,您可以,但是当您输出 HTML 时,它变得更难阅读。 string test = "";
    • 确实如此,这真的取决于使用情况。即使是带有 html 的 String.Format 也很难阅读
    • 是的,但没有任何要求,何必费心……做复杂的事情只是为了让以后更容易,这只是在浪费时间。
    【解决方案5】:

    原始实现:

    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"}});
    

    您也可以使用匿名类,但这会慢得多,因为您需要反射。

    对于真正的实现,你应该使用正则表达式来

    • 允许转义 {}
    • 检查是否有未替换的占位符,这很可能是编程错误。

    【讨论】:

      【解决方案6】:

      虽然 C# 6.0 现在可以使用字符串插值来做到这一点,但有时需要在运行时使用动态格式字符串来做到这一点。我无法使用其他需要 DataBinder.Eval 的方法,因为它们在 .NET Core 中不可用,并且对 Regex 解决方案的性能不满意。

      考虑到这一点,我编写了一个基于状态机的免费正则表达式解析器。它处理无限级别的{{{escaping}}} 并在输入包含不平衡的大括号和/或其他错误时抛出FormatException。虽然 main 方法采用 Dictionary&lt;string, object&gt;,但 helper 方法也可以采用 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。当参数关闭时,键StringBuffer的值用于在字典中查找参数的值,然后将其推入输出StringBuffer

      编辑:

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

      【讨论】:

      • 是否有与 databinder.eval 等效的核心?
      猜你喜欢
      • 2010-12-27
      • 2010-11-12
      • 2019-10-17
      • 2018-04-27
      • 1970-01-01
      • 2015-06-02
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多