Sealed keyword affects the compiler's opinion

2019-02-05 21:01发布

问题:

I have a situation where I'd like the behaviour of the compiler explained. Given a little code:

interface IFoo<T>
{
    T Get();
}

class FooGetter : IFoo<int>
{
    public int Get()
    {
        return 42;
    }
}

The following compiles and runs:

static class FooGetterGetter
{
    public static IFoo<T> Get<T>()
    {
        return (IFoo<T>)new FooGetter();
    }
}

If we make a change to the signature of the Foo class and add the sealed keyword:

sealed class FooGetter : IFoo<int> // etc

Then I get a compiler error on the following line:

 return (IFoo<T>)new FooGetter();

Of:

Cannot convert type 'MyNamespace.FooGetter' to 'MyNamespace.IFoo<T>'

Can someone explain what is happening here with regards to the sealed keyword? This is C# 4 against a .NET 4 project in Visual Studio 2010.

Update: interestingly enough I stumbled on that part of the behaviour when I was wondering why the following code fixes it when sealed is applied:

return (IFoo<T>)(IFoo<int>)new FooGetter();

Update: just for clarification, it all runs fine when the type of T requested is the same as the type of T used by the concrete type. If the types differ, the cast fails at runtime with something like:

Unable to cast object of type 'MyNamespace.StringFoo' to type 'MyNamespace.IFoo`1[System.Int32]'

In the above example, StringFoo : IFoo<string> and the caller asks to get an int.

回答1:

Because FooGetter is an explicit implementation of IFoo<int> instead of implementing IFoo<T> generically. Since it is sealed, the compiler knows there's no way to cast it to a generic IFoo<T> if T is anything other than an int. If it were not sealed, the compiler would allow it to compile and throw an exception at runtime if T was not an int.

If you try to use it with anything other than an int (e.g. FooGetterGetter.Get<double>();) you get an exception:

Unable to cast object of type 'MyNamespace.FooGetter' to type 'MyNamespace.IFoo`1[System.Double]'.

What I'm not sure of is why the compiler does not generate an error for the non-sealed version. How could your sub-class FooGetter such that new FooGetter() give you anything that implements IFoo<{something_other_than_int}>?

Update:

Per Dan Bryant and Andras Zoltan there are methods to return a derived class from a constructor (or possibly more precisely for the compiler to return a different type by analyzing attributes). So technically this is feasible if the class is not sealed.



回答2:

When a class in unsealed any derived class could implement IFoo<T>:

class MyClass : FooGetter, IFoo<double> { }

When FooGetter is marked as sealed, the compiler knows that it cannot be possible for any additional implementations of IFoo<T> other than IFoo<int> could exist for FooGetter.

This is good behavior, it allows you to catch problems with your code at compile time instead of at runtime.

The reason that (IFoo<T>)(IFoo<int>)new FooGetter(); works is because you are now representing your sealed class as IFoo<int> which could be implemented by anything. It is also a nice work around as you are not accidentally, but purposefully overriding the compiler check.



回答3:

Just to add to the existing answers: This really has nothing to do with the generics used.

Consider this simpler example:

interface ISomething
{
}

class OtherThing
{
}

Then saying (inside a method):

OtherThing ot = XXX;
ISomething st = (ISomething)ot;

works just fine. The compiler does not know if an OtherThing might be an ISomething, so it believes us when we say it will succeed. However, if we change OtherThing to a sealed type (namely sealed class OtherThing { } or struct OtherThing { }), then the cast is no longer allowed. The compiler knows it can't go well (except if ot were to be null, but the rules of C# still disallow the cast from a sealed type to an interface not implemented by that sealed type).

Regarding the update of the question: Writing (IFoo<T>)(IFoo<int>)new FooGetter() is not much different than writing (IFoo<T>)(object)new FooGetter(). You can "make allowed" any cast (with generics or without) by going through some intermediate type that is certainly/possibly an ancestor to both of the types you want to convert between. It's very much similar to this pattern:

void MyMethod<T>(T t)    // no "where" constraints on T
{
  if (typeof(T) = typeof(GreatType))
  {
    var tConverted = (GreatType)(object)t;
    // ... use tConverted here
  }
  // ... other stuff
}