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.
There are many advantages to using generic collections and delegates: Type safety. Generics shift the burden of type safety from you to the compiler. There is no need to write code to test for the correct data type because it is enforced at compile time.
Constraints can specify interfaces, base classes, or require a generic type to be a reference, value, or unmanaged type. They declare capabilities that the type argument must have, and must be placed after any declared base class or implemented interfaces.
Generics allow you to define the specification of the data type of programming elements in a class or a method, until it is actually used in the program. In other words, generics allow you to write a class or method that can work with any data type.
Generic is a class which allows the user to define classes and methods with the placeholder. Generics were added to version 2.0 of the C# language. The basic idea behind using Generic is to allow type (Integer, String, … etc and user-defined types) to be a parameter to methods, classes, and interfaces.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With