14

``I've run across an interesting curiosity when compiling some C# code that uses generics with type constraints. I've written a quick test case for illustration. I'm using .NET 4.0 with Visual Studio 2010.

namespace TestCast
{
    public class Fruit { }

    public class Apple : Fruit { }

    public static class Test
    {
        public static void TestFruit<FruitType>(FruitType fruit) 
            where FruitType : Fruit
        {
            if (fruit is Apple)
            {
                Apple apple = (Apple)fruit;
            }
        }
    }
}

The cast to Apple fails with the error: "Cannot convert type 'FruitType' to 'TestCast.Apple'". However, if I change the line to use the as operator, it compiles without error:

Apple apple = fruit as Apple;

Could someone please explain why this is the case?

hatch22
  • 797
  • 6
  • 18

6 Answers6

25

I used this question as the basis for a blog article in October 2015. Thanks for the great question!

Could someone please explain why this is the case?

"Why" questions are hard to answer; the answer is "because that's what the spec says" and then the natural question is "why does the spec say that?"

So let me make the question more crisp:

What language design factors influenced the decision to make the given cast operator illegal on constrained type parameters?

Consider the following scenario. You have a base type Fruit, derived types Apple and Banana, and, now comes the important part, a user-defined conversion from Apple to Banana.

What do you think this should do when called as M<Apple>?

void M<T>(T t) where T : Fruit
{
    Banana b = (Banana)t;
}

Most people reading the code would say that this should call the user-defined conversion from Apple to Banana. But C# generics are not C++ templates; the method is not recompiled from scratch for every generic construction. Rather, the method is compiled once, and during that compilation the meaning of every operator, including casts, is determined for every possible generic instantiation.

The body of M<Apple> would have to have a user-defined conversion. The body of M<Banana> would have an identity conversion. M<Cherry> would be an error. We cannot have three different meanings of an operator in a generic method, so the operator is rejected.

Instead what you have to do is:

void M<T>(T t) where T : Fruit
{
    Banana b = (Banana)(object)t;
}

Now both conversions are clear. The conversion to object is an implicit reference conversion; the conversion to Banana is an explicit reference conversion. The user-defined conversion is never called, and if this is constructed with Cherry then the error is at runtime, not compile time, as it always is when casting from object.

The as operator is not like the cast operator; it always means the same thing no matter what types it is given because the as operator does not ever invoke a user-defined conversion. Therefore it can be used in a context where a cast would be illegal.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • I see what you mean, though "We cannot have three different meanings of an operator in a generic method" seems to be not entirely clear, since operators for casting can be both implicitly and explicitly overloaded. It seems strange not to defer determination of an error until runtime, as `(Apple)t` would succeed with an explicit user-defined conversion, `(Banana)t` would convert to itself, and only `(Cherry)t` would produce an exception. – hatch22 Oct 03 '13 at 18:50
  • I guess I'm just surprised that the compiler is disallowing something that could be valid just because it might not be. I thought that was what Exceptions were for (among other things). – hatch22 Oct 03 '13 at 18:54
  • @hatch22: It's not *just* that it might be invalid, it's that the code might do something completely different than what a reasonable person reading the code would expect. When you write a cast you are taking responsibility for the conversion succeeding; we want to make sure that it is crystal clear to the author what conversion that is. – Eric Lippert Oct 03 '13 at 18:56
  • I see. That makes sense. So "as" is allowed because it restricts the type of conversion, making it clear what is being attempted, where a cast can have different effects depending on if there are user-defined conversions available. It seems to me that the multi-purposing of the cast operator muddies the waters, as I normally think of a direct explicit cast doing the same thing that "as" actually does, and would rather write a separate function to do explicit conversions. One can of course do this by convention, but the ambiguity in the language definition is confusing. – hatch22 Oct 03 '13 at 19:00
  • 7
    @hatch22: I agree with you. The cast operator is a bizarre platypus, neither fully mammal nor lizard. A cast operator can mean "compiler! this value will really be of that type, so let me violate your type checking rules, and throw an exception if I'm wrong." It can also mean "compile! this value is not of that type but you know what function to call in order to get an equivalent value of that type; call the function". Those are *opposites*, and that's why a cast is so weird. – Eric Lippert Oct 03 '13 at 20:17
4

"The as operator is like a cast operation. However, if the conversion is not possible, as returns null instead of raising an exception."

You don't receive a compile time error with the as operator because the compiler does not check for undefined explicit casts when using the as operator; its purpose is to allow attempted run-time casts that may be valid or not, and if they are not, return null rather than throw an exception.

In any case, if you plan to handle the case where fruit is not Apple, you should implement your check as

var asApple = fruit as Appple;
if(asApple == null)
{
    //oh no
}
else
{
   //yippie!
}
Preston Guillot
  • 6,493
  • 3
  • 30
  • 40
  • What is the use of "is" then? I get that "as" allows potentially invalid casts at runtime, returning null in that case, but if checking for null is the reasonable way to handle this, what does "is" give you? Is it wrong to use "is" in the way that I have? – hatch22 Oct 03 '13 at 18:23
  • 2
    @hatch22 The use of `is` is designed for determining a type *in a situation where you don't actually need to cast the value to the derived type*. – Servy Oct 03 '13 at 18:24
  • @Servy: OK that makes sense. I still don't see why the compiler doesn't just allow the cast to be tried at runtime and throw an invalid cast exception, which would seem to be an appropriate response. Why label it as an error instead? – hatch22 Oct 03 '13 at 18:27
  • In this case it's happening because you're casting from a generic, even though the generic is constrained to `Fruit`. Apparently the compiler doesn't consider constraints on generics when casting, which is new to me. If you remove the generic from your method, the compiler won't complain, but the run time will. – Preston Guillot Oct 03 '13 at 18:30
  • 1
    @PrestonGuillot: The compiler does consider constraints when casting, just not in the way you think it should. See my answer for the reason why. – Eric Lippert Oct 03 '13 at 18:39
  • I sensed an incoming Lippert explanation when I realized what was really going on here :) – Preston Guillot Oct 03 '13 at 18:42
2

To answer the question why the compiler won't let you write your code like you want to. The if gets evaluated at runtime, so the compiler doesn't know that the cast only happens if it would be valid.

To get it to work you "could" do something like this in your if:

Apple apple = (Apple)(object)fruit;

Here's some more on the same question.

Of course using the as operator is the best solution.

Community
  • 1
  • 1
shriek
  • 5,157
  • 2
  • 36
  • 42
  • OK. It just seems like a strange design choice to disallow potentially valid casts at compile-time because they might be invalid, rather than waiting until run-time when you know whether the cast was valid or not. – hatch22 Oct 03 '13 at 18:41
0

It's explain in msdn doc

The as operator is like a cast operation. However, if the conversion isn't possible, as returns null instead of raising an exception. Consider the following example:

expression as type The code is equivalent to the following expression except that the expression variable is evaluated only one time.

expression is type ? (type)expression : (type)null Note that the as operator performs only reference conversions, nullable conversions, and boxing conversions. The as operator can't perform other conversions, such as user-defined conversions, which should instead be performed by using cast expressions.

tdelepine
  • 1,986
  • 1
  • 13
  • 19
  • What if I want to catch an InvalidCastException? Why does the compiler refuse to compile a potentially valid cast just because it might be potentially invalid? Why not wait until runtime and throw an InvalidCastException? – hatch22 Oct 03 '13 at 18:24
  • the herited class Apple contains more attributes or functions that the base class Fruit. the TestFruit function imposes that fruit parameters is type of Fruit class and dosen't contain's attributs of Apple – tdelepine Oct 03 '13 at 18:34
  • I'm sorry, but I don't see what that has to do with not waiting until runtime to throw an InvalidCastException but instead marking the code as an invalid conversion, even though it could be valid. – hatch22 Oct 03 '13 at 18:37
  • C# is compiled language and support type checking, or code generation and optimization in contrast to runtime languages (exemple javascript) – tdelepine Oct 03 '13 at 19:03
0

It is possible for a variable of a base class type to hold a derived type. To access the derived type's method, it is necessary to cast the value back to the derived type. Using as will prevent you from getting an InvalidCastException. If you want to handle specific null reference scenario's you can do this.

public class Fruit
{
    public static explicit operator bool(Fruit D)
    {
         // handle null references
         return D.ToBoolean();
    }

    protected virtual bool ToBoolean()
    {
         return false;
    }
}
Ronaldus
  • 29
  • 2
  • 7
  • But I can't get an InvalidCastException in this case anyway, because the compiler refuses to compile it ("Cannot convert..."). – hatch22 Oct 03 '13 at 18:31
-1

The AS Operator Keyword inherits its operation from Visual Basic.

Those in the know will tell you that Visual Basic is a more capable language than C#, which itself is an ongoing attempt to make a C-like syntax language but with Visual Basic's functionality

This is because C syntax languages are more popular with professionals, as are languages that don't advertise themselves as being Basic.

WonderWorker
  • 8,539
  • 4
  • 63
  • 74