代码之家  ›  专栏  ›  技术社区  ›  Romain Verdier

将连续的相同项分组:IEnumerable<t>到IEnumerable<IEnumerable<t>>

  •  7
  • Romain Verdier  · 技术社区  · 14 年前

    我有一个相互关联的问题:给定 IEnumerable<string> ,是否可以生成一个序列 IEnumerable<IEnumerable<string>> 在一次传递中对相同的相邻字符串进行分组?

    让我解释一下。

    1。基本示例:

    考虑以下事项 IEnumerable<字符串> (伪表示):

    {"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;}
    }
    

    现在我们要根据键生成子序列,(将每个连续的 任何东西 具有相同的键)以后使用它们以便按组计算总值:

    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);
    }
    

    三。重要音符

    • 只有 一次迭代 在原来的序列上
    • 无中间收款 分配(我们可以假定原始序列中有数百万个项目,每组中有数百万个连续项目)
    • 保留枚举器和 延期执行 行为
    • 我们可以假定生成的子序列只迭代一次,并且将按顺序迭代。

    有可能吗?你会怎么写?

    4 回复  |  直到 12 年前
        1
  •  5
  •   dss539    12 年前

    这就是你要找的吗?

    • 只迭代一次列表。
    • 推迟执行。
    • 没有中间集合(我的另一篇文章在此标准上失败)。

    此解决方案依赖于对象状态,因为很难在使用yield的两个IEnumerable方法之间共享状态(没有ref或out参数)。

    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();
            }
        }
    }
    

    编辑:添加了用于清除器用法的扩展方法。固定循环测试逻辑,以便首先评估“更多”。

    编辑:完成后释放枚举器

        2
  •  3
  •   Dan Tao    14 年前

    更好的解决方案,满足所有需求

    好的,废弃我以前的解决方案(我将把它留在下面,仅供参考)。这里有一个更好的方法,在我做了最初的职位后发生在我身上。

    编写一个实现 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> 实现的类 i分组<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 这是有效的。


    原始的,混乱的,有点愚蠢的解决方案

    有件事告诉我说这个我会被完全驳倒,但是…

    是的 ,这是可能的(我想)。见下文 该死 我把杂乱的解决方案放在一起。(捕获一个异常以知道它何时结束,所以您 知道 这是一个伟大的设计!)

    现在,乔恩的观点是,在你试图做的事情中存在一个非常真实的问题,例如, 托利斯特 ,然后按索引访问结果列表中的值,是完全有效的。但是如果你 只有 这里的目的是能够循环 IEnumerable<t> 使用A 前额 你是 只有 在你的 拥有 代码——那么,好吧,我想这对你来说是可行的。

    不管怎样,这里有一个简单的例子说明它是如何工作的:

    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;
        }
    }
    
        3
  •  2
  •   Jon Skeet    14 年前

    你的第二颗子弹是有问题的。这就是为什么:

    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)
    
        4
  •  2
  •   Jon    14 年前

    这里有一个解决方案,我认为它可以满足您的需求,适用于任何类型的数据项,并且非常简短和易读:

    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();
        }
    }
    

    笔记:

    1. 有延迟执行(两者都有 TakeWhile SkipWhile 做这件事。
    2. 我认为它只在整个集合中迭代一次(使用 船长 );在处理返回的IEnumerable时,它会再次迭代集合,但分区本身只迭代一次。
    3. 如果不关心值类型,可以添加约束并更改 while 测试的条件 null .

    如果我弄错了,我会特别感兴趣的评论指出错误!

    除此之外非常重要:

    此解决方案将 允许您以除提供它们的顺序以外的任何顺序枚举生成的可枚举项。但是,我认为原始的海报在评论中已经很清楚了,这不是问题。