实现不可变的枚举器

时间:2017-07-23 09:34:43

标签: c# ienumerable immutability ienumerator

考虑以下可变通用枚举器的可能接口:

interface IImmutableEnumerator<T>
{
    (bool Succesful, IImmutableEnumerator<T> NewEnumerator) MoveNext();
    T Current { get; }
}

如何在c#中以合理的方式实现这一点?我有点想法,因为.NET中的IEnumerator基础设施本质上是可变的,我无法找到解决方法。

一个天真的实现是简单地在每个MoveNext()上创建一个新的枚举器,用current.Skip(1).GetEnumerator()传递一个新的内部可变枚举器,但效率非常低。

我正在实现一个需要能够向前看的解析器;使用不可变的枚举器可以使事情更清晰,更容易理解,所以我很好奇是否有一种简单的方法可以做到这一点,我可能会失踪。

输入是IEnumerable<T>,我无法改变。我总是可以使用ToList()来实现可枚举(手持IList,向前看是微不足道的),但数据可能非常大,如果可能的话我想避免使用它。

2 个答案:

答案 0 :(得分:2)

就是这样:

public class ImmutableEnumerator<T> : IImmutableEnumerator<T>, IDisposable
{
    public static (bool Succesful, IImmutableEnumerator<T> NewEnumerator) Create(IEnumerable<T> source)
    {
        var enumerator = source.GetEnumerator();
        var successful = enumerator.MoveNext();
        return (successful, new ImmutableEnumerator<T>(successful, enumerator));
    }
    private IEnumerator<T> _enumerator;
    private (bool Succesful, IImmutableEnumerator<T> NewEnumerator) _runOnce = (false, null);
    private ImmutableEnumerator(bool successful, IEnumerator<T> enumerator)
    {
        _enumerator = enumerator;
        this.Current = successful ? _enumerator.Current : default(T);
        if (!successful)
        {
            _enumerator.Dispose();
        }
    }
    public (bool Succesful, IImmutableEnumerator<T> NewEnumerator) MoveNext()
    {
        if (_runOnce.NewEnumerator == null)
        {
            var successful = _enumerator.MoveNext();
            _runOnce = (successful, new ImmutableEnumerator<T>(successful, _enumerator));
        }
        return _runOnce;
    }
    public T Current { get; private set; }
    public void Dispose()
    {
        _enumerator.Dispose();
    }
}

我的测试代码很成功:

var xs = new[] { 1, 2, 3 };

var ie = ImmutableEnumerator<int>.Create(xs);
if (ie.Succesful)
{
    Console.WriteLine(ie.NewEnumerator.Current);
    var ie1 = ie.NewEnumerator.MoveNext();
    if (ie1.Succesful)
    {
        Console.WriteLine(ie1.NewEnumerator.Current);
        var ie2 = ie1.NewEnumerator.MoveNext();
        if (ie2.Succesful)
        {
            Console.WriteLine(ie2.NewEnumerator.Current);
            var ie3 = ie2.NewEnumerator.MoveNext();
            if (ie3.Succesful)
            {
                Console.WriteLine(ie3.NewEnumerator.Current);
                var ie4 = ie3.NewEnumerator.MoveNext();
            }
        }
    }
}

输出:

1
2
3

它是不可改变的,而且效率很高。

这是根据评论中的请求使用Lazy<(bool, IImmutableEnumerator<T>)>的版本:

public class ImmutableEnumerator<T> : IImmutableEnumerator<T>, IDisposable
{
    public static (bool Succesful, IImmutableEnumerator<T> NewEnumerator) Create(IEnumerable<T> source)
    {
        var enumerator = source.GetEnumerator();
        var successful = enumerator.MoveNext();
        return (successful, new ImmutableEnumerator<T>(successful, enumerator));
    }
    private IEnumerator<T> _enumerator;
    private Lazy<(bool, IImmutableEnumerator<T>)> _runOnce;
    private ImmutableEnumerator(bool successful, IEnumerator<T> enumerator)
    {
        _enumerator = enumerator;
        this.Current = successful ? _enumerator.Current : default(T);
        if (!successful)
        {
            _enumerator.Dispose();
        }
        _runOnce = new Lazy<(bool, IImmutableEnumerator<T>)>(() =>
        {
            var s = _enumerator.MoveNext();
            return (s, new ImmutableEnumerator<T>(s, _enumerator));
        });
    }
    public (bool Succesful, IImmutableEnumerator<T> NewEnumerator) MoveNext()
    {
        return _runOnce.Value;
    }
    public T Current { get; private set; }
    public void Dispose()
    {
        _enumerator.Dispose();
    }
}

答案 1 :(得分:1)

通过使用单链表,您可以实现适合此特定方案的伪不变性。它允许无限前瞻(仅限于您的堆大小),而无法查看以前处理的节点(除非您碰巧存储对先前处理的节点的引用 - 您不应该这样做)。

此解决方案满足所述要求(除了不符合您的确切界面,其所有功能都完好无损)。

此类链表的使用可能如下所示:

IEnumerable<int> numbersFromZeroToNine = Enumerable.Range(0, 10);

using (IEnumerator<int> enumerator = numbersFromZeroToNine.GetEnumerator())
{
    var node = LazySinglyLinkedListNode<int>.CreateListHead(enumerator);

    while (node != null)
    {
        Console.WriteLine($"Current value: {node.Value}.");

        if (node.Next != null)
        {
            // Single-element look-ahead. Technically you could do node.Next.Next...Next.
            // You can also nest another while loop here, and look ahead as much as needed.
            Console.WriteLine($"Next value: {node.Next.Value}.");
        }
        else
        {
            Console.WriteLine("End of collection reached. There is no next value.");
        }

        node = node.Next;

        // At this point the object which used to be referenced by the "node" local
        // becomes eligible for collection, preventing unbounded memory growth.
    }
}

输出:

Current value: 0.
Next value: 1.
Current value: 1.
Next value: 2.
Current value: 2.
Next value: 3.
Current value: 3.
Next value: 4.
Current value: 4.
Next value: 5.
Current value: 5.
Next value: 6.
Current value: 6.
Next value: 7.
Current value: 7.
Next value: 8.
Current value: 8.
Next value: 9.
Current value: 9.
End of collection reached. There is no next value.

实施如下:

sealed class LazySinglyLinkedListNode<T>
{
    public static LazySinglyLinkedListNode<T> CreateListHead(IEnumerator<T> enumerator)
    {
        return enumerator.MoveNext() ? new LazySinglyLinkedListNode<T>(enumerator) : null;
    }

    public T Value { get; }

    private IEnumerator<T> Enumerator;
    private LazySinglyLinkedListNode<T> _next;

    public LazySinglyLinkedListNode<T> Next
    {
        get
        {
            if (_next == null && Enumerator != null)
            {
                if (Enumerator.MoveNext())
                {
                    _next = new LazySinglyLinkedListNode<T>(Enumerator);
                }
                else
                {
                    Enumerator = null; // We've reached the end.
                }
            }

            return _next;
        }
    }

    private LazySinglyLinkedListNode(IEnumerator<T> enumerator)
    {
        Value = enumerator.Current;
        Enumerator = enumerator;
    }
}

这里需要注意的一件重要事情是,源集合只能枚举一次,懒惰,每个节点的生命周期最多调用一次MoveNext,无论您访问多少次Next

使用双向链接列表会允许后视,但会导致无限的内存增长并需要定期修剪,这并非易事。单链接列表可以避免此问题,只要您不在主循环之外存储节点引用即可。在上面的示例中,您可以使用numbersFromZeroToNine生成器替换IEnumerable<int>,该生成器无限地生成整数,并且循环将永远运行而不会耗尽内存。