Montag, 30. Januar 2012

Closures in C#

Closures kommen eigentlich auch den funktionalen Programmiersprachen. Einige Konzepte solcher Sprachen haben in C# einzug gehalten, und durch Lambda-Expressions zum Beispiel auch die Closures.

Hier ein kleines Beispiel:

public void DoSomething(int i)
{
  var x = 2 * i;
}

private Action GetAction(int initial)
{
  var i = 2 * initial;
  return () => DoSomething(i);
}

public void CallAction()
{
  Action a = GetAction(5);
  a();
}

Die Variable i in GetAction ist lokal und wird im Lambda-Ausdruck () => DoSomething(i) festgehalten. Beim Aufruf der Action in CallAction ist i eigentlich schon gar nicht mehr im Scope. Wegen des Closures kann der Delegat trotzdem darauf zugreifen.

Soweit so gut. Jetzt ein kleines Beispiel wo mich dieses Feature ein kleine Debugging-Session gekostet hat. Dazu ein vereinfachtes Beispiel:

private List<Action> actions = new List<Action>();

private void CreateActions()
{
  for (int i = 0; i < 10; i++)
  {
    var param = i * 2;
    AddAction(() => DoSomething(param));
  }
}

private void AddAction(Action a)
{
  if (!actions.Contains(a))
    actions.Add(a);
}

public void DoSomething(object i)
{
  var x = 2 * (int)i;
}

Jetzt werden die Actions zunächst in einer Liste gespeichert, aber nur falls die gleiche Action nicht schon enthalten ist.

Die Preisfrage lautet: wie viele Einträge enthält die Liste actions nach der Ausführung von CreateActions?

Die richtige Antwort ist: 10. Denn obwohl der Ausdruck () => DoSomething(param) immer gleich aussieht, ist es durch den Closure jedesmal eine andere Action, weshalb die Bedingung !actions.Contains(a) niemals greift.

Bei folgender Varianten ist die Menge der Varianten in der Liste gleich 1:

private void CreateActions2()
{
  for (int i = 0; i < 10; i++)
  {
    AddAction(() => DoSomething(1));
  }
}

Soweit klar, hier wird ja kein Element aus dem externen Scope eingeschlossen.
Aber auch diese Variante liefert einen Eintrag in der Liste actions:

private object context = 1;
private void CreateActions3()
{
  for (int i = 0; i < 10; i++)
  {
    context = 2 * i;
    AddAction(() => DoSomething(context));
  }
}

Hier ist die Variable context ein Referenztyp und das Closure schlíeßt nur die Addresse der Variablen mit ein.
Wenn man es mal so einfach hinschreibt, eigentlich logisch :-)

Keine Kommentare:

Kommentar veröffentlichen