Last active
October 20, 2015 15:29
-
-
Save eouw0o83hf/bbfe8d1575ddc55052e0 to your computer and use it in GitHub Desktop.
GetSome(): A Scala-inspired C# IEnumerable Extension
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static class Extensions | |
{ | |
/// <summary> | |
/// Grants the ability to peek into an IEnumerable and determine if some | |
/// (n > 0) items are in the collection *without multiply-enumerating it* | |
/// </summary> | |
public static ISomeEnumerable<T> GetSome<T>(this IEnumerable<T> source) | |
{ | |
return new RepeatableEnumerableWrapper<T>(source); | |
} | |
} | |
/// <summary> | |
/// Defines the abstraction to peek into an IEnumerable and determine if some | |
/// (n > 0) items are in the collection *without multiply-enumerating it* | |
/// </summary> | |
public interface ISomeEnumerable<out T> : IEnumerable<T> | |
{ | |
/// <summary> | |
/// Returns true if there are more than 0 elements in the IEnumerable. Can | |
/// be executed before, during or after enumeration without incurring a | |
/// re-enumeration penalty. | |
/// </summary> | |
bool Some(); | |
} | |
/// <summary> | |
/// Implements peeking into an IEnumerable and determine if some | |
/// (n > 0) items are in the collection *without multiply-enumerating it* | |
/// </summary> | |
public class RepeatableEnumerableWrapper<T> : ISomeEnumerable<T> | |
{ | |
private readonly IEnumerator<T> _inputEnumerator; | |
// The method that gets fed to the output enumerable executes | |
// a value immediately, so we store it as a Lazy<> to defer | |
// all execution since that's idiomatic to IEnumerables. Also, | |
// by storing this to a field, it verifies that all attempts | |
// to enumerate this class fall to the same underlying IEnumerable. | |
private readonly Lazy<IEnumerable<T>> _outputEnumerable; | |
private bool? _someResult; | |
public RepeatableEnumerableWrapper(IEnumerable<T> input) | |
{ | |
if (input == null) | |
{ | |
_someResult = false; | |
return; | |
} | |
_inputEnumerator = input.GetEnumerator(); | |
_outputEnumerable = new Lazy<IEnumerable<T>>(Enumerate); | |
} | |
/// <summary> | |
/// Pulls the first result off of the enumerator and caches it. If | |
/// an a result was previously reached, uses that cached result instead. | |
/// </summary> | |
/// <returns></returns> | |
public bool Some() | |
{ | |
// Guarantees that this method will never run twice for one instance. | |
// Also, this is set to false in the constructor if the input is null, | |
// so that edge case is handled. | |
if (_someResult.HasValue) | |
{ | |
return _someResult.Value; | |
} | |
// Moves the enumerator onto the IEnumerable, at position 0. Returns | |
// true if the move was successful (and there are > 0 elements in the | |
// IEnumerable) and false if none. | |
_someResult = _inputEnumerator.MoveNext(); | |
// Now _inputEnumerator is ready to be moved to the next element, but | |
// we'll defer that to the actual enumeration down in Enumerate() | |
return _someResult.Value; | |
} | |
private IEnumerable<T> Enumerate() | |
{ | |
// This call is guaranteed to either (a) fall off the end of the | |
// IEnumerable [in which case we just quit because there are 0 items] | |
// or (b) move to the first element. | |
if (!Some()) | |
{ | |
yield break; | |
} | |
// The enumerator is already pointing at the first element, so | |
// just return it without iterating. | |
yield return _inputEnumerator.Current; | |
// Now we're in the happy case of just enumerating the rest of the IEnumerable. | |
// Keep going until we fall off the end and MoveNext() returns false. | |
while (_inputEnumerator.MoveNext()) | |
{ | |
yield return _inputEnumerator.Current; | |
} | |
} | |
public IEnumerator<T> GetEnumerator() | |
{ | |
return _outputEnumerable.Value.GetEnumerator(); | |
} | |
IEnumerator IEnumerable.GetEnumerator() | |
{ | |
return ((IEnumerable) _outputEnumerable.Value).GetEnumerator(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[TestFixture] | |
public class Tests | |
{ | |
private class TestEnumerable<T> : IEnumerable<T> | |
{ | |
public int EnumerationCount { get; private set; } | |
private readonly IEnumerable<T> _input; | |
public TestEnumerable(IEnumerable<T> input) | |
{ | |
_input = input; | |
EnumerationCount = 0; | |
} | |
private IEnumerable<T> Enumerate() | |
{ | |
++EnumerationCount; | |
return _input; | |
} | |
public IEnumerator<T> GetEnumerator() | |
{ | |
return Enumerate().GetEnumerator(); | |
} | |
IEnumerator IEnumerable.GetEnumerator() | |
{ | |
return Enumerate().GetEnumerator(); | |
} | |
} | |
[Test] | |
public void WhenGivenNullInput_ShouldNotBeSome_ShouldNotExplode() | |
{ | |
var enumerable = (IEnumerable<object>)null; | |
var some = enumerable.GetSome(); | |
Assert.That(some, Is.Not.Null); | |
Assert.That(some.Some(), Is.False); | |
Assert.Throws<NullReferenceException>(() => some.GetEnumerator()); | |
} | |
[Test] | |
public void WhenGivenEmptyInput_ShouldNotBeSome_ShouldSinglyEnumerate() | |
{ | |
var wrapper = new TestEnumerable<object>(new object[0]); | |
var some = wrapper.GetSome(); | |
Assert.That(some, Is.Not.Null); | |
Assert.That(some.Some(), Is.False); | |
CollectionAssert.IsEmpty(some); | |
Assert.That(wrapper.EnumerationCount, Is.EqualTo(1)); | |
} | |
[Test] | |
public void WhenGivenSingleInput_ShouldBeSome_ShouldSinglyEnumerate() | |
{ | |
var wrapper = new TestEnumerable<object>(new object[] { 0 }); | |
var some = wrapper.GetSome(); | |
Assert.That(some, Is.Not.Null); | |
Assert.That(some.Some(), Is.True); | |
Assert.That(some.Count(), Is.EqualTo(1)); | |
Assert.That(wrapper.EnumerationCount, Is.EqualTo(1)); | |
} | |
[Test] | |
public void WhenGivenMultipleInput_ShouldBeSome_ShouldSinglyEnumerate() | |
{ | |
var wrapper = new TestEnumerable<object>(new object[] { 0, "0", 0.0, 0f, '0', 0m }); | |
var some = wrapper.GetSome(); | |
Assert.That(some, Is.Not.Null); | |
Assert.That(some.Some(), Is.True); | |
Assert.That(some.Count(), Is.EqualTo(6)); | |
Assert.That(wrapper.EnumerationCount, Is.EqualTo(1)); | |
} | |
[Test] | |
public void WhenGivenMultipleInput_WhenSomeCalledWithinLoop_ShouldBeSome_ShouldSinglyEnumerate() | |
{ | |
var wrapper = new TestEnumerable<object>(new object[] { 0, "0", 0.0, 0f, '0', 0m }); | |
var some = wrapper.GetSome(); | |
Assert.That(some, Is.Not.Null); | |
Assert.That(some.Some(), Is.True); | |
var index = 0; | |
foreach (var s in some) | |
{ | |
Assert.That(some.Some()); | |
++index; | |
} | |
Assert.That(index, Is.EqualTo(6)); | |
Assert.That(some.Some()); | |
Assert.That(wrapper.EnumerationCount, Is.EqualTo(1)); | |
} | |
[Test] | |
public void WhenMultiplyEnumerated_ShouldError() | |
{ | |
var wrapper = new [] { 0 }.GetSome(); | |
Assert.DoesNotThrow(() => wrapper.ToList()); | |
Assert.Throws<InvalidOperationException>(() => wrapper.ToList()); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment