views:

152

answers:

2

When trying to use delegates in C# to solve a problem in a functional way, I've come across a pitfall that I want to share resp. for which I would like to hear your suggestions.

Background

I want to fill a grid from a list of objects where the values for single columns are get using delegates (idea borrowed from Philip Pipers ObjectListView control).

Additionally I want to automatically insert columns containing the (numerical) difference between two values.

So my objects having properties FirstValue, SecondValue and ThirdValue I want to have columns with FirstValue, (SecondValue-FirstValue), SecondValue, (ThirdValue-SecondValue), ThirdValue.

I already have adapted an existing grid control to use delegates on an object list, this part works fine.

First attempt

First, I tried something like:

class MyGridClass : DelegateGrid
{
  DelegateGrid.ValueGetter lastGetter;

  public MyGridClass() {
    AddMyColumn(delegate(MyObj obj) { return obj.FirstValue; });
    AddMyColumn(delegate(MyObj obj) { return obj.SecondValue; });
    AddMyColumn(delegate(MyObj obj) { return obj.ThirdValue; });
  }

  private void AddMyColumn(DelegateGrid.ValueGetter getter) {
    if (lastGetter != null)
      base.AddColumn(new DelegateColumn(delegate(MyObj obj) { 
        return getter(obj)-lastGetter(obj); 
      }));
    base.AddColumn(new DelegateColumn(getter));
  }
};

Problem

In a functional language, calculating the difference in this way would work fine, since the new delegate (constructed inside AddMyColumn) would use the value of lastGetter at the time of construction. But in C#, the new delegate uses a reference to lastGetter, so when executed, it uses the actual value at the time of execution. So the difference will always be built against the last column (i.e. obj.ThirdValue).

Solution

One solution I've found for myself is

public AddMyColumn(DelegateGrid.ValueGetter getter) {
  if (lastGetter != null) {
    DelegateGrid.ValueGetter newLastGetter = 
      new DelegateGrid.ValueGetter(lastGetter);
    base.AddColumn(new DelegateColumn(delegate(MyObj obj) { 
     return getter(obj)-newLastGetter(obj); 
    }));
  }
  // ...
}

Note that

if (lastGetter != null) {
  DelegateGrid.ValueGetter newLastGetter = 
    delegate(MyObject obj){return lastGetter(obj); };

wouldn't have solved the problem.

Question

Already having found a solution, this part is a bit pro forma, but

  • Does anyone have a suggestion for a better solution
  • I'm using C#2.0 and have only a theoretical knowledge of lambda expressions in C#3.0: Would they allow for a cleaner solution (and thus deserve their name...)?
+7  A: 

The problem is just that the variable is being captured rather than the value. Here's a solution which is much the same, but slightly simpler:

public AddMyColumn(DelegateGrid.ValueGetter getter) {
  if (lastGetter != null) {
    DelegateGrid.ValueGetter newLastGetter = lastGetter;
    base.AddColumn(new DelegateColumn(delegate(MyObj obj) { 
     return getter(obj)-newLastGetter(obj); 
    }));
  }
  // ...
}

Basically there's no need to create a new delegate instance - delegates are immutable, so you can just copy the value with assignment.

This isn't really a delegate-specific problem in terms of the value being captured - it's a common problem for anonymous methods and lambda expressions in general. The typical example is;

List<Action> actions = new List<Action>();
for (int i=0; i < 10; i++)
{
    actions.Add(() => Console.WriteLine(i));
}
foreach (Action action in actions)
{
    action();
}

This prints "10" 10 times. To print 0-9, you again need to change the scope of the captured variable:

List<Action> actions = new List<Action>();
for (int i=0; i < 10; i++)
{
    int copy = i;
    actions.Add(() => Console.WriteLine(copy));
}
foreach (Action action in actions)
{
    action();
}
Jon Skeet
A: 

To answer your other points, lambda syntax is going to make it much nicer, since they'll reduce the verbose code.

delegate(MyObj obj) { 
    return getter(obj)-newLastGetter(obj); 
}

Becomes:

obj => getter(obj)-newLastGetter(obj)
MichaelGG