使用LINQ将字符串拆分为N个字符长度的字符串列表

20

我知道String.Split的概念以前已经被提及过,有很多不同的方法来解决这个问题,但我特别感兴趣的是使用LINQ来解决这个问题。

我尝试编写一个扩展类来处理分割,但两次尝试都存在一些主要问题。所以对于以下内容:

string s = "ABCDEFGHIJKLMNOPQRSTUVWX";
var results = s.SplitEvery(4);

我希望得到如下格式的列表:

{ "ABCD", "EFGH", "IJKL", "MNOP", "QRST", "UVWX" }

这是我的扩展类:

public static class Extensions
{
    public static List<string> SplitEvery(this string s, int n)
    {
        List<string> list = new List<string>();

        var Attempt1 = s.Select((c, i) => i % n== 0 ? s.Substring(i, n) : "|").Where(x => x != "|").ToList();

        var Attempt2 = s.Where((c, i) => i % n== 0).Select((c, i) => s.Substring(i, n)).ToList();

        return list;
    }
}

尝试1:每次条件不满足时插入一个虚拟字符串“|”,然后删除所有虚拟字符串的实例以创建最终列表。它可以工作,但创建错误的字符串似乎是一个不必要的额外步骤。此外,如果字符串不能被n整除,则此尝试失败。
尝试2:我试图仅选择索引可被N整除的子字符串,但是Select语句中的“i”值与Where语句中的“i”值不对应,因此我得到了像“ABCD”,“BCDE”等结果。
我觉得我离一个好的解决方案很近,但需要在正确的方向上获得有用的帮助。 有什么建议吗?
[编辑]
最终我采用了一些建议来处理我的字符串分割器。这可能不是最快的方法,但作为LINQ的新手,这个实现对我来说最简洁且易于理解。
public static List<string> SplitEvery(this string s, int size)
{
    return s.Select((x, i) => i)
        .Where(i => i % size == 0)
        .Select(i => String.Concat(s.Skip(i).Take(size))).ToList();
}

感谢各位提供的卓越建议。

旁注:指定您的“更好”的标准会很好。即,在这种情况下,它似乎是“查询可读性尽可能接近描述的初学者LINQ用户,倾向于使用 Enumerable 方法而不是所有的性能考虑。” 在这种情况下,ConcatTake 的确看起来是最佳方法。 - Alexei Levenkov
非常抱歉,您的评估是公正的。我主要关注的是一种干净、一行代码的方法,类似于我之前尝试的方法。在我的情况下,可读性对我来说比可扩展性更重要。希望没有人会试图把大量文本文件转储到我的字符串中。 :) - MadHenchbot
1
在LINQ中要注意的另一个随机提示是,您的最终方法会多次迭代序列。对于字符串来说没问题,但不适用于“一次性”序列,例如SQL查询结果或File.ReadAllLines。有几个答案(即使用yield return)展示了只迭代集合一次的方法。 - Alexei Levenkov
8个回答

27
string s = "ABCDEFGHIJKLMNOPQRSTUVWX";
var results = s.Select((c, i) => new { c, i })
            .GroupBy(x => x.i / 4)
            .Select(g => String.Join("",g.Select(y=>y.c)))
            .ToList();
您也可以使用 morelinq 的 batch
var res = s.Batch(4).Select(x => String.Join("", x)).ToList();
如果您不介意使用副作用,这也是可能的。
var res2 = s.SplitEvery(4).ToList();

public static IEnumerable<string> SplitEvery(this string s, int n)
{
    int index = 0;
    return s.GroupBy(_=> index++/n).Select(g => new string(g.ToArray()));
}

当然,每个字符串操作问题都值得使用正则表达式来解决 :)

var res3 = Regex.Split(s, @"(?<=\G.{4})");

1
认为正则表达式的解决方案应该放在这个答案的顶部,因为它比其他解决方案更快(根据我的测试)且更短。 - Kamarey

12

这里有另外一种解决方案:

var result = s.Select((x, i) => i)
              .Where(i => i % 4 == 0)
              .Select(i => s.Substring(i, s.Length - i >= 4 ? 4 : s.Length - i));

是的,这正是我想从中获得的。对我来说,这是最直接和易读的选择,因为我对LINQ的了解相当有限。非常感谢! - MadHenchbot

10
你可以使用这个扩展方法,它使用简单的子字符串获取(我相信它比枚举字符并将它们连接成字符串更快):
public static IEnumerable<string> SplitEvery(this string s, int length)
{
    int index = 0;
    while (index + length < s.Length)
    {
        yield return s.Substring(index, length);
        index += length;                
    }

    if (index < s.Length)
        yield return s.Substring(index, s.Length - index);
}

6
public static IEnumerable<string> SplitEvery(this string s, int length)
{
    return s.Where((c, index) => index % length == 0)
           .Select((c, index) => String.Concat(
                s.Skip(index * length).Take(length)
             )
           );
}

对于这个问题,关于 new String(chars.ToArray())String.Concat(chars) 哪种方法更快还有争议。

当然,你可以在后面添加一个 .ToList() 来返回一个 List,而不是 IEnumerable


我曾担心最终的 .Take(length) 会抛出索引超出范围的错误,但看起来这已经在方法内部处理了。好的解决方案! - MadHenchbot
1
是的,有一些可读性受损的技巧表明当我写这个时已经很晚了...也就是说,Where 调用的结果(即每个分割索引处的 char)从未直接使用——它只是为了限制以下 Select 应该返回的结果数量。只有当你调用 Take 的源是 null 时,它才会抛出异常。其余时间,它会做正确的事情。 - JimmiTh
换句话说,s.Where 可以被替换为 Enumerable.Range(0, x),其中 x 是计算出的拆分索引数量。例如查看 @AlexeiLevenkov 的答案。这将更清晰地传达意图。 - JimmiTh

4

Substring函数可以轻松选择字符串的四个字符部分。只需注意最后一部分即可:

new Func<string, int, IEnumerable<string>>(
        (string s, int n) => 
           Enumerable.Range(0, (s.Length + n-1)/n)
           .Select(i => s.Substring(i*n, Math.Min(n, s.Length - i*n)))) 
("ABCDEFGHIJKLMNOPQRSTUVWX", 4)

注意:如果将此答案转换为对通用可枚举对象的操作,则必须多次迭代集合(Count()Substring 将被转换为 Skip(i*n).Take(n))。

3

这似乎可以工作:

public static IEnumerable<string> SplitEvery(this string s, int n) {
    var enumerators = Enumerable.Repeat(s.GetEnumerator(), n);
    while (true) {
        var chunk = string.Concat(enumerators
            .Where(e => e.MoveNext())
            .Select(e => e.Current));
        if (chunk == "") yield break;
        yield return chunk;
    }
}

1
这是一些使用LINQ的方法:
```

这里有几种使用LINQ的方法:

```
public static IEnumerable<string> SplitEvery( this IEnumerable<char> s , int n )
{
  StringBuilder sb = new StringBuilder(n) ;
  foreach ( char c in s )
  {
    if ( sb.Length == n )
    {
      yield return sb.ToString() ;
      sb.Length = 0 ;
    }
    sb.Append(c) ;
  }
}

或者

public static IEnumerable<string> SplitEvery( this string s , int n )
{
  int limit = s.Length - ( s.Length % n ) ;
  int i = 0 ;

  while ( i < limit )
  {
    yield return s.Substring(i,n) ;
    i+=n ;
  }

  if ( i < s.Length )
  {
    yield return s.Substring(i) ;
  }

}

5
想知道它们为什么“有点像 LINQ”? - Simon Whitehead
2
为了拥有LINQ风范,你应该使用LINQ。 - recursive
1
它们是LINQ扩展方法。你可能想要阅读有关如何扩展LINQ的内容。 - Nicholas Carey
1
我不想和你争论。根据微软的定义,至少第一个是LINQ的扩展。祝您拥有愉快的一天,感谢参与。 - Nicholas Carey
我并不想卷入无意义的争吵,也没有给你点踩。只是我们对问题的理解似乎有所不同。 - recursive
1
这是深入探讨LINQ核心的东西,有点像自定义的LINQ或高级的LINQ,我不认为新手能理解这种工作。称它为LINQy并不真的很糟糕。对那个人点个赞。 - King King

1
这也可以实现,但需要“解包”IGrouping<x,y>
public static IEnumerable<String> Split(this String me,int SIZE) {
  //Works by mapping the character index to a 'modulo Staircase'
  //and then grouping by that 'stair step' value
  return me.Select((c, i) => new {
    step = i - i % SIZE,
    letter = c.ToString()
  })
  .GroupBy(kvp => kvp.step)
  .Select(grouping => grouping
    .Select(g => g.letter)
    .Aggregate((a, b) => a + b)
  );
}

编辑:使用LINQ的延迟计算机制(yield return),你也可以使用递归来实现这一点。

public static IEnumerable<String> Split(this String me, int SIZE) {      
  if (me.Length > SIZE) {
    var head = me.Substring(0,SIZE);
    var tail = me.Substring(SIZE,me.Length-SIZE);
    yield return head;        
    foreach (var item in tail.Split(SIZE)) {
      yield return item; 
    }
  } else { 
    yield return me;
  }
}

尽管个人而言,我避免使用Substring,因为它会鼓励状态化的代码(计数器、索引等在父级或全局范围内)。


阅读答案,这种方法与 @I4V 的第一个答案几乎相同,只是没有使用 floor 整数除法或空字符串连接。 - theoski

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