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

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

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

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

如果dataItems中的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}"

合并Damovisa和Joe的答案。 我已经更新了Aydsman的nad activa评论的答案。

 int count = Regex.Matches(templateString, @"(?() // cast MatchCollection to IEnumerable, 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}}”=> count = 4

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

“{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}})。 重复大括号用于在输出字符串中将大括号插入文字。 我将把编码留作练习:) – 但我不认为它可以或应该在最常见的情况下使用Regex(即使用length和/或formatString)。

即使您今天没有使用length或formatString,未来的开发人员可能会认为添加一个是一个无害的变化 – 这会破坏您的代码将是一种耻辱。

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

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

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

实际上不是你问题的答案,而是你问题的可能解决方案(虽然不是一个非常优雅的问题); 您可以使用许多string.Empty实例填充dataItems集合,因为string.Format不关心冗余项。

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

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

 Regex.Matches(templateString, @"(?() .DefaultIfEmpty() .Max(m => m==null?-1:int.Parse(m.Groups[1].Value)) + 1; 

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

 Regex.Matches(templateString, @"(? 

问题是在寻找它使用的关闭时。*允许初始}作为匹配。 因此,将其更改为停在第一个}会使后缀检查工作。 换句话说,使用它作为正则表达式:

 Regex.Matches(templateString, @"(? 

我基于这一切制作了几个静态函数,也许你会发现它们很有用。

 public static class StringFormat { static readonly Regex FormatSpecifierRegex = new Regex(@"(? EnumerateArgIndexes(string formatString) { return FormatSpecifierRegex.Matches(formatString) .Cast() .Select(m => int.Parse(m.Groups[1].Value)); } ///  /// 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. ///  ///  ///  public static int GetMinimumArgCount(string formatString) { return EnumerateArgIndexes(formatString).DefaultIfEmpty(-1).Max() + 1; } } 

也许你正试图用大锤打破坚果?

为什么不在调用String.Format时调用try / catch

它有点难看,但是以一种需要最少努力,最少测试的方式解决你的问题,并且即使还有其他关于你没有考虑的格式化字符串的东西(例如{{literals,或更复杂的格式),也可以保证工作其中包含非数字字符的字符串:{0:$#,## 0.00;($#,## 0.00); Zero})

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

由于我没有权限编辑post,我会提出我的更短(和正确)版本的Marqus答案:

 int num = Regex.Matches(templateString,@"(?() .Max(m => int.Parse(m.Groups[0].Value)) + 1; 

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

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

即使使用unit testing(即缺少参数),String.Format也存在问题。 开发人员放入错误的位置占位符或编辑格式化的字符串并编译正常,但它在另一个代码位置或甚至另一个程序集中使用,并且您在运行时获得FormatException。 理想情况下,unit testing或集成测试应该抓住这个。

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

 int q = 123456, r = 76543; List args = new List() { 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())); 

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

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

基于这个答案 ,David White的答案是更新版本:

 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() .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为null,这也可以工作
  4. 即使格式无效,这也会有用,比如'{3444444456}’。 通常这会导致整数溢出。