如何在运行时使用Interpolated [Named]值获取String.Format之类的行为?

时间:2016-04-05 21:30:28

标签: c# string formatting

我有一类允许的替换值:

class MaskDictionary
{
    public int id { get; set; }
    public string last { get; set; }
    public string lastinitial { get; set; }
    public string first { get; set; }
    public string firstinitial { get; set; }
    public string salutation { get; set; }
    public DateTime today { get; set; }
}

我希望将格式化字符串作为用户输入,例如:

string userFormat = "{last}, {first} {today}";

并生成插值。概念上类似于:

string.Format("{last}, {first} {today}", MaskDictionary);

但是输入字符串动态失败:

string.Format(userFormat, MaskDictionary);

提供运行时格式的简单,干净的方法是什么?

有些笨重的选项使用反射和递归替换,比如

        string userFormat = "{last}, {first} {today}";
        PropertyInfo[] properties = typeof(MaskDictionary).GetProperties();
        foreach (PropertyInfo property in properties)
        {
            userFormat = string.Replace(property.name, property.GetValue(mask));
        }

但必须有更好的方法。

- 通过比较答案进行更新 -

我在性能答案中测试了两个提出的解决方案并获得了非常令人惊讶的结果。

  static class Format2
    {
        static public string Format(string format, MaskDictionary md)
        {
            string val = format;
            foreach (PropertyInfo property in typeof(MaskDictionary).GetProperties())
            {
                val = val.Replace("{" + property.Name + "}", property.GetValue(md).ToString());
            }
            return val;
        }
    }
static class Format1
{
    public static string FormatWith(this string format, IFormatProvider provider, object source)
    {
        if (format == null)
            throw new ArgumentNullException("format");

        Regex r = new Regex(@"(?<start>\{)+(?<property>[\w\.\[\]]+)(?<format>:[^}]+)?(?<end>\})+",
          RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);

        List<object> values = new List<object>();
        string rewrittenFormat = r.Replace(format, delegate (Match m)
        {
            Group startGroup = m.Groups["start"];
            Group propertyGroup = m.Groups["property"];
            Group formatGroup = m.Groups["format"];
            Group endGroup = m.Groups["end"];

            values.Add((propertyGroup.Value == "0")
              ? source
              : DataBinder.Eval(source, propertyGroup.Value));

            return new string('{', startGroup.Captures.Count) + (values.Count - 1) + formatGroup.Value
              + new string('}', endGroup.Captures.Count);
        });

        return string.Format(provider, rewrittenFormat, values.ToArray());
    }
}

Regex解决方案速度慢,速度慢得多。使用1000个迭代的短格式字符串(20个字符,3个替换)与5属性字典对象和105属性字典对象,以及长格式字符串(2000个字符,3个替换)和一个长字典对象,我得跟随结果:

短格式,小字典
正则表达式 - 2150毫秒
更换 - 3毫秒
短格式,大字典
正则表达式 - 2160毫秒
更换 - 30毫秒
长格式,短字典
正则表达式 - 2170毫秒
更换 - 26毫秒
长格式,大字典
正则表达式 - 2250毫秒
替换 - 330毫秒

替换不会使用大型字典进行缩放,但它的启动速度要快得多,以至于需要大字典和非常长的格式字符串才能更慢。使用105属性字典,需要大约16,000个字符格式字符串才能处理相同的时间,~2500ms。使用5属性小字典,正则表达式从未如此快。正则表达式的600K字符格式字符串需要14000毫秒,替换需要7000毫秒,1.7M字符格式字符串需要38000毫秒和21000毫秒。只要字典对象的大小合理且格式字符串短于80页,就替换wins。

4 个答案:

答案 0 :(得分:2)

詹姆斯·牛顿·金(JSON人)使用FormatWith()定义的in this blog post扩展方法,它基本上可以完成你想要做的事情:

public static string FormatWith(this string format, IFormatProvider provider, object source)
{
  if (format == null)
    throw new ArgumentNullException("format");

  Regex r = new Regex(@"(?<start>\{)+(?<property>[\w\.\[\]]+)(?<format>:[^}]+)?(?<end>\})+",
    RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);

  List<object> values = new List<object>();
  string rewrittenFormat = r.Replace(format, delegate(Match m)
  {
    Group startGroup = m.Groups["start"];
    Group propertyGroup = m.Groups["property"];
    Group formatGroup = m.Groups["format"];
    Group endGroup = m.Groups["end"];

    values.Add((propertyGroup.Value == "0")
      ? source
      : DataBinder.Eval(source, propertyGroup.Value));

    return new string('{', startGroup.Captures.Count) + (values.Count - 1) + formatGroup.Value
      + new string('}', endGroup.Captures.Count);
  });

  return string.Format(provider, rewrittenFormat, values.ToArray());
}

它基本上依赖正则表达式和.NET Databinder类来处理执行实际的匹配和替换。

答案 1 :(得分:1)

框架中没有任何内容,但是有一个聪明的extension method可以用来按名称将属性注入到格式字符串中:

string result = "{last}, {first} {today}".FormatWith(MaskDictionary);

你可以获得最接近扩展名的是在C#6中使用字符串插值:

string result = $"{MaskDictionary.last}, {MaskDictionary.first} {MaskDictionary.today}";

答案 2 :(得分:1)

您可以将代码转换为ToString(...)函数:

class MaskDictionary
{
    // ... properties ...
    [DisplayName("bar")]
    public string foo {get;set;}
    public int baz {get;set;}

    public string ToString(string format)
    {
        string val = format;
        foreach (PropertyInfo property in typeof(MaskDictionary).GetProperties())
        {
            var dispAttr = (DisplayNameAttribute)Attribute.GetCustomAttribute(property, typeof(DisplayNameAttribute));
            string pName = dispAttr != null ? dispAttr.DisplayName : property.Name;
            val = val.Replace("{" + pName + "}", property.GetValue(this).ToString());
        }
        return val;
    }
}

修改:这是一个允许您重命名用户格式代码而无需重命名属性的版本:

var m = new MaskDictionary();
m.foo = "hello";
m.baz = 111;
Console.WriteLine(m.ToString("{foo} {bar} {baz}"));
//output: {foo} hello 111

用法:

{{1}}

答案 3 :(得分:0)

我有一个名为FormatWith的库,它使用字符串扩展方法(FormatWith())来完成此操作。它与James Newton King的实现类似,但有一些优点:

  • 没有正则表达式。扩展使用状态机解析器来处理输入字符串,这更快并正确处理转义括号。

  • 不依赖于DataBinder。 DataBinder在.NET Core上不可用,与未经过无限制的输入一起使用时也很危险。

  • 适用于实现.NET Standard 2.0的任何内容,因此可以在.NET Core和ASP.NET Core应用程序中使用。