LINQ、输出参数和 'Use of Unassigned Local Variable' 错误

LINQ, output arguments, and 'Use of Unassigned Local Variable' error

我有一些类似下面的代码。

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!
    }
}

但是,在上面指示的行中,我收到错误:

Use of unassigned local variable 'value'

在我看来 value 在这种情况下将始终被初始化,因为它是 Enum.TryParseout 参数。

这是 C# 编译器的错误吗?

不,不是。

编译器无法保证Enum.TryParse(r, out value)会被执行。

如果 roles 是一个空集合怎么办?

即使您在方法中初始化集合,CSC 也不认为 roles 具有值 - 这是编译器目前无法做到的事情。

如果带有 Enum.TryParse(r, out value) 的 lambda 不会被执行怎么办 - value 不会通过闭包获取它的值?

编译器不能给你这样的保证。


您的代码(部分)等同于:

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;
        }
    }
}

并且这段代码对于编译器来说是干净且易于理解的(没有错误)——它知道如果不执行 Enum.TryParse(roleArray[i], out value) 你就不会尝试 return value


但是使用函数式 LINQ 查询就没那么简单了。

如果我们用 Enumerable 扩展重写它,我们将有:

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

我们再次收到错误。

编译器看不到 value 将被毫无疑问地设置,因为它不理解所用方法的内部结构 - Where 可能或(理论上)可能不会执行 lambda,所以如果你加上 value 变量在闭包中使用的事实,在没有误报的情况下做出这样的保证成为一项非常重要的任务。

TL;DR:错误表明变量(可证明)未分配——错误。实际上,变量未被证明赋值(使用编译器可用的证明定理)。


LINQ 的设计假设 函数...那些 return 基于输入输出并且没有副作用的函数。

一旦重写,它将是:

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

并再次重写为

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

这些 LINQ 函数将在对选择 lambda 的任何调用之前调用过滤器 lambda,但编译器无法知道这一点(至少,在没有特殊外壳或跨模块数据流分析的情况下)。更有问题的是,如果通过重载决策选择了 Where 的不同实现,那么可能不会调用带有 TryParse 的 lambda。

编译器对明确赋值的规则非常简单,并且在安全方面犯了错误。

这是另一个例子:

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

不可能使用未初始化的值,但数据流分析的语言规则导致编译错误value is used without being "definitely assigned".

但是,编译器错误措辞不佳。不是 "definitely assigned" 与肯定是 "unassigned" 不同,正如错误所指出的那样。