为啥这个 (null || !TryParse) 条件会导致“使用未分配的局部变量”?

Posted

技术标签:

【中文标题】为啥这个 (null || !TryParse) 条件会导致“使用未分配的局部变量”?【英文标题】:Why does this (null || !TryParse) conditional result in "use of unassigned local variable"?为什么这个 (null || !TryParse) 条件会导致“使用未分配的局部变量”? 【发布时间】:2013-04-24 16:36:12 【问题描述】:

以下代码导致使用未分配的局部变量“numberOfGroups”

int numberOfGroups;
if(options.NumberOfGroups == null || !int.TryParse(options.NumberOfGroups, out numberOfGroups))

    numberOfGroups = 10;

但是,这段代码可以正常工作(尽管ReSharper 说= 10 是多余的):

int numberOfGroups = 10;
if(options.NumberOfGroups == null || !int.TryParse(options.NumberOfGroups, out numberOfGroups))

    numberOfGroups = 10;

是我遗漏了什么,还是编译器不喜欢我的||

我已将其缩小到导致问题的dynamicoptions 是我上面代码中的一个动态变量)。问题仍然存在,为什么我不能这样做

这段代码不能编译:

internal class Program

    #region Static Methods

    private static void Main(string[] args)
    
        dynamic myString = args[0];

        int myInt;
        if(myString == null || !int.TryParse(myString, out myInt))
        
            myInt = 10;
        

        Console.WriteLine(myInt);
    

    #endregion

但是,这段代码确实

internal class Program

    #region Static Methods

    private static void Main(string[] args)
    
        var myString = args[0]; // var would be string

        int myInt;
        if(myString == null || !int.TryParse(myString, out myInt))
        
            myInt = 10;
        

        Console.WriteLine(myInt);
    

    #endregion

我没有意识到 dynamic 会是其中的一个因素。

【问题讨论】:

不要认为知道你没有使用传递给out参数的值作为输入足够聪明 这里给出的代码没有展示所描述的行为;它工作得很好。请发布实际演示您所描述的行为我们可以自己编译的代码。给我们整个文件。 啊,现在我们有了一些有趣的东西! 编译器对此感到困惑并不奇怪。动态调用站点的帮助程序代码可能有一些不能保证分配给out 参数的控制流。考虑编译器应该生成什么帮助代码来避免这个问题,或者这是否可能,这当然很有趣。 乍一看,这确实像一个错误。 【参考方案1】:

From MSDN(强调我的):

动态类型允许其发生的操作绕过编译时类型检查。相反,这些操作在运行时解决。动态类型简化了对 COM API(例如 Office Automation API)的访问,也简化了对动态 API(例如 IronPython 库)和 html 文档对象模型 (DOM) 的访问。

在大多数情况下,动态类型的行为类似于类型对象。但是,包含动态类型表达式的操作不会被编译器解析或类型检查。

由于编译器不进行类型检查或解析任何包含动态类型表达式的操作,因此它无法确保将通过使用TryParse() 来分配变量。

【讨论】:

如果满足第一个条件,则分配numberGroups(在if true 块中),如果不满足,则第二个条件保证分配(通过out)。 这是一个有趣的想法,但是代码在没有myString == null(仅依赖TryParse)的情况下编译得很好。 @leppie 关键是因为第一个条件(实际上是整个if 表达式)涉及一个dynamic 变量,它在编译时没有被解析(因此编译器无法生成那些假设)。 @NominSim:我明白你的意思:) +1 可能是编译器的牺牲品(违反 C# 规则),但其他建议似乎暗示了一个错误。 Eric 的 sn-p 表明这不是牺牲,而是错误。 @NominSim 这不可能;仅仅因为某些编译器函数被延迟并不意味着它们都是。有大量证据表明,在稍有不同的情况下,编译器可以毫无问题地进行明确的赋值分析,尽管存在动态表达式。【参考方案2】:

我很确定这是一个编译器错误。很好的发现!

编辑:这不是错误,正如 Quartermeister 所展示的那样; dynamic 可能会实现一个奇怪的 true 运算符,这可能会导致 y 永远不会被初始化。

这是一个最小的复制:

class Program

    static bool M(out int x) 
     
        x = 123; 
        return true; 
    
    static int N(dynamic d)
    
        int y;
        if(d || M(out y))
            y = 10;
        return y; 
    

我看不出这应该是非法的;如果你用 bool 替换 dynamic 它编译就好了。

实际上我明天要与 C# 团队会面;我会跟他们提的。为错误道歉!

【讨论】:

我很高兴知道我不会发疯 :) 我已经更新了我的代码以仅依赖 TryParse,所以我现在就准备好了。感谢您的洞察力! @NominSim:假设运行时分析失败:那么在读取本地之前抛出异常。假设运行时分析成功:那么在运行时要么d 为真且设置y,要么d 为假且M 设置y。无论哪种方式,都设置了 y。分析被推迟到运行时这一事实并没有改变任何事情。 万一有人好奇:我刚刚检查过,Mono 编译器做对了。 imgur.com/g47oquT 我认为编译器的行为实际上是正确的,因为d 的值可能是具有重载true 运算符的类型。我已经发布了一个答案,其中没有一个分支。 @Quartermeister 在这种情况下 Mono 编译器出错了 :)【参考方案3】:

如果动态表达式的值属于具有重载的true 运算符的类型,则可能会取消分配变量。

|| 操作符将调用true 操作符来决定是否计算右侧,然后if 语句将调用true 操作符来决定是否计算其主体。对于普通的bool,这些将始终返回相同的结果,因此将准确评估一个,但对于用户定义的运算符,没有这样的保证!

建立在 Eric Lippert 的 repro 之上,这里有一个简短而完整的程序,它演示了两个路径都不会被执行并且变量将具有其初始值的情况:

using System;

class Program

    static bool M(out int x)
    
        x = 123;
        return true;
    

    static int N(dynamic d)
    
        int y = 3;
        if (d || M(out y))
            y = 10;
        return y;
    

    static void Main(string[] args)
    
        var result = N(new EvilBool());
        // Prints 3!
        Console.WriteLine(result);
    


class EvilBool

    private bool value;

    public static bool operator true(EvilBool b)
    
        // Return true the first time this is called
        // and false the second time
        b.value = !b.value;
        return b.value;
    

    public static bool operator false(EvilBool b)
    
        throw new NotImplementedException();
    

【讨论】:

这里做得很好。我已将其传递给 C# 测试和设计团队;明天我看到他们时,我会看看他们是否有任何cmets。 这对我来说很奇怪。为什么要对d 进行两次评估? (正如您所展示的那样,我并不争辩它显然 。)我本来希望 true 的评估结果(来自第一个运算符调用,由 || 引起)被“传递”给if 声明。例如,如果你在其中放置一个函数调用,肯定会发生这种情况。 @DanTao:正如您所料,表达式d 只计算一次。 true 运算符被调用了两次,一次由|| 调用,一次由if 调用。 @DanTao:如果我们将它们放在单独的语句中,例如var cond = d || M(out y); if (cond) ... ,可能会更清楚。首先我们评估d 以获得EvilBool 对象引用。为了评估||,我们首先使用该引用调用EvilBool.true。这返回真,所以我们短路并且不调用M,然后将引用分配给cond。然后,我们转到if 语句。 if 语句通过调用 EvilBool.true 来评估其条件。 现在这真的很酷。我不知道有真假运算符。

以上是关于为啥这个 (null || !TryParse) 条件会导致“使用未分配的局部变量”?的主要内容,如果未能解决你的问题,请参考以下文章

如果 Int32.TryParse 无法解析字符串,如何将其输出设置为 null? [复制]

Parse and TryParse

int.Parse()与int.TryParse()

int.TryParse & Convert.ToInt32

将 int.TryParse 与可为空的 int 一起使用 [重复]

读书笔记---改善c#编程的157个建议