将字符串分割成特定大小的块

280
假设我有一个字符串:
string str = "1111222233334444";

如何将这个字符串分成指定大小的块?
例如,将其分成大小为4的块将返回字符串:
"1111"
"2222"
"3333"
"4444"

19
C#的标准字符串处理函数可以用更少的工作量和更快的速度完成这项任务,为什么要使用LINQ或正则表达式?另外,如果字符串长度为奇数,会发生什么情况? - Ian Kemp
8
“我希望避免循环” - 为什么? - Mitch Wheat
12
使用简单的循环确实可以提供最佳性能。 - Guffa
5
这是一篇关于linq和实际循环处理数组之间性能比较的文章。我认为你不太可能找到比手动编写代码更快的linq,因为它会不断调用运行时委托,难以进行优化。不过,使用linq可能更有趣 :) - Blindy
2
无论您使用LINQ还是正则表达式,循环仍然存在。 - Anton Tykhyy
显示剩余5条评论
39个回答

300
static IEnumerable<string> Split(string str, int chunkSize)
{
    return Enumerable.Range(0, str.Length / chunkSize)
        .Select(i => str.Substring(i * chunkSize, chunkSize));
}

请注意,可能需要额外的代码来优雅地处理边缘情况(如null或空输入字符串,chunkSize == 0,输入字符串长度不能被chunkSize整除等)。原始问题没有指定这些边缘情况的任何要求,在实际生活中,要求可能会有所不同,因此它们超出了本答案的范围。

3
不错!可以通过在substring的计数参数上添加一个三元表达式来解决这个问题。类似这样:(i * chunkSize + chunkSize <= str.Length) ? chunkSize : str.Length - i * chunkSize。另一个问题是该函数没有考虑到str为空的情况。可以通过将整个返回语句包装在另一个三元表达式中来修复它:(str != null) ? ... : Enumerable.Empty<String>(); - Drew Spickes
10
这很接近正确,但不同于前30个点赞者,我必须将Range的循环计数限制从str.Length / chunkSize更改为double length = str.Length; double size = chunkSize; int count = (int)Math.Ceiling(length/size); return Enumerable.Range(0, count)... - gap
8
@KonstantinSpirin 如果这段代码可用,我同意。但它只处理字符串长度为chunkSize的倍数的情况,剩下的字符串将会丢失,请修正。另外请注意,LINQ及其魔力并不是那些仅想要查看此问题解决方案的人容易理解的。现在一个人必须理解Enumerable.Range()和.Select()函数的作用。我不会争论你写C#/.NET代码时应该了解这些函数,因为这些函数已经在BCL中存在多年了。 - CodeMonkeyKing
6
主题发起者在评论中说“ StringLength % 4 总是为0”。如果Linq不容易理解,那么还有其他使用循环和yield的答案。任何人都可以自由选择她喜欢的解决方案。你可以将你的代码发布为答案,人们会高兴地投票支持它。 - Konstantin Spirin
9
Enumerable.Range(0, (str.Length + chunkSize - 1) / chunkSize) .Select(i => str.Substring(i * chunkSize, Math.Min(str.Length - i * chunkSize, chunkSize)))这段代码的作用是将一个字符串按照指定的长度分块,每个块的长度为chunkSize。如果字符串的长度不足chunkSize,则最后一块的长度会小于chunkSize。返回的结果是分块后的子串构成的序列。 - Sten Petrov
显示剩余15条评论

179
鸽子康斯坦丁的回答的结合中...
static IEnumerable<string> WholeChunks(string str, int chunkSize) {
    for (int i = 0; i < str.Length; i += chunkSize)
        yield return str.Substring(i, chunkSize);
}

这将适用于所有可以分割成整数个块的字符串,并在其他情况下抛出异常。
如果您想支持任意长度的字符串,您可以使用以下代码:
static IEnumerable<string> ChunksUpto(string str, int maxChunkSize) {
    for (int i = 0; i < str.Length; i += maxChunkSize)
        yield return str.Substring(i, Math.Min(maxChunkSize, str.Length-i));
}

然而,OP明确表示他不需要这个;这个选项稍微长一些,阅读起来更困难,速度稍慢。按照KISS和YAGNI的原则,我会选择第一个选项:它可能是最高效的实现方式,而且非常简短、易读,更重要的是,对于不符合要求的输入会抛出异常。

4
+1 值得认可。比较贴切地点出了问题的症结。他在寻找简洁的语法,而您也提供了(可能)更好的性能。 - dove
9
如果您将其设置为 "static ... Chunk(this string str, int chunkSize) {",那么您甚至可以在其中添加一个新的 C# 特性。然后,您可以编写 "1111222233334444".Chunk(4)。 - MartinStettner
1
@MartinStettner:如果这是一个常见的操作,那肯定是个不错的想法。 - Eamon Nerbonne
各有所好。这取决于你想要什么 - 另一种变体可以生成比块大小更小的块 - 这可能会导致自己的错误,特别是如果您最初总是具有整个块大小(如OP)。其他一切相等,我喜欢强大的后置条件,因此,如果您有整个块大小,我更喜欢保证并检测到无效输入,而不是生成可能有错误的输出(太短的块)(即快速失败)。当然,如果您期望非整数大小的块,则情况就不同了。 - Eamon Nerbonne
需要处理最后一块大小小于给定大小的情况,否则会抛出错误。(https://www.codegrepper.com/code-examples/csharp/divide+string+in+chunks+c%23) - Arnold Vakaria
显示剩余8条评论

63
使用循环。这是一个非常好的方法:
string str = "111122223333444455";
int chunkSize = 4;
int stringLength = str.Length;
for (int i = 0; i < stringLength ; i += chunkSize)
{
    if (i + chunkSize > stringLength) chunkSize = stringLength  - i;
    Console.WriteLine(str.Substring(i, chunkSize));
}

Console.ReadLine();

我不知道你如何处理字符串不是4的因子的情况,但我并不是说你的想法不可能,只是想知道如果一个简单的for循环可以很好地完成任务,那么这样做的动机是什么。显然,上述代码可以进行优化,甚至可以作为一个扩展方法添加进去。
或者如评论中提到的,你可以知道它是除以4的。
str = "1111222233334444";
for (int i = 0; i < stringLength; i += chunkSize)
{
    Console.WriteLine(str.Substring(i, chunkSize));
}

1
你可以将 int chunkSize = 4 移到循环外面。它只会在最后一次通过时被修改。 - John Feminella
+1 对于一个简单而有效的解决方案 - 这就是我会这样做的方式,尽管我会使用 i += chunkSize - Ian Kemp
可能只是一个小问题,但你应该将 str.Length 从循环中提取出来,放到一个局部变量中。C# 优化器可能能够内联数组长度,但我认为按照现有的代码会在每次循环中进行一次方法调用,这不是很高效,因为 str 的大小永远不会改变。 - Daniel Pryden
@Daniel,请把你的想法放进去。虽然我不确定这是否会在运行时计算,但这是另一个问题 ;) - dove
@Daniel 回到这个问题,我很确定这个优化将会被编译器提取出来。 - dove
这个工作得很好 @dove - anandd360

49
这是基于 dove's solution的实现,但作为扩展方法实现。
优点:
- 扩展方法 - 覆盖边缘情况 - 可以使用任何字符拆分字符串:数字、字母、其他符号
代码
public static class EnumerableEx
{
    public static IEnumerable<string> SplitBy(this string str, int chunkLength)
    {
        if (String.IsNullOrEmpty(str)) throw new ArgumentException();
        if (chunkLength < 1) throw new ArgumentException();

        for (int i = 0; i < str.Length; i += chunkLength)
        {
            if (chunkLength + i > str.Length)
                chunkLength = str.Length - i;

            yield return str.Substring(i, chunkLength);
        }
    }
}

使用方法
var result = "bobjoecat".SplitBy(3); // bob, joe, cat

为了简洁起见,已删除单元测试(请参阅上一版本)。

有趣的解决方案,但为了避免在输入上进行空值检查,似乎更合理的做法是允许一个空字符串返回一个单独的空字符串部分:if (str.Length == 0) yield return String.Empty; else { for... } - Nyerguds
我的意思是,这就是普通的String.Split处理空字符串的方式;它返回一个空字符串条目。 - Nyerguds
旁注:您的用法示例是错误的。您不能将 IEnumerable 强制转换为数组,尤其不是隐式地。 - Nyerguds
我个人喜欢将那个方法称为“Chunkify”...虽然这不是我的方法,我也不记得我在哪里看到过这个名称,但它给我留下了很好的印象。 - quetzalcoatl

45
使用正则表达式和LINQ:
List<string> groups = (from Match m in Regex.Matches(str, @"\d{4}")
                       select m.Value).ToList();

我觉得这样更易读,但这只是个人意见。也可以写成一行:)。

7
将模式更改为@"\d{1,4}",就可以适用于任何字符串长度。 :) - Guffa
3
虽然这种方法比其他方法慢,但绝对非常易读。我不确定原帖作者需要数字还是任意字符;最好将\d字符类替换为.,并指定RegexOptions.Singleline - Eamon Nerbonne
4
或者只需使用Regex.Matches(s, @"\d{1,4}").Select(m => m.Value).ToList()。我从未理解这种仅用于混淆我们正在使用扩展方法的替代语法的意义所在。 - The Dag

43

从 .NET 6 开始,我们还可以使用 Chunk 方法:

var result = str
    .Chunk(4)
    .Select(x => new string(x))
    .ToList();

26
使用这个来写一行代码:
List<string> result = new List<string>(Regex.Split(target, @"(?<=\G.{4})", RegexOptions.Singleline));

使用这个正则表达式时,最后一块字符少于四个字符并不重要,因为它只查看它之前的字符。
我确定这不是最高效的解决方案,但我只是想提出来。

如果 target.Length % ChunckSize == 0,则会返回一个额外的空行,例如 List<string> result = new List<string>(Regex.Split("fooo", @"(?<=\G.{4})", RegexOptions.Singleline)); - fubo

9

虽然不太美观也不快,但它可以工作,只需要一行代码就能实现,而且还具有LINQ的特点:

List<string> a = text.Select((c, i) => new { Char = c, Index = i }).GroupBy(o => o.Index / 4).Select(g => new String(g.Select(o => o.Char).ToArray())).ToList();

分组操作(GroupBy)是否保证元素的顺序不变? - Konstantin Spirin
由于stringIEnumerable<char>,因此ToCharArray是不必要的。 - juharr

8

最近我在工作中写了一些相关的内容,所以我想把解决这个问题的方法发表出来。作为额外的奖励,这个解决方案的功能提供了一种将字符串反向拆分的方式,并且正如Marvin Pinto之前提到的那样,它确实能正确处理Unicode字符。因此,这里是我的解决方案:

using System;
using Extensions;

namespace TestCSharp
{
    class Program
    {
        static void Main(string[] args)
        {    
            string asciiStr = "This is a string.";
            string unicodeStr = "これは文字列です。";

            string[] array1 = asciiStr.Split(4);
            string[] array2 = asciiStr.Split(-4);

            string[] array3 = asciiStr.Split(7);
            string[] array4 = asciiStr.Split(-7);

            string[] array5 = unicodeStr.Split(5);
            string[] array6 = unicodeStr.Split(-5);
        }
    }
}

namespace Extensions
{
    public static class StringExtensions
    {
        /// <summary>Returns a string array that contains the substrings in this string that are seperated a given fixed length.</summary>
        /// <param name="s">This string object.</param>
        /// <param name="length">Size of each substring.
        ///     <para>CASE: length &gt; 0 , RESULT: String is split from left to right.</para>
        ///     <para>CASE: length == 0 , RESULT: String is returned as the only entry in the array.</para>
        ///     <para>CASE: length &lt; 0 , RESULT: String is split from right to left.</para>
        /// </param>
        /// <returns>String array that has been split into substrings of equal length.</returns>
        /// <example>
        ///     <code>
        ///         string s = "1234567890";
        ///         string[] a = s.Split(4); // a == { "1234", "5678", "90" }
        ///     </code>
        /// </example>            
        public static string[] Split(this string s, int length)
        {
            System.Globalization.StringInfo str = new System.Globalization.StringInfo(s);

            int lengthAbs = Math.Abs(length);

            if (str == null || str.LengthInTextElements == 0 || lengthAbs == 0 || str.LengthInTextElements <= lengthAbs)
                return new string[] { str.ToString() };

            string[] array = new string[(str.LengthInTextElements % lengthAbs == 0 ? str.LengthInTextElements / lengthAbs: (str.LengthInTextElements / lengthAbs) + 1)];

            if (length > 0)
                for (int iStr = 0, iArray = 0; iStr < str.LengthInTextElements && iArray < array.Length; iStr += lengthAbs, iArray++)
                    array[iArray] = str.SubstringByTextElements(iStr, (str.LengthInTextElements - iStr < lengthAbs ? str.LengthInTextElements - iStr : lengthAbs));
            else // if (length < 0)
                for (int iStr = str.LengthInTextElements - 1, iArray = array.Length - 1; iStr >= 0 && iArray >= 0; iStr -= lengthAbs, iArray--)
                    array[iArray] = str.SubstringByTextElements((iStr - lengthAbs < 0 ? 0 : iStr - lengthAbs + 1), (iStr - lengthAbs < 0 ? iStr + 1 : lengthAbs));

            return array;
        }
    }
}

此外,这是一个运行此代码的结果的图片链接:http://i.imgur.com/16Iih.png

1
我注意到这段代码有问题。你在第一个IF语句的末尾使用了{str.ToString()}。你确定你不是想用str.String吗?我之前也遇到了同样的问题,做了这个改变后,一切正常了。 - gunr2171
看起来如果 str == null,那么这行代码也会抛出 NullReferenceException 异常。 - John Zabroski
你是指“by Seth”吗? - undefined

5

这将比使用LINQ或其他方法更快,更高效。

public static IEnumerable<string> Splice(this string s, int spliceLength)
{
    if (s == null)
        throw new ArgumentNullException("s");
    if (spliceLength < 1)
        throw new ArgumentOutOfRangeException("spliceLength");

    if (s.Length == 0)
        yield break;
    var start = 0;
    for (var end = spliceLength; end < s.Length; end += spliceLength)
    {
        yield return s.Substring(start, spliceLength);
        start = end;
    }
    yield return s.Substring(start);
}

1
这看起来像是进行了早期检查,但实际上并没有。直到开始枚举可枚举对象时才会出现错误。您需要将函数分成两部分,第一部分进行参数检查,然后返回第二部分的结果,该部分是私有的并进行枚举。 - ErikE

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接