假设我有一个工具可以自动删除编译器检测到的无法访问的C#代码。是否存在这样的操作会让我陷入困境的情况?请分享有趣的案例。
答案 0 :(得分:13)
这是一个有趣的例子。考虑这样的函数:
public static IEnumerable<int> Fun()
{
if (false)
{
yield return 0;
}
}
yield
的行被检测为无法访问。但是,删除它将使程序无法编译。函数中包含的yield
为编译器提供重新组织函数的信息,以便不执行任何操作只返回空集合。移除yield
行后,它看起来就像普通函数一样,在需要时没有返回。
与评论一样,这个例子是设计的,但是false
代替yield
,我们可以从其他生成的项目等中获得一个常量值(即这样的代码看起来不会像这样显而易见情况)。
修改强>:
请注意,async/await
构造实际上与{{1}}非常相似。然而,后者的语言创造者采用了一种不同的(IMO更好)方法来阻止这种情况。迭代器块可以用相同的方式定义(在函数签名中使用一些关键字而不是从函数体中检测它)。
答案 1 :(得分:3)
由于其他答案中提到的原因,我不会自动 ,但我会在这里说,我强烈倾向于删除未使用的代码而不是保留它。毕竟,跟踪过时的代码是源代码控制的目的。
答案 2 :(得分:3)
这个例子是设计的,但这会被标记为删除(默认DEBUG
设置)并在删除时产生不同的行为。
public class Baz { }
public class Foo
{
public void Bar()
{
if (false)
{
// Will be flagged as unreachable code
var baz = new Baz();
}
var true_in_debug_false_in_release =
GetType()
.GetMethod("Bar")
.GetMethodBody()
.LocalVariables
.Any(x => x.LocalType == typeof(Baz));
Console.WriteLine(true_in_debug_false_in_release);
}
}
在Release
模式下(使用默认设置),“无法访问的代码”将被优化掉,并产生与在if
模式下删除DEBUG
块时相同的结果。
与使用yield
的示例不同,无论是否删除了无法访问的代码,此代码都会编译。
答案 3 :(得分:3)
继Dan Bryant's answer之后,这里有一个程序示例,其行为将被一个比C#编译器更聪明的工具更改,以查找和删除无法访问的代码:
using System;
class Program
{
static bool tru = true;
static void Main(string[] args)
{
var x = new Destructive();
while (tru)
{
GC.Collect(2);
}
GC.KeepAlive(x); // unreachable yet has an effect on program output
}
}
class Destructive
{
~Destructive()
{
Console.WriteLine("Blah");
}
}
C#编译器并没有非常努力地证明GC.KeepAlive
无法访问,所以在这种情况下它并没有消除它。结果,程序永远循环而不打印任何东西。
如果某个工具证明它实际上无法访问(在此示例中相当简单)并将其删除,则程序行为会发生变化。它将立即打印“Blah”,然后永远循环。所以它已成为一个不同的程序。如果你有疑虑,试试吧;只是注释掉那条无法到达的行并看到行为改变。
如果GC.KeepAlive
出于某种原因,那么这种改变实际上是不安全的,并且会使程序在某些时候行为不端(可能只是崩溃)。
答案 4 :(得分:0)
一个罕见的边界情况是无法访问的代码包含GC.KeepAlive调用。这很少出现(因为它与非托管/托管互操作的特定hacky用例有关),但是如果你与需要这种非托管代码的非托管代码进行互操作,如果你不幸有GC触发器,那么删除它可能会导致间歇性失败在错误的时刻。
更新:
我已经对此进行了测试,我可以确认Servy是正确的; GC.KeepAlive调用没有生效,因为编译器证明它永远不会实际使用该引用。这不是因为该方法永远不会执行(该方法不必因为它影响GC行为而执行它),而是因为当可证明无法访问时,编译器会忽略GC.KeepAlive。
我正在离开这个答案,因为它对于反案例仍然很有意思。如果您修改代码使其无法访问,这是一种破坏程序的机制,但不要移动GC.KeepAlive以确保它仍然使引用保持活动状态。