網上關于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
該示例是在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();
}
}