天天看點

【Practical C#】C#中的疊代器 Iterator(IEnumerable和IEnumerator)

網上關于C#的疊代器教程也很多,但很多例程隻是簡單的封裝一個可以疊代的執行個體變量,然後直接傳回此執行個體變量的疊代器或者使用yield return語句。這樣的例程沒有觸及到真正疊代器的實作。

問題域描述:

給定一個整數序列,統計連續的奇數或者偶數的個數。

例如對于以下整數序列

1, 2, 4, 10, 10, 8, 25, 13, 5, 7

輸出:

IsOdd = True, Count = 1 (根據 1 中統計得到)

IsOdd = False, Count = 5 (根據 2, 4, 10, 10, 8 統計得到)

IsOdd = True, Count = 4 (根據 25, 13, 5, 7 統計得到)

這樣的問題應該比較簡單,大家都比較容易了解問題域本身。

在此例程中,我試着用TDD的方式去解決問題,是以以下單元測試很有參考價值。雖然需要寫更多的代碼,但TDD的方式确實還是值得推崇的。

Solution

【Practical C#】C#中的疊代器 Iterator(IEnumerable和IEnumerator)

該示例是在VS2017 + NUnit中示範的。

AnalysisItemEnumerator(疊代器)實作IEnumerator<AnalysisItem>

Analyzer(可疊代)實作IEnumerable<AnalysisItem>

再次強調那些單元測試真的能驅動開發程式。

AnalysisItem.cs

public class AnalysisItem : IEquatable<AnalysisItem>
    {
        public bool IsOdd { get; }

        public int Count { get; }

        public AnalysisItem(bool isOdd, int count)
        {
            IsOdd = isOdd;
            Count = count;
        }

        public override string ToString()
        {
            return $"IsOdd = {IsOdd}, Count = {Count}";
        }

        public bool Equals(AnalysisItem other)
        {
            if (ReferenceEquals(null, other))
                return false;
            if (ReferenceEquals(this, other))
                return true;
            if (GetType() != other.GetType())
                return false;

            return IsOdd == other.IsOdd && Count == other.Count;
        }

        public override bool Equals(object obj)
        {
            return Equals(obj as AnalysisItem);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                return (IsOdd.GetHashCode() * 397) ^ Count;
            }
        }

        public static bool operator ==(AnalysisItem left, AnalysisItem right)
        {
            return ReferenceEquals(left, null)
                ? ReferenceEquals(right, null)
                : left.Equals(right);
        }

        public static bool operator !=(AnalysisItem left, AnalysisItem right)
        {
            return !(left == right);
        }
    }
           

說明

  • ToString(): 我們應該實作ToString,VS或者其他工具很多地方都會調用ToString來顯示目前執行個體的資訊。不然預設的實作都是一樣的,無法區分
  • IEquatable: 我們實作IEquatable是為了單元測試的需要

AnalysisItemTests.cs

[TestFixture]
    public class AnalysisItemTests
    {
        [Test]
        public void Equal_TwoEqualItems_ShouldBeEqual()
        {
            var item1 = new AnalysisItem(true, 1);
            var item2 = new AnalysisItem(true, 1);

            Assert.That(item1.Equals(item2), Is.True);
            Assert.That(item2.Equals(item1), Is.True);

            Assert.That(item1 == item2, Is.True);

            Assert.That(item1 != item2, Is.False);
        }

        [Test]
        public void Equal_OneItemVsNull_ShouldNotBeEqual()
        {
            var item = new AnalysisItem(true, 1);

            Assert.That(item.Equals(null), Is.False);

            Assert.That(item == null, Is.False);
            Assert.That(null == item, Is.False);

            Assert.That(item != null, Is.True);
            Assert.That(null != item, Is.True);
        }

        [Test]
        public void Equal_TwoItemsWithDifferentIsOdd_ShouldNotBeEqual()
        {
            var item1 = new AnalysisItem(true, 1);
            var item2 = new AnalysisItem(false, 1);

            Assert.That(item1.Equals(item2), Is.False);
            Assert.That(item2.Equals(item1), Is.False);

            Assert.That(item1 == item2, Is.False);
            Assert.That(item2 == item1, Is.False);

            Assert.That(item1 != item2, Is.True);
            Assert.That(item2 != item1, Is.True);
        }

        [Test]
        public void Equal_TwoItemsWithDifferentCount_ShouldNotBeEqual()
        {
            var item1 = new AnalysisItem(true, 1);
            var item2 = new AnalysisItem(true, 2);

            Assert.That(item1.Equals(item2), Is.False);
            Assert.That(item2.Equals(item1), Is.False);

            Assert.That(item1 == item2, Is.False);
            Assert.That(item2 == item1, Is.False);

            Assert.That(item1 != item2, Is.True);
            Assert.That(item2 != item1, Is.True);
        }
    }
           

說明:

有些人可能認為一個單元測試中應該隻有一個Assert,但個人覺得那太教條了。隻要Assert是同一個“單元”,多個Assert也是可以的。

AnalysisItemEnumerator.cs

public class AnalysisItemEnumerator : IEnumerator<AnalysisItem>
    {
        private readonly IReadOnlyList<int> _numbers;
        private int _currentIndex = -1;
        private bool _currentProcessed = true;
        private AnalysisItem _currentAnalysisItem;

        public AnalysisItemEnumerator(IReadOnlyList<int> numbers)
        {
            _numbers = numbers ?? throw new ArgumentNullException(nameof(numbers));
        }

        public void Dispose() { }

        public bool MoveNext()
        {
            var haveNext = _currentIndex + 1 < _numbers.Count;

            if (haveNext)
                _currentProcessed = false;
            else
            {
                _currentProcessed = true;
                _currentAnalysisItem = null;
            }

            return haveNext;
        }

        public void Reset()
        {
            _currentIndex = -1;
            _currentProcessed = true;
            _currentAnalysisItem = null;
        }

        public AnalysisItem Current
        {
            get
            {
                if (!_currentProcessed)
                {
                    ++_currentIndex;
                    var isOdd = IsOdd(_numbers[_currentIndex]);
                    var count = 1;

                    while (++_currentIndex < _numbers.Count)
                    {
                        if (IsOdd(_numbers[_currentIndex]) == isOdd)
                            ++count;
                        else
                        {
                            --_currentIndex;
                            break;
                        }
                    }

                    _currentAnalysisItem = new AnalysisItem(isOdd, count);
                    _currentProcessed = true;
                }
                return _currentAnalysisItem;
            }
        }

        private static bool IsOdd(int number)
        {
            return number % 2 == 1;
        }

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

AnalysisItemEnumeratorTests.cs

[TestFixture]
    public class AnalysisItemEnumeratorTests
    {
        private readonly IReadOnlyList<int> _numbers = new[] { 1, 3, 5, 2, 7, 9 };
        private AnalysisItemEnumerator _enumerator;

        [SetUp]
        public void SetUp()
        {
            _enumerator = new AnalysisItemEnumerator(_numbers);
        }

        [Test]
        public void Constructor_WhileNumbersIsNull_ShouldThrowArgumentNullException()
        {
            Assert.Throws<ArgumentNullException>(() => new AnalysisItemEnumerator(null));
        }

        [Test]
        public void Current_WhileCalledAtFirstTime_ShouldReturnNull()
        {
            Assert.That(_enumerator.Current, Is.Null);
        }

        [Test]
        public void EnumeratingItem_AfterEachEnumeration_ShouldBehaveAsExpected()
        {
            Assert.That(_enumerator.MoveNext, Is.True);
            Assert.That(_enumerator.Current, Is.EqualTo(new AnalysisItem(true, 3)));

            Assert.That(_enumerator.MoveNext, Is.True);
            Assert.That(_enumerator.Current, Is.EqualTo(new AnalysisItem(false, 1)));

            Assert.That(_enumerator.MoveNext, Is.True);
            Assert.That(_enumerator.Current, Is.EqualTo(new AnalysisItem(true, 2)));

            Assert.That(_enumerator.MoveNext, Is.False);
            Assert.That(_enumerator.Current, Is.Null);
        }

        [Test]
        public void Current_WhileCalledTwice_ShouldReturnSameItem()
        {
            _enumerator.MoveNext();

            var current1 = _enumerator.Current;
            var current2 = _enumerator.Current;

            Assert.That(current1, Is.SameAs(current2));
        }

        [Test]
        public void Reset_WhileEnumeratingItemsAgain_FirstCurrentShouldBeNull()
        {
            _enumerator.MoveNext();

            _enumerator.Reset();

            Assert.That(_enumerator.Current, Is.Null);
        }

        [Test]
        public void Reset_WhileEnumeratingItemsAgain_ShouldEnumerateFromFirstItem()
        {
            _enumerator.MoveNext();
           
            _enumerator.Reset();

            Assert.That(_enumerator.MoveNext, Is.True);
            Assert.That(_enumerator.Current, Is.EqualTo(new AnalysisItem(true, 3)));
        }
    }
           

Analyzer.cs

public class Analyzer : IEnumerable<AnalysisItem>
    {
        private readonly IReadOnlyList<int> _numbers;

        public Analyzer(IReadOnlyList<int> numbers)
        {
            _numbers = numbers ?? new int[] { };
        }

        public IEnumerator<AnalysisItem> GetEnumerator()
        {
            return new AnalysisItemEnumerator(_numbers);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
           

Program.cs

class Program
    {
        static void Main(string[] args)
        {
            var numbers = new[] { 1, 2, 4, 10, 10, 8, 25, 13, 5, 7 };
            var analyzer = new Analyzer(numbers);

            foreach (var item in analyzer)
                Console.WriteLine(item);

            Console.ReadKey();
        }
    }
           
【Practical C#】C#中的疊代器 Iterator(IEnumerable和IEnumerator)