19

This got a bit long-winded, so here's the quick version:

Why does this cause a runtime TypeLoadException? (And should the compiler prevent me from doing it?)

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class D : C<System.Object>, I { } 

The exception occurs if you try to instantiate D.


Longer, more exploratory version:

Consider:

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class some_other_class { }

class D : C<some_other_class>, I { } // compiler error CS0425

This is illegal because the type constraints on C.Foo() don't match those on I.Foo(). It generates compiler error CS0425.

But I thought I might be able to break the rule:

class D : C<System.Object>, I { } // yep, it compiles

By using Object as the constraint on T2, I'm negating that constraint. I can safely pass any type to D.Foo<T>(), because everything derives from Object.

Even so, I still expected to get a compiler error. In a C# language sense, it violates the rule that "the constraints on C.Foo() must match the constraints on I.Foo()", and I thought the compiler would be a stickler for the rules. But it does compile. It seems the compiler sees what I'm doing, comprehends that it's safe, and turns a blind eye.

I thought I'd gotten away with it, but the runtime says not so fast. If I try to create an instance of D, I get a TypeLoadException: "Method 'C`1.Foo' on type 'D' tried to implicitly implement an interface method with weaker type parameter constraints."

But isn't that error technically wrong? Doesn't using Object for C<T1> negate the constraint on C.Foo(), thereby making it equivalent to - NOT stronger than - I.Foo()? The compiler seems to agree, but the runtime doesn't.

To prove my point, I simplified it by taking D out of the equation:

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class some_other_class { }

class C : I<some_other_class> // compiler error CS0425
{
    public void Foo<T>() { }
}

But:

class C : I<Object> // compiles
{
    public void Foo<T>() { }
}

This compiles and runs perfectly for any type passed to Foo<T>().

Why? Is there a bug in the runtime, or (more likely) is there a reason for this exception that I'm not seeing - in which case shouldn't the compiler have stopped me?

Interestingly, if the scenario is reversed by moving the constraint from the class to the interface...

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C
{
    public void Foo<T>() { }
}

class some_other_class { }

class D : C, I<some_other_class> { } // compiler error CS0425, as expected

And again I negate the constraint:

class D : C, I<System.Object> { } // compiles

This time it runs fine!

D d := new D();
d.Foo<Int32>();
d.Foo<String>();
d.Foo<Enum>();
d.Foo<IAppDomainSetup>();
d.Foo<InvalidCastException>();

Anything goes, and that makes perfect sense to me. (Same with or without D in the equation)

So why does the first way break?

Addendum:

I forgot to add that there is a simple workaround for the TypeLoadException:

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class D : C<Object>, I 
{
    void I.Foo<T>() 
    {
        Foo<T>();
    }
}

Explicitly implementing I.Foo() is fine. Only the implicit implementation causes the TypeLoadException. Now I can do this:

        I d = new D();
        d.Foo<any_type_i_like>();

But it's still a special case. Try using anything else other than System.Object, and this won't compile. I feel a bit dirty doing this because I'm not sure if it intentionally works this way.

Igby Largeman
  • 16,495
  • 3
  • 60
  • 86
  • Not all types really inherit from `Object`. All heap instances are of types derived from `Object`, but a value-type storage location just holds the fields (public and private) of that type, without any attached type information. Such a collection of fields is implicitly convertable to an `Object`, but isn't one. Note that constraining something to a generic type parameter of type `Object` effectively adds a `class` constraint. Note further a generic parameter with both an interface constraint and a `class` constraint will accept `struct` implementations of that interface, if they are... – supercat Jun 13 '12 at 16:57
  • ...converted to heap objects before they are passed. – supercat Jun 13 '12 at 16:57

4 Answers4

4

It's a bug - see Implementing Generic Method From Generic Interface Causes TypeLoadException and Unverifiable Code with Generic Interface and Generic Method with Type Parameter Constraint. It's not clear to me whether it's a C# bug or a CLR bug, though.

[Added by OP:]

Here's what Microsoft says in the second thread you linked to (my emphasis):

There is a mismatch between the algorithms used by the runtime and the C# compiler to determine if one set of constraints is as strong as another set. This mismatch results in the C# compiler accepting some constructs that the runtime rejects and the result is the TypeLoadException you see. We are investigating to determine if this code is a manifestation of that problem. Regardless, it is certainly not "By Design" that the compiler accepts code like this that results in a runtime exception.

Regards,

Ed Maurer C# Compiler Development Lead

From the part I bolded, I think he's saying this is a compiler bug. That was back in 2007. I guess it's not serious enough to be a priority for them to fix it.

Igby Largeman
  • 16,495
  • 3
  • 60
  • 86
kvb
  • 54,864
  • 2
  • 91
  • 133
  • Thanks. I hope you don't mind, I edited your answer to include what Microsoft said about the bug, which pretty well answers my question. I'll accept this answer unless someone like Eric Lippert happens to weigh in with more details soon. – Igby Largeman May 23 '11 at 03:15
  • @IgbyLargeman: It sure looks to me like a C# compiler bug. I see no reason why a method `Foo() where T:U` should be considered an implementation of `Foo()`. I think the difficulty stems in part from C# not examining constraints when matching method signatures, though even if C# matches the signatures it should notice the incompatible constraints. – supercat Jun 13 '12 at 16:45
3

The only explanation is that the constraint is considered as being part of the method declaration. That is why in the first case it is a compiler error.

The compiler not getting the error when you use object... well, that is a bug of the compiler.

Other "constraints" have the same properties of the generic contraint:

interface I
{
    object M();
}

class C
{
    public some_type M() { return null; }
}

class D : C, I
{
}

I could ask: why this does not work?

You see? This is quite the same question as yours. It is perfectly valid to implement object with some_type, but neither the run-time, nor the compiler will accept it.

If you try to generate MSIL code, and force the implementation of my example, the run-time will complain.

Miguel Angelo
  • 23,796
  • 16
  • 59
  • 82
  • I see your point. What if the runtime didn't complain? Would it work? In my second example where the constraint is on I.Foo, the runtime seems to be happy. I'm still confused about that. – Igby Largeman May 16 '11 at 18:21
  • Hi! If the runtime didn't complain, your 1st example would work. But the fact is that it complains. There is no intelligible answer to your question... I think that both examples should work, but the way that runtime checks the constraints makes it not work. It is a "rule" of the runtime to checks constrains in this way, because it has been coded this way. Maybe in a future version they make the check so that when `object` is used, it works like there were no constraints at all. – Miguel Angelo May 21 '11 at 16:47
3

Implicit interface implementation has a requirement that the generic constraints on the method declarations be equivalent, but not necessarily exactly the same in code. Additionally, generic type parameters have an implicit constraint of "where T : object". That is why specifying C<Object> compiles, it causes the constraint to become equivalent to the implicit constraint in the interface. (Section 13.4.3 of the C# Language Spec).

You're also correct that using an explicit interface implementation that calls into your constrained method will work. It provides a very clear mapping from the interface method to your implementation in the class where the constraints cannot differ, and then proceeds to call a similarly-named generic method (one that now has nothing to do with the interface). At that point, constraints on the secondary method can be resolved in the same way as any generic method call without any interface resolution issues.

Moving the constraints from the class to the interface, in your second example, is better because the class will take its constraints from the interface by default. This also means that you must specify the constraints in your class implementation, if applicable (and in the case of Object it is not applicable). Passing I<string> means that you can't directly specify that constraint in code (because string is sealed) and so it must either be part of an explicit interface implementation or a generic type that will be equal to the constraints in both places.

As far as I know, the runtime and the compiler use separate verification systems for constraints. The compiler allows this case but the runtime verifier doesn't like it. I want to stress that I don't know for sure why it has a problem with this, but I would guess that it doesn't like the potential in that class definition to not fulfill the interface constraints depending on what T ends up being set to. If anyone else has a definitive answer on this, that would be great.

Chris Hannon
  • 4,134
  • 1
  • 21
  • 26
  • The use of string in my examples was a bad choice because it's sealed - thanks for pointing that out (fixed now). However in the end it makes no difference since I'm deliberately not matching the constraints, so nothing other than Object will compile. Helpful answer though, thank you. I looked at the part of the spec you referred to - hurts my head a bit. – Igby Largeman May 20 '11 at 16:47
0

In response to your interface based snippet:

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C : I<string> // compiler error CS0425
{
    public void Foo<T>() { }
}

I believe the problem is that the compiler is recognizing that:

  1. you haven't declared the necessary type constraints on C.Foo().
  2. if you choose string as your type there is no valid T on C.Foo() since a type cannot inherit from string.

To see this work in practice specify an actual class that could be inherited from as T1.

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C : I<MyClass>
{
    public void Foo<T>() where T : MyClass { }
}

public class MyClass
{
}

To show that the string type is not being treated special in any way just add the sealed keyword to the MyClass declaration above to see it fail in the same way if you were to specify T1 as string along with string as the type constraint on C.Foo().

public sealed class MyClass
{
}

This is because string is sealed and cannot form the basis of a constraint.

jpierson
  • 16,435
  • 14
  • 105
  • 149
  • String was the first type off the top of my head - a bad example. Thanks for pointing it out, I'll edit the question to avoid confusion. As for your other point - yes, you can make it work by adding the proper contraints, but that would deafeat the point of this question. – Igby Largeman May 19 '11 at 21:17
  • Why would specifying constraints defeat the point of the question. It happens to be a pretty straight forward rule set by the compiler that overridden methods or methods that implement an interface must restate and satisfy the constraints of the method signature of the base method. You wouldn't run into the same problem if you wanted to write a new method Foo2 or the like since it doesn't have the restriction you are running into but the point is that the method must obey it's base otherwise you make rules necessary for polymorphism impossible to enforce. – jpierson May 20 '11 at 03:01
  • The first thing I do is demonstrate the expected-and-understood compiler error CS0425 in order to set the backdrop for the rest of the question. The point is that I can circumvent the requirement of matching constraints when I use Object as the type parameter being used as a constraint, but this causes a runtime error. If I specified matching constraints I would have no question to ask. :) (My question is obviously too long and conversational instead of being clear and concise, and I apologize for that) – Igby Largeman May 20 '11 at 16:59
  • I think it's a good topic, I guess I just wasn't sure that you understood what the compiler was actually complaining about. Now I realize that the issue is more about a few special edge cases and why the compiler waits until run-time to enforce what seems to be more of a compile time constraint. Good luck finding your answer. – jpierson May 21 '11 at 02:42