【问题标题】:Adding a separator to a list of items for display将分隔符添加到要显示的项目列表中
【发布时间】:2009-05-13 01:06:25
【问题描述】:

我有一个我希望在 C# 中用分隔符显示的项目列表。使用普通的迭代器,我会在开头或结尾添加一个额外的分隔符:

string[] sa = {"one", "two", "three", "four"};
string ns = "";
foreach(string s in sa)
{
    ns += s + " * ";
}
// ns has a trailing *:
// one * two * three * four * 

现在我可以像这样使用 for 循环来解决这个问题:

ns = "";
for(int i=0; i<sa.Length; i++)
{
    ns += sa[i];
    if(i != sa.Length-1)
        ns += " * ";
}
// this works:
// one * two * three * four

虽然第二种解决方案有效,但它看起来不是很优雅。有没有更好的方法来做到这一点?

【问题讨论】:

    标签: c# algorithm


    【解决方案1】:

    你需要内置的String.Join方法:

    string ns = string.Join(" * ", sa);
    

    如果你想对其他集合类型做同样的事情,那么如果你首先使用 LINQ 的 ToArray 方法创建一个数组,你仍然可以使用 String.Join

    string ns = string.Join(" * ", test.ToArray());
    

    【讨论】:

      【解决方案2】:

      除了优雅之外,您可能还需要考虑跨类型而不是 String 的速度和可重用性。为了优雅,我建议使用扩展方法来抽象出细节,以便常见用法看起来像:

      ns = sa.Join(" * ");
      

      为了提高速度,请考虑以下变体测试,包括其他回答问题的人提出的一些解决方案:

      public void Test_variants()
      {
          const string item = "a";
          const int numberOfTimes = 100000;
          const string delimiter = ", ";
          string[] items = new List<string>(Enumerable.Repeat(item, numberOfTimes)).ToArray();
          string expected = String.Join(delimiter, items);
      
          Time(StringJoin, items, delimiter, expected);
          Time(Aggregate, items, delimiter, expected);
          Time(CheckForEndInsideLoop_String, items, delimiter, expected);
          Time(CheckForBeginningInsideLoop_String, items, delimiter, expected);
          Time(RemoveFinalDelimiter_String, items, delimiter, expected);
          Time(CheckForEndInsideLoop_StringBuilder, items, delimiter, expected);
          Time(RemoveFinalDelimiter_StringBuilder, items, delimiter, expected);
      }
      
      private static void Time(Func<string[], string, string> func, string[] items, string delimiter, string expected)
      {
          Stopwatch stopwatch = new Stopwatch();
          stopwatch.Start();
          string result = func(items, delimiter);
          stopwatch.Stop();
          bool isValid = result == expected;
          Console.WriteLine("{0}\t{1}\t{2}", stopwatch.Elapsed, isValid, func.Method.Name);
      }
      
      private static string CheckForEndInsideLoop_String(string[] items, string delimiter)
      {
          string result = "";
          for (int i = 0; i < items.Length; i++)
          {
              result += items[i];
              if (i != items.Length - 1)
              {
                  result += delimiter;
              }
          }
          return result;
      }
      
      private static string RemoveFinalDelimiter_String(string[] items, string delimiter)
      {
          string result = "";
          for (int i = 0; i < items.Length; i++)
          {
              result += items[i] + delimiter;
          }
          return result.Substring(0, result.Length - delimiter.Length);
      }
      
      private static string CheckForBeginningInsideLoop_String(string[] items, string delimiter)
      {
          string result = "";
          foreach (string s in items)
          {
              if (result.Length != 0)
              {
                  result += delimiter;
              }
              result += s;
          }
          return result;
      }
      
      private static string CheckForEndInsideLoop_StringBuilder(string[] items, string delimiter)
      {
          StringBuilder result = new StringBuilder();
          for (int i = 0; i < items.Length; i++)
          {
              result.Append(items[i]);
              if (i != items.Length - 1)
              {
                  result.Append(delimiter);
              }
          }
          return result.ToString();
      }
      
      private static string RemoveFinalDelimiter_StringBuilder(string[] items, string delimiter)
      {
          StringBuilder result = new StringBuilder();
          for (int i = 0; i < items.Length; i++)
          {
              result.Append(items[i]);
              result.Append(delimiter);
          }
          result.Length = result.Length - delimiter.Length;
          return result.ToString();
      }
      
      private static string StringJoin(string[] items, string delimiter)
      {
          return String.Join(delimiter, items);
      }
      
      private static string Aggregate(string[] items, string delimiter)
      {
          return items.Aggregate((c, s) => c + delimiter + s);
      }
      

      我的盒子上的结果如下:

      00:00:00.0027745 真正的字符串连接 00:00:24.5523967 真正的聚合 00:00:47.8091632 True CheckForEndInsideLoop_String 00:00:47.4682981 真正的 CheckForBeginningInsideLoop_String 00:00:23.7972864 True RemoveFinalDelimiter_String 00:00:00.0076439 真正的 CheckForEndInsideLoop_StringBuilder 00:00:00.0052803 True RemoveFinalDelimiter_StringBuilder

      这意味着,如果您只使用字符串数组,最好的选择是 String.Join,紧随其后的是 StringBuilder 变体。请注意,在使用字符串时检查循环内的最后一项会比使用 StringBuilder 时产生更大的差异。当要分隔的项目列表很小时,基于字符串的实现的性能也有很大提高。我将 numberOfItems 设置为 10 进行了相同的测试,并收到了以下结果:

      00:00:00.0001788 真正的字符串连接 00:00:00.0014983 真实聚合 00:00:00.0001666 True CheckForEndInsideLoop_String 00:00:00.0002202 True CheckForBeginningInsideLoop_String 00:00:00.0002061 True RemoveFinalDelimiter_String 00:00:00.0002663 True CheckForEndInsideLoop_StringBuilder 00:00:00.0002278 True RemoveFinalDelimiter_StringBuilder

      您可能要考虑的下一件事是可重用性。如果您想从由分隔符分隔的整数列表构建字符串,则 String.Join 仅在您对每个整数运行 .ToString() 并创建字符串数组后才可以选择(因为 String.Join 无法对 IEnumerable 进行操作)。

      因此,总而言之,您可以考虑使用以下几行的扩展方法来获得优雅、速度和可重用性的良好组合:

      public static string Join<T>([CanBeNull] this IEnumerable<T> items, [CanBeNull] string delimiter)
      {
          StringBuilder result = new StringBuilder();
          if (items != null && items.Any())
          {
              delimiter = delimiter ?? "";
              foreach (T item in items)
              {
                  result.Append(item);
                  result.Append(delimiter);
              }
              result.Length = result.Length - delimiter.Length;
          }
          return result.ToString();
      }
      

      用法:

      ns = sa.Join(" * ");
      

      【讨论】:

      【解决方案3】:

      这种方法的优点是您可以在任何类型的序列上使用它,而不仅仅是字符串数组。

      var ns = sa.Aggregate( (c, s) => c + " * " + s);
      

      【讨论】:

      • Joe,因为无论如何您都在使用 LINQ,如果您从序列创建一个数组然后使用 String.Join,您将获得更好的性能和更易读的代码。例如:string.Join(" * ", test.ToArray());
      • 是的,可以将某个字段的对象序列投影到字符串序列中并使用您的方法。我之前不知道 string.Join - 为什么 LINQ 不是灵丹妙药的一个完美例子:)
      【解决方案4】:

      我更喜欢卢克的解决方案。

      string ns = string.Join(" * ", sa);
      

      如果您的集合不可索引而只能枚举,您也可以这样做:

      string ns = "";
      foreach(string s in sa)
      {
          if (ns.Length != 0)
          {
              ns += " * ";
          }
      
          ns += s;
      }
      

      这就像您的第二个示例,但它将测试放在循环的开头,并且不太可能像您的第二个示例中那样遇到一次性错误。数组显然是可索引的,但在某些情况下,您会得到不可索引的容器(即 System.Collections.Generic.Dictionary.Values),而您会想要这样的东西。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-11-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-02-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多