在 VB.Net 中使用 LINQ 将集合拆分为 n 个部分

Split a collection into n parts with LINQ, in VB.Net

问题

在VB.Net中,如果我有这样的合集:

Dim collection As IEnumerable(Of Integer) = Enumerable.Range(0, 99)

如何将它拆分为 groups/Ienumerables 个不确定数量的元素?


条件

使用 LINQ 查询(不是 MORELinq 或任何其他第 3 方库)

不写 Function,只是使用(或附加到集合)LINQ 查询,避免插入自定义和通用过程以拆分为多个部分。

不生成 Anonymous 类型(因为我会尊重 VB.Net Option 声明)。


研究

我已经阅读了这些问题,但我无法将 C# LINQ 查询正确翻译成 VB.Net(甚至在线翻译器也会失败并需要大量修改) :

Split a collection into `n` parts with LINQ?

How can I split an IEnumerable<String> into groups of IEnumerable<string>

Split List into Sublists with LINQ

Split a entity collection into n parts

这是我尝试从 S.O 中给出的解决方案中改编的两种不同方法。上面的问题,但我的翻译不起作用,第一个无法编译,因为 Group By 的条件,第二个不生成拆分集合,它为每个元素生成一个集合:

1.

    Dim parts As Integer = 4
    Dim i As Integer = 0

    Dim splits As IEnumerable(Of IEnumerable(Of Integer)) =
        From item As Integer In collection
        Group By (Math.Max(Interlocked.Increment(i), i - 1) Mod parts)
        Into Group
        Select Group.AsEnumerable

2.

    Dim parts As Integer = 4

    Dim result As IEnumerable(Of IEnumerable(Of Integer)) =
        collection.Select(Function(s, i) New With
            {
                Key .Value = s,
                Key .Index = i
            }
        ).GroupBy(Function(item)
                      Return (item.Index >= (item.Index / parts)) And (item.Index >= item.Value)
                  End Function,
               Function(item) item.Value).Cast(Of IEnumerable(Of Integer))()

预期结果

所以,如果我有这样的源集合:

Dim collection As IEnumerable(Of Integer) = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

假设我有一个假想的 LINQ 查询,它可以根据变量 parts:

上指定的值拆分为一个名为 splits 的变量
Dim parts as Integer = ...
Dim splits As IEnumerable(Of IEnumerable(Of Integer)) =
   From value As Integer In collection (Linq Query that splits by given 'parts' value)

如果我选择 parts 值是 4 那么结果将是一个 IEnumerable(Of IEnumerable(Of Integer)) 其中包含 3 个集合,其中:

splits(0) = {1, 2, 3, 4}
splits(1) = {5, 6, 7, 8}
splits(2) = {9, 10}

如果我选择 parts 值是 5 那么结果将是一个 IEnumerable(Of IEnumerable(Of Integer)) 其中包含 2 个集合,其中:

splits(0) = {1, 2, 3, 4, 5}
splits(1) = {6, 7, 8, 9, 10}

如果我选择 parts 值为 1 那么结果将是一个 IEnumerable(Of IEnumerable(Of Integer)) 它将包含相同的源集合,因为我选择分成 1 部分:

splits(0) = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

如果我选择 parts 值是 10 那么结果将是一个 IEnumerable(Of IEnumerable(Of Integer)) 其中包含 10 集合,源示例集合的每个值一个集合:

splits(0) = {1}
splits(1) = {2}
splits(2) = {3}
and so on...

您要查找的查询可能如下所示:

Dim partitionSize As Integer = 4
Dim collection As IEnumerable(Of Integer) = Enumerable.Range(0, 100)
Dim parts As Integer = collection.Count \ partitionSize
Dim splits = (From i In collection
              Group i By key = i Mod parts Into g = Group
              Select key, g)

这取决于您展示的示例。但是如果实际数据的排列比较随机,这种方式会使用索引处的值按照索引值分组:

Dim partitionSize As Integer = 4
Dim collection As IEnumerable(Of Integer) = Enumerable.Range(100, 100)
Dim parts As Integer = collection.Count \ partitionSize
Dim splits = (From i In Enumerable.Range(0, 100)
              Group collection(i) By key = i Mod parts Into g = Group
              Select key, g)

你的 post 说你不想使用匿名类型。但是 Group By 使用匿名类型。您可以通过将集合转换为字典来使匿名类型成为临时类型:

Dim splits = (From i In Enumerable.Range(0, 100)
              Group collection(i) By key = i Mod parts Into g = Group
              Select key, g).ToDictionary(Function(x) x.key, Function(x) x.g)

您可以使用 Range, Skip and Take 来实现您的目标。

Dim collection As IEnumerable(Of Integer) = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
Dim parts As Integer = 4
Dim count As Integer = CInt(Math.Ceiling(collection.Count() / parts)) '= 3
Dim result As IEnumerable(Of IEnumerable(Of Integer)) = Nothing

'Method 1:
result = From i In Enumerable.Range(0, count) Select collection.Skip(i * parts).Take(parts)

'Method 2:
result = Enumerable.Range(0, count).Select(Function(i) collection.Skip(i * parts).Take(parts))

For Each item In result
    Debug.WriteLine(String.Join(", ", item))
Next

1, 2, 3, 4
5, 6, 7, 8
9, 10

强制扩展方法:

Public Module Extensions

    <System.Runtime.CompilerServices.Extension()>
    Public Function Split(Of TSource)(collection As IEnumerable(Of TSource), ByVal parts As Integer) As IEnumerable(Of IEnumerable(Of TSource))
        If (collection Is Nothing) Then Throw New ArgumentNullException("collection")
        If (parts < 1) Then Throw New ArgumentOutOfRangeException("parts")
        Dim count As Integer = collection.Count()
        If (count = 0) Then Return {}
        If (parts >= count) Then Return {collection}
        Return Enumerable.Range(0, CInt(Math.Ceiling(count / parts))).Select(Function(i) collection.Skip(i * parts).Take(parts))
    End Function

End Module

result = collection.Split(4)

迟交了一个答案,但只是想向您展示我的版本,因为在我看来它很简单 :) 这是查询:-

Dim result4 As IEnumerable(Of IEnumerable(Of Integer)) = collection
                            .Select(Function(v, i) New With {.Value = v, .Index = i}) _
                            .GroupBy(Function(x) x.Index \ 4) _
                            .Select(Function(x) x.Select(Function(z) z.Value))

这里是解释:-
1. 我使用了 Selectthis 重载来获取索引及其值。
2. 一旦我们有了索引,第 2 步就是简单的数学运算,因为我们将索引 (0,1,2---) 除以我们想要的部分数量(在本例中为 4),然后简单地将它们组合在一起。
3. 由于在第 1 步中我们分别投影了 valueindex,因此在最后一步中仅 select 值。

这里是 Complete Working Fiddle,里面有 2 个例子。