Consider the following source:
static void Main(string[] args)
{
bool test;
Action lambda = () => { test = true; };
lambda();
if (test)
Console.WriteLine("Ok.");
}
It should compile, right? Well, it doesn't. My question is: according to C# standard, should this code compile or is this a compiler bug?
Use of unassigned local variable 'test'
Note: I know, how to fix the error and i partially know, why does it happen. However, the local variable is assigned unconditionally and I guess, that compiler should notice that, but it does not. I wonder, why.
Comment for answers: C# allows declaring unassigned variables and that's actually quite useful, ie.
bool cond1, cond2;
if (someConditions)
{
cond1 = someOtherConditions1;
cond2 = someOtherConditions2;
}
else
{
cond1 = someOtherConditions3;
cond2 = someOtherConditions4;
}
Compiler compiles this code properly and I think, that leaving variables unassigned actually makes the code a little bit better, because:
On the margin: That's even more interesting. Consider the same example in C++:
int main(int argc, char * argv[])
{
bool test;
/* Comment or un-comment this block
auto lambda = [&]() { test = true; };
lambda();
*/
if (test)
printf("Ok.");
return 0;
}
If you comment the block out, compilation ends with warning:
main.cpp(12): warning C4700: uninitialized local variable 'test' used
However, if you remove the comment, compiler emits no warnings whatsoever. It seems to me, that it is able to determine, if the variable is set after all.
A lambda expression can't define any new scope as an anonymous inner class does, so we can't declare a local variable with the same which is already declared in the enclosing scope of a lambda expression. Inside lambda expression, we can't assign any value to some local variable declared outside the lambda expression.
In this case, a lambda expression may only use local variables that are effectively final. An effectively final variable is one whose value does not change after it is first assigned. There is no need to explicitly declare such a variable as final, although doing so would not be an error.
The basic reason this won't compile is that the lambda is capturing the value of start, meaning making a copy of it. Forcing the variable to be final avoids giving the impression that incrementing start inside the lambda could actually modify the start method parameter.
A lambda expression can use a local variable in outer scopes only if they are effectively final.
My question is: according to C# standard, should this code compile or is this a compiler bug?
This is not a bug.
Section 5.3.3.29 of the C# Language Specification (4.0) outlines the definite assignment rules regarding anonymous functions, including lambda expressions. I will post it here.
5.3.3.29 Anonymous functions
For a lambda-expression or anonymous-method-expression expr with a body (either block or expression) body:
The definite assignment state of an outer variable v before body is the same as the state of v before expr. That is, definite assignment state of outer variables is inherited from the context of the anonymous function.
The definite assignment state of an outer variable v after expr is the same as the state of v before expr.
The example
delegate bool Filter(int i); void F() { int max; // Error, max is not definitely assigned Filter f = (int n) => n < max; max = 5; DoWork(f); }
generates a compile-time error since max is not definitely assigned where the anonymous function is declared. The example
delegate void D(); void F() { int n; D d = () => { n = 1; }; d(); // Error, n is not definitely assigned Console.WriteLine(n); }
also generates a compile-time error since the assignment to n in the anonymous function has no affect on the definite assignment state of n outside the anonymous function.
You can see how this applies to your specific example. The variable test
is not specifically assigned prior to the declaration of the lambda expression. It is not specifically assigned prior to the execution of the lambda expression. And it is not specifically assigned after the completion of the lambda expression execution. By rule, the compiler does not consider the variable to be definitely assigned at the point of it being read in the if
statement.
As for why, I can only repeat what I have read on the matter, and only what I can remember as I cannot produce a link, but C# does not attempt to do this because, although this is a trivial case that the eye can see, it is far more often the case that this type of analysis would be non-trivial and indeed could amount to solving the halting problem. C# therefore "keeps it simple" and requires you to play by much more readily applicable and solvable rules.
You are using unassigned variable. Even though the variable is actually assigned, compiler has no way of inferring that from the code you've posted.
All local variables should be initialized when declared anyway, so this is interesting, but still erroneous.
When the compiler is performing control flow analysis of methods to determine whether or not a variable is definitely assigned it will only look within the scope of the current method. Eric Lippert discusses this in this blog post. It's theoretically possible for the compiler to analyze methods called from within the "current method" to reason about when a variable is definitely assigned.
As I mentioned before, we could do interprocedural analysis, but in practice that gets real messy real fast. Imagine a hundred mutually recursive methods that all go into an infinite loop, throw, or call another method in the group. Designing a compiler that can logically deduce reachability from a complex topology of calls is doable, but potentially a lot of work. Also, interprocedural analysis only works if you have the source code for the procedures; what if one of these methods is in an assembly, and all we have to work with is the metadata?
Keep in mind that your code example is not truely a single method. The anonymous method will be refactored into another class, an instance of it will be created, and it will be calling a method that resembles your definition. Additionally the compiler would need to analyze the definition of the delegate
class as well as the definition of Action
to reason that the method you provided was actually executed.
So while it's within the bounds of theoretical possibility for the compiler to know that the variable is reachable in that context, the compiler writers deliberately choose not to both due to the complexity of writing the compiler for it, and also the (potentially significant) increase in time it would take to compile programs.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With