简体   繁体   中英

Why is “dynamic” not covariant and contravariant with respect to all types when used as a generic type parameter?

I am wondering if dynamic is semantically equivalent to object when used as a generic type parameter. If so, I am curious why this limitation exists since the two are different when assigning values to variables or formal parameters.

I've written a small experiment in C# 4.0 to tease apart some of the details. I defined some simple interfaces and implementations:

interface ICovariance<out T> { T Method(); }

interface IContravariance<in T> { void Method(T argument); }

class Covariance<T> : ICovariance<T>
{
    public T Method() { return default(T); }
}

class Contravariance<T> : IContravariance<T>
{
    public void Method(T argument) { }
}

The interesting details of the experiment:

class Variance
{
    static void Example()
    {
        ICovariance<object> c1 = new Covariance<string>();
        IContravariance<string> c2 = new Contravariance<object>();

        ICovariance<dynamic> c3 = new Covariance<string>();
        IContravariance<string> c4 = new Contravariance<dynamic>();

        ICovariance<object> c5 = new Covariance<dynamic>();
        IContravariance<dynamic> c6 = new Contravariance<object>();

        // The following statements do not compile.
        //ICovariance<string> c7 = new Covariance<dynamic>();
        //IContravariance<dynamic> c8 = new Contravariance<string>();

        // However, these do.
        string s = new Covariance<dynamic>().Method();
        new Contravariance<string>().Method((dynamic)s);       
    }
}

The first two statements with c1 and c2 demonstrate that basic covariance and contravariance are working. I then use c3 and c4 to show that dynamic can be used as a generic type parameter in the same fashion.

The statements with c5 and c6 reveal that a conversion from dynamic to object is always valid. This isn't really too surprising, since object is an ancestor of all other types.

The final experiment with c7 and c8 is where I start to become confused. It implies that methods that return dynamic objects are not substitutes for methods that return string ones, and similarly that methods that accept string objects cannot take dynamic ones. The final two statements with the assignment and method call show this is clearly not the case, hence my confusion.

I thought about this a little, and wondered if this is to prevent programmers from using ICovariance<dynamic> as a stepping stone between type conversions that would result in run-time errors, such as:

ICovariance<dynamic> c9 = new Covariance<Exception>();
ICovariance<string> c10 = c9;
// While this is definitely not allowed:
ICovariance<string> c11 = new Covariance<Exception>();

However, this is unconvincing in the case of dynamic since we lose type-safety anyway:

dynamic v1 = new Exception();
string  v2 = v1;

Put another way, the question is "Why does the semantics of dynamic differ between assignment and covariance/contravariance with generics?"

I am wondering if dynamic is semantically equivalent to object when used as a generic type parameter.

Your conjecture is completely correct.

"dynamic" as a type is nothing more than "object" with a funny hat on, a hat that says "rather than doing static type checking for this expression of type object, generate code that does the type checking at runtime". In all other respects, dynamic is just object, end of story.

I am curious why this limitation exists since the two are different when assigning values to variables or formal parameters.

Think about it from the compiler's perspective and then from the IL verifier's perspective.

When you're assigning a value to a variable, the compiler basically says "I need to generate code that does an implicit conversion from a value of such and such a type to the exact type of the variable". The compiler generates code that does that, and the IL verifier verifies its correctness.

That is, the compiler generates:

Frob x = (Frob)whatever;

But limits the conversions to implicit conversions, not explicit conversions.

When the value is dynamic, the compiler basically says "I need to generate code that interrogates this object at runtime, determines its type, starts up the compiler again, and spits out a small chunk of IL that converts whatever this object is to the type of this variable, runs that code, and assigns the result to this variable. And if any of that fails, throw."

That is, the compiler generates the moral equivalent of:

Frob x = MakeMeAConversionFunctionAtRuntime<Frob>((object)whatever);

The verifier doesn't even blink at that. The verifier sees a method that returns a Frob. That method might throw an exception if it is unable to turn "whatever" into a Frob; either way, nothing but a Frob ever gets written into x.

Now think about your covariance situation. From the CLR's perspective, there is no such thing as "dynamic". Everywhere that you have a type argument that is "dynamic", the compiler simply generates "object" as a type argument. "dynamic" is a C# language feature, not a Common Language Runtime feature. If covariance or contravariance on "object" isn't legal, then it isn't legal on "dynamic" either. There's no IL that the compiler can generate to make the CLR's type system work differently.

This then explains why it is that you observe that there is a conversion from, say, List<dynamic> to and from List<object> ; the compiler knows that they are the same type. The specification actually calls out that these two types have an identity conversion between them; they are identical types.

Does that all make sense? You seem very interested in the design principles that underly dynamic; rather than trying to deduce them from first principles and experiments yourself, you could save yourself the bother and read Chris Burrows' blog articles on the subject . He did most of the implementation and a fair amount of the design of the feature.

for this one:

ICovariance<string> c7 = new Covariance<dynamic>();

reason is obvious, if it was possible then you could do:

c7.Method().IndexOf(...);

and it will definitely fail, except if dynamic is not string or has those method.

since (even after all the changes) c# is not dynamic language. Covariance is allowed only when it is definitely safe. You can of course shot into your feet and call IndexOf on dynamic variable, but you can't let users of your API to do it unintentionally. For example if you return such a ICovariance<string> with dynamic undercover calling code might fail!

Remember the rule, D is covariant to B if there is a cast from D to B . In this case there is no cast from dynamic to string .

But dynamic is covariant to object just because everything is derived from it.

Because dynamic and covariant/contravariant keywords are so new?

I would guess that you kind of answered your own question. Assignment type-safety is relaxed in assignment statements, because that's how dynamic works; it short-circuits compile-time type-checking so you can make assignments you EXPECT to work from objects the compiler has no clue about.

However, generic covariance/contravariance is rigidly controlled; without the use of the in/out keywords (which were also introduced alongside dynamic in C# 4.0) you couldn't convert either way. The generic parameters, even with co/contravariance allowed, require the types to be in the same branch of the inheritance hierarchy. A String is not a dynamic and a dynamic is not a string (though both are Objects and a dynamic may refer to what could be accessed as a string), so the generic type-checking inherent in the covariance/contravariance checks fails, while OTOH, the compiler is expressly told to ignore most non-generic operations involving dynamic.

"Why does the semantics of dynamic differ between assignment and covariance/contravariance with generics?"

The answer is that when using generics you are abstracted from the data type itself. However, it also implies that generic is generic enough that all types will share the same functionality.

So if you have 'ICovariance c9 = new Covariance();` both dynamic and exception do not have the same functionalities (as base types). More over, the compiler doesn't have a clue as to how to convert from dynamic to exception (even though they both inherit from object).

If there was an explicit inheritance hierarchy between dynamic and Exception (other than object), than this would be somewhat ok.

The reason somewhat is because you can downcast, but not upcast. EG, if exception inherits from dynamic, than it would be fine. If dynamic inherits from Exception it would be an upcast kinda deal and that would not be ok, since there could be the condition where the 'dynamic 's data is not present in Exception`.

.NET has these explicit typecasts built in, and you can see them in action in the System.Convert object. However, types that are super specific cannot be easily implicitly or explicitly casted between one another without custom code. And this is one of the reasons why having multi-types fails (as is the case with 'ICovariance c9 = new Covariance();` ). This is also built to preserve type safety.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM