int
in C# represents the same thing as int32
in CIL, which is a 4-byte primitive generally treated as a signed number. (Though CIL can do unsigned operations on it without a cast).
It's one of the lowest-level building blocks from which we can go on to create more complicated structures and classes.
But as such, it doesn't have any methods defined on it.
System.Int32
meanwhile looks pretty much like a struct that wraps an int
/int32
and does provide some methods.
Let's consider it as that; let's think about what it would be like in a world without int
being aliased with System.Int32
:
In this hypothetical situation, we would only be allowed to use the methods System.Int32
provides if we treated it as a special "boxed int" type, creating a System.Int32
from an int
when we needed it, and extracting the int
back again when we needed that.
So, without aliasing to do (3).CompareTo(2)
we would have to do:
new System.Int32{m_value = 3}.CompareTo(2)
But consider that the in-memory representation of int
is 4 bytes and the in-memory representation of System.Int32
is the same 4 bytes. If we didn't have a strong type-system that barred considering one type as another type we could just treat one as the other whenever we wanted.
Now, C# does not allow us to do this. E.g. we can't do:
public struct MyInt32
{
private int _value;
}
/* … */
MyInt32 = 3;
We would need to add a cast method that would be called, or else C# will just refuse to work on it like this.
CIL though has no such rule. It can just treat one type as another layout-compatible type whenever it wants. So the IL for (3).CompareTo(2)
is:
ldc.i4.3 // Push the 32-bit integer 3 on the stack.
ldc.i4.2 // Push the 32-bit integer 2 on the stack.
call instance int32 [mscorlib]System.Int32::CompareTo(int32)
The call at the end just assumes that the 3
is a System.Int32
and calls it.
This breaks the rules of C# type-safety, but those rules are not CIL's rules. The C# compiler also doesn't have to follow all of the rules that it enforces.
So there's no need to put anything into m_value
, we just say "oh those four bytes there, they're the m_value
field of a System.Int32
", and so it is magically done. (If you know C or C++ consider what would happen if you had two structs with equivalent members and cast a pointer to one of those types to void*
and then back to a pointer of another. It's a bad practice and IIRC undefined rather than guaranteed, but the lower-level code is allowed to do those sort of things).
And that is how aliasing works; .Net languages' compilers special-case the cases where we need to call a method on a primitive to do this sort of type-coercion that C# code itself does not allow.
Likewise, it special cases the fact that a value-type cannot hold a field of its own type, and allows System.Int32
to have an int
field, though generally struct S { public S value; }
would not be allowed.