网上关于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();
}
}