4

I have some code similar to the following.

class MyClass<TEnum> where TEnum : struct
{
    public IEnumerable<TEnum> Roles { get; protected set; }

    public MyClass()
    {
        IEnumerable<string> roles = ... ;

        TEnum value;
        Roles = from r in roles
                where Enum.TryParse(r, out value)
                select value;   // <---- ERROR HERE!
    }
}

However, on the line indicated above, I get the error:

Use of unassigned local variable 'value'

Seems to me that value will always be initialized in this case since it is an out parameter to Enum.TryParse.

Is this a bug with the C# compiler?

8
  • Similar to linq with unassigned variable parameters Commented Mar 11, 2015 at 19:09
  • @SwDevMan81: That's not at all the same. Commented Mar 11, 2015 at 19:11
  • 1
    btw.: Even if it would compile and do its thing --- your code would still be buggy. As you select an outside variable, that is closed over, the value is the same for all members of your resulting list. You might as well select 0. The most elegant solution would be a TryParse extension method that returns int? Commented Mar 11, 2015 at 19:27
  • 1
    @JonathanWood: It's got a lot in common, actually. The fact that the "definitely assigned" status is checked when the lambda is constructed, not when it is called, is the root cause of both. Commented Mar 11, 2015 at 19:28
  • @DasKrümelmonster: You mean Parse? But that throws an exception on failure. He wants zero items from each parsing failure and one item from each successful parse... which could be accomplished using SelectMany. Commented Mar 11, 2015 at 19:29

2 Answers 2

6

No it is not.

Compiler cannot guarantee that Enum.TryParse(r, out value) will be executed.

What if roles is an empty collection?

Even if you initialize your collection in the method CSC does not consider roles to have values - it is a thing that compiler currently cannot do.

What if lambda with Enum.TryParse(r, out value) will not be executed - value won't get its value through the closure?

Compiler cannot give you such guarantees.


Your code is (partially) equivalent to:

class MyClass<TEnum> where TEnum : struct
{
    public IEnumerable<TEnum> Roles { get; protected set; }

    public MyClass()
    {
        IEnumerable<string> roles = ... ;


        Roles = GetValues();   // <---- ERROR HERE!
    }

    public static IEnumerable<TEnum> GetValues(IEnumerable<String> roles)
    {       
        TEnum value; 
        String[] roleArray = roles.ToArray(); // To avoid the foreach loop.

        // What if roleArray.Length == 0?
        for(int i = 0; i < roleArray.Length; i++)
        {
             // We will never get here
             if (Enum.TryParse(roleArray[i], out value))
                 yield return value;
        }
    }
}

And this code is clean and understandable for the compiler(no errors) - it knows that without the execution of the Enum.TryParse(roleArray[i], out value) you won't try to return value.


But it is not so simple with functional LINQ query.

If we rewrite it with Enumerable extensions we will have:

 TEnum value;
 Roles =  roles
     .Where(role => Enum.TryParse(role, out value))
     .Select(role => value);   <---- STILL ERROR HERE!

And we again get the error.

Compiler cannot see that value will be unquestionably set, because it does not understand the innards of the methods used - Where may or (theoretically)may not execute the lambda, so if you add the fact that value variable is used in closure, it becomes a non trivial task to make such guarantees without false positives.

Sign up to request clarification or add additional context in comments.

19 Comments

Then value will not be evaluated either.
"What if roles is an empty collection?" , that is +1.
@JonathanWood - It's not a bug since the compiler doesn't know that Where will call TryParse at all. Where is just an extension method and isn't special.
@Habib: So? That's a very different case.
Poor wording, may be, But there is no way for compiler to determine with static flow analysis that value will definitely be assigned.
|
5

TL;DR: Error says the variable is (provably) unassigned -- FALSE. Reality, variable is not provably assigned (using the proof theorems available to the compiler).


LINQ is designed with the assumption of pure functions... those that return outputs based on the input and have no side effects.

Once this is rewritten, it will be:

roles.Where(r => Enum.TryParse(r, out value)).Select(r => value);

and rewritten again to

Enumerable.Select(Enumerable.Where(roles, r => Enum.TryParse(r, out value)), r => value);

These LINQ functions will call the filter lambda before any calls to the selection lambda, but the compiler can't know that (at least, not without either special-casing or cross-module dataflow analysis). More problematically, if a different implementation of Where were chosen by overload resolution, then possibly the lambda with TryParse would NOT be called.

The compiler's rules for definite assignment are very simplistic, and err on the side of safety.

Here is another example:

bool flag = Blah();
int value;
if (flag) value = 5;
return flag? value: -1;

It is impossible for an uninitialized value to be used, yet the language rules for dataflow analysis lead to a compile error that value is used without being "definitely assigned".

The compiler error is worded poorly, however. Not being "definitely assigned" is not the same as definitely being "unassigned", as the error purports.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.