C# IEnumerable and Yield
I have a class which contains two lists of strings. I want to iterate over both lists at the same time getting pairs of values. The neat way to do this is to make the class enumerable, which means it can be used in a foreach loop.
There are three things to do:
- The class needs to inherit from the
IEnumerable<T>
interface. - The class needs to implement the method
IEnumerator<T> GetEnumerator()
. This method returns anIEnumerator<T>
, so we need one of those. C# has the yield keyword which lets you do this easily. - Because I want to return two values from my class, I can't return a simple type like string. Instead I need a class or struct to put the pairs of values in.
The collection class implements IEnumerable
, which indicates it can be iterated over. The IEnumerator
is the thing which does the iteration. You, the programmer, never actually see the IEnumerator
; it's hidden behind the the foreach construct.
using System.Collections;
using System.Collections.Generic;
using MbUnit.Framework;
namespace EnumeratorDemo
{
[TestFixture]
public class EnumeratorTest
{
public struct Element
{
private string propertyName;
private string legendString;
public string PropertyName { get { return propertyName; } }
public string LegendString { get { return legendString; } }
public Element(string propertyName, string legendString)
{
this.propertyName = propertyName;
this.legendString = legendString;
}
}
public class Series : IEnumerable<Element>
{
private List<string> propertyNames;
private List<string> legendStrings;
public Series(List<string> propertyNames, List<string> legendStrings)
{
this.propertyNames = propertyNames;
this.legendStrings = legendStrings;
}
public IEnumerator<Element> GetEnumerator()
{
int i = 0;
foreach (string propertyName in propertyNames)
{
yield return new Element(propertyName, i < legendStrings.Count ? legendStrings[i] : string.Empty);
++i;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
[Test]
public void Test()
{
List<Element> series = new List<Element>();
foreach (Element element in new Series(new List<string>(){"prop1", "prop2"},
new List<string>(){"leg1"}))
{
series.Add(element);
}
Assert.AreEqual(2, series.Count);
Assert.Contains(series, new Element("prop1", "leg1"));
Assert.Contains(series, new Element("prop2", string.Empty));
}
}
}
I spent a while with the following rather annoying compile error:
'EnumeratorDemo.EnumeratorTest.Series' does not implement interface member 'System.Collections.IEnumerable.GetEnumerator()'. 'EnumeratorDemo.EnumeratorTest.Series.GetEnumerator()' cannot implement 'System.Collections.IEnumerable.GetEnumerator()' because it does not have the matching return type of 'System.Collections.IEnumerator'.
After a bit of googling I found out this is because I was not implementing the non-generic IEnumerator
interface, which the generic IEnumerator<T>
inherits from. Hence the extra code in the class implementing the non-generic IEnumerator IEnumerable.GetEnumerator()
method.
In the real application the Series class is not so trivial. It's constructor takes two strings as arguments, which contain comma separated values. The Series class parses the comma separated values to make the two lists.