条件属性是如何工作的?

12

我有一些标记为[Conditional("XXX")]的辅助方法,意图是在只有 XXX 条件编译符号存在时有条件地编译这些方法。我们将其用于调试和跟踪功能,它非常有效。

在我研究条件编译工作原理时,我发现几个来源称,使用Conditional属性标记的方法将被放置在IL中,但是对这些方法的调用将不会被执行。

代码如何编译成IL但不会被执行呢?我该如何验证实际上行为与描述的一样?我在IL方面的技能还比较薄弱。

3个回答

18

这由编译器控制。所有带有 [Conditional] 的方法仍会被包含在 MSIL 中,但会包括一个细节了 [Conditional].custom 实例 行。对于方法调用者,在编译时编译器进行词法分析、语义分析和重载解析,并在你使用 [Conditional] 的方法中找到 .custom 实例 IL。因此它不会编译该调用。

所以:编译器编译目标方法,但是不会编译任何对该方法的调用。注意:该方法仍然存在,你仍然可以使用反射调用它。参见规范

条件方法的调用要么包含要么省略,具体取决于该符号在调用点是否定义。如果定义了该符号,则调用将被包含;否则,该调用(包括接收方和调用参数的评估)将被省略。

如何验证?打开开发人员命令提示符,键入ildasm <enter>并打开相关的dlls/exes。查看调用方和被调用的 [Conditional] 方法。你会看到被调用的方法具有额外的 IL 和 .custom 实例,而在预期情况下省略了调用方的行。在控制台应用程序中尝试下面的代码。

为什么?在某些情况下,这使得条件调用比使用 #if 更简单。参见Eric Lippert: 条件编译和条件属性之间有什么区别?

class Program
{
    static void Main(string[] args)
    {
        AlwaysEmit();
        DebugEmit();
        VerboseEmit();
    }

    public static void AlwaysEmit()
    {
        Console.WriteLine("Beam me up");
    }

    [Conditional("DEBUG")]
    public static void DebugEmit()
    {
        Console.WriteLine("Kirk out");
    }

    [Conditional("VERBOSE")]
    public static void VerboseEmit()
    {
        Console.WriteLine("Say that again?");
    }
}

在相应的 MSIL 中,VerboseEmit 被包含了,但并没有从 Main 中调用:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       14 (0xe)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  call       void RateScope.SdrApi.UploaderConsoleApp.Program::AlwaysEmit()
  IL_0006:  nop
  IL_0007:  call       void RateScope.SdrApi.UploaderConsoleApp.Program::DebugEmit()
  IL_000c:  nop
  IL_000d:  ret
} // end of method Program::Main

...

.method public hidebysig static void  VerboseEmit() cil managed
{
  .custom instance void [mscorlib]System.Diagnostics.ConditionalAttribute::.ctor(string)
     = ( 01 00 07 56 45 52 42 4F 53 45 00 00 ) // ...VERBOSE..
  // Code size       13 (0xd)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "Say that again\?"
  IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_000b:  nop
  IL_000c:  ret
} // end of method Program::VerboseEmit

加分项。查看控制台输出和此内容的中间语言(根据需要修改Emit方法):

static void Main(string[] args)
{
    int callCount = 0;
    AlwaysEmit(++callCount);
    VerboseEmit(++callCount);
    DebugEmit(++callCount);
    Console.WriteLine("Call count = " + callCount);
    Console.ReadLine();
}

8
(有人争议这是否合格回答,但我认为值得一提。如果有人不同意是DVs或评论,我很乐意删除。)
这个特性的一个重要特点是它影响调用点而不是方法本身,因此这个特性在汇编之间起作用,并且对调用点有影响的是编译符号。
因此,实际方法必须在编译后插入到已编译的程序集中,因为在那时,我们并不知道该方法是否会被调用。
在后期,当消费应用程序正在编译时,我们才知道该方法是否被使用。这也意味着,在复杂的解决方案中,当有多个消费者在多个级别上时,某些调用可能会发生(在某些项目中),而其他调用则不会发生。

1
如果您的条件语句无法正常工作,而在 VS 中看起来像这样:

enter image description here

请确保在项目构建属性中添加Debug编译符号:

enter image description here

你可以在条件语句中使用 DEBUG 代替 Debug,这样也可以正常工作。

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