【问题标题】:Is there a lazy `String.Split` in C#C# 中是否有一个惰性的`String.Split`
【发布时间】:2015-01-27 19:33:31
【问题描述】:

所有 string.Split 方法似乎都返回一个字符串数组 (string[])。

我想知道是否有一个惰性变体返回一个IEnumerable<string>,这样一个用于大字符串(或无限长度IEnumerable<char>),当一个人只对第一个子序列感兴趣时,一个人可以节省计算工作量以及记忆。如果字符串是由设备/程序(网络、终端、管道)构造的,并且整个字符串因此不需要立即完全可用,它也可能很有用。这样就可以处理第一次出现的事件。

.NET框架中是否有这样的方法?

【问题讨论】:

  • C# 没有标准库。您似乎指的是 .NET Framework,它并不特定于 C#、VB.NET 或任何其他特定语言。
  • @JohnSaunders:修改...

标签: c# string ienumerable lazy-evaluation enumerator


【解决方案1】:

你可以很容易地写一个:

public static class StringExtensions
{
    public static IEnumerable<string> Split(this string toSplit, params char[] splits)
    {
        if (string.IsNullOrEmpty(toSplit))
            yield break;

        StringBuilder sb = new StringBuilder();

        foreach (var c in toSplit)
        {
            if (splits.Contains(c))
            {
                yield return sb.ToString();
                sb.Clear();
            }
            else
            {
                sb.Append(c);
            }
        }

        if (sb.Length > 0)
            yield return sb.ToString();
    }
}

显然,我还没有测试它是否与 string.split 进行奇偶校验,但我相信它的工作原理应该差不多。

正如 Servy 所指出的,这不会在字符串上拆分。这不是那么简单,也不是那么高效,但它基本上是相同的模式。

public static IEnumerable<string> Split(this string toSplit, string[] separators)
{
    if (string.IsNullOrEmpty(toSplit))
        yield break;

    StringBuilder sb = new StringBuilder();
    foreach (var c in toSplit)
    {
        var s = sb.ToString();
        var sep = separators.FirstOrDefault(i => s.Contains(i));
        if (sep != null)
        {
            yield return s.Replace(sep, string.Empty);
            sb.Clear();
        }
        else
        {
            sb.Append(c);
        }
    }

    if (sb.Length > 0)
        yield return sb.ToString();
}

【讨论】:

  • 这只会分割字符,而不是字符串。
  • 请注意,当给定一个空字符串时,这将产生一个空的可枚举,而string.Split 返回一个string[1] { "" }(一个带有空字符串的数组)。
【解决方案2】:

没有内置这样的东西。如果我正确解释反编译的代码,Regex.Matches 是懒惰的。也许你可以利用它。

或者,您只需编写自己的拆分函数。

实际上,您可以将大多数string 函数映射到任意序列。通常,甚至是T 的序列,而不仅仅是char。 BCL 根本不强调这一点。例如,没有Enumerable.Subsequence

【讨论】:

  • 我希望 .NET 包含一个“T 的不可变数组”类型; String 可以简单地成为“不可变字符数组”的简写。我知道有很多次我会使用“不可变字节数组”或“不可变 Int32 数组”(如果它们存在的话),并且希望泛化在许多其他情况下也很有用。
  • @supercat:是的,这就是 Haskell 处理字符串的方式。它可以将许多字符串方法推广到列表...
  • @supercat 有IReadOnlyList&lt;T&gt;string 可能是 IReadOnlyList&lt;char&gt;,只是它不在 .NET 1.0 中。
  • @Servy:但如果我没记错的话,在 .NET 1.0 中,字符串也不是 IEnumerable&lt;char&gt;。所以我猜可以稍微修改一下设计吗?
  • @Servy 字符串不能有虚拟方法。这将允许任意更改语义。对于这种基本类型,这是一个非常脆弱的模型。此外,在旧的 CAS 安全模型下,会打开各种漏洞。
【解决方案3】:

没有内置任何东西,但请随意撕掉我的Tokenize 方法:

 /// <summary>
/// Splits a string into tokens.
/// </summary>
/// <param name="s">The string to split.</param>
/// <param name="isSeparator">
/// A function testing if a code point at a position
/// in the input string is a separator.
/// </param>
/// <returns>A sequence of tokens.</returns>
IEnumerable<string> Tokenize(string s, Func<string, int, bool> isSeparator = null)
{
    if (isSeparator == null) isSeparator = (str, i) => !char.IsLetterOrDigit(str, i);

    int startPos = -1;

    for (int i = 0; i < s.Length; i += char.IsSurrogatePair(s, i) ? 2 : 1)
    {
        if (!isSeparator(s, i))
        {
            if (startPos == -1) startPos = i;
        }
        else if (startPos != -1)
        {
            yield return s.Substring(startPos, i - startPos);
            startPos = -1;
        }
    }

    if (startPos != -1)
    {
        yield return s.Substring(startPos);
    }
}

【讨论】:

    【解决方案4】:

    据我所知,没有内置的方法可以做到这一点。但这并不意味着你不能写一个。这是一个示例,可以让您了解一下:

    public static IEnumerable<string> SplitLazy(this string str, params char[] separators)
    {
        List<char> temp = new List<char>();
        foreach (var c in str)
        {
            if (separators.Contains(c) && temp.Any())
            {
                 yield return new string(temp.ToArray());
                 temp.Clear();
            }
            else
            {
                temp.Add(c);
            }
        }
        if(temp.Any()) { yield return new string(temp.ToArray()); }
    }
    

    当然,这并不能处理所有情况,可以改进。

    【讨论】:

    • 这只会分割字符,而不是字符串。
    【解决方案5】:

    我编写了这个变体,它也支持 SplitOptions 和 count。 在我尝试过的所有测试用例中,它的行为与 string.Split 相同。 nameof 运算符是 C# 6 专用的,可以用“count”代替。

    public static class StringExtensions
    {
        /// <summary>
        /// Splits a string into substrings that are based on the characters in an array. 
        /// </summary>
        /// <param name="value">The string to split.</param>
        /// <param name="options"><see cref="StringSplitOptions.RemoveEmptyEntries"/> to omit empty array elements from the array returned; or <see cref="StringSplitOptions.None"/> to include empty array elements in the array returned.</param>
        /// <param name="count">The maximum number of substrings to return.</param>
        /// <param name="separator">A character array that delimits the substrings in this string, an empty array that contains no delimiters, or null. </param>
        /// <returns></returns>
        /// <remarks>
        /// Delimiter characters are not included in the elements of the returned array. 
        /// If this instance does not contain any of the characters in separator the returned sequence consists of a single element that contains this instance.
        /// If the separator parameter is null or contains no characters, white-space characters are assumed to be the delimiters. White-space characters are defined by the Unicode standard and return true if they are passed to the <see cref="Char.IsWhiteSpace"/> method.
        /// </remarks>
        public static IEnumerable<string> SplitLazy(this string value, int count = int.MaxValue, StringSplitOptions options = StringSplitOptions.None, params char[] separator)
        {
            if (count <= 0)
            {
                if (count < 0) throw new ArgumentOutOfRangeException(nameof(count), "Count cannot be less than zero.");
                yield break;
            }
    
            Func<char, bool> predicate = char.IsWhiteSpace;
            if (separator != null && separator.Length != 0)
                predicate = (c) => separator.Contains(c);
    
            if (string.IsNullOrEmpty(value) || count == 1 || !value.Any(predicate))
            {
                yield return value;
                yield break;
            }
    
            bool removeEmptyEntries = (options & StringSplitOptions.RemoveEmptyEntries) != 0;
            int ct = 0;
            var sb = new StringBuilder();
            for (int i = 0; i < value.Length; ++i)
            {
                char c = value[i];
                if (!predicate(c))
                {
                    sb.Append(c);
                }
                else
                {
                    if (sb.Length != 0)
                    {
                        yield return sb.ToString();
                        sb.Clear();
                    }
                    else
                    {
                        if (removeEmptyEntries)
                            continue;
                        yield return string.Empty;
                    }
    
                    if (++ct >= count - 1)
                    {
                        if (removeEmptyEntries)
                            while (++i < value.Length && predicate(value[i]));
                        else
                            ++i;
                        if (i < value.Length - 1)
                        {
                            sb.Append(value, i, value.Length - i);
                            yield return sb.ToString();
                        }
                        yield break;
                    }
                }
            }
    
            if (sb.Length > 0)
                yield return sb.ToString();
            else if (!removeEmptyEntries && predicate(value[value.Length - 1]))
                yield return string.Empty;
        }
    
        public static IEnumerable<string> SplitLazy(this string value, params char[] separator)
        {
            return value.SplitLazy(int.MaxValue, StringSplitOptions.None, separator);
        }
    
        public static IEnumerable<string> SplitLazy(this string value, StringSplitOptions options, params char[] separator)
        {
            return value.SplitLazy(int.MaxValue, options, separator);
        }
    
        public static IEnumerable<string> SplitLazy(this string value, int count, params char[] separator)
        {
            return value.SplitLazy(count, StringSplitOptions.None, separator);
        }
    }
    

    【讨论】:

      【解决方案6】:

      我想要Regex.Split 的功能,但是以一种懒惰评估的形式。下面的代码只是遍历输入字符串中的所有Matches,并产生与Regex.Split相同的结果:

      public static IEnumerable<string> Split(string input, string pattern, RegexOptions options = RegexOptions.None)
      {
          // Always compile - we expect many executions
          var regex = new Regex(pattern, options | RegexOptions.Compiled);
      
          int currentSplitStart = 0;
          var match = regex.Match(input);
      
          while (match.Success)
          {
              yield return input.Substring(currentSplitStart, match.Index - currentSplitStart);
      
              currentSplitStart = match.Index + match.Length;
              match = match.NextMatch();
          }
      
          yield return input.Substring(currentSplitStart);
      }
      

      请注意,将其与模式参数 @"\s" 一起使用会得到与 string.Split() 相同的结果。

      【讨论】:

      • 仅供读者参考。在生产中使用此代码时,将 Regex 定义移出方法范围。否则,每次拆分执行都会发生正则表达式编译
      【解决方案7】:

      不创建临时字符串的延迟拆分。

      使用系统 coll mscorlib String.SubString 复制的字符串块。

      public static IEnumerable<string> LazySplit(this string source, StringSplitOptions stringSplitOptions, params string[] separators)
      {
          var sourceLen = source.Length;
      
          bool IsSeparator(int index, string separator)
          {
              var separatorLen = separator.Length;
      
              if (sourceLen < index + separatorLen)
              {
                  return false;
              }
      
              for (var i = 0; i < separatorLen; i++)
              {
                  if (source[index + i] != separator[i])
                  {
                      return false;
                  }
              }
      
              return true;
          }
      
          var indexOfStartChunk = 0;
      
          for (var i = 0; i < source.Length; i++)
          {
              foreach (var separator in separators)
              {
                  if (IsSeparator(i, separator))
                  {
                      if (indexOfStartChunk == i && stringSplitOptions != StringSplitOptions.RemoveEmptyEntries)
                      {
                          yield return string.Empty;
                      }
                      else
                      {
                          yield return source.Substring(indexOfStartChunk, i - indexOfStartChunk);
                      }
      
                      i += separator.Length;
                      indexOfStartChunk = i--;
                      break;
                  }
              }
          }
      
          if (indexOfStartChunk != 0)
          {
              yield return source.Substring(indexOfStartChunk, sourceLen - indexOfStartChunk);
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-12-14
        • 1970-01-01
        相关资源
        最近更新 更多