我发现了一个案例,我有一些代码我认为是不可访问的,并且没有被检测到。编译器和Visual Studio均未发出警告。
考虑此代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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语句中的条件为false。不过,我不明白为什么不对无法访问的代码发出警告。我唯一的假设是,如果您在一个多线程程序中有一个争用条件,那么它可能是可以到达的。这是正确的吗?
- 在运行时进行检查。
- 或者可能Foo.A可能等于Foo.B。
- 编译时可访问性检查并不能覆盖所有内容。因此,您通常会在单元测试期间或通过手动运行程序来记录代码覆盖率。虽然争用条件是有效的,但更可能的情况是Bar类修改值本身。这在您的示例中可能不是这样,但在实际应用程序中肯定是可能的。
- 我知道在堆栈溢出上有这个问题的副本,可以很好地解释这个问题,但是@manfredradlwinmer已经在上面很好地解释了它。编译器不进行这种特殊类型的控制流分析,因此不会检测到永远不会执行此代码。
- 这种分析要求编译器解决臭名昭著的暂停问题。设计师们明智的做法是不要试图解决这个问题。
- 在编译时不可访问,但在运行时不一定可以。您可以在"if"语句上放置一个断点,然后更改BAR变量。
- @克丽丝涅夫,我很确定语言的规范不会在调试会话中考虑到这种修改。当然,理论上,如果你有足够的坏运气,一些高能粒子撞到了错误的晶体管,而不使用调试程序就可能发生…
- 不管怎样,我很确定有一些工具可以对无法访问的代码(以及其他东西)进行静态分析。请注意,由于基础问题不可确定,因此它们必须在正确性和完整性之间进行选择(即,它们能够找到所有无法访问的代码的情况,但它们也会错误地将某些可访问的代码检测为无法访问,或者它们不会检测到某些无法访问的代码)。
- @Giacomo Alzetta这是一个很好的观点,你是对的,规范可能没有考虑到这种情况,但是完全并且总是不可访问的代码(例如在另一个返回之后立即返回)在运行时也是不可访问的。
- 在很多情况下,编译器不会在可能的时候报告无法访问的代码。int x = M(); if (x == 123.456) { /* unreachable */ }比较是合法的,因为x可转换为两倍,无论x的值是多少,比较总是错误的。编译器不够聪明,无法推断出这一点,规范也不要求它如此聪明。如果您习惯于编写无法到达的代码,那么就养成了使用代码覆盖工具的习惯。
- 顺便说一句,编译器曾将int x = M(); if (x * 0 != 0) { ... } 中的结果视为不可到达的结果,认为任何int乘以0都是零,因此条件是错误的。虽然这个推理是正确的,但是在规范中找不到这个规则,因此它是我修复的编译器中的一个bug。由于c 3.0,编译器已经根据规范正确地处理了if的结果,只有当条件为假并且只涉及常量表达式时,它才是不可访问的。
- @Ericlippert从JSON中删除了注释。当效果不那么令人惊讶,而且更具互操作性时,即使是(更大的)麻烦也是值得的。
- @克丽丝涅夫是对的。如果另一个线程上的某些代码修改了Foo.type,该怎么办?它不是局部变量,而是某个对象中的字段。
- @Ericlippert可能是规范中的一个错误,应该已经修复了?
- @詹斯迪默尔曼:不是。规范是正确的,实现是错误的。记住,规范的目标之一是简单易懂,它已经有800页长了。规范中的规则很简单:只有当条件为常量false且常量表达式不包含变量时,if的结果才是不可访问的。
静态分析只能做这么多,如果它能证明一个值是不能更改的,那么它只能将代码标记为不可访问的。在您的代码中,Bar中发生的事情超出了方法流的范围,不能静态地进行推理。如果Bar的构造函数启动了一个线程,将type的值设置回B该怎么办?编译器不知道这一点,因为Bar的内部结构也不属于该方法的范围。
如果您的代码正在检查局部变量的值,那么编译器就可以知道它是否无法更改。但情况并非如此。
- 型Static analysis can only do so much,也就是说,最有用的静态分析是健全的,但不是完整的——也就是说,不能保证推断出程序的所有属性,只是推断出的内容是真实的。推断程序的所有属性(包括它们是否停止或变化)被证明是不可决定的,我们喜欢我们的编辑器不可能无限期地挂起,对吗?
- 型@另一方面,编辑器可以区分"不可访问代码"、"可能不可访问代码"和"不可访问代码"。
- 型例如,resharper添加了编译器不需要的各种分析和警告。比如关于可能意外的相互递归或可能的堆栈溢出异常的警告。但是这些都有明显的标记,并与编译器输出分开
C规格说明,
The first embedded statement of an if statement is reachable if the if statement is reachable and the boolean expression does not have the constant value false.
关于常数表达式,
A constant expression must be the null literal or a value with one of the following types: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, object, string, or any enumeration type.
Only the following constructs are permitted in constant expressions:
-
Literals (including the null literal).
-
References to const members of class and struct types.
-
References to members of enumeration types.
-
References to const parameters or local variables
-
Parenthesized sub-expressions, which are themselves constant expressions.
-
Cast expressions, provided the target type is one of the types listed above.
checked and unchecked expressions
-
Default value expressions
-
The predefined +, –, !, and ~ unary operators.
-
The predefined +, –, *, /, %, <<, >>, &, |, ^, &&, ||, ==, !=, <, >, <=, and >= binary operators, provided each operand is of a type listed above.
-
The ?: conditional operator.
成员访问表达式不在此列表中,因此布尔表达式不是常量。因此,if块的主体是可到达的。
- 型对于那些好奇的人来说,提到"const参数"是规范中的一个错误。C没有。
- 型看起来规范应该使用"潜在的可访问"来代替,因为受其限制的分析显然是不完整的。
因为在编译时不能做出这样的保证。考虑这个可选的吧类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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.type从一块地变成了一个地产,一个完全不同的动物。
您期望的警告没有实现,因为它不是一个有用的警告。
在现实的应用程序中,编译器经常面临着完全证明是不可访问的代码,甚至可能是像
1 2 3 4 5 6 7 8 9 10
| static class Program
{
private static void Main()
{
if (false)
{
Console.WriteLine("lol");
}
}
} |
我在这台电脑上没有C编译器,但我敢打赌也没有警告。这是因为,当您将if (false) { ... }放在一个代码块周围时,您是故意这样做的,可能是为了暂时禁用某个实验。对你唠叨这件事没有帮助。
更常见的是,它不是字面上的false,它是编译时常量,生成系统将根据配置设置为true或false;您希望编译器删除一个生成中不可访问的代码,但不删除另一个生成中不可访问的代码,并且您也不希望投诉。
比这更常见的是,早期的优化(如内联和持续传播)发现条件总是错误的;假设您有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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替换了一个真实的条件,却忘记把它改回去了,那该怎么办?警告会救你的。
- 型示例:imgur.com/jcefglk
- 型具体来说,如果您有意放置一块不可访问的代码,那么您有责任为编译器显式地标记它,以避免使用#pragma warning disable发出警告,这绝对不是编译器的工作。
- 型此外,fizz示例也没有完全切掉它,因为C编译器不会针对这种情况(再次测试)发出无法访问的代码警告,因为它的分析范围是单一方法,并且它不会假定它总是在当前程序中调用一次时被调用。
- 型@avnershahar kashtan我对C没有太多的经验,但是我认为编译器和/或语言规范的缺陷足以使语言不适合大规模编程。
- 型当然,您有权对语言及其规范发表自己的意见。但是从"静态分析器的作用域到方法"到"语言不可用"是……不是一个完全理性的论点。
- 型在编译时应明确删除但有条件的代码由预处理器指令正确处理;#if DONTRUN .. #endif不会触发警告(显然)。我不知道有什么语言可以保证在它的规范中删除死代码,即使您显式地用if (false)之类的东西包围它,仅仅是因为它不需要详细说明规则并让编译器遵循它们。(C)缺少任何一种静态的元程序设计,所以EDCOX1〔8〕不存在代码不是问题。