"as"关键字在内部是如何工作的?

8

我知道这个关键字的功能,但我想知道它在更低层次上是如何工作的。

哪个更快?它们总是产生相同的结果吗?如果是,为什么有两种不同的方法?

// Is there an overhead? An internal try catch?
Class123 obj = someobject as Class123;

if (Class123 != null)
{
    //OK
}

或者

Class123 obj = null;

if (someobject is Class123)
{
    obj = (Class123)someobject;
}

1
从答案来看,我认为 "is" 只是用来判断某个对象是否属于某种类型,并执行相应的操作,而不仅仅是一个强制转换。例如:if (obj is ClassManager){Write("You are a manager")} - Felipe Pessoto
5个回答

22

根据MSDN:as(C#参考)

as运算符类似于转换操作。但是,如果转换不可能,as将返回null而不是引发异常。考虑以下表达式:

expression as type

它与以下表达式等价,只是该表达式仅被评估一次。

expression is type ? (type)expression : (type)null

第一个变量(作为操作数)...

string str1 = strAsObject as string;
if (str1 != null)
{
    this.blabla(str1);
}

...编译成以下IL代码:

L_0009: ldloc.1 
L_000a: isinst string
L_000f: stloc.2 
L_0010: ldloc.2 
L_0011: ldnull 
L_0012: ceq 
L_0014: stloc.s CS$4$0000
L_0016: ldloc.s CS$4$0000
L_0018: brtrue.s L_0024
L_001a: nop 
L_001b: ldarg.0 
L_001c: ldloc.2 
L_001d: call instance void TestWinFormsApplication001.Form1::blabla(string)
L_0022: nop 
L_0023: nop 

...和第二种变体(是操作数+转换)...

if (strAsObject is string)
{
    string str2 = (string) strAsObject;
    this.blabla(str2);
}

...编译成以下IL代码:

L_0024: ldloc.1 
L_0025: isinst string
L_002a: ldnull 
L_002b: cgt.un 
L_002d: ldc.i4.0 
L_002e: ceq 
L_0030: stloc.s CS$4$0000
L_0032: ldloc.s CS$4$0000
L_0034: brtrue.s L_0047
L_0036: nop 
L_0037: ldloc.1 
L_0038: castclass string
L_003d: stloc.3 
L_003e: ldarg.0 
L_003f: ldloc.3 
L_0040: call instance void TestWinFormsApplication001.Form1::blabla(string)
L_0045: nop 
L_0046: nop 

... 所以你可以看到唯一的区别是第L_0038行额外的castclass转换代码。


16
< p >使用 as 关键字时,没有内部的try-catch发生。据我所知,这个功能是内置在编译器/CLR中的,因此类型检查是隐式和自动化的。

简单规则
您始终期望对象具有已知类型(因此如果不小心使用了错误类型,将收到一个有用的错误提示)时,请使用直接转换。而使用 as 关键字时,则表示对象总是属于已知类型

存在 as 关键字的原因纯粹是为了程序员方便(尽管您正确地指出了try-catch会更慢)。您可以像下面这样手动实现它:

var castObj = (obj is NewType) ? (NewType)obj : null;

这凸显了关键字“as”主要是为了简洁而存在的事实。

现在,两者之间的性能差异很可能是微不足道的。由于类型检查,“as”关键字可能略慢一些,但这不太可能影响到绝大多数情况下的代码。就像经常说的那样,过早地优化永远不是明智的做法。如果你真的想要进行基准测试,但我建议只使用更方便/适合您情况的方法,不需要担心性能问题(或者如果您确实必须担心性能问题,请稍后处理)。


我之前没有考虑到空值的情况,这是很重要的。但是当我提到“尝试”时,如果对象无法转换(抱歉我的英语不好),…… - Felipe Pessoto
4
当 null 存在时,“as” 和强制转型的行为是相同的吗?难道不应该是“当您从不期望对象为任何其他类型时,请使用直接转换,当对象可能是另一种类型时,请使用 'as'”? - Grokys
1
虽然使用 "as" 可能会略微慢一些,但是在 if(x is T) (T)x.Foo() 中,需要进行两次类型检查,可能比 x==null 稍微慢一些。 - H H
@activa:我的观点是这主要是为了方便,而语言关键字提供了这个功能。 "as" 关键字本身是否具有唯一的 CIL 代码,还是只是被转换为类型检查后跟随一个强制转换?如果是前者,那么你是正确的。然而,我倾向于认为它是后者。 - Noldorin
@activa:你似乎是对的。不过,它是一个关键字这一事实意味着编译器有某种作用。无论如何,感谢你澄清了这一点。 - Noldorin
显示剩余7条评论

9

为了澄清一些事情:

类型转换应该在您确定对象是要转换的类型时进行。它可以为空(在这种情况下,将返回null,除非您要转换为值类型)。

当您不确定时,可以使用“as”运算符。当对象无法转换或对象为空时,将返回null

“as”运算符被翻译为专用的IL语句(isinst),而类型转换被翻译为castclass IL语句,因此它内置于运行时。编译器只需发出正确的IL语句。


是的,这确实是正确的。点赞了,因为你在这里的澄清工作中功不可没。 - Noldorin
我本来想编辑你的回答而不是新建一个,但我还没有被允许这样做 :-) - Philippe Leybaert

5

这个问题已经得到了很好的回答,但是目前缺少硬性数据。

Over 100000000 iterations
AS   : Failure  00:00:00.9282403
Cast : Failure  00:00:00.9868966
AS   : Success  00:00:00.9350227
Cast : Success  00:00:01.1382759

这些数字始终保持这些比例。 我想指出的是,从性能角度来看,唯一的结论是选择其中一种方法并没有太多好处。对于单个调用来说,差异很小(非常接近零)。尽管如此,“as”更快 :) 在此之后,上述数字大多是合理的。 “as”在失败时需要比成功时花费更长的时间。成功时什么也不会发生,该值可以直接使用或者简单地复制。失败时,它需要跳转以复制空引用。 “Cast”在失败时更快,只需一次“is”调用即可完成。但是在成功时,它要慢得多,因为需要调用“is”,然后再进行强制转换。 但是我很惊讶Cast在失败时需要的时间比AS失败还长。 编辑 按照要求,在try / catch块中添加了cast的数字。
Over 100000000 iterations
Catch : Failure 05.05:00:00 // approximately, because I didn't hang around
Catch : Success 00:00:01.4000952

生成第一组图形的代码。
class Program
{
    const int ITERATION_COUNT = 100000000;
    private static UInt64 stringCount = 0;
    private static UInt64 objectCount = 0;
    static void Main(string[] args)
    {
        Console.WriteLine("Over {0} iterations ", ITERATION_COUNT);

        string s = "Hello";
        object o = new Int32();

        RunTest("AS   : Failure  {0}", TestAs, o);
        RunTest("Cast : Failure  {0}", TestIs_And_Cast, o);
        RunTest("AS   : Success  {0}", TestAs, s);
        RunTest("Cast : Success  {0}", TestIs_And_Cast, s);

        Console.WriteLine("Press any key to stop");
        Console.ReadKey();

    }
    private static void RunTest(string testDescription, Action<object> testToRun, object arg)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < ITERATION_COUNT; i++)
            testToRun(arg);
        sw.Stop();
        Console.WriteLine(testDescription, sw.Elapsed);
    }
    static void TestAs(object obj)
    {
        string s = obj as string;
        if (s != null)
            stringCount++;
        else
            objectCount++;
    }
    static void TestIs_And_Cast(object obj)
    {
        string s = null;
        if (obj is string)
        {
            s = (string)obj;
            stringCount++;
        }
        else
            objectCount++;
    }
}

2
“as”比跟着一个转换更加优雅。我同意性能影响可以忽略不计。也许你可以添加一个使用转换和异常处理的测试用例。希望这能让一些人意识到,为这种事情捕获异常是“不可取的”。 :) - Philippe Leybaert
那么,每毫秒可以进行多少个 (as 或 cast) 的操作呢?假设在大约 1 秒内有 1亿次检查:--> 100,000,000 / 1000 = 100,000。因此,每毫秒可以进行 100,000 次 as 或 cast 检查。或者说:每个检查(无论是哪种)大约需要 100 百万分之一秒的时间。 - Nicholas Petersen

5

使用“as”可能更快,因为只需要检查类型一次,而“is + cast”需要检查类型两次。


我不确定这是否正确。据我所知,“as”关键字隐式地执行完全相同的操作。 - Noldorin
这个解释也在 FxCop 中作为性能规则给出。所以我想这是正确的。 - Scoregraphic

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