Lambda assigning local variables

2019-03-25 11:56发布

问题:

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.

回答1:

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.



回答2:

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.



回答3:

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.



回答4:

A snippet from the ECMA Standard section 8.3 Variables and Parameters:

A variable shall be assigned before its value can be obtained. The example

class Test
{
    static void Main() {
    int a;
    int b = 1;
    int c = a + b; // error, a not yet assigned

    }
}

results in a compile-time error because it attempts to use the variable a before it is assigned a value. The rules governing definite assignment are defined in §12.3.

Therefore it states that the variable must be assigned before it is used otherwise it results in a compiler error. Because you are creating a delegate and invoking it, the method that is contained within the delegate invocation is technically not known. Therefore the compiler would not be to figure it out. Remember it is the Delegate's Invoke method that is being called not the actual method.

ECMA Standard for C#