是否可以克隆IEnumerable <t>实例,保存迭代状态的副本?</t>

时间:2009-12-16 05:26:55

标签: c# .net linq ienumerable clone

我想创建一个IEnumerator<T>的副本,以便我可以从集合中的特定位置重新启动枚举过程。显然,对于实现IList的集合,这样做没有任何好处,因为我们可以记住感兴趣的索引。

使用yield语句和Linq函数的组合是否有一种聪明的方法来完成此任务?我找不到合适的Clone()方法来复制枚举器,并且希望避免使用Enumerable.Skip()将新的枚举器重新定位到所需的恢复点。

此外,我希望尽可能保持解决方案的通用性,而不必依赖任何具体集合中的状态。

6 个答案:

答案 0 :(得分:4)

你能做的最好的事情就是写一些保留缓冲区(可能是Queue<T>)从一个而不是另一个所消耗的数据(如果你将一个迭代器提升到1M的位置会变得混乱/昂贵,但是另一个人离开了。我真的认为你最好不要重新考虑设计,而只是使用GetEnumerator()(即另一个foreach)重新开始 - 或缓冲数据(如果短)在列表/数组/中。

没有任何优雅的内置。


更新:这里可能有一个有趣的替代设计是“PushLINQ”;它不是克隆迭代器,而是允许多个“事物”同时消耗相同的数据提要

在这个例子中(从Jon的页面中解除),我们并行计算多个聚合:

// Create the data source to watch
DataProducer<Voter> voters = new DataProducer<Voter>();

// Add the aggregators
IFuture<int> total = voters.Count();
IFuture<int> adults = voters.Count(voter => voter.Age >= 18);
IFuture<int> children = voters.Where(voter => voter.Age < 18).Count();
IFuture<int> youngest = voters.Min(voter => voter.Age);
IFuture<int> oldest = voters.Select(voter => voter.Age).Max();

// Push all the data through
voters.ProduceAndEnd(Voter.AllVoters());

// Write out the results
Console.WriteLine("Total voters: {0}", total.Value);
Console.WriteLine("Adult voters: {0}", adults.Value);
Console.WriteLine("Child voters: {0}", children.Value);
Console.WriteLine("Youngest vote age: {0}", youngest.Value);
Console.WriteLine("Oldest voter age: {0}", oldest.Value);

答案 1 :(得分:3)

没有通用的方法,因为iEnumerable可能依赖于系统状态的任意方面,这些方面无法通过Reflection或任何其他方法检测到。例如,PaperTapeReader类可能实现一个枚举器,该枚举器从磁带读取字符,直到传感器指示机器中没有磁带。这种枚举器的状态将是磁带的物理位置,这可能无法以编程方式恢复。

给定一个iEnumerable,可以生成两个或更多个iEnumebles,每个iEnumerables都可以像原始或克隆一样。对“最远的那个”的MoveNext请求将从原始iEnumerable中读取新数据并将其缓冲为其他数据。但是,除非原始的iEnumerable支持这种“钩子”功能,否则我认为没有任何方法可以锁定其数据。

答案 2 :(得分:2)

这完全是不是的答案,但我觉得有趣的思想实验......如果你有一个基于产量的IEnumerable,我想你知道它是所有编译器生成的魔法。如果你有这样的野兽,你可以做这样的事情......;)

class Program
{
    static void Main(string[] args)
    {
        var bar = new Program().Foo();

        // Get a hook to the underlying compiler generated class
        var barType = bar.GetType().UnderlyingSystemType;
        var barCtor = barType.GetConstructor(new Type[] {typeof (Int32)});
        var res = barCtor.Invoke(new object[] {-2}) as IEnumerable<int>;

        // Get our enumerator
        var resEnum = res.GetEnumerator();
        resEnum.MoveNext();
        resEnum.MoveNext();
        Debug.Assert(resEnum.Current == 1);

        // Extract and save our state
        var nonPublicMap = new Dictionary<FieldInfo, object>();
        var publicMap = new Dictionary<FieldInfo, object>();
        var nonpublicfields = resEnum.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
        var publicfields = resEnum.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance);
        foreach(var field in nonpublicfields)
        {
            var value = field.GetValue(resEnum);
            nonPublicMap[field] = value;
        }
        foreach (var field in publicfields)
        {
            var value = field.GetValue(resEnum);
            publicMap[field] = value;                
        }

        // Move about
        resEnum.MoveNext();
        resEnum.MoveNext();
        resEnum.MoveNext();
        resEnum.MoveNext();
        Debug.Assert(resEnum.Current == 5);

        // Restore state            
        foreach (var kvp in nonPublicMap)
        {
            kvp.Key.SetValue(resEnum, kvp.Value);
        }
        foreach (var kvp in publicMap)
        {
            kvp.Key.SetValue(resEnum, kvp.Value);                
        }

        // Move about
        resEnum.MoveNext();
        resEnum.MoveNext();
        Debug.Assert(resEnum.Current == 3);
    }

    public IEnumerable<int> Foo()
    {
        for (int i = 0; i < 10; i++)
        {
            yield return i;
        }
        yield break;
    }

}

答案 3 :(得分:1)

您是希望能够保存状态,继续枚举,然后返回已保存状态,还是只想简单地枚举,执行其他操作,然后继续枚举?

如果是后者,可能会有以下内容:

public class SaveableEnumerable<T> : IEnumerable<T>, IDisposable
{
    public class SaveableEnumerator : IEnumerator<T>
    {
        private IEnumerator<T> enumerator;

        internal SaveableEnumerator(IEnumerator<T> enumerator)
        {
            this.enumerator = enumerator;
        }

        public void Dispose() { }

        internal void ActuallyDispose()
        {
            enumerator.Dispose();
        }

        public bool MoveNext()
        {
            return enumerator.MoveNext();
        }

        public void Reset()
        {
            enumerator.Reset();
        }

        public T Current
        {
            get { return enumerator.Current; }
        }

        object IEnumerator.Current
        {
            get { return enumerator.Current; }
        }
    }

    private SaveableEnumerator enumerator;

    public SaveableEnumerable(IEnumerable<T> enumerable)
    {
        this.enumerator = new SaveableEnumerator(enumerable.GetEnumerator());
    }

    public IEnumerator<T> GetEnumerator()
    {
        return enumerator;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return enumerator;
    }

    public void Dispose()
    {
        enumerator.ActuallyDispose();
    }
}

现在你可以做到:

using (IEnumerable<int> counter = new SaveableEnumerable<int>(CountableEnumerable()))
{
    foreach (int i in counter)
    {
        Console.WriteLine(i);
        if (i > 10)
        {
            break;
        }
    }
    DoSomeStuff();
    foreach (int i in counter)
    {
        Console.WriteLine(i);
        if (i > 20)
        {
            break;
        }
    }
}

答案 4 :(得分:1)

JerKimball有一个有趣的方法。我试着将它提升到一个新的水平。这使用反射创建新实例,然后在新实例上设置值。我也从深度的C#中发现这一章非常有用。 Iterator block implementation details: auto-generated state machines

static void Main()
{
    var counter = new CountingClass();
    var firstIterator = counter.CountingEnumerator();
    Console.WriteLine("First list");
    firstIterator.MoveNext();
    Console.WriteLine(firstIterator.Current);

    Console.WriteLine("First list cloned");
    var secondIterator = EnumeratorCloner.Clone(firstIterator);

    Console.WriteLine("Second list");
    secondIterator.MoveNext();
    Console.WriteLine(secondIterator.Current);
    secondIterator.MoveNext();
    Console.WriteLine(secondIterator.Current);
    secondIterator.MoveNext();
    Console.WriteLine(secondIterator.Current);

    Console.WriteLine("First list");
    firstIterator.MoveNext();
    Console.WriteLine(firstIterator.Current);
    firstIterator.MoveNext();
    Console.WriteLine(firstIterator.Current);
}

public class CountingClass
{
    public IEnumerator<int> CountingEnumerator()
    {
        int i = 1;
        while (true)
        {
            yield return i;
            i++;
        }
    }
}

public static class EnumeratorCloner
{
    public static T Clone<T>(T source) where T : class, IEnumerator
    {
        var sourceType = source.GetType().UnderlyingSystemType;
        var sourceTypeConstructor = sourceType.GetConstructor(new Type[] { typeof(Int32) });
        var newInstance = sourceTypeConstructor.Invoke(new object[] { -2 }) as T;

        var nonPublicFields = source.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
        var publicFields = source.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance);
        foreach (var field in nonPublicFields)
        {
            var value = field.GetValue(source);
            field.SetValue(newInstance, value);
        }
        foreach (var field in publicFields)
        {
            var value = field.GetValue(source);
            field.SetValue(newInstance, value);
        }
        return newInstance;
    }
}

答案 5 :(得分:0)

所以真正想要的是能够稍后恢复迭代,我是否正确?克隆调查员或集合是你认为你做这样的事情吗?

你可以创建一个包装IEnumerable的类,并公开一个自定义枚举器,它在内部克隆内部IEnumerable,然后枚举它。然后,使用GetEnumerator()将为您提供一个可以传递的枚举器。

这将为“飞行中”的每个枚举器创建一个额外的IEnumerable副本,但我认为它可以满足您的需求。