Selman Genç Selman Genç - 3 months ago 14
C# Question

How is "as" operator translated when the right-side operand is generic?

I have just posted an answer to this question but I'm not entirely convinced of my answer.There are two things I'm wondering, consider this code:

class Foo<T>
{
void SomeMethod()
{
string str = "foo";
Foo<T> f = str as Foo<T>;
}
}


According to
C# Specification 5.0
, there are two different kinds of conversion of
as operator
.


If the compile-time type of
E
is not
dynamic
, the operation
E as T
produces the same result as

E is T ? (T)(E) : (T)null


If the compile-time type of
E
is
dynamic
, unlike the cast operator the
as operator
is not dynamically bound (§7.2.2). Therefore the expansion in this case is:

E is T ? (T)(object)(E) : (T)null



Since, this is invalid because of
(Foo<T>)str


str is Foo<T> ? (Foo<T>)str : (Foo<T>)null;


I thought it should be translated as:

str is Foo<T> ? (Foo<T>)(object)str : (Foo<T>)null;


But the spec says this only happens when the type of
E
is
dynamic
.

So my questions are:


  1. Is the compiler translating this expression to a code that is normally invalid?

  2. When the type of
    E
    is dynamic why first it casts
    E
    to
    object
    then
    T
    while the
    (T)E
    is completely valid?


Answer

Is the compiler translating this expression to a code that is normally invalid?

After staring at the spec for about an hour, I'm starting to convince myself that this is simply an edge-case which was overlooked in the specification. Note that this is merely a way for the C# language composers to express the as operator with the semantics of the is operator.

The compiler doesn't actually convert the as operator to a ternary operator with an is. It will emit an IL call to isinst, both for as and is:

IL_0000: nop
IL_0001: ldstr "foo"
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: isinst class ConsoleApplication2.Foo`1<!T>
IL_000d: stloc.1
IL_000e: ret

Looking at the compiled DLL, the as operator remains untouched.

When the type of E is dynamic why first it casts E to object then T while the (T)E is completely valid?

This is described in the fine-print of the specification:

If the compile-time type of E is dynamic, unlike the cast operator the as operator is not dynamically bound (§7.2.2). Therefore the expansion in this case is:

E is T ? (T)(object)(E) : (T)null

The cast to object is needed to make the use of as possible with dynamic objects. as is a compile-time operation while dynamic objects are bound only at run-time.

The compiler actually treats dynamic type objects as type object to begin with:

class Foo<T> 
{
    public void SomeMethod() 
    {
        dynamic str = "foo";
        Foo<T> f = str as Foo<T>;
    }
}

str is actually treated as object to begin with:

.class private auto ansi beforefieldinit Foo`1<T>
    extends [mscorlib]System.Object
{
    // Methods
    .method public hidebysig 
        instance void SomeMethod () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 15 (0xf)
        .maxstack 1
        .locals init (
            [0] object,
            [1] class Foo`1<!T>
        )

        IL_0000: nop
        IL_0001: ldstr "foo"
        IL_0006: stloc.0
        IL_0007: ldloc.0
        IL_0008: isinst class Foo`1<!T>
        IL_000d: stloc.1
        IL_000e: ret
    } // end of method Foo`1::SomeMethod
}

Edit:

After talking to Vladimir Reshetnikov from the Managed Languages Team, he explains what the semantic of the representation from the "as operator" to "cast operator" actually tries to convay:

I agree, there is some imprecise language in the spec too. It says 'as' operator is always applicable if an open type involved, but then describes its evaluation in terms of casts, that might be not valid in some cases. It should say that casts in the expansion do not represent normal C# cast operator but just represent conversions that are permitted in 'as' operators. I'll take a note to fix it. Thanks!