1. System.Object
The runtime requires every type to ultimately be derived from the
System.Object
type.
Because all types are ultimately derived from System.Object, you are guaranteed that every object of every type has a minimum set of methods. Specifically, the System.Object class offers the
public
instance methods listed in below.
Equals
GetHashCode
ToString
GetType
In addition, types that derive from System.Object have access to the
protected
methods listed in below.
MemberwiseClone
Finalize
The CLR requires all objects to be created using the new operator.
2. new
Here’s what the
new
operator does:
1. It
calculates the number of bytes required
by all instance fields defined in the type and all of its base types up to and including System.Object (which defines no instance fields of its own). Every object on the heap requires some additional members—called
the type object pointer and the sync block index—used by the CLR to manage the object. The bytes for these additional members are added to the size of the object.
2. It allocates memory for the object by allocating the number of bytes required for the specified type from
the managed heap; all of these bytes are then set to
zero
(0).
3. It initializes the object’s type object pointer and sync block index members.
4. The type’s instance constructor is called, passing it any arguments (the string "ConstructorParam1" in the preceding example) specified in the call to new. Most compilers automatically emit code in a constructor to call a base class’s constructor. Each constructor is responsible for initializing the instance fields defined by the type whose constructor is being called. Eventually, System.Object’s constructor is called, and this constructor method does nothing but return. You can verify this by using ILDasm.exe to load MSCorLib.dll and examine System.Object’s constructor method.
3. Casting Between Types
One of the most important features of the CLR is type
safety. At runtime, the CLR always knows what type an object is. You can always discover an object’s exact type by calling the
GetType
method. Because this method is nonvirtual, it is impossible for a type to spoof another type.
The CLR allows you to cast an object to its type or to any of its base types.
C# doesn’t require any special syntax to cast an object to any of its base types, because casts to base types are considered safe implicit conversions. However, C# does require the developer to
explicitly cast an object to any of its derived types
since such a cast could fail at runtime.
System.InvalidCastException
4. Castring with the C#
as
and
is
Operators
The
is
operator
checks whether an object is
compatible
with a given type, and the
result
of the evaluation
is a Boolean: true or false. The is operator will
never throw an exception.
The is operator is typically used as follows:
if (o is Employee) {
Employee e = (Employee) o;
// Use e within the remainder of the 'if' statement.
}
The CLR’s type checking
improves security, but it certainly
comes at a performance cost, because the CLR must determine the actual type of the object referred to by the variable (o), and then the CLR must walk the inheritance hierarchy, checking each base type against the specified type (Employee). Because this programming paradigm is quite common, C# offers a way to simplify this code and improve its performance by providing an
as
operator:
Employee e = o as Employee;
if (e != null) {
// Use e within the 'if' statement.
}
Notice that the as operator causes the CLR to verify an object’s type just once. The
as operator
works just as
casting
does except that the as operator will
never throw an exception. Instead, if the object can’t be cast, the result is
null.
5. namespaces and assemblies
using namespace;
Be aware that a namespace and an assembly (the file that implements a type) aren’t necessarily related. In particular, the various types belonging to a single namespace might be implemented in multiple assemblies.
6. How things relate at Runtime
Stack space
is used for passing arguments to a method and for local variables defined within a method.
All but the simplest of methods contain some
prologue code, which initializes a method before it can start doing its work. These methods also contain
epilogue code, which cleans up a method after it has performed its work so that it can return to its caller.
When a method is called, the arguments will be pushed on the stack firstly, then the address indicating where the called method should return to in the calling method is pushed on the stack.
The Progress:
(1) As the just-in-time (JIT) compiler converts method's Intermediate Language (IL) code into native CPU instructions, it notices all of the types that are referred to inside the method. At this time,
the CLR ensures that the assemblies that define these types are loaded.
Then, using the assembly’s metadata, the CLR extracts information about these types and creates some data structures to represent the
types themselves.
(2) All objects on the heap contain two overhead members:
the type object pointer and the sync block index.
(3) When you define a type, you can define static data fields within it.
The bytes that back these static data fields are allocated within the type objects themselves. Finally, inside each type object is a method table with one entry per method defined within the type.
(4)
Whenever
a
new object
is created
on the heap, the
CLR
automatically
initializes
the internal
type object pointer
member
to refer to
the object’s corresponding
type object.
(5)
Furthermore, the
CLR
initializes the
sync block index
and
sets
all of the object’s
instance fields to null or 0
(zero) prior to calling the type’s constructor, a method that will likely modify some of the instance data fields.
(6) When
calling
a
static method, the
JIT compiler
locates the type object that
corresponds to the type that defines the static method.
Then, the
JIT compiler locates
the entry in the type object’s
method
table that refers to the method being called,
JITs the method
(if necessary), and
calls the JITted code.
(7) When
calling
a
nonvirtual instance method, the
JIT compiler locates
the
type object
that corresponds to the type of the variable being used to make the call.
JIT compiler walks down
the
class hierarchy toward Object
looking for this method. It can do this because each type object has a field in it that refers to its base type. Then, the JIT compiler locates the entry in the type object’s method table that refers to the method being called, JITs the method (if necessary), and then calls the JITted code.
(8) When
calling
a
virtual instance method, the
JIT compiler produces some additional code in the method, which will be executed each time the method is invoked. This code will
first look in the variable
being used to make the call and then follow the address to the
calling object. Then,
the code will
examine
the object’s internal
type object pointer
member; this member refers to the actual type of the object.
The code
then
locates
the entry in
the type object’s method table that refers to the method being called, JITs the method (if necessary), and calls the JITted code.
(9)
Type objects are actually objects
themselves, so they all contail
type object pointer. When the CLR creates type objects, the CLR must initialize these members.
(10) When the
CLR starts running in a process,
it immediately creates a special type object for the System.Type type
(defined in MSCorLib.dll).
(11)
System.Type
type object is an object itself and therefore
also has a type object pointer
member in it, and it is logical to ask what this member refers to.
It refers to itself
because the System.Type type object is itself an 「instance」 of a type object.
(12) By the way,
System.Object’s
GetType
method simply
returns the address stored in the specified object’s type object pointer member. In other words, the
GetType method returns a pointer to an object’s type object, and this is how you can determine
the true type of any object in the system (including type objects).