At first glance, C# dynamic is just an object backed by compiler machinery. But not really.

The core of the runtime is DLR (Dynamic Language Runtime), a subsystem/framework for supporting dynamic programming languages. There is an implementation for C# itself, which comes with .NET, and a separate one for Iron languages.

When we work with generics, the CLR has its own optimizations for them. At the moment when CLR+DLR have to work with generics together, the behavior of the written code can become unpredictable.

Preamble

First you need to remember how generics are supported by the CLR. Each generic type has its own implementation, i.e. there is no type-erasure. But for reference types, the framework uses the System.__Canon type for code sharing. This is necessary for resolving cyclic dependencies between types.

I already wrote about this:

The fact is that generic types can contain cyclic dependencies on other types, which is prone to an endless creation of code specializations. For example:

class GenericClassOne<T>
{
    private T field;
}

class GenericClassTwo<U>
{
    private GenericClassThree<GenericClassOne<U>> field
}

class GenericClassThree<S>
{
    private GenericClassTwo<GenericClassOne<S>> field
}
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine((new GenericClassTwo<object>()).ToString());
        Console.Read();
    }
}

However, this code will not fail and will output GenericClassTwo`1[System.Object].

Type loader aka type loader) scans each generic type for a cyclic dependency and assigns an order (so-called LoadLevel for the class). Although all specializations for ref-types have System.__Canon as a type argument, this is a consequence, not a cause.

Load phases (aka ClassLoadLevel):

enum ClassLoadLevel
{
    CLASS_LOAD_BEGIN,
    CLASS_LOAD_UNRESTOREDTYPEKEY,
    CLASS_LOAD_UNRESTORED,  
    CLASS_LOAD_APPROXPARENTS,
    CLASS_LOAD_EXACTPARENTS,
    CLASS_DEPENDENCIES_LOADED,
    CLASS_LOADED,
    CLASS_LOAD_LEVEL_FINAL = CLASS_LOADED,
};

Infinite loop

Since such a feature exists for generalizations, accordingly, other subsystems must also follow this rule. But DLR is an exception.

Consider the class hierarchy:

NB: this is a real code from structuremap project, although it has undergone changes by this point. The example was used during my talk “Effective use of DLR”.

public class LambdaInstance<T> : LambdaInstance<T, T>
{
}

public class LambdaInstance<T, TPluginType> 
    : ExpressedInstance<LambdaInstance<T, TPluginType>, T, TPluginType>
{
}

public abstract class ExpressedInstance<T>
{
}

public abstract class ExpressedInstance<T, TReturned, TPluginType> : ExpressedInstance<T>
{
}

And the code itself:

class Program
{
    static LambdaInstance<object> ShouldThrowException(object argument)
    {
        throw new NotImplementedException();
    }

    static void Main(string[] args)
    {
        // будет ли брошено исключение?
        ShouldThrowException((dynamic)new object());
    }
}

Question: will an exception be thrown?

Answer: no. ShouldThrowException method will never complete. And stackoverflow will not happen.

Question: Why though? Answer: It’s simple: LambdaInstance<object>. Consider the class hierarchy again.

LambdaInstance<T> inherits from LambdaInstance<T, TPluginType>, which in turn is inherited from ExpressedInstance<LambdaInstance<T, TPluginType>, T, TPluginType>.

Did you notice the nested inheritance?

As discussed above, the CLR has optimizations for circular type dependencies.

For the expression ShouldThrowException((dynamic)new object()); must inspect the code section/method signature. During that process, LambdaInstance<object> is encountered the code turns into an endless loop.

Question: Why doesn’t it crash? Answer: The DLR does not use recursion. Moreover, memory consumption is growing (because additional metadata is being created), but not much.

Epilog

It may seem that dynamic, as such, is a dangerous thing. Next time we will look at an example where its use is correct.