开发者

Why does this generics scenario cause a TypeLoadException?

开发者 https://www.devze.com 2023-03-06 07:14 出处:网络
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?)

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;
}

开发者_JAVA技巧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.


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.


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.


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.


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.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号