C# is considered a statically-typed language. However, it contains keywords such as:
var, which infers the type at compile time, and
dynamic, which determines the type at runtime.
Is this a contradiction?
C# has a static type system. So it is reasonable to describe it as a statically typed language. Just like any other OOP language, it also provides for some degree of dynamic typing. C# goes a lot further here than other statically typed languages by providing a
dynamic static type, but this does not break its static type system.
With dynamic typing, values or objects have types. E.g.
1 is an int, and the result of evaluating
new Foo() is an object of type
Foo. We can think of every object being “tagged” with a type.
With static typing, expressions in our code can be assigned types. For example,
x + 1 might be an int, and the variable
foo might be declared to only contain
Foo instances. These static types are guarantees about the values that the expression might evaluate to. This is useful because it might detect potential bugs, before running the code. Or viewed another way: a static type system allows for basic proofs of correctness. Static type systems also enable interesting features such as method overloading.
A lot of time, the static type of an expression is obvious. We don't have to manually annotate every expression with its static type. The compiler has rules to infer static types itself. When declaring a variable, we can instruct the compiler to infer its type with the
With statically typed OOP languages, we have both static and dynamic typing, due to the use of inheritance and interfaces. Now, the static types do not describe exactly what values are possible at runtime, but only constrain them. For example, we might define a variable
IFoo foo = .... This guarantees us statically that any object referred to by
foo will conform with the
IFoo interface, providing certain properties or methods. But we do not know which classes those objects might have exactly – we don't yet know their dynamic types.
C# has a unified inheritance hierarchy, with all objects being derived from the
System.Object class. If we declare a variable with static type
Object, then we can assign anything to it. However, we are only guaranteed that it will provide the methods that are part of the
Object class, such as
ToString(). If we want to call other methods that the object instance supports, we have to cast it to its actual type, which requires checking its dynamic type. We can do that via a cast. Casts let us write statically checked code dependent on the object's dynamic types, because casts can fail – the code can only be executed if the object really has the expected type.
A lot of code doesn't benefit that much from static type safety. As a shortcut for casting the object to the correct type before accessing a method or property, C# offers the
dynamic type. This pseudy-type behaves differently from other static types, since it just defers any type checks to runtime. However, this is still perfectly safe. This cannot be used to violate the static types in other parts of the code, because the compiler will insert the suitable checks at runtime.
For example, consider a function that converts a dynamic object to an integer:
static int IntFromDynamic(dynamic o) => o;
This function is compiled roughly as if it were the following:
static int IntFromDynamic(Object o) => (int)o;
Neither variant would allow us to break the static type system, for example by trying to launder a string into an int:
IntFromDynamic("foo") will fail at runtime.
Per the comment, "var" just means "infer the static type as if I'd written it here". It compiles as if you'd written a specific type there.
Dynamic is a little different. Let's see what it says in the documentation:
The dynamic type is a static type, but an object of type dynamic bypasses static type checking. In most cases, it functions like it has type object. At compile time, an element that is typed as dynamic is assumed to support any operation.
Look right there: "dynamic" is a static type!
That is, the precise type of the object is to be determined at runtime, but the fact that it's dynamic is recorded in the static type system.
You can even see what this gets turned into in MSIL. The method invocations against the dynamic object are done using reflection.
From a compiler/language theory perspective, you're confusing static/dynamic typing with implicit/explicit typing. Implicit typing is also called type inference.
In an explicitly typed language, the programmer has to explicitly declare the type of every variable. In an implicitly typed language, types can be inferred by the compiler. It is important to note that in principle implicit/explicit typing has nothing to do with static/dynamic typing. You can have the combinations: static/explicit, static/implicit, and dynamic/implicit.
The classic statically typed, explicitly typed language is C. Everything must be explicitly typed, and the compiler assumes nothing. Every variable definition looks something like:
typedef varname = r_value;
or for example:
char* x = "Hello, world";
int y = 42;
int z = 0; //Depends on which C standard you're using, but in general
//0 is false and any nonzero value is true
A statically typed, implicitly typed language is Rust. The philosophy of Rust is to empower programmers, so while the static type system is strict, it also doesn't require you to be overly-explicit. Statements like:
let x = "Hello, world";
let y = 42;
let z = false;
are perfectly clear to the programmer and compiler, so they are implicitly typed. You can write them explicitly if you prefer or need to:
let x: &str = "Hello, world";
let y: u16 = 42;
let z: bool = false;
x = "Hello, world";
x = 42;
x = false;
In a statically typed language, the type checker would error out on the second line, because once
x is assigned a string type, it cannot hold a numeric value.
Note that there are some other topics that crop up here that can conflate the issue. Rust is statically typed, but it does allow redefinition of variables, so the result is that you can write code that looks like dynamically typed code, even though it's entirely static. In Rust, this code fails to compile with a type error:
let x = "Hello, world!";
x = 42;
However, also in Rust, this code does compile:
let x = "Hello, world!";
let x = 42;
The difference is the
let keyword. This explicitly redeclares the variable
x and when this happens the new version of
x is allowed to have a different type from the old version. Both versions of
x are statically typed at compile time, so while this isn't great coding style, it's technologically no more complex than declaring a new variable with any other name and doesn't require any special support.
Ultimately there is no clear answer about what kind of system is best. The choice can dramatically limit the kind of code you can write, and especially impacts the job of the compiler/runtime implementer. In particular, dynamically typed means that types are checked at runtime, at the point of evaluating expressions. For example, the following Python code runs without errors:
x = "five"
if isinstance(x, int):
y = x + 1
This code runs just fine in Python, because the if statement is false and the following line never executes. (If it did execute, this program would throw a TypeError and stop running.) However, a statically typed language like Rust would choke and die at compile time on the third line at the type-checking stage, because
x is a string and you can't add 1 to a string, regardless of the fact that later on a reachability analysis could show that this code never executes.
This has a lot of other knock-on effects as well. If you've ever wondered how an interpreted language could literally be 100 times slower than C or C++, typing plays a huge role. In Python, every single operation like
f() + g() needs to run some code that verifies the types of the left and right operands and potentially does some dynamic typing as well. In C/C++/Rust, the static type checker already verified at compile time that f() and g() return the proper types, and could potentially implement
f() + g() in as little as a single assembly instruction.
C# is very much an odd exception in that it allows both static and dynamic typing in the same language. First, note that
var does not violate static typing- this is implicit typing. The type is decided by the compiler at compile time, and then that is enforced. However,
dynamic definitely does violate static typing rules. It is made clear that the default and standard mode is static typing, with
dynamic being used to add more flexibility in certain corner cases that are expected to be less common. Writing large parts of your program with
dynamic is probably a good sign that you're doing something against the intention of the language designer.