C#:将字符串数组最干净地分成N个长度为N的实例的方法

19
我知道如何用丑陋的方式来做这件事,但我想知道是否有一种更优雅和简洁的方法。
我有一个电子邮件地址的字符串数组。假设该字符串数组长度是任意的——它可以只有几个项目,也可以有很多项。我希望构建另一个字符串,其中包含来自字符串数组的50个电子邮件地址,直到数组结束,并在每50个地址后调用Send()方法发送邮件。
更普遍的问题是怎样最清晰地完成这种操作。我有一个解决方案,那是我从VBScript学习遗留下来的,但我相信在C#中会有更好的方法。

感谢大家提供的惊人答案——事实上,你们的方法要比我曾经实现的简单循环解决方案好得多。这正是我所期望得到的指导! - Socrates
可能是 Split List into Sublists with LINQ 的重复问题。 - nawfal
8个回答

47

你想要优雅简洁,我将给你优雅简洁:

var fifties = from index in Enumerable.Range(0, addresses.Length) 
              group addresses[index] by index/50;
foreach(var fifty in fifties)
    Send(string.Join(";", fifty.ToArray());

如果可以不必用循环代码就能完成,为什么还要麻烦呢?如果你想把东西分成五十个一组,那就把它们分成五十个一组。 这就是group操作符的作用!

更新:评论者MoreCoffee问如何使用此操作符。假设我们想按三个一组进行分组,因为这样更容易打字。

var threes = from index in Enumerable.Range(0, addresses.Length) 
              group addresses[index] by index/3;

假设有从0到8的九个地址。

这个查询是什么意思呢?

Enumerable.Range 是一个从零开始的九个数字的范围,即 0, 1, 2, 3, 4, 5, 6, 7, 8

范围变量 index 依次取这些值。

然后我们遍历每个对应的 addresses[index] 并将其分配到一个组中。

我们将它分配到哪个组呢?是分配到 index/3 组。在 C# 中整数算术向零舍入,所以索引 0、1 和 2 在除以 3 时变为 0。索引 3、4、5 在除以 3 时变为 1。索引 6、7、8 在除以 3 时变为 2。

因此,我们将 addresses[0]addresses[1]addresses[2] 分配到第 0 组,将 addresses[3]addresses[4]addresses[5] 分配到第 1 组,以此类推。

查询的结果是一个由三个组成的序列,每个组都是由三个项目组成的序列。

有意义吗?

请记住,查询表达式的结果是代表此操作的查询。它在 foreach 循环执行之前不会执行该操作。


除了可怕的循环代码,是否有一种同样优雅的解决方案来批处理IEnumerable(即当您不知道大小时)? - Erich Mirabal
当然。看一下DTB的回答。 - Eric Lippert
它是如何按索引/50分组的?我知道它可以运行,但语法让我有点困惑。 - user412045
1
@MoreCoffee:我添加了一些解释性文本。 - Eric Lippert
非常干净,确实不错 :) 基于您的回答的通用扩展方法 - 在这里 - turdus-merula

14

这似乎类似于这个问题:如何使用LINQ将集合拆分成n个部分?

那里的Hasan Khan的答案的修改版应该可以解决问题:

public static IEnumerable<IEnumerable<T>> Chunk<T>(
    this IEnumerable<T> list, int chunkSize)
{
    int i = 0;
    var chunks = from name in list
                 group name by i++ / chunkSize into part
                 select part.AsEnumerable();
    return chunks;
}

使用示例:

var addresses = new[] { "a@example.com", "b@example.org", ...... };

foreach (var chunk in Chunk(addresses, 50))
{
    SendEmail(chunk.ToArray(), "Buy V14gr4");
}

这对我来说似乎有点不必要!仍然需要调用string.Join方法,它可以直接访问数组中的子范围。 - Daniel Earwicker
不错,但我们可以在不改变“i”的情况下完成这个任务。请看我的答案。 - Eric Lippert
也许我漏掉了什么,但是Chunk<T>不是扩展方法吗?所以你应该做addresses.Chunk吧? - Stan R.

2
听起来输入是由大量单独的电子邮件地址字符串组成的数组,而不是一个字符串中包含多个电子邮件地址,对吗?在输出中,每个批次都是一个单独的组合字符串。
string[] allAddresses = GetLongArrayOfAddresses();

const int batchSize = 50;

for (int n = 0; n < allAddresses.Length; n += batchSize)
{
    string batch = string.Join(";", allAddresses, n, 
                      Math.Min(batchSize, allAddresses.Length - n));

    // use batch somehow
}

2
假设您正在使用.NET 3.5和C# 3,以下类似的代码应该可以很好地工作:
string[] s = new string[] {"1", "2", "3", "4"....};

for (int i = 0; i < s.Count(); i = i + 50)
{
    string s = string.Join(";", s.Skip(i).Take(50).ToArray());
    DoSomething(s);
}

那个skip/take的东西是不必要的复制 - 有一个重载的string.Join可以让你在数组中指定一个范围。 - Daniel Earwicker
然而,如果您指定的项比数组中的项多,Take 不会抛出异常。虽然 Math.Min 可以解决这个问题,但我认为 Linq 解决方案更易读且更容易理解。 - Erik Funkenbusch
这里有一个bug。在循环的第一次迭代中,您将跳过0并取50,在第二次迭代中,您将跳过2500(i变为50,因此i * 50 = 2500),而不是50。 - Oleg D.

1
我会遍历数组并使用 StringBuilder 创建列表(我假设它是用分号分隔的,就像您为电子邮件一样)。当达到 mod 50 或结尾时发送。
void Foo(string[] addresses)
{
    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < addresses.Length; i++)
    {
        sb.Append(addresses[i]);
        if ((i + 1) % 50 == 0 || i == addresses.Length - 1)
        {
            Send(sb.ToString());
            sb = new StringBuilder();
        }
        else
        {
            sb.Append("; ");
        }
    }
}

void Send(string addresses)
{
}

0

我认为这很简单且足够快速。下面的示例将长句分成了15个部分,但您可以传递批处理大小作为参数以使其动态化。在这里,我仅使用“/n”进行划分。

 private static string Concatenated(string longsentence)
 {
     const int batchSize = 15;
     string concatanated = "";
     int chanks = longsentence.Length / batchSize;
     int currentIndex = 0;
     while (chanks > 0)
     {
         var sub = longsentence.Substring(currentIndex, batchSize);
         concatanated += sub + "/n";
         chanks -= 1;
         currentIndex += batchSize;
     }
     if (currentIndex < longsentence.Length)
     {
         int start = currentIndex;
         var finalsub = longsentence.Substring(start);
         concatanated += finalsub;
     }
     return concatanated;
 }

这显示了分割操作的结果。

 var parts = Concatenated(longsentence).Split(new string[] { "/n" }, StringSplitOptions.None);

0

基于Eric的回答的扩展方法:

public static IEnumerable<IEnumerable<T>> SplitIntoChunks<T>(this T[] source, int chunkSize)
{
    var chunks = from index in Enumerable.Range(0, source.Length)
                 group source[index] by index / chunkSize;

    return chunks;
}

public static T[][] SplitIntoArrayChunks<T>(this T[] source, int chunkSize)
{
    var chunks = from index in Enumerable.Range(0, source.Length)
                 group source[index] by index / chunkSize;

    return chunks.Select(e => e.ToArray()).ToArray();
}

0

我认为我们需要更多关于这个列表的具体信息才能给出明确的答案。目前我假设它是一个由分号分隔的电子邮件地址列表。如果是这样,您可以执行以下操作来获取分块列表。

public IEnumerable<string> DivideEmailList(string list) {
  var last = 0;
  var cur = list.IndexOf(';');
  while ( cur >= 0 ) {
    yield return list.SubString(last, cur-last);
    last = cur + 1;
    cur = list.IndexOf(';', last);
  }
}

public IEnumerable<List<string>> ChunkEmails(string list) {
  using ( var e = DivideEmailList(list).GetEnumerator() ) {
     var list = new List<string>();
     while ( e.MoveNext() ) {
       list.Add(e.Current);
       if ( list.Count == 50 ) {
         yield return list;
         list = new List<string>();
       }
     }
     if ( list.Count != 0 ) {
       yield return list;
     }
  }
}

实际上他说他有一个电子邮件地址数组,所以不需要按分号拆分。 - Stan R.
输出应该是一个合并的字符串。 - Daniel Earwicker

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