【问题标题】:Is there a better way to count string format placeholders in a string in C#?有没有更好的方法来计算 C# 中字符串中的字符串格式占位符?
【发布时间】:2010-10-31 04:52:33
【问题描述】:

我有一个模板字符串和一个来自不同来源但需要匹配以创建新的“填充”字符串的参数数组:

string templateString = GetTemplate();   // e.g. "Mr {0} has a {1}"
string[] dataItems = GetDataItems();     // e.g. ["Jones", "ceiling cat"}

string resultingString = String.Format(templateString, dataItems);
// e.g. "Mr Jones has a ceiling cat"

使用此代码,我假设模板中字符串格式占位符的数量将等于数据项的数量。在我的情况下,这通常是一个公平的假设,但即使假设错误,我也希望能够生成resultingString 而不会失败。我不介意是否有缺失数据的空格。

如果dataItems 中的项目太多,String.Format 方法可以很好地处理它。如果不够,我会得到一个异常。

为了克服这个问题,我会计算占位符的数量,如果不够,我会在 dataItems 数组中添加新项目。

要计算占位符,我目前正在使用的代码是:

private static int CountOccurrences(string haystack)
{
    // Loop through all instances of the string "}".
    int count = 0;
    int i = 0;
    while ((i = text.IndexOf("}", i)) != -1)
    {
        i++;
        count++;
    }
    return count;
}

显然,这是假设没有任何右花括号不用于格式占位符。它也只是感觉错了。 :)

有没有更好的方法来计算字符串中的字符串格式占位符?


许多人正确地指出,我标记为正确的答案在许多情况下都行不通。主要原因是:

  • 计算占位符数量的正则表达式不考虑文字大括号 ({{0}})
  • 计算占位符不考虑重复或跳过的占位符(例如"{0} has a {1} which also has a {1}"

【问题讨论】:

标签: c# .net string


【解决方案1】:

计算占位符没有帮助 - 请考虑以下情况:

“{0} ... {1} ... {0}” - 需要 2 个值

“{1} {3}” - 需要 4 个值,其中两个被忽略

第二个例子并不牵强。

例如,你可能有这样的美国英语:

String.Format("{0} {1} {2} has a {3}", firstName, middleName, lastName, animal);

在某些文化中,可能不使用中间名,您可能有:

String.Format("{0} {2} ... {3}", firstName, middleName, lastName, animal);

如果你想这样做,你需要寻找具有最大索引的格式说明符 {index[,length][:formatString]},忽略重复的大括号(例如 {{n} })。重复大括号用于在输出字符串中插入大括号作为文字。我将把编码留作练习:) - 但我认为在最一般的情况下(即使用长度和/或格式字符串)可以或不应该使用正则表达式来完成。

即使您今天没有使用 length 或 formatString,未来的开发人员可能会认为添加一个更改是无害的 - 如果这样做会破坏您的代码,那就太可惜了。

我会尝试模仿 StringBuilder.AppendFormat(由 String.Format 调用)中的代码,即使它有点难看 - 使用 Lutz Reflector 来获取此代码。基本上遍历字符串寻找格式说明符,并获取每个说明符的索引值。

【讨论】:

  • 是的,很好。教会我在标记正确答案之前我绝对应该再等一会儿。
【解决方案2】:

合并 Damovisa 和 Joe 的答案。 在 Aydsman 的 nad activa 的 cmets 之后,我已经更新了答案。

int count = Regex.Matches(templateString, @"(?<!\{)\{([0-9]+).*?\}(?!})")  //select all placeholders - placeholder ID as separate group
                 .Cast<Match>() // cast MatchCollection to IEnumerable<Match>, so we can use Linq
                 .Max(m => int.Parse(m.Groups[1].Value)) + 1; // select maximum value of first group (it's a placegolder ID) converted to int

这种方法适用于以下模板:

"{0} aa {2} bb {1}" => count = 3

"{4} aa {0} bb {0}, {0}" => count = 5

"{0} {3} , {{7}}" => 计数 = 4

【讨论】:

  • 要正确处理文字花括号,请更改正则表达式以忽略它们:@"(?
  • 今天需要用这个,发现了一个问题... var (var matches = Regex.Matches(inputText, @"(?().Max(m => int.Parse(m.Groups[1].Value)) + 1;) 首先检查匹配有超过 0 个结果...
  • “{0} {{{3}}} , {{7}}” 失败 - 返回 1 而不是 4。如果你真的想这样做,我会复制代码在StringBuilder.AppendFormat 中,而不是希望正则表达式(难以阅读,最重要的是难以测试)将是等效的。
  • 这个答案仍然有缺陷:当它应该返回 8 时,为 "{0} {3} , {{{7}}}" 返回 4。
【解决方案3】:

您始终可以使用正则表达式:

using System.Text.RegularExpressions;
// ... more code
string templateString = "{0} {2} .{{99}}. {3}"; 
Match match = Regex.Matches(templateString, 
             @"(?<!\{)\{(?<number>[0-9]+).*?\}(?!\})")
            .Cast<Match>()
            .OrderBy(m => m.Groups["number"].Value)
            .LastOrDefault();
Console.WriteLine(match.Groups["number"].Value); // Display 3

【讨论】:

  • 谢谢,我试试看。
  • 作为参考,有效的代码是: int len = new System.Text.RegularExpressions.Regex("{[0-9]+.*?}").Matches(template).Count ;
  • 问题是字符 { 和 } 在正则表达式中是特殊的,根据文档:msdn.microsoft.com/en-us/library/3206d374.aspx
  • 这行不通 - 它不考虑文字大括号 - {{ 或 }} 并且无论如何计算格式说明符的数量并没有多大用处 - 请参阅我的答案。跨度>
  • 这个答案不应该被标记为正确答案,因为它是错误的。您实际上需要编号最高的标签,因此如果 {4} 是最高的,则需要 5 个参数。
【解决方案4】:

实际上不是您问题的答案,而是您问题的可能解决方案(尽管不是一个完美优雅的解决方案);你可以用一些string.Empty 实例填充你的dataItems 集合,因为string.Format 不关心冗余项。

【讨论】:

  • 没错,我想到的。不过,我会假设占位符的最大数量。如果计数匹配(通常是这样),那就有点浪费时间和空间......
  • 浪费多少取决于您如何创建“dataItems”数组...如果您已经在“新”中构建它,那么浪费的时间将可以忽略不计,并且空间的浪费受到您使用对“string.Empty”的引用这一事实的限制,无论您多久引用它,它都是一个实例;只要数组不会停留很长时间,空间浪费的范围实际上也相当小......所有这些显然很大程度上取决于这些数组的创建方式和频率。
【解决方案5】:

如果模板字符串中没有占位符,Marqus 的回答将失败。

添加.DefaultIfEmpty()m==null 条件可解决此问题。

Regex.Matches(templateString, @"(?<!\{)\{([0-9]+).*?\}(?!})")
     .Cast<Match>()
     .DefaultIfEmpty()
     .Max(m => m==null?-1:int.Parse(m.Groups[1].Value)) + 1;

【讨论】:

  • 很抱歉,我无法让您的版本正常工作,我使用没有占位符的模板字符串进行的单元测试在您的 linq 代码中失败,尽管这个想法看起来不错(我正在学习)。跨度>
【解决方案6】:

上面提出的正则表达式存在一个问题,它会匹配“{0}}”:

Regex.Matches(templateString, @"(?<!\{)\{([0-9]+).*?\}(?!})")
...

问题是在寻找结束时 } 它使用 .* 允许初始 } 作为匹配项。因此,将其更改为在第一个 } 上停止会使该后缀检查起作用。换句话说,将其用作正则表达式:

Regex.Matches(templateString, @"(?<!\{)\{([0-9]+)[^\}]*?\}(?!\})")
...

我根据这一切做了几个静态函数,也许你会发现它们很有用。

public static class StringFormat
{
    static readonly Regex FormatSpecifierRegex = new Regex(@"(?<!\{)\{([0-9]+)[^\}]*?\}(?!\})", RegexOptions.Compiled);

    public static IEnumerable<int> EnumerateArgIndexes(string formatString)
    {
        return FormatSpecifierRegex.Matches(formatString)
         .Cast<Match>()
         .Select(m => int.Parse(m.Groups[1].Value));
    }

    /// <summary>
    /// Finds all the String.Format data specifiers ({0}, {1}, etc.), and returns the
    /// highest index plus one (since they are 0-based).  This lets you know how many data
    /// arguments you need to provide to String.Format in an IEnumerable without getting an
    /// exception - handy if you want to adjust the data at runtime.
    /// </summary>
    /// <param name="formatString"></param>
    /// <returns></returns>
    public static int GetMinimumArgCount(string formatString)
    {
        return EnumerateArgIndexes(formatString).DefaultIfEmpty(-1).Max() + 1;
    }

}

【讨论】:

  • 这个也失败了 "{0} {3} , {{{7}}}" => 返回 4 而不是 8。
【解决方案7】:

您可以“滥用”ICustomFormatter 来收集占位符并将它们返回给调用者。这只是重用了内置解析算法,而不是尝试重新实现它(并且可能偏离内置算法)。

using System;
using System.Collections.Generic;
using System.Linq;

namespace FormatPlaceholders {

    class Program {

        class FormatSnooper : IFormatProvider, ICustomFormatter {

            public object GetFormat(Type formatType) {
                return this;
            }

            public string Format(string format, object arg, IFormatProvider formatProvider) {
                Placeholders.Add(((int)arg, format));
                return null;
            }

            internal readonly List<(int index, string format)> Placeholders = new List<(int index, string format)>();

        }

        public static IEnumerable<(int index, string format)> GetFormatPlaceholders(string format, int max_count = 100) {

            var snooper = new FormatSnooper();

            string.Format(
                snooper,
                format,
                Enumerable.Range(0, max_count).Cast<object>().ToArray()
            );

            return snooper.Placeholders;

        }

        static void Main(string[] args) {
            foreach (var (index, format) in GetFormatPlaceholders("{1:foo}{4:bar}{1:baz}"))
                Console.WriteLine($"{index}: {format}");
        }

    }

}

哪些打印:

1: foo
4: bar
1: baz

然后您可以轻松找到index 的最大值、计数、查找“孔”等...


我意识到我迟到了(几年),但我需要类似于 OP 要求的东西,所以我在这里分享我想出的解决方案,以防有人发现它有用...

【讨论】:

  • 很好,这正是我所需要的。对我来说,实际的索引并不重要,所以“{3} 正在运行”应该返回 1 而不是 4。这个实现就是这样做的。
【解决方案8】:

也许您正试图用大锤敲碎坚果?

为什么不在您对 String.Format 的调用周围放置一个 try/catch

它有点难看,但以一种需要最少努力、最少测试的方式解决了您的问题,并且即使您没有考虑其他关于格式化字符串的其他问题(例如 {{ 文字,或更复杂的格式字符串,其中包含非数字字符:{0:$#,##0.00;($#,##0.00);Zero})

(是的,这意味着您不会检测到比格式说明符更多的数据项,但这是个问题吗?大概您的软件用户会注意到他们已经截断了输出并纠正了他们的格式字符串?)

【讨论】:

  • 有效的建议,但我需要它来接受输入并生成一条消息,无论它有多“错误”。
  • 捕获异常是一项昂贵的操作。
  • @NineTails:只有当他们被抛出时。
【解决方案9】:

由于我无权编辑帖子,我将提出我更短(且正确)的 Marqus 答案版本:

int num = Regex.Matches(templateString,@"(?<!\{)\{([0-9]+).*?\}(?!})")
             .Cast<Match>()
             .Max(m => int.Parse(m.Groups[0].Value)) + 1;

我正在使用 Aydsman 提出的正则表达式,但尚未对其进行测试。

【讨论】:

    【解决方案10】:

    这个问题很晚,但从另一个切线发生。

    即使使用单元测试(即缺少参数),String.Format 也是有问题的。开发人员输入了错误的位置占位符或格式化的字符串被编辑并且编译良好,但它被用于另一个代码位置甚至另一个程序集,并且您在运行时获得 FormatException。理想情况下,单元测试或集成测试应该能够捕捉到这一点。

    虽然这不是答案的解决方案,但它是一种解决方法。你(们)能做到 接受格式化字​​符串和对象列表(或数组)的辅助方法。在辅助方法内部,将列表填充为预定义的固定长度,该长度将超过消息中占位符的数量。因此,例如下面假设 10 个占位符就足够了。填充元素可以为 null 或类似“[Missing]”的字符串。

    int q = 123456, r = 76543;
    List<object> args = new List<object>() { q, r};     
    
    string msg = "Sample Message q = {2:0,0} r = {1:0,0}";
    
    //Logic inside the helper function
    int upperBound = args.Count;
    int max = 10;
    
    for (int x = upperBound; x < max; x++)
    {
        args.Add(null); //"[No Value]"
    }
    //Return formatted string   
    Console.WriteLine(string.Format(msg, args.ToArray()));
    

    这是理想的吗?不,但是对于日志记录或某些用例,它是防止运行时异常的一种可接受的替代方法。您甚至可以用“[No Value]”替换空元素和/或添加数组位置,然后在格式化字符串中测试 No Value,然后将其记录为问题。

    【讨论】:

      【解决方案11】:

      基于this answer 和大卫怀特的回答是更新版本:

      string formatString = "Hello {0:C} Bye {{300}} {0,2} {34}";
      //string formatString = "Hello";
      //string formatString = null;
      
      int n;
      var countOfParams = Regex.Matches(formatString?.Replace("{{", "").Replace("}}", "") ?? "", @"\{([0-9]+)")
          .OfType<Match>()
          .DefaultIfEmpty()
          .Max(m => Int32.TryParse(m?.Groups[1]?.Value, out n) ? n : -1 )
          + 1;
      
      Console.Write(countOfParams);
      

      注意事项:

      1. 更换是一种更直接的方法来处理双花括号。这类似于StringBuilder.AppendFormatHelper 在内部处理它们的方式。
      2. 与消除“{{”和“}}”一样,正则表达式可以简化为“{([0-9]+)”
      3. 即使 formatString 为空,这也可以工作
      4. 即使存在无效格式(例如“{3444444456}”),它也可以正常工作。通常这会导致整数溢出。

      【讨论】:

        【解决方案12】:

        您可以使用正则表达式来计算仅具有您将在它们之间使用的格式的 {} 对。 @"\{\d+\}" 已经足够好了,除非你使用格式化选项。

        【讨论】:

        • 谢谢,是的,我没有格式化任何东西 - 一切都以字符串的形式出现。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-11-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-03-29
        • 1970-01-01
        相关资源
        最近更新 更多