Можно ли клонировать экземпляр IEnumerable <T>, сохранив копию состояния итерации? - PullRequest
9 голосов
/ 16 декабря 2009

Я хотел бы создать копию IEnumerator<T>, чтобы я мог перезапустить процесс перечисления из определенного места в коллекции. Очевидно, что для коллекций, которые реализуют IList, это не выгодно, поскольку мы можем запомнить индекс интереса.

Есть ли умный способ выполнить эту задачу, используя комбинацию операторов yield и функций Linq? Я не смог найти подходящий метод Clone() для копирования перечислителя и хотел бы избежать использования Enumerable.Skip() для перемещения нового перечислителя в желаемую точку возобновления.

Кроме того, я бы хотел, чтобы решения были как можно более общими и не зависеть от состояния каких-либо конкретных коллекций.

Ответы [ 6 ]

4 голосов
/ 16 декабря 2009

Лучшее, что вы можете сделать, это написать что-то, что хранит буфер (возможно, Queue<T>) данных, потребляемых от одного, а не от другого (что может стать грязным / дорогим, если вы продвинетесь на один итератор на 1M позиций, но уйдете другой один). Я действительно думаю, что вам лучше переосмыслить дизайн, и просто использовать GetEnumerator() (т. Е. Еще один foreach) для повторного запуска - или буферизировать данные (если они короткие) в списке / массиве /whatever.

Ничего элегантного встроенного.


Обновление: возможно, интересный альтернативный дизайн здесь " PushLINQ "; вместо того, чтобы клонировать итератор, он позволяет нескольким «вещам» потреблять один и тот же поток данных одновременно .

В этом примере (снято со страницы Джона) мы вычисляем несколько агрегатов параллельно:

// 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);
3 голосов
/ 16 октября 2010

Нет общего способа сделать это, так как iEnumerable может зависеть от произвольных аспектов состояния системы, которые не могут быть обнаружены с помощью Reflection или любым другим способом. Например, класс PaperTapeReader может реализовывать перечислитель, который считывает символы с ленты, пока датчик не покажет, что в машине больше нет ленты. Состояние такого перечислителя будет физическим местоположением ленты, которое может быть невозможно восстановить программным способом.

Учитывая iEnumerable, можно создать два или более элементов iEnumerable, каждый из которых будет действовать как оригинал или как его клон. Запросы MoveNext для того, который был «дальше всех», будут считывать новые данные из исходного iEnumerable и буферизовать его для остальных. Однако если исходный iEnumerable не поддерживает такую ​​функцию «ловушки», я не думаю, что можно было бы каким-либо образом зафиксировать его данные по мере их поступления.

2 голосов
/ 17 декабря 2009

Это полностью не ответ, но мысленный эксперимент, который я нашел интересным ... если у вас есть 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;
    }

}
1 голос
/ 17 февраля 2010

У JerKimball был интересный подход. Я пытаюсь поднять это на следующий уровень. При этом используется отражение для создания нового экземпляра, а затем устанавливаются значения для нового экземпляра. Я также нашел эту главу из C # в глубине, чтобы быть очень полезным. Детали реализации блока итератора: автоматически генерируемые конечные автоматы

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;
    }
}
1 голос
/ 16 декабря 2009

Хотите ли вы сохранить состояние, продолжить перечисление, затем вернуться в сохраненное состояние, или вы просто хотите иметь возможность перечислять, делать какие-то другие вещи, затем продолжать перечисление?

Если это последнее, может сработать что-то вроде следующего:

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;
        }
    }
}
0 голосов
/ 16 декабря 2009

Так что вы действительно хотите, чтобы иметь возможность возобновить итерацию позже, я прав? А клонирование перечислителя или коллекции - это то, как вы думаете, вы бы поступили так?

Вы можете создать класс, который оборачивает IEnumerable и предоставляет собственный перечислитель, который внутренне клонирует внутренний IEnumerable, а затем перечисляет его. Затем использование GetEnumerator () даст вам перечислитель, который можно обойти.

Это создаст дополнительную копию IEnumerable для каждого перечислителя "в полете", но я думаю, что он будет соответствовать вашим потребностям.

...