【问题标题】:Why isn't this code unreachable?为什么这段代码无法访问?
【发布时间】:2018-09-23 12:42:57
【问题描述】:

我发现了一些我认为无法访问且未被检测到的代码的情况。 编译器和 Visual Studio 都不会发出警告。

考虑这段代码:

enum Foo { A, B, C }
class Bar { public Foo type; }

static class Program
{
    private static void Main()
    {
        var bar = new Bar { type = Foo.A };

        if (bar.type == Foo.B)
        {
            Console.WriteLine("lol");
        }
    }
}

显然,程序不会打印出“lol”,因为 if 语句中的条件为假。 我不明白为什么不为无法访问的代码发出警告。 我唯一的假设是,如果您在多线程程序中存在竞争条件,那么这可能是可以实现的。这是正确的吗?

【问题讨论】:

  • 运行时检查。
  • 编译时检查可达性并不能涵盖所有内容。因此,您通常会在单元测试期间或通过手动运行程序来记录代码覆盖率。虽然竞争条件是一个有效的情况,但更有可能的是Bar 类修改了值本身。在您的示例中可能不是这种情况,但在现实世界的应用程序中肯定有可能。
  • 这种分析需要编译器解决臭名昭著的停机问题。设计师明智地不要试图解决这个问题。
  • 在很多情况下,编译器不会报告无法访问的代码。 int x = M(); if (x == 123.456) { /* unreachable */ } 比较是合法的,因为x 可以转换为double,无论x 的值是多少,比较总是为false。编译器不够聪明,无法推断出这一点,并且规范并不要求那么聪明。如果您习惯于编写无法访问的代码,那么养成使用代码覆盖工具的习惯
  • 顺便说一句,编译器曾经将int x = M(); if (x * 0 != 0) { ... } 中的结果视为不可达,推理任何int 乘以零为零,因此条件为假。虽然这个推理是正确的,但规范中找不到该规则,因此是我修复的编译器中的一个错误。从 C# 3.0 开始,编译器已根据规范正确地将 if 的结果视为不可达仅当条件为假且仅涉及常量表达式时

标签: c# .net unreachable-code


【解决方案1】:

静态分析只能做这么多,如果它可以证明一个值不能改变,它只会将代码标记为不可达。在您的代码中,Bar 内部发生的事情超出了方法流的范围,无法静态推理。如果Bar 的构造函数启动一个将type 的值设置回B 的线程怎么办?编译器无法知道它,因为Bar 的内部结构并不局限于该方法。

如果您的代码正在检查 local 变量的值,那么编译器可以知道它是否无法更改。但这里不是这样。

【讨论】:

  • Static analysis can only do so much 即,大多数(有用的)静态分析是合理但不完整的 - 即它保证推断程序的所有属性,只是它 确实推断是真的。推断程序的所有属性(包括它们是否停止或变化)被证明是无法确定的,我们喜欢我们的编辑器不会无限期挂起,对吧?
  • @TobiaTesan - 另一方面,编辑器可以区分“没有无法访问的代码”、“可能无法访问的代码”和“无法访问的代码”。
  • 例如,ReSharper 添加了编译器没有的各种分析和警告。就像有关可能意外的相互递归或可能的堆栈溢出异常的警告。但这些都被明确标记并与编译器输出分开
【解决方案2】:

C# specification 说,

如果 if 语句可达且布尔表达式不具有常量值 false,则 if 语句的第一个嵌入语句是可达的。

关于constant expressions

常量表达式必须是 null 文字或具有以下类型之一的值:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool、object、string ,或任何枚举类型。

在常量表达式中只允许以下结构:

  • 文字(包括 null 文字)。
  • 对类和结构类型的 const 成员的引用。
  • 对枚举类型成员的引用。
  • 对 const 参数或局部变量的引用
  • 带括号的子表达式,它们本身就是常量表达式。
  • 强制转换表达式,前提是目标类型是上面列出的类型之一。 检查和未检查的表达式
  • 默认值表达式
  • 预定义的+!~ 一元运算符。
  • 预定义的+*/%<<>>&,|,||||,@98645 、==!=<><=>= 二元运算符,前提是每个操作数都属于上面列出的类型。
  • ?: 条件运算符。

成员访问表达式不在此列表中,因此布尔表达式不是常量。因此 if 块的主体是可访问的。

【讨论】:

  • 对于那些好奇的人,提到“常量参数”是a bug in the specification。 C# 没有。
  • 看起来规范应该改用“潜在可达”,因为受其限制的分析显然听起来不完整。
【解决方案3】:

因为在编译时无法做出这样的保证。考虑这个替代 Bar 类

class Bar
{
   Random random = new Random();
   Array Foos = Enum.GetValues(typeof(Foo));

    private Foo _type;
    public Foo type
    {
        get { return _type; }
        set
        {
            _type = (Foo)Foos.GetValue(random.Next(3));
        }
    }
}

请注意,“可达”是在功能级别定义的。即使在安全的情况下,也不允许到达正在测试的功能之外。

【讨论】:

  • 很高兴看到引用标准中将该定义置于“可达”上的部分。
  • 唉,这不是一个替代的 Bar 类,因为您将 Bar.type 从一个字段更改为一个属性,一个完全不同的动物。
【解决方案4】:

您预期的警告没有实现,因为它不是一个有用的警告。

在现实世界的应用程序中,编译器经常面临着它可以完全证明是不可访问的代码,甚至可能是像光头一样的代码

static class Program
{
    private static void Main()
    {
        if (false)
        {
            Console.WriteLine("lol");
        }
    }
}

我在这台计算机上没有 C# 编译器,但我打赌你也没有警告。这是因为,当您将 if (false) { ... } 放在代码块周围时,您是故意这样做的,也许是为了暂时禁用某些东西以进行实验。唠叨你是没有用的。

更常见的是它不是文字false,它是一个编译时常量,构建系统将根据配置将其设置为true或false;您希望编译器在一个构建中删除无法访问的代码,而不是在另一个构建中,并且您不希望以任何方式抱怨。

比这更常见的是早期优化,如内联和不断传播,以发现条件始终为假;假设你有类似的东西

static class Program
{
    private static void Fizz(int i)
    {
        if (i % 3 == 0) {
            Console.WriteLine("fizz");
        } else {
            Console.WriteLine(i);
        }
    }

    private static void Main()
    {
        Fizz(4);
    }
}

您显然不想被告知 Fizz() 中条件的一侧是无法访问的,因为它只在此程序中使用参数 4 调用过

【讨论】:

  • 确实如此。实际上,我发现 Java 使其中一些分析成为错误 非常烦人。因此,您可以通过在其前面加上 return; 来快速确保某些代码不会运行,只会遇到不再编译的代码(if (false) 很好,因为检查不是非常聪明......)。
  • 这一切都非常好,除了你的核心前提根本不正确。带有if (false) 的周围代码 会生成编译器警告,因为您确实 有无法访问的代码。编译器不应该对 为什么 你有无法访问的代码做出假设,只是假设它就在那里。如果您暂时用false 替换了一个真实条件并忘记将它改回来怎么办?警告会救你的。
  • 具体来说,如果您有意放置了一大块无法访问的代码,那么您的有责任为编译器显式标记它以避免使用@发出警告987654329@,绝对不是编译器的工作。
  • 此外,Fizz 示例也没有完全解决问题,因为 C# 编译器不会针对这种情况发出 Unreachable Code 警告(再次经过测试),因为它的分析范围仅限于单个方法,并且它不会假设它总是会在当前程序中被调用一次。
  • 当然,您有权对语言及其规范发表意见。但是从“静态分析器被限定为一个方法”到“语言不可用”是……不是一个完全合理的论点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-22
  • 1970-01-01
  • 2021-09-26
  • 1970-01-01
  • 2014-04-29
相关资源
最近更新 更多