Lambda分配本地变量

11

请考虑以下源代码:

static void Main(string[] args)
{
    bool test;

    Action lambda = () => { test = true; };
    lambda();

    if (test)
        Console.WriteLine("Ok.");
}

它应该编译,对吗?但实际上并没有成功。我的问题是:根据C#标准,这段代码应该编译还是编译器出现了错误?


错误信息:

Use of unassigned local variable 'test'

注意:我知道如何修复错误,并且我部分地知道为什么会发生这种情况。然而,局部变量被无条件地赋值,我想编译器应该会注意到,但它没有。我想知道为什么。


回答评论:C#允许声明未赋值的变量,这实际上非常有用,即。

bool cond1, cond2;
if (someConditions)
{
    cond1 = someOtherConditions1;
    cond2 = someOtherConditions2;
}
else
{
    cond1 = someOtherConditions3;
    cond2 = someOtherConditions4;
}
编译器正确编译此代码,我认为,不给变量赋值实际上使代码更好,因为:
  • 它告诉读者,值稍后被赋予(很可能在以下条件语句中)
  • 强制程序员在内部条件的所有分支中分配变量(如果这是代码最初的目的),因为编译器将拒绝编译代码,如果其中一个分支没有分配其中一个变量。

在旁边说一下: 这更有趣。考虑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;
}

如果您将该代码块注释掉,编译过程会以警告结束:

main.cpp(12): warning C4700: uninitialized local variable 'test' used

然而,如果您删除该注释,则编译器将完全没有任何警告。看起来对我来说,它能够确定变量最终是否被设置。


3
编译器不会假设已经到达分配 test 的行。我不知道这是否是因为基本的代码流分析没有跟随方法调用,还是因为当闭合局部变量转换为代码生成类成员时的方式,或者两者都有可能,也可能两者都不是。但无论如何,在声明时将变量初始化为 false。 - Anthony Pegram
9
未包含错误信息,应打-1分。 - Servy
@Servy 和另一个用户都因为明显表明你本可以发布错误信息(猜猜为什么),但选择编写自己的小型 StackOverflow 推理小说而被扣1分。 - Earlz
2
@Spook 首先,不是必须要lambda表达式,而是需要一个闭包,它也可以是匿名委托 (delegate { test = true; };)。其次,知道局部变量在委托中被赋值并不难。事实上,为了保持闭包语义,必须已经知道该局部变量在闭包中被访问并将其提升。困难的问题是确定是否在某个时候执行了该委托,并在那时将变量标记为具有定义的值。这是一个非平凡的一般情况问题。 - Servy
2
这个案例(几乎完全一样)在C#规范的12.3.3.27中有详细说明。 - Austin Salonen
显示剩余4条评论
4个回答

17
我的问题是:根据C#标准,这段代码应该编译还是这是编译器的错误?
这不是一个bug。 C#语言规范(4.0版)第5.3.3.29节概述了关于匿名函数,包括lambda表达式的明确赋值规则。我将在此处发布它。

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.

您可以看到这如何应用于您的具体示例。在lambda表达式声明之前,变量test没有被明确赋值。在lambda表达式执行之前也没有明确赋值。在lambda表达式执行完成后,也没有明确赋值。按照规则,编译器不认为在读取if语句中的变量时已经被确定地赋值了。
至于为什么,我只能重复我所读过的,并且只能记住我不能提供一个链接,但是C#没有尝试做到这一点,因为虽然这是眼睛可以看到的简单情况,但更常见的情况是这种类型的分析将是非平凡的,事实上可能等同于解决停机问题。因此,C#“保持简单”,要求您遵守更易于应用和解决的规则。

这正是我所要问的。谢谢!(有趣的是,在C++中这些规则似乎有点不同) - Spook
2
@Spook:在C++中,使用未初始化的变量不会导致编译时错误,但在运行时会出现未定义的行为。你收到的警告只是编译器友好提醒;它肯定不会在每种情况下都试图弄清楚这个问题。 - GManNickG
好的,但是当我从代码的lambda块中删除注释时,编译器停止通知变量未被分配 - 换句话说,标记变量为未分配使用的条件不再满足。至少看起来是这样的。 - Spook

5

您正在使用未赋值的变量。尽管变量实际上已被赋值,但编译器无法从您发布的代码中推断。

所有本地变量在声明时都应该初始化,所以这很有趣,但仍然是错误的。


我的实际问题不是:为什么代码无法编译,而是:为什么编译器无法判断变量是否被赋值,尽管没有条件语句? - Spook
-1:本地变量is在使用之前已经被赋值,编译器只是太密集而无法弄清楚。 - Chris Dodd
@ChrisDodd:虽然答案错误地表示它“无法推断”,但您同样错误地表示它“过于密集”,从而将问题轻描淡写。这是一个可以解决的问题,但通常并不是微不足道的。 - GManNickG
2
更好的表述方式是:(1)确定明确赋值的一般问题是不可解决的,因为它等同于停机问题;(2)因此,C#规范以严格的方式定义“明确赋值”,有时会报告“未明确赋值”,即使变量实际上已经明确赋值;(3)在这种特定情况下,一个跨过程的流跟踪算法可能有效,但与指定的行为不一致。 - Eric Lippert

2
当编译器在执行方法的控制流分析以确定变量是否已经被赋值时,它只会查找当前方法的范围。Eric Lippert在这篇博客文章中讨论了这个问题。理论上,编译器可以分析从“当前方法”中调用的方法,以推断变量何时被赋值。
正如我之前提到的,我们可以进行跨过程分析,但实际上这很快就会变得非常混乱。想象一下一百个相互递归的方法,它们都进入了一个无限循环、抛出异常或调用组中的另一个方法。设计一个能够从复杂的调用拓扑中逻辑推断可达性的编译器是可行的,但可能需要大量的工作。此外,跨过程分析仅在您拥有程序过程源代码的情况下才有效;如果其中一个方法在程序集中,而我们所拥有的只是元数据,该怎么办呢?
请记住,您的代码示例并不是真正的单个方法。匿名方法将被重构为另一个类,它的一个实例将被创建,并且它将调用一个类似于您的定义的方法。此外,编译器还需要分析delegate类和Action的定义,以确定所提供的方法是否实际执行。
因此,虽然编译器在理论上可以知道变量在该上下文中是可达的,但编译器编写者故意选择不这样做,因为编写编译器的复杂性以及编译程序所需时间(可能显著增加)。

1

来自ECMA标准第8.3节变量和参数的片段:

在获取其值之前,必须对变量进行赋值。例如:

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

    }
}

由于它试图在变量a被赋值之前使用它,因此会导致编译时错误。明确分配的规则在§12.3中定义。

因此,它指出变量必须在使用之前被赋值,否则会导致编译器错误。因为您正在创建委托并调用它,所以包含在委托调用中的方法在技术上是未知的。因此,编译器无法找出它。请记住,调用的是委托的Invoke方法,而不是实际的方法。

C#的ECMA标准


3
为了让这个回答达到合适的水平,你需要更多地说明,“规范说明未明确赋值”并附上规范链接是不够的。这个案例值得讨论的原因在于变量已经被明确赋值,只是由于示例的复杂性,编译器无法判断。 - Servy
首先,最后一段之前的所有内容和链接都是无用的,应该被删除。显然,这对OP来说并不新鲜。其次,虽然你开始讨论这个问题,但这并不是一个真正的答案,它只是在问一个问题:“为什么编译器不能知道调用委托(已经完成)并将本地变量分配给结果,从而使本地变量被确定地分配?” 这并不是一个很好的答案。 - Servy
1
这个案例(几乎完全一样)在C#规范的12.3.3.27中有详细说明。 - Austin Salonen
@AustinSalonen 是的,在我查看规格时没有注意到那个。感谢你指出来。 - Brad Semrad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接