An "is" operator puzzle, part two

An "is" operator puzzle, part two

Rate This
  • Comments 13

As I said last time, that was a pretty easy puzzle: either FooBar, or the type of local variable x, can be a type parameter. That is:

void M<FooBar>()
{
  int x = 0;
  bool b = x is FooBar;  // legal, true if FooBar is int.
  FooBar fb = (FooBar)x; // illegal
}

or

struct FooBar { /* ... */ }
void M<X>()
{
  X x = default(X);
  bool b = x is FooBar; // legal, true if X is FooBar
  FooBar fb = (FooBar)x; // illegal
}

This not only illustrates an interesting fact about "is" -- that an "is" expression can result in true even if the corresponding cast would be illegal -- but also an interesting fact about casts. Generally speaking, a cast is allowed if the conversion either is know at compile time to always succeed, or possibly succeed. But in these cases we have a situation where the cast could possibly succeed but is still illegal. What's up with that? There are two main factors that come to mind, based on the dual nature of casts that I've mentioned before: a cast can mean "I know that this value is of the given type, even though the compiler does not know that, the compiler should allow it", and a cast can mean "I know that this value is not of the given type; generate special-purpose, type-specific code to convert a value of one type to a value of a different type."

But neither of these things are logical when type parameters are involved. In the context of the first meaning, a cast between a type parameter and a regular type essentially means "I know that the type parameter supplied is of the given type." But in that case, why do you have a type parameter in the first place? It's like having an integer formal parameter and then asserting that it is always twelve. Why did you have the parameter at all if you know ahead of time what the argument will be?

And in the context of the second meaning, in generic code we have no way to generate the special-purpose conversion logic. Let's take our first example code, above. If FooBar is double then we have to generate different code for int-to-double than if FooBar is long. We don't have a cheap and easy way to generate that code, and if user-defined conversions are involved then we need to do overload resolution at runtime. We added that feature to C# 4; if you want to generate fresh code at runtime that can do arbitrary conversions, use dynamic.

Next time: we'll explore the question "under what circumstances will the "is" operator give a warning at compile time stating that the "is" is unnecessary?"

  • But if you down cast it, it is legal. I do it all the time.  Maybe I shouldn't?

    FooBar fb = (FooBar)(object)x; // legal

  • Eric, You are obviously not refering to:

    var x = GetSomeValue();

    if( x is MyType )

    {

      DoSomething( x as MyType);

    }

    Which should be replaced with:

    var x = GetSomeValue();

    MyType y = x as MyType;

    if( y != null )

    {

      DoSomething( y );

    }

  • Oh, that is about CS0183 "The given expression is always of the provided type", but there is also CS0184 "The given expression is never of the provided type", which is shown when testing whether an expression of type X "is" of type Y, where Y does not derive from X and (if Y is an interface type) cannot be implemented by a class that does derive from X (because X is sealed). But for that one, I'm sure I've missed some subtleties.

  • Kabwla: Your code doesn't work when MyType is a value type.

  • Don't know where my comment to the last post got eaten, but your statement here "an `is` expression can result in true even if the corresponding cast would be illegal" contradicts the documentation "An is expression evaluates to true if the provided expression is non-null, and the provided object can be cast to the provided type without causing an exception to be thrown."  msdn.microsoft.com/.../scekt9xw.aspx

    Apparently it's a documentation bug.

  • I see no contradiction. Whether a cast is a legal C# expression and whether performing a cast would cause an exception are two very different questions.

    If object X, which exists when the program is run, may be cast to type T without causing an exception in the running program, it does not follow that the compiler must accept any particular piece of code where an identifier that will reference object X at run time is used in a cast expression to a type that will be T at run time.

  • Completely off topic- Eric will you be making an appearance at the BUILD event in October? It would be awesome if you make an appearance at one of the events.

  • Gabe: true.

    Yet, I seldom produce code where casting needed, casting Value-Types is even more rare.

    I prefer to use generics.

    Probably I'm just lucky with the kinds of projects I am working on and/or what libraries I have to integrate with.

  • @Ben, if I were a lawyer I would argue that something being compile-time illegal does not contradict a statement about something not producing a runtime exception.

  • There are a few other fun corner cases where `is` behaves strangely.

       var a=new int[0];

       a is uint[] => false

       (object)a is uint[] => true

    The underlying issue is that the CLR allows this conversion, but C# doesn't.

  • @Anthony: I'd say "can be cast without causing an exception to be thrown" is two requirements. 1) "Can be cast" -- the cast must be accepted by the compiler. 2) "without causing an exception to be thrown" -- the cast must be accepted by the .NET runtime.

  • I went a little simpler & just derived FooBar2 from FooBar :).

  • Interesting topic Eric. You can get it to work if you use 'as' instead, but you also need the change the struct to a class. This is certainly bad coding for standards, but to show a point, this works:

    class FooBar { /* ... */ }

    void M<X>()

    {

     X x = default(X);

     bool b = x is FooBar; // legal, true if X is FooBar

     FooBar fb = x as FooBar; // legal, may return null

    }

Page 1 of 1 (13 items)