object, and Why Not dynamic

Every type derives from object, boxing a value type allocates, and dynamic trades the compiler's checking for runtime failures.

Syntaxobject anything = 42; int back = (int)anything; dynamic loose = 42;

C# has one type at the top of everything. Every type, built in or written by you, value type or reference type, derives from System.Object, whose keyword shorthand is object. The two spellings mean the same type. That is why a variable of type object can hold anything, and why every value has ToString, Equals and GetHashCode available on it.

Boxing

A reference type variable already holds an address, so storing one in an object variable just copies that address. A value type variable holds the data, and object has nowhere to put data. So the runtime copies the value into a small object on the heap, the pool of memory the garbage collector manages, and the object variable holds the address of that. This is boxing, and it costs one allocation every time. Getting the value back out is unboxing, and needs a cast that names the exact type that went in. Generics exist largely so that collections of numbers need neither.

dynamic

dynamic looks like a type but is an instruction to the compiler: stop checking. Member names, argument counts and operators on a dynamic value are resolved when the line runs. A typo compiles and throws a RuntimeBinderException in production instead. It exists for COM interop and for genuinely shapeless data, and almost nothing else needs it.

What trips people up

object keeps the compiler on your side and costs you a cast. dynamic removes the cast and the compiler with it. Reach for a real type, or a generic type parameter, before either.

Example

Example · csharp
using Microsoft.CSharp.RuntimeBinder;

// Every type in C# ends up at object, so an object variable can hold anything.
object number = 42;      // boxing: the int is copied into an object on the heap
object text = "hello";
Console.WriteLine(number.GetType().Name);

int unboxed = (int)number;                              // unboxing needs a cast
Console.WriteLine(unboxed + 1);
Console.WriteLine(((string)text).ToUpperInvariant());   // object offers almost nothing

// dynamic skips compile-time checking and resolves members when the line runs.
dynamic value = "hello";
Console.WriteLine(value.Length);
try
{
    Console.WriteLine(value.Lenght);   // a typo the compiler no longer catches
}
catch (RuntimeBinderException)
{
    Console.WriteLine("RuntimeBinderException at run time");
}

// Output:
// Int32
// 43
// HELLO
// 5
// RuntimeBinderException at run time

When to use it

  • Explain why a method taking object allocates when you pass it a number in a hot loop.
  • Fix an InvalidCastException from unboxing by casting to the type that was boxed first.
  • Replace an object-based container or parameter with a generic type parameter.
  • Decide against dynamic for JSON and reach for a typed model instead.

More examples

Each box is a separate copy

Boxing copies, so a box is a snapshot rather than a window onto the variable. The cast at the end compiles without complaint and fails at run time, because unboxing checks for the exact type and int is not long. The fix is to unbox to int and then widen: (long)(int)boxed. This is the single most common boxing bug, and it only ever appears in production data.

Example · csharp
// Boxing happens every time a value type is stored in an object.
int number = 7;
object boxed = number;        // one allocation on the heap
object boxedAgain = number;   // a second, separate allocation

// Each box holds its own copy, so changing the int afterwards changes neither.
number = 99;
Console.WriteLine($"{number} {boxed} {boxedAgain}");

// Unboxing must name the exact type that was boxed.
Console.WriteLine((int)boxed + 1);

try
{
    long wrong = (long)boxed;   // the box holds an int, not a long
    Console.WriteLine(wrong);
}
catch (InvalidCastException)
{
    Console.WriteLine("InvalidCastException: the box holds an int");
}

// Output:
// 99 7 7
// 8
// InvalidCastException: the box holds an int

A generic type instead of object

The two halves store the same number. The object version allocates a box and gives the compiler no way to stop you casting it to the wrong type. The List<int> version stores plain ints with no boxing, and numbers.Add("six") would not compile at all. Whenever you find yourself writing object to mean "some type I will decide later", a generic type parameter is almost always what you actually want.

Example · csharp
// Before generics, a general-purpose container stored object. Every int that
// went in was boxed, and every value that came out needed a cast.
object held = 5;
int backOut = (int)held;
Console.WriteLine(backOut);

// A generic type keeps the real type, so there is no boxing and no cast.
// List<T> comes in a later stage; this is the problem it was built to solve.
List<int> numbers = new List<int>();
numbers.Add(5);
numbers.Add(6);
Console.WriteLine(numbers[0] + numbers[1]);
Console.WriteLine(numbers.Count);

// Output:
// 5
// 11
// 2

What dynamic actually does

One line of source, two completely different operations, decided by whatever the variable happened to hold. That flexibility is occasionally exactly what you need and is usually a bug waiting to happen, because 105 is not an error anyone will notice. Note also that dynamic is a compile-time instruction only: at run time the variable is an ordinary object, which is why GetType reports Int32 for the object version.

Example · csharp
// dynamic is for talking to things the compiler cannot see: COM objects,
// scripting hosts, or data whose shape is only known at run time.
dynamic looselyTyped = 10;
Console.WriteLine(looselyTyped + 5);   // whole-number addition, chosen at run time

looselyTyped = "10";
Console.WriteLine(looselyTyped + 5);   // string joining, from the very same line

// object would have refused both of those while building, which is the point.
object strictlyTyped = 10;
Console.WriteLine(strictlyTyped.GetType().Name);

// Output:
// 15
// 105
// Int32

Discussion

  • Be the first to comment on this lesson.