Linq/C#:如何根据列表项信息将列表拆分为可变长度的块
本文关键字:列表 拆分 信息 何根 Linq | 更新日期: 2023-09-27 17:57:55
我正试图根据某些Type
信息将Linq中类型为Record
的列表拆分为子列表。每组记录之前总是有一条类型为"a"的记录,之后总是有一个类型为"b"的记录。我有一个课程Record
:
class Record
{
public string Type { get; set; }
public string SomeOtherInformation { get; set; }
}
以下是示例列表(List<Record> records
):
Type SomeOtherInformation
a ......
x ......
x ......
b ......
a ......
b ......
a ......
x ......
x ......
x ......
x ......
x ......
b ......
所需输出为(List<List<Record>> lists
):
List #1: List #2: List #3:
a ...... a ...... a ......
x ...... b ...... x ......
x ...... x ......
b ...... x ......
x ......
x ......
b ......
我目前正在使用for循环浏览这个列表,每当类型为"a"时创建一个新列表,当项目的类型为"b"时将其添加到子列表中。我想知道林克是否有更好的方法。林克能做到这一点吗?如果可以,怎么做?
据我所知,用普通的LINQ不能干净地做到这一点。LINQ中的流媒体运营商依赖于您能够根据仅该项目,以及可能在原始源中的索引,对项目做出决定(例如,是否过滤它,如何投影它,如何对它进行分组)。在您的情况下,您确实需要更多的信息——您需要知道您已经看到了多少b
项目。
你可以这样做:
int bs = 0;
var groups = records.GroupBy(item => item.Type == 'b' ? bs++ : bs,
(key, group) => group.ToList())
.ToList();
然而,这取决于分组投影中b++
的副作用(以跟踪我们已经看到的b
项目的数量)-这肯定不是惯用的LINQ,我不推荐它。
我会为此使用一个扩展方法:
public static IEnumerable<IEnumerable<TSource>> SplitItems<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> startItem,
Func<TSource, bool> endItem)
{
var tempList = new List<TSource>();
int counter = 0;
foreach (var item in source)
{
if (startItem(item) || endItem(item)) counter++;
tempList.Add(item);
if (counter%2 == 0)
{
yield return tempList;
tempList = new List<TSource>();
}
}
}
以下是用法:
var result = list.SplitItems(x => x.Type == "a", x => x.Type == "b").ToList();
这将返回一个包含3
项目的List<IEnumerable<Record>>
。当然,该方法假设至少在开头有一个开始项,在结尾有一个结束项。您可能需要添加一些检查,并根据您的要求进行改进。
绝对不是纯LINQ,但我可以想象在循环中使用TakeWhile来实现这一点:
List<Record> data;
List<List<Record>> result = new List<List<Record>>();
IEnumerable<Record> workingData = data;
while (workingData.Count() > 0)
{
IEnumerable<Record> subList = workingData.Take(1).Concat(workingData.Skip(1).TakeWhile(c => c.Type != 'a'));
result.Add(subList.ToList());
workingData = workingData.Except(subList);
}
为了解释,我们在序列的开头得到一个我们知道的"a",然后跳过它,直到我们遇到另一个"a"。这构成了其中一个子记录,所以我们将其添加到结果中。然后,我们从"工作"集中删除这个subList,并再次枚举,直到元素用完为止。
我不确定这会比你现有的解决方案更好,但希望它能有所帮助!
这实际上是有效的(在VS2013,.NET4.5.1上测试),使用workingData而不是循环中的数据(我的一个拼写错误,上面已经修复)。Except
将使用默认的比较器来比较对象,因为我们没有重写.Equals,它将比较引用(实际上是指针)。因此,重复数据不是问题。如果重写了.Equals,则需要确保每条记录都是唯一的。
如果有人想验证这一点,这里是我的测试程序(只需在Console.ReadKey
上放一个断点,你就会看到结果有正确的数据):
class Program
{
static void Main(string[] args)
{
List<Record> testData = new List<Record>()
{
new Record() { Type = 'a', Data="Data" },
new Record() { Type = 'x', Data="Data" },
new Record() { Type = 'b', Data="Data" },
new Record() { Type = 'a', Data="Data" },
new Record() { Type = 'x', Data="Data" },
new Record() { Type = 'x', Data="Data" },
new Record() { Type = 'x', Data="Data" },
new Record() { Type = 'x', Data="Data" },
new Record() { Type = 'x', Data="Data" },
new Record() { Type = 'x', Data="Data" },
new Record() { Type = 'b', Data="Data" },
new Record() { Type = 'a', Data="Data" },
new Record() { Type = 'x', Data="Data" },
new Record() { Type = 'x', Data="Data" },
new Record() { Type = 'x', Data="Data" },
new Record() { Type = 'b', Data="Data" }
};
List<List<Record>> result = new List<List<Record>>();
IEnumerable<Record> workingData = testData;
while (workingData.Count() > 0)
{
IEnumerable<Record> subList = workingData.Take(1).Concat(workingData.Skip(1).TakeWhile(c => c.Type != 'a'));
result.Add(subList.ToList());
workingData = workingData.Except(subList);
}
Console.ReadKey();
}
}
class Record
{
public char Type;
public String Data;
}
如前所述,LINQ不能很好地处理这种情况,因为您只能根据当前项目而不是以前看到的项目来做出决策。您需要维护某种状态来跟踪分组。依赖副作用
编写自己的扩展方法将是更好的选择。您可以保持状态,并使其完全自包含(与现有的操作符(如GroupBy()
和其他操作符)非常相似)。这是我的一个实现,它可以选择性地包括起始项和结束项中不包含的项。
public static IEnumerable<IImmutableList<TSource>> GroupByDelimited<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> startDelimiter,
Func<TSource, bool> endDelimiter,
bool includeUndelimited = false)
{
var delimited = default(ImmutableList<TSource>.Builder);
var undelimited = default(ImmutableList<TSource>.Builder);
foreach (var item in source)
{
if (delimited == null)
{
if (startDelimiter(item))
{
if (includeUndelimited && undelimited != null)
{
yield return undelimited.ToImmutable();
undelimited = null;
}
delimited = ImmutableList.CreateBuilder<TSource>();
}
else if (includeUndelimited)
{
if (undelimited == null)
{
undelimited = ImmutableList.CreateBuilder<TSource>();
}
undelimited.Add(item);
}
}
if (delimited != null)
{
delimited.Add(item);
if (endDelimiter(item))
{
yield return delimited.ToImmutable();
delimited = null;
}
}
}
}
但是,如果真的愿意,您仍然可以使用LINQ运算符(Aggregate()
)来完成此操作,但这不是一个真正的LINQ解决方案。它将再次看起来像一个自包含的foreach
循环。
var result = records.Aggregate(
Tuple.Create(default(List<Record>), new List<List<Record>>()),
(acc, record) =>
{
var grouping = acc.Item1;
var result = acc.Item2;
if (grouping == null && record.Type == "a")
{
grouping = new List<Record>();
}
if (grouping != null)
{
grouping.Add(record);
if (record.Type == "b")
{
result.Add(grouping);
grouping = null;
}
}
return Tuple.Create(grouping, result);
},
acc => acc.Item2
);