Udostępnij za pośrednictwem


An "is" operator puzzle, part two

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?"

Comments

  • Anonymous
    August 27, 2012
    The comment has been removed

  • Anonymous
    August 27, 2012
    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 ); }

  • Anonymous
    August 27, 2012
    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.

  • Anonymous
    August 28, 2012
    Kabwla: Your code doesn't work when MyType is a value type.

  • Anonymous
    August 28, 2012
    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.

  • Anonymous
    August 28, 2012
    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.

  • Anonymous
    August 29, 2012
    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.

  • Anonymous
    August 29, 2012
    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.

  • Anonymous
    August 30, 2012
    @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.

  • Anonymous
    September 01, 2012
    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.

  • Anonymous
    September 02, 2012
    @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.

  • Anonymous
    September 03, 2012
    The comment has been removed

  • Anonymous
    September 10, 2012
    The comment has been removed