Kürzlich trat mal die Frage auf, was das Schlüsselwort „yield“ bewirkt. Meine Antwort darauf war grob so: „ Es werden Werte für einen IEnumerable erst abgerufen, wenn diese benötigt werden“.
Ich denke, im Grunde ist diese Aussage schon richtig, aber vielleicht sollte ich da ein bisschen besser ins Detail gehen. Ich muss allerdings gleich vorweg dazu sagen, dass ich schon etliche Male versucht habe, dieses Feature zu verwenden, aber bisher hab ich das irgendwie nie geschafft. Obwohl es eigentlich wirklich interessant wäre. Daher hab ich mir jetzt auch vorgenommen, dass ich mich damit mal genauer beschäftige.

Schauen wir mal, was die MSDN dazu schreibt:

Wenn Sie das yield-Schlüsselwort in einer Anweisung verwenden, geben Sie damit an, dass die Methode, der Operator oder der get-Accessor, in dem es vorkommt, ein Iterator ist. Ein Iterator wird verwendet, um eine benutzerdefinierte Iteration durch eine Auflistung auszuführen.
Sie erzeugen eine Iteratormethode, indem Sie eine foreach-Anweisung oder eine LINQ-Abfrage verwenden. Jede Iteration der foreach-Schleife ruft die Iteratormethode auf. Wenn eine yield return-Anweisung im Iterator erreicht wird, wird ein expression-Ausdruck zurückgegeben, und die aktuelle Position im Code wird beibehalten. Die Ausführung von dieser Stelle wird beim nächsten Mal neu gestartet, so dass die Iteratorfunktion aufgerufen wird.

Grob erklärt bedeutet das, dass die einzelnen Durchläufe der Schleife, die die Rückgabe der Funktion erzeugt erst durchlaufen werden, wenn die Rückgabe abgefragt wird.
Ich denke, man sollte sich das an einem Beispiel ansehen:

static void Main(string[] args)
{
	Console.WriteLine("Main START");
	IEnumerable values = GetValues();
	Console.WriteLine("Main: before loop");
	foreach (int i in values)
		Console.WriteLine("Main: {0}", i);
	Console.WriteLine("Main END");

	Console.ReadKey();
}

private static IEnumerable<int> GetValues()
{
	Console.WriteLine("GetValues START");
	for (int i = 0; i < 3; i++)
	{
		Console.WriteLine("GetValues: {0}", i);
		yield return i;
	}
	Console.WriteLine("GetValues END");
}

So. Wir haben hier also eine Methode GetValues(), die in einer Schleife die Integer-Werte von 0 bis 2 in einem IEnumerable – also einer Liste sammelt und dann zurück gibt. Das erste, was dabei schon auffällt ist, dass die Rückgabe nicht am Ende der Methode erfolgt, sondern innerhalb der Schleife. Außerdem wird nicht ein IEnuerable erzeugt, wie in der Deklaration angegeben, sondern es wird immer nur der Integer-Wert zurück gegeben.
Schauen wir uns am besten mal an, wie die Ausgabe aus diesem kurzen Beispiel aussieht:

Main START
Main: before loop
GetValues START
GetValues: 0
Main: 0
GetValues: 1
Main: 1
GetValues: 2
Main: 2
GetValues END
Main END

Man sieht jetzt eigentlich ganz deutlich, was passiert:
Die Methode GetValues wird aufgerufen, aber läuft nicht bis zum Ende durch. Es wird in der Methode nur die Ausgabe erzeugt, dass die Methode gestartet wurde. Der Zeiger gelangt aber weder zum Return noch zum Ende der Methode. Erst innerhalb der Iteration in der Methode Main werden dann die Ausgaben aus der Methode GetValues erzeugt. Es wird also die Methode erst an dem Punkt fortgesetzt, an dem die Werte, die aus der Methode kommen würden abgefragt werden.

Und wenn wir schon am Kontrollieren sind, schauen wir auch gleich mal an, wie der Quellcode aussieht, wenn man sich das Ganze im Reflektor ansieht:

internal class Program
{
	[CompilerGenerated]
	private sealed class d__0 : IEnumerable, IEnumerable, IEnumerator, IEnumerator, IDisposable
	{
		private int <>2__current;
		private int <>1__state;
		private int <>l__initialThreadId;
		public int <i>5__1;
		int IEnumerator.Current
		{
			[DebuggerHidden]
			get
			{
				return this.<>2__current;
			}
		}
		object IEnumerator.Current
		{
			[DebuggerHidden]
			get
			{
				return this.<>2__current;
			}
		}
		[DebuggerHidden]
		IEnumerator IEnumerable.GetEnumerator()
		{
			Program.d__0 result;
			if (Environment.CurrentManagedThreadId == this.<>l__initialThreadId && this.<>1__state == -2)
			{
				this.<>1__state = 0;
				result = this;
			}
			else
			{
				result = new Program.d__0(0);
			}
			return result;
		}
		[DebuggerHidden]
		IEnumerator IEnumerable.GetEnumerator()
		{
			return this.System.Collections.Generic.IEnumerable.GetEnumerator();
		}
		bool IEnumerator.MoveNext()
		{
			switch (this.<>1__state)
			{
			case 0:
				this.<>1__state = -1;
				Console.WriteLine("GetValues START");
				this.<i>5__1 = 0;
				break;
			case 1:
				this.<>1__state = -1;
				this.<i>5__1++;
				break;
			default:
				goto IL_96;
			}
			bool result;
			if (this.<i>5__1 < 5)
			{
				Console.WriteLine("GetValues: {0}", this.<i>5__1);
				this.<>2__current = this.<i>5__1;
				this.<>1__state = 1;
				result = true;
				return result;
			}
			Console.WriteLine("GetValues END");
			IL_96:
			result = false;
			return result;
		}
		[DebuggerHidden]
		void IEnumerator.Reset()
		{
			throw new NotSupportedException();
		}
		void IDisposable.Dispose()
		{
		}
		[DebuggerHidden]
		public d__0(int <>1__state)
		{
			this.<>1__state = <>1__state;
			this.<>l__initialThreadId = Environment.CurrentManagedThreadId;
		}
	}
	private static void Main()
	{
		Console.WriteLine("Main START");
		IEnumerable values = Program.GetValues();
		Console.WriteLine("Main: before loop");
		foreach (int i in values)
		{
			Console.WriteLine("Main: {0}", i);
		}
		Console.WriteLine("Main END");
	}
	private static IEnumerable GetValues()
	{
		return new Program.d__0(-2);
	}
}

Aus der kleinen Anweisung ist also gleich eine ganze Inline-Klasse geworden, die einen Iterator erzeugt. In der Methode IEnumerator.MoveNext() finden wir dann auch die Ausgaben, die wir ursprünglich geschrieben haben, aber vollkommen anders aufbereitet. Hier sieht man also gleich, welche Auswirkungen ein so kleines Schlüsselwort haben kann.