[英]Grouping consecutive identical items: IEnumerable<T> to IEnumerable<IEnumerable<T>>
我有一个有趣的问题:给定一个IEnumerable<string>
,是否有可能产生一个IEnumerable<IEnumerable<string>>
序列,它在一次传递中对相同的相邻字符串进行分组?
让我解释。
1.基本说明性样本:
考虑以下IEnumerable<string>
(伪表示):
{"a","b","b","b","c","c","d"}
如何获得IEnumerable<IEnumerable<string>>
,它将产生以下形式:
{ // IEnumerable<IEnumerable<string>>
{"a"}, // IEnumerable<string>
{"b","b","b"}, // IEnumerable<string>
{"c","c"}, // IEnumerable<string>
{"d"} // IEnumerable<string>
}
方法原型将是:
public IEnumerable<IEnumerable<string>> Group(IEnumerable<string> items)
{
// todo
}
但它也可能是:
public void Group(IEnumerable<string> items, Action<IEnumerable<string>> action)
{
// todo
}
......每个子序列都会调用action
。
2.更复杂的样本
好的,第一个样本非常简单,只是为了使高级意图清晰。
现在假设我们正在处理IEnumerable<Anything>
,其中Anything
是这样定义的类型:
public class Anything
{
public string Key {get;set;}
public double Value {get;set;}
}
现在,我们要基于密钥的子序列,(组每个连续Anything
具有相同的密钥),以便按组来计算总价值在以后使用它们:
public void Compute(IEnumerable<Anything> items)
{
Console.WriteLine(items.Sum(i=>i.Value));
}
// then somewhere, assuming the Group method
// that returns an IEnumerable<IEnumerable<Anything>> actually exists:
foreach(var subsequence in Group(allItems))
{
Compute(subsequence);
}
3.重要说明
它有可能吗,你会怎么写呢?
这是你想要的?
此解决方案依赖于对象状态,因为很难在使用yield(无ref或out params)的两个IEnumerable方法之间共享状态。
internal class Program
{
static void Main(string[] args)
{
var result = new[] { "a", "b", "b", "b", "c", "c", "d" }.Partition();
foreach (var r in result)
{
Console.WriteLine("Group".PadRight(16, '='));
foreach (var s in r)
Console.WriteLine(s);
}
}
}
internal static class PartitionExtension
{
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> src)
{
var grouper = new DuplicateGrouper<T>();
return grouper.GroupByDuplicate(src);
}
}
internal class DuplicateGrouper<T>
{
T CurrentKey;
IEnumerator<T> Itr;
bool More;
public IEnumerable<IEnumerable<T>> GroupByDuplicate(IEnumerable<T> src)
{
using(Itr = src.GetEnumerator())
{
More = Itr.MoveNext();
while (More)
yield return GetDuplicates();
}
}
IEnumerable<T> GetDuplicates()
{
CurrentKey = Itr.Current;
while (More && CurrentKey.Equals(Itr.Current))
{
yield return Itr.Current;
More = Itr.MoveNext();
}
}
}
编辑:添加了清洁用法的扩展方法。 固定循环测试逻辑,以便首先评估“更多”。
编辑:完成后处理枚举器
好的,废弃我之前的解决方案(我将其留在下面,仅供参考)。 这是我初次发布后发生的更好的方法。
编写一个实现IEnumerator<T>
的新类,并提供一些其他属性: IsValid
和Previous
。 这就是你真正需要通过使用yield
来维护迭代器块内的状态来解决所有问题。
这就是我做的方式(非常简单,你可以看到):
internal class ChipmunkEnumerator<T> : IEnumerator<T> {
private readonly IEnumerator<T> _internal;
private T _previous;
private bool _isValid;
public ChipmunkEnumerator(IEnumerator<T> e) {
_internal = e;
_isValid = false;
}
public bool IsValid {
get { return _isValid; }
}
public T Previous {
get { return _previous; }
}
public T Current {
get { return _internal.Current; }
}
public bool MoveNext() {
if (_isValid)
_previous = _internal.Current;
return (_isValid = _internal.MoveNext());
}
public void Dispose() {
_internal.Dispose();
}
#region Explicit Interface Members
object System.Collections.IEnumerator.Current {
get { return Current; }
}
void System.Collections.IEnumerator.Reset() {
_internal.Reset();
_previous = default(T);
_isValid = false;
}
#endregion
}
(我称之为ChipmunkEnumerator
因为保留以前的价值让我想起花栗鼠的脸颊是如何在他们保持坚果的情况下装袋。这真的很重要吗?不要取笑我。)
现在,在扩展方法中使用此类来提供您想要的行为并不是那么难!
请注意,下面我已经定义了GroupConsecutive
实际返回一个IEnumerable<IGrouping<TKey, T>>
,原因很简单,如果这些按键分组,那么返回一个IGrouping<TKey, T>
而不仅仅是一个IEnumerable<T>
。 事实证明,无论如何,这将帮助我们以后...
public static IEnumerable<IGrouping<TKey, T>> GroupConsecutive<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector)
where TKey : IEquatable<TKey> {
using (var e = new ChipmunkEnumerator<T>(source.GetEnumerator())) {
if (!e.MoveNext())
yield break;
while (e.IsValid) {
yield return e.GetNextDuplicateGroup(keySelector);
}
}
}
public static IEnumerable<IGrouping<T, T>> GroupConsecutive<T>(this IEnumerable<T> source)
where T : IEquatable<T> {
return source.GroupConsecutive(x => x);
}
private static IGrouping<TKey, T> GetNextDuplicateGroup<T, TKey>(this ChipmunkEnumerator<T> e, Func<T, TKey> keySelector)
where TKey : IEquatable<TKey> {
return new Grouping<TKey, T>(keySelector(e.Current), e.EnumerateNextDuplicateGroup(keySelector));
}
private static IEnumerable<T> EnumerateNextDuplicateGroup<T, TKey>(this ChipmunkEnumerator<T> e, Func<T, TKey> keySelector)
where TKey : IEquatable<TKey> {
do {
yield return e.Current;
} while (e.MoveNext() && keySelector(e.Previous).Equals(keySelector(e.Current)));
}
(为了实现这些方法,我编写了一个简单的Grouping<TKey, T>
类,以最直接的方式实现IGrouping<TKey, T>
。我省略了代码以便继续前进......)
好的,看看吧。 我认为下面的代码示例很好地捕获了类似于您在更新的问题中描述的更真实的场景。
var entries = new List<KeyValuePair<string, int>> {
new KeyValuePair<string, int>( "Dan", 10 ),
new KeyValuePair<string, int>( "Bill", 12 ),
new KeyValuePair<string, int>( "Dan", 14 ),
new KeyValuePair<string, int>( "Dan", 20 ),
new KeyValuePair<string, int>( "John", 1 ),
new KeyValuePair<string, int>( "John", 2 ),
new KeyValuePair<string, int>( "Bill", 5 )
};
var dupeGroups = entries
.GroupConsecutive(entry => entry.Key);
foreach (var dupeGroup in dupeGroups) {
Console.WriteLine(
"Key: {0} Sum: {1}",
dupeGroup.Key.PadRight(5),
dupeGroup.Select(entry => entry.Value).Sum()
);
}
输出:
Key: Dan Sum: 10
Key: Bill Sum: 12
Key: Dan Sum: 34
Key: John Sum: 3
Key: Bill Sum: 5
请注意,这也解决了我处理作为值类型的IEnumerator<T>
对象的原始答案的问题。 (用这种方法,没关系。)
如果您尝试在此处调用ToList
,仍然会出现问题,因为您会发现是否尝试过。 但考虑到你将延期执行作为一项要求 ,我怀疑你是否会这样做。 对于foreach
,它有效。
有些东西告诉我,我会因为这样说完全被驳斥,但......
是的 ,有可能(我认为)。 请看下面我扔的一个该死的混乱解决方案。 (抓住一个例外,知道什么时候结束,所以你知道这是一个很棒的设计!)
现在,Jon的观点是,如果您尝试执行事件,例如ToList
,然后通过索引访问结果列表中的值,则完全有效。 但是如果你在这里的唯一目的是能够使用foreach
IEnumerable<T>
- 并且你只是在自己的代码中执行此操作 - 那么,我认为这可能适合你。
无论如何,这是一个如何工作的快速示例:
var ints = new int[] { 1, 3, 3, 4, 4, 4, 5, 2, 3, 1, 6, 6, 6, 5, 7, 7, 8 };
var dupeGroups = ints.GroupConsecutiveDuplicates(EqualityComparer<int>.Default);
foreach (var dupeGroup in dupeGroups) {
Console.WriteLine(
"New dupe group: " +
string.Join(", ", dupeGroup.Select(i => i.ToString()).ToArray())
);
}
输出:
New dupe group: 1
New dupe group: 3, 3
New dupe group: 4, 4, 4
New dupe group: 5
New dupe group: 2
New dupe group: 3
New dupe group: 1
New dupe group: 6, 6, 6
New dupe group: 5
New dupe group: 7, 7
New dupe group: 8
现在为(麻烦的废话)代码:
请注意,由于此方法需要在几个不同的方法之间传递实际的枚举器 ,因此如果该枚举器是值类型则不起作用 ,因为在一个方法中对MoveNext
仅影响本地副本。
public static IEnumerable<IEnumerable<T>> GroupConsecutiveDuplicates<T>(this IEnumerable<T> source, IEqualityComparer<T> comparer) {
using (var e = source.GetEnumerator()) {
if (e.GetType().IsValueType)
throw new ArgumentException(
"This method will not work on a value type enumerator."
);
// get the ball rolling
if (!e.MoveNext()) {
yield break;
}
IEnumerable<T> nextDuplicateGroup;
while (e.FindMoreDuplicates(comparer, out nextDuplicateGroup)) {
yield return nextDuplicateGroup;
}
}
}
private static bool FindMoreDuplicates<T>(this IEnumerator<T> enumerator, IEqualityComparer<T> comparer, out IEnumerable<T> duplicates) {
duplicates = enumerator.GetMoreDuplicates(comparer);
return duplicates != null;
}
private static IEnumerable<T> GetMoreDuplicates<T>(this IEnumerator<T> enumerator, IEqualityComparer<T> comparer) {
try {
if (enumerator.Current != null)
return enumerator.GetMoreDuplicatesInner(comparer);
else
return null;
} catch (InvalidOperationException) {
return null;
}
}
private static IEnumerable<T> GetMoreDuplicatesInner<T>(this IEnumerator<T> enumerator, IEqualityComparer<T> comparer) {
while (enumerator.Current != null) {
var current = enumerator.Current;
yield return current;
if (!enumerator.MoveNext())
break;
if (!comparer.Equals(current, enumerator.Current))
break;
}
}
你的第二颗子弹是有问题的。 原因如下:
var groups = CallMagicGetGroupsMethod().ToList();
foreach (string x in groups[3])
{
...
}
foreach (string x in groups[0])
{
...
}
在这里,它试图迭代第四组,然后是第一组...如果所有组都被缓冲或者它可以重新读取序列,那么这显然只会起作用,这两者都不是理想的。
我怀疑你想要一个更“反应”的方法 - 我不知道Reactive Extensions是否做你想要的(“连续”要求是不寻常的)但你基本上应该提供某种行动来对每个组执行..这样一来,这个方法就不用担心必须在你读完之后再给你一些可以用的东西了。
如果您希望我尝试在Rx中找到解决方案,或者您是否对以下内容感到满意,请告诉我们:
void GroupConsecutive(IEnumerable<string> items,
Action<IEnumerable<string>> action)
这是一个我认为满足您的要求的解决方案,适用于任何类型的数据项,并且非常简短和可读:
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> list)
{
var current = list.FirstOrDefault();
while (!Equals(current, default(T))) {
var cur = current;
Func<T, bool> equalsCurrent = item => item.Equals(cur);
yield return list.TakeWhile(equalsCurrent);
list = list.SkipWhile(equalsCurrent);
current = list.FirstOrDefault();
}
}
笔记:
TakeWhile
和SkipWhile
都这样做)。 SkipWhile
); 当您处理返回的IEnumerables时,它会再次迭代集合,但分区本身只迭代一次。 while
条件更改为null
测试。 如果我在某种程度上错了,我会特别感兴趣的是指出错误的评论!
非常重要的旁边:
该解决方案将不允许枚举产生可枚举比它提供他们在一个以外的任何命令。不过,我认为原来的海报已在意见很清楚,这是没有问题的。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.