【问题标题】:Why is 'is' implemented as 'as'?为什么'is'被实现为'as'?
【发布时间】:2010-02-12 11:07:43
【问题描述】:

鉴于这是一个非常自然的用例(如果您不知道 as 实际做了什么),

if (x is Bar) {
   Bar y = x as Bar;
   something();
}

实际上等价于(也就是说,编译器从上述代码生成的CIL 将等价于):

Bar y = x as Bar;
if (y != null) {
    y = x as Bar; //The conversion is done twice!
    something();
}

编辑:

我想我的问题没有说清楚。我永远不会写第二个 sn-p 因为它当然是多余的。我声称编译器在编译第一个 sn-p 时生成的 CIL 等效于第二个 sn-p,这是多余的。问题: a) 这是正确的吗? b) 如果是这样,为什么is 是这样实现的?

这是因为我发现第一个 sn-p 比实际写得好得多清晰和漂亮

Bar y = x as Bar;
if (y != null) {
   something();
}

结论:

优化 is/as 的情况不是编译器的责任,而是 JIT 的责任。

此外,与空值检查相比,它比两种替代方法(isasiscast)具有更少(且成本更低)的指令。

附录:

CIL for as with nullcheck (.NET 3.5):

L_0001: ldarg.1
L_0002: isinst string
L_0007: stloc.0
L_0008: ldloc.0
L_0009: ldnull
L_000a: ceq
L_000c: stloc.1
L_000d: ldloc.1
L_000e: brtrue.s L_0019
L_0011: ldarg.0
L_0019: ret

适用于 is 和 cast (.NET 3.5) 的 CIL:

L_0001: ldarg.1
L_0002: isinst string
L_0007: ldnull
L_0008: cgt.un
L_000a: ldc.i4.0
L_000b: ceq
L_000d: stloc.1
L_000e: ldloc.1
L_000f: brtrue.s L_0021
L_0012: ldarg.1
L_0013: castclass string
L_0018: stloc.0
L_0019: ldarg.0
L_0021: ret

is 和 as (.NET 3.5) 的 CIL:

L_0001: ldarg.1
L_0002: isinst string
L_0007: ldnull
L_0008: cgt.un
L_000a: ldc.i4.0
L_000b: ceq
L_000d: stloc.1
L_000e: ldloc.1
L_000f: brtrue.s L_0021
L_0012: ldarg.1
L_0013: isinst string
L_0018: stloc.0
L_0019: ldarg.0
L_0021: ret

为了简短起见,已对这些进行了编辑(删除了方法声明、nops 和对某事()的调用)。

【问题讨论】:

  • 我不会称之为典型用例,更典型的是 Bar y = x as Bar; if (y != null) { do_stuff(); }。如果您仍然使用 as,为什么要先检查 is
  • 老实说,第一个给我的感觉好像是由一个不知道 as 做什么的人写的。
  • 是的,但那是因为你知道 as 是做什么的 :)。如果你忘记了你所知道的,第一个对你来说不是更漂亮吗?
  • 因为我刚刚更新了我的答案,第一个 sn-p 在面对多个线程时会受到竞争条件的影响,即 y 仍然可以为空。 as+null 检查版本不受这种竞争的影响。
  • +1 个不错的问题标题 :-)

标签: c# .net as-keyword


【解决方案1】:

a) 这是正确的

是的,尽管我会以另一种方式陈述。您是说“is”是空值检查的语法糖。我会以另一种方式说:“as”是“检查类型实现,如果成功则强制转换,如果失败则返回 null”的语法糖。

也就是说,我会更倾向于说

if (x is Bar) { 
   Bar y = x as Bar; 
   something(); 
} 

实际上等同于

if (x is Bar) { 
   Bar y = (x is Bar) ? (Bar)x : (Bar) null; 
   something(); 
} 

你看,你想用“is”来定义“as”,而不是相反。真正的问题应该是“为什么要按原样执行?” :-)

b) 如果是这样,为什么是这样实现的?

因为这是规范的正确实现

我想我在这里没有遵循您的思路。该实现有问题吗?您希望如何实施?您可以使用“isinst”和“castclass”指令;描述您希望看到的程序的代码生成器。

【讨论】:

  • 实现没有错。它只是呈现了一个我个人喜欢的特定用例,而不是正确的替代方案(我认为它更具可读性和直观性)效率低下。所以我想知道这是否有原因。然后,为了使它适用于这个用例,编译器应该进行优化,以保留测试中使用的强制转换实例: if (x is Bar) { Bar y = x as Bar; //我希望这里使用 if 测试中使用的相同实例 }。但这在某些情况下可能不是线程安全的,并且可能会有其他一些问题......
  • @Vinko:但是使用了相同的实例。它始终是对同一个实例的相同引用。我还是不关注你。您是说您不希望进行三次型式测试吗?因为在这里的成功案例中执行了 3 次——一次在第一个“is”上,一次在作为“as”一部分的“is”上,一次在演员表中。如果您不想要冗余测试,则不要在结果中使用“as”。说“Bar y = (Bar) x;”。然后你只能完成两次测试。
  • 虽然这仍然不能解释为什么在第一种情况下使用 is 和 as 没有优化。或者有吗?
  • @Vinko:这取决于抖动来决定。 C# 编译器只是根据要求生成 IL。如果您说“if (x == 2) { if (x % 2 == 0) ...”,编译器不会说“好吧,我知道如果满足第一个条件,那么第二个条件也将满足,所以我将省略第二个测试”。如果您编写执行冗余测试的代码,我们会生成执行冗余测试的代码。如果抖动足够聪明,可以优化掉它,那很好,但 C# 编译器不是。
  • 正确。谢谢。感谢您从我之前的评论中解释我的困惑(我不是指实例。)
【解决方案2】:

好吧,可用的 IL 指令 (isinst) 将返回适当类型的对象,如果无法进行此类转换,则返回 null。如果无法进行转换,它也不会抛出异常。

鉴于此,“is”和“as”都易于实现。在这种情况下,我不会声称“is”被实现为“as”,只是底层的 IL 指令允许两者发生。现在,为什么编译器不能将“is”后跟“as”优化为单个 isinst 调用,这是另一回事。可能,在这种情况下,它与变量作用域有关(即使当这是 IL 时,作用域并不真正存在)

编辑

再想一想,您不能在不知道讨论中的变量不会从其他线程更新的情况下将“is”后跟“as”优化为单个 isinst 调用。

假设 x 是一个字符串:

//Thread1
if(x is string)

//Thread2
x = new ComplexObject();

//Thread1
    y = x as string

这里,y 应该为空。

【讨论】:

  • 只要变量不是 volatile 并且在 is 和 as 操作之间没有障碍,就没有理由不重用第一个实例。
  • @erikkallen - 我不会反对。我只是认为这将是一个非常具体的优化,特别是考虑到“is”和“as”的“正常”用法
  • 这是我期望在 JIT 级别而不是在 C# 编译器中进行的优化。
【解决方案3】:

在您的示例中,as 的使用无论如何都是多余的。既然您已经知道 x is Bar,您应该使用演员表:

if (x is Bar)
{
    Bay y = (Bar)x;
}

或者,使用as 进行转换并检查是否为空:

Bar y = x as Bar;
if (y != null)
{

}

【讨论】:

  • 问题的关键是编译器生成了冗余代码。
  • 第一个示例进行两次强制转换,而第二个仅进行一次强制转换,而且强制转换成本很高。所以当 Bar 是一个类时,我更喜欢第二种方法。 (当然,对于值类型,只能使用第一个。)
【解决方案4】:

首先,我不同意您的假设,即这是更典型的用例。这可能是你最喜欢的方法,但惯用的方法是“as + null check”样式:

Bar y = x as Bar; 
if (y != null) { 
   something(); 
}

正如您发现的那样,“is”方法需要额外的“as”或强制转换,这就是为什么根据我的经验,带有 null 检查的“as”是执行此操作的标准方法。

我认为这种“as”方法没有任何冒犯之处,我个人认为它不会比任何其他代码更令人不快。

至于您的实际问题,为什么 is 关键字是根据 as 关键字实现的,我不知道,但我确实喜欢您问题中的文字游戏:)我怀疑两者都没有实际实现就另一个而言,但是您用来从 IL 生成 C# 的工具(我猜是反射器)将 IL 解释为 as

【讨论】:

  • 好的,我将更典型的用例更改为“非常自然”的用例。我没有发现空检查特别令人反感,我只是发现另一个 sn-p 更漂亮:)
  • 当然,我并不是要批评你的观点,但我确实认为“as”风格或多或少是标准方法。
【解决方案5】:

你不会再做第二个y = x as Bar;,因为你已经有了 y,即 Bar。

【讨论】:

  • 我不会,但是如果我用更自然(is)的方式写,编译器会
【解决方案6】:

你现在可以把代码写成

DoIfOfType<Bar>(possibleBar, b => b.something())

我想说的是更清楚一点,但如果没有编译器的真正魔法,速度就不会那么快。

【讨论】:

    【解决方案7】:

    根据 Eric Lippert 的博客文章 How Many Passes?,这是一个编译器通道。引用:

    然后我们运行一个优化过程 重写琐碎的“is”和“as” 运营商。

    所以也许这就是您看到为两个 sn-ps 生成相同 CIL 的原因。

    【讨论】:

      【解决方案8】:

      如果将声明放在循环内,'y' 的范围会缩小。

      写它的人可能更喜欢将 'x as T' 转换为 '(T)x',并希望限制 'y' 的范围。

      【讨论】:

        【解决方案9】:

        您忘记了值类型。例如:

            static void Main(string[] args)
            {
                ValueType vt;
                FooClass f = vt as FooClass;
        
            }
        
            private class FooClass
            {
                public int Bar { get; set; }
            }
        

        不会编译,因为值类型不能像这样转换。

        【讨论】:

        • 我不明白这与这里有什么关系。当然我说的是它可以用在什么地方……要详细说明吗?
        • 相关,因为这就是您无法摆脱 is 运算符的原因。
        【解决方案10】:

        我强烈怀疑 isas 快并且不需要分配。因此,如果 x 很少是 Bar,那么第一个 sn-p 是好的。如果 x 主要是 Bar,则建议使用 as,因为不需要第二次强制转换。这取决于代码的使用和环境。

        【讨论】:

        • 谁会“作为”分配?它返回的指针将在堆栈上,因为指针是 value 类型。
        • as 不需要分配,如果被审查的对象实现了所需的特性(即实现接口或者是其子类),则返回指针,否则返回空指针。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-08-16
        • 2017-06-24
        • 2011-07-05
        • 2020-09-25
        • 2023-03-14
        • 2021-06-12
        • 2013-03-09
        相关资源
        最近更新 更多