C# Dynamic vs Var: Type Safety and Runtime Behavior
C# dynamic vs var: Understand the real differences between compile-time type inference and runtime binding, including performance costs, error handling, and when to choose each.
The main difference between C# dynamic and var is when type information is used. With var, the compiler infers the type from the initializer and fixes it at compile time. With dynamic, the static type of the variable is dynamic, and the compiler defers member lookup and overload resolution until runtime. That difference drives everything else: type safety, performance, API design, and error handling.
var x = 10; // x is int, fixed at compile time x = "hello"; // compile error: cannot implicitly convert string to int dynamic d = 10; // d's static type is dynamic; d's runtime type is int d = "hello"; // allowed; binding happens at runtime
In the first line, var produces an int variable. Attempting to assign a string later is a compile-time error. In contrast, dynamic does not lock in a more specific compile-time type; its static type is dynamic, and operations are bound at runtime. The assignment d = "hello" is valid because binding happens at runtime, and the assignment succeeds if the runtime type supports it.
Type Safety: Compile-Time vs Runtime
The key tradeoff is where type errors surface. var gives you compile-time type safety because the compiler infers the exact type from the initializer. Any invalid operation is caught when the code is compiled. dynamic defers all binding to runtime, so errors like calling a method that does not exist are only caught when that line executes.
Consider a method that returns an object:
object obj = GetData(); // obj is object; members of the runtime type are not directly accessible without casting. // (string)obj would throw if obj isn't a string. var v = GetData(); // v is statically typed as whatever GetData returns // If GetData returns an int, v is int. dynamic dyn = GetData(); // dyn is dynamic; binding happens at runtime // If GetData returns an int, dyn.Length would throw at runtime because int has no Length property.
With var, the compiler knows the declared type, so IntelliSense and refactoring tools work as expected. With dynamic, you lose IntelliSense for member access because the compiler doesn't know the type until execution.
The Hidden Cost: Dynamic Runtime Binding
Using dynamic has a performance overhead. The runtime must generate a call site, determine the actual type of the object, and then perform the operation. For single calls, the overhead is small, but in loops or frequently called code, it adds up. Reflection can be even slower, but dynamic uses the DLR (Dynamic Language Runtime), which caches call sites, making repeated calls faster than raw reflection.
List<Person> people = GetPeople(); dynamic sum = 0; foreach (var p in people) { sum += p.Age; // dynamic binding adds overhead }
If Person.Age is an int, the addition is still performed at runtime, and the DLR must infer the operator's behavior. A straightforward int loop would be faster. However, the actual difference depends on the operation's complexity and the number of iterations.
When to Use var
var is designed for readability when the type is obvious from the initializer. Common cases include:
- When the right side is a constructor call:
var list = new List<int>(); - When the initializer is an anonymous type:
var item = new { Name = "Alice", Age = 30 }; - When the method name clearly indicates the return type:
var file = File.Open("temp.txt", FileMode.Create);
The principle is to use var when it improves clarity without hiding the type. If the type isn't evident from the initializer, a developer may need to hover over the variable to understand it. For instance, var result = GetEmployeeDetails(); is less helpful if GetEmployeeDetails returns an obscure type.
When to Use dynamic
dynamic is needed for scenarios where compile-time types are insufficient:
- Interacting with COM objects like Office interop, where the APIs were written for dynamic languages.
- Calling methods on objects from dynamic languages (e.g., IronPython) where the shape isn't known until runtime.
- Working with JSON or other serialized data where the schema is dynamic.
- When reflection would be overly verbose; dynamic can simplify member access.
dynamic obj = GetJsonObject(); string name = obj.name; // accesses property 'name' at runtime
Without dynamic, you might need to use reflection or parse the JSON into a strongly typed object first.
Performance Implications and Caching
The DLR does cache call sites for simple operations. Once a call site is bound to a given type, subsequent calls with the same types are faster. However, if the runtime types vary frequently, the cache is ineffective, and each call incurs a binding overhead.
For example, a method that receives a dynamic parameter and is called many times with different runtime types will see erratic performance. If the types are consistent, the performance is acceptable for most scenarios but still slower than static dispatch. Use dynamic sparingly in hot paths.
Errors in Dynamic Code Are Runtime Exceptions
A common mistake is to assume that code that compiles is correct. With dynamic, many errors only appear when the method runs. A typo in a property name, a wrong argument type, or a missing method will result in a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException at runtime.
static void PrintName(dynamic person) { Console.WriteLine(person.FirstName); } var alice = new { FirstName = "Alice" }; // this is fine PrintName(alice); // works var bob = new { LastName = "Bob" }; // no FirstName property PrintName(bob); // throws RuntimeBinderException at runtime
Because PrintName is compiled without knowing the type of person, the call to FirstName is bound at runtime. Passing bob fails, but the failure occurs only when the method is invoked, not when the code is compiled.
Dynamic and Overload Resolution
Overload resolution with dynamic is deferred to runtime as well. If a method has overloads, the runtime selects the one that matches the actual types of the arguments. This can lead to surprising behavior if the runtime types don't match the intended overload.
void Process(int value) { } void Process(string value) { } void Call(dynamic argument) { Process(argument); } Call(1); // calls Process(int) Call("x"); // calls Process(string)
At compile time, Call is valid because the method call to Process is bound with dynamic. The runtime picks the correct overload based on the actual argument type. If no overload matches, a RuntimeBinderException occurs.
Compatibility: Dynamic and Language Features
Not all C# features work with dynamic as you might expect. Extension methods are resolved by the compiler at compile time, so the dynamic runtime binder does not consider them. A call such as d.SomeExtensionMethod() is compiled as a dynamic invocation; if no matching instance method exists on the runtime type, it throws a RuntimeBinderException.
dynamic d = GetValue(); d.SomeExtensionMethod(); // no compile-time error; throws RuntimeBinderException at runtime // unless the runtime type happens to have an instance method with that name. var list = new List<int>(); var hasAny = list.Any(); // fine, with using System.Linq; dynamic dynList = new List<int>(); var hasAny2 = dynList.Any(); // compiles, but throws RuntimeBinderException at runtime
To use an extension method with a dynamic value, cast the value to the underlying static type first. At that point, normal compile-time extension method resolution applies.
Decision Criteria: Choosing Between dynamic and var
Use var unless you have a concrete reason to use dynamic. The defaults are:
- Use
varfor local variable declarations where the type is obvious and you want to avoid repeating it. - Use
dynamiconly when the static type is unavailable or impossible to express at compile time.
Choose var if:
- You want compile-time type checking.
- You want better performance.
- You want IntelliSense support.
- The type is known from the initializer.
Choose dynamic if:
- You are interoperating with a dynamic language or COM object.
- You must handle inherently dynamic data (JSON with unknown structure).
- You want to simplify reflection-based code, accepting runtime binding and potential runtime errors.
Avoid dynamic in public APIs when a strongly typed alternative exists. An API that accepts dynamic pushes the burden of correctness onto the caller and makes the contract unclear.