Why is 'is' implemented as 'as'?

2019-03-14 08:04发布

Given that this is a very natural use case (if you don't know what as actually does),

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

is effectively equivalent (that is, the compiler-generated CIL from the above code will be equivalent) to:

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

EDIT:

I guess I hadn't made my question clear. I wouldn't ever write the second snippet as it's of course redundant. I'm claiming that the CIL generated by the compiler when compiling the first snippet is equivalent to the second snippet, which is redundant. Questions: a) Is this correct? b) If so, why is is implemented like that?

This is because I find the first snippet a lot clearer and prettier than the actually well-written

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

CONCLUSION:

Optimizing the is/as case is not the compiler's responsibility, but the JIT's.

Also, as with a null check it has fewer (and less expensive) instructions than both of the alternatives (is and as and is and cast).

Addendum:

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

CIL for is and cast (.NET 3.5):

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

CIL for is and as (.NET 3.5):

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

These have been edited for shortness (method declarations, nops and calls to something() removed).

10条回答
▲ chillily
2楼-- · 2019-03-14 08:19

You could write the code now as

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

That I would say was a bit clearer, but not as fast without real magic from the compiler.

查看更多
霸刀☆藐视天下
3楼-- · 2019-03-14 08:19

The scope of 'y' is reduce if you place the declaration inside the loop.

Whoever wrote it probably prefers casting 'x as T' more than '(T)x', and wanted to limit the scope of 'y'.

查看更多
Lonely孤独者°
4楼-- · 2019-03-14 08:20

Firstly I disagree with your premise that this is more typical use case. It may be your favourite approach, but the idiomatic approach is the "as + null check" style:

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

As you have found the "is" approach requires the extra "as" or a cast, which is why the "as" with null check is the standard way of doing this in my experience.

I see nothing offensive about this "as" approach, personally I don't think it any more unpleasant on the eye than any other code.

As to your actual question, why is the is keyword implemented in terms of the as keyword, I have no idea, but I do like the play on words in your question:) I suspect neither is actually implemented in terms of the other, but the tool (Reflector I guess) you used to generate C# from the IL interpreted the IL in terms of as.

查看更多
狗以群分
5楼-- · 2019-03-14 08:21

Well, the IL instruction that is available (isinst) will return either an object of the appropriate type, or null if such a conversion is not possible. And it doesn't throw an exception if the conversion isn't possible.

Given that, both "is" and "as" are trivial to implement. I wouldn't claim that "is" is implemented as "as" in this case, just that the underlying IL instruction allows both to occur. Now, why the compiler isn't able to optimize the "is" followed by "as" into a single isinst call, that's another matter. Probably, in this case, it's related to variable scope (even though by the time this is IL, scope doesn't really exist)

Edit

On second thoughts, you can't optimise "is" followed by "as" into a single isinst call, without knowing that the variable under discussion isn't subject to update from other threads.

Assuming x is a string:

//Thread1
if(x is string)

//Thread2
x = new ComplexObject();

//Thread1
    y = x as string

Here, y should be null.

查看更多
兄弟一词,经得起流年.
6楼-- · 2019-03-14 08:26

According to the blog post How Many Passes? by Eric Lippert that is a compiler pass. To quote:

Then we run an optimization pass that rewrites trivial "is" and "as" operators.

So perhaps that is why you are seeing the same CIL generated for both snippets.

查看更多
Animai°情兽
7楼-- · 2019-03-14 08:29

I suspect strongly that is is faster than as and does not require an allocation. So if x is rarely ever Bar, then the first snippet is good. If x is mostly Bar, then an as would be recommended, as no second cast is required. It depends on the usage and circumstances of the code.

查看更多
登录 后发表回答