Clearly the Java scheme is more restrictive, but it does make life significantly simpler, too. Local variables behave in the same way they've always done, and in many cases the code is easier to understand, too. For example, look at the following code, using the Java Runnable interface and the .NET Action delegate - both of which represent actions taking no parameters and returning no value. First let's see the C# code:
// In Example3a.cs
static void Main()
{
// First build a list of actions
List<Action> actions = new List<Action>();
for (int counter = 0; counter < 10; counter++)
{
actions.Add(() => Console.WriteLine(counter));
}
// Then execute them
foreach (Action action in actions)
{
action();
}
}
What's the output? Well, we've only actually declared a single counter variable - so that same counter variable is captured by all the Action instances. The result is the number 10 being printed on every line. To "fix" the code to make it display the output most people would expect (i.e. 0 to 9) we need to introduce an extra variable inside the loop:
// In Example3b.cs
static void Main()
{
// First build a list of actions
List<Action> actions = new List<Action>();
for (int counter = 0; counter < 10; counter++)
{
int copy = counter;
actions.Add(() => Console.WriteLine(copy));
}
// Then execute them
foreach (Action action in actions)
{
action();
}
}
Each time we go through the loop we're said to get a different instance of the copy variable - each Action captures a different variable. This makes perfect sense if you look at what the compiler's actually doing behind the scenes, but initially it flies in the face of the intuition of most developers (including me).
Java forbids the first version entirely - you can't capture the counter variable at all, because it's not final. To use a final variable, you end up with code like this, which is very similar to the C# code:
// In Example3a.java
public static void main(String[] args)
{
// First build a list of actions
List<Runnable> actions = new ArrayList<Runnable>();
for (int counter=0; counter < 10; counter++)
{
final int copy = counter;
actions.add(new Runnable()
{
public void run()
{
System.out.println(copy);
}
});
}
// Then execute them
for (Runnable action : actions)
{
action.run();
}
}
The meaning is reasonably clear with the "captured value" semantics. The resulting code is still less pleasant to look at than the C# due to the wordier syntax, but Java forces the correct code to be written as the only option. The downside is that when you want the behaviour of the original C# code (which is certainly the case on occasion) it's cumbersome to achieve in Java. (You can have a single-element array, and capture a reference to the array, then change the value of the element when you want to, but that's a nasty kludge).
Comments