Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lambda assigning local variables

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?


The error message:
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:

  • It tells the reader, that values are assigned later (mostly probably in the following conditional statement)
  • Forces the programmer to assign the variables in all branches of internal conditions (if it was the purpose of this code from the beginning), because compiler will refuse to compile the code if one of the branches does not assign one of them.

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.

like image 552
Spook Avatar asked Jan 08 '13 21:01

Spook


People also ask

How do you use local variables in lambda expression?

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.

Can you pass local variables to lambda expressions?

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.

Why do local variables used in lambdas have to be final?

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.

Can a lambda expression access a local variable without the final modifier?

A lambda expression can use a local variable in outer scopes only if they are effectively final.


3 Answers

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.

like image 71
Anthony Pegram Avatar answered Nov 15 '22 13:11

Anthony Pegram


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.

like image 45
Toni Petrina Avatar answered Nov 15 '22 14:11

Toni Petrina


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.

like image 30
Servy Avatar answered Nov 15 '22 13:11

Servy



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!