Lazy Yield Problems
23-02-2014

Zanim przejdę do sedna sprawy nakreślę najpierw ciąg wydarzeń który mnie ku napisaniu tego posta skłonił.
Ostatnimi czasy wykonywałem małą biblioteczkę "na własne potrzeby" której podstawą był interfejs który można przedstawić następująco:
public interface IProcessor<T>
{
	IEnumerable<Result> Process(T input);
}


Interfejs ten ma wiele implementacji jednak z punktu widzenia tego postu kluczowy jest "AggregateProcessor" który łączy wyniki kilku procesorów.
class AggregateProcessor<T> : IProcessor<T>
{
	private IProcessor<T>[] subProcessors;

	public AggregateProcessor(params IProcessor<T>[] subProcessors)
	{
		this.subProcessors = subProcessors;
	}

	public IEnumerable<Result> Process(T input)
	{
		return subProcessors.SelectMany(p => p.Process(input));

		/* ekwiwalent
		foreach (var processor in subProcessors)
		{
			foreach (var result in processor.Process(input))
			{
				yield return result;
			}
		}
		 */
	}
}


Następnie dla tej klasy napisałem prosty test sprawdzający czy podprocesor zostanie wywołany po wywołaniu Process na AggregateProcessorze.
Test jest napisany w NUnicie do Mockowania wykorzystałem FakeItEasy.

[Test]
public void CheckIfSubProcessorIsCalled()
{
	// Given
	var parameter = new TestClass();

	var fakeProcessor = A.Fake<IProcessor<TestClass>>();
	A.CallTo(() => fakeProcessor.Process(parameter)).Returns(Enumerable.Empty<Result>());

	var aggregate = new AggregateProcessor<TestClass>(fakeProcessor);

	// When
	aggregate.Process(parameter);

	// Then
	A.CallTo(() => fakeProcessor.Process(parameter)).MustHaveHappened();
}


Wielkie było moje zdziwienie gdy okazało się że Test nie przechodzi.

Przyczyną tej sytuacji jest fakt iż zarówno operacje w zapytaniu LINQ jak i operacja yield wywoływane są w momencie iteracji po wyniku metody której w tym przykładzie nie ma. W większości przypadków fakt ten nie ma większego znaczenia ponieważ iteracja zazwyczaj w końcu następuje, chyba że tak jak w tym przypadku metody będącej funkcją używamy jak metody będącej procedurą, albo wykonujemy na wyniku operacje typu Any, First/FirstOrDefault które nie iterują po całym wyniku.
Wracając do mojego przypadku ostatecznie stwierdziłem że fakt wywołania metody wewnętrznej nie jest dla mnie istotny, a bardziej obchodzi mnie zwrócenie wyniku procesora wewnętrznego.

[Test]
public void CheckIfSubProcessorIsCalled()
{
	// Given
	var parameter = new TestClass();
	var result = new Result();

	var fakeProcessor = A.Fake<IProcessor<TestClass>>();
	A.CallTo(() => fakeProcessor.Process(parameter)).Returns(new[] { result });

	var aggregate = new AggregateProcessor<TestClass>(fakeProcessor);

	// When
	var resultCollection = aggregate.Process(parameter);

	// Then
	CollectionAssert.Contains(resultCollection, result);
}


Drugim zagrożeniem jakie wiąże się z yield i LINQ jest fakt iż wszystkie operacje wykonywane pod spodem wykonują się przy każdej iteracji po wynikowej kolekcji. Sam się na tym nigdy nie przejechałem ponieważ przed błędem tego typu skutecznie chroni mnie Resharper, jednak ludzi nie posiadających tego genialnego narzędzia warto ostrzec że poniższy kod:
static void Main(string[] args)
{
	var sourceCollection = Enumerable.Range(1, 10);

	var resultCollection = from number in sourceCollection
						   select Compute(number);

	foreach (var number in resultCollection)
	{
		Console.Write("{0}, ", number);
	}

	Console.WriteLine();

	foreach (var number in resultCollection)
	{
		Console.Write("{0}, ", number);
	}
}

private static int Compute(int parameter)
{
	// Some Heavy operation
	Thread.Sleep(1000);
	return parameter + 1;
}

będzie uruchamiał się 20 a nie 10 sekund. Jakimś obejściem tego problemu jest użycie metody ToList. Najlepiej w następujący sposób:
static void Main(string[] args)
{
	var sourceCollection = Enumerable.Range(1, 10);

	var resultCollection = from number in sourceCollection
						   select Compute(number);

	/*
	 * Przed wywołaniem ToList upewniam się że kolekcja nie jest już listą
	 * W tym przypadku nie ma to sensu ponieważ wiem że kolekcja nie jest listą
	 */
	var resultList = resultCollection as List<int> ?? resultCollection.ToList();

	foreach (var number in resultList)
	{
		Console.Write("{0}, ", number);
	}

	Console.WriteLine();

	foreach (var number in resultList)
	{
		Console.Write("{0}, ", number);
	}
}


Podsumowując zarówno LINQ jak i yield są bardzo fajnymi narzędziami, przed ich użyciem należy jednak przynajmniej w minimalnym stopniu dowiedzieć się jak one działają.

Tagi:

Programowanie C Sharp


Komentarze:

Ostatnie wpisy

Subskrybuj kanał RSS Lazy Yield Problems
Świąteczne małpki
Problem zwrotnych referencji w WCF
Słów kilka o krotkach.
Wrażenia po wizycie na 4Developers
Komunikacja PHP i C Sharp
Słów kilka o chciwości
Upgrade bloga przy użyciu jQuery
Osobisty sukces na CodeGuru
Google web elements

O sobie samym

Nazywam się Wojciech Gomoła i jestem programistą .Net w firmie Atena usługi informatyczne i finansowe S.A. Na stronie tej będę dzielił się swoimi doświadczeniami związanymi z programowaniem w różnych językach programowania. Ale nie tylko.
Moje CV

Ciekawe linki

CodeGuru
DotnetoManiak
bloghellix.pl - Blog kozaka :P
blog.bojkar.pl - bojkar's dev blog
Instytut Ludwika von Misesa
Sławomir Sobótka Holistycznie o inżynierii oprogramowania

Kontakt

e-mail:
szog...@interia.pl
szog...@gmail.com




Ciekawe książki

http://img.amazon.ca/images/I/31bmX1EXVZL._SL75_SS50_.jpgThinking in Java
pictures/sztukawojny.jpgSztuka wojny
pictures/bogatyojciec.jpgBogaty ojciec biedny ojciec

Dług publiczny




Kalendarz



Znaczniki

Browser side
C Sharp
Ekonomia
Java
Java ME
Konferencje
NHibernate
PHP
Programowanie
Recaptchia
Wzorce projektowe
Zaloguj Valid XHTML 1.0 Transitional