object (System.Object
) is the ultimate base class for
all types. Any type can be upcast to object
.
To illustrate how this is useful, consider a general-purpose stack. A stack is a data structure based on the principle of LIFO——“Last in, First out.” A stack has two operations: push an object on the stack, and pop an object off the stack.
Here is a simple implementation that can hold up to 10 objects:
public class Stack { int position;object[]
data = new object[10]; public void Push (object
obj) { data[position++] = obj; } publicobject
Pop() { return data[--position]; } }
Because Stack
works with the object type, we can
Push
and Pop
instances of any type to and from the Stack
:
Stack stack = new Stack(); stack.Push ("sausage"); // Explicit cast is needed because we're downcasting: string s = (string) stack.Pop(); Console.WriteLine (s); // sausage
object
is a reference type, by virtue of being a
class. Despite this, value types, such as int
, can also
be cast to and from object
, and so be added to our stack.
This feature of C# is called type unification:
stack.Push (3); int three = (int) stack.Pop();
When you cast between a value type and object
, the
CLR must perform some special work to bridge the difference in semantics between value and
reference types. This process is called boxing and
unboxing.
Boxing is the act of casting a value type instance to a reference
type instance. The reference type may be either the object
class, or an interface (which we will visit later). In this example,
we box an int
into an
object:
int x = 9; object obj = x; // box the int
Unboxing reverses the operation, by casting the object back to the original value type:
int y = (int)obj; // unbox the int
Unboxing requires an explicit cast. The runtime checks that the stated value type
(exactly) matches the actual object type, and throws an InvalidCastException
if the check fails. For instance, the following throws
an exception because long
does not exactly match
int
:
object obj = 9; // 9 is inferred to be of type int long x = (long) obj; // InvalidCastException
The following succeeds, however:
object obj = 9; long x = (int) obj;
as does this:
object obj = 3.5; // inferred type is double int x = (int) (double) obj; // x is now 3
In the last example, (int
) performs a
conversion; (double
) performs an
unboxing.
C# checks types both statically and dynamically.
Static type checking occurs at compile time. Static type checking enables the compiler to verify the correctness of your program without running it. The following code will fail because the compiler enforces static typing:
int x = "5";
Dynamic type checking occurs at runtime. Whenever an unboxing or downcast occurs, the runtime checks the type dynamically. For example:
object y = "5"; int z = (int)y; // Runtime error, downcast failed
Dynamic type checking is possible because each object on the heap internally stores a
little type token. This token can be retrieved by calling the GetType
method of object
.
Here are all the members of object:
public class Object { public Object(); public extern Type GetType(); public virtual bool Equals (object obj); public static bool Equals (object objA, object objB); public static bool ReferenceEquals (object objA, object objB); public virtual int GetHashCode(); public virtual string ToString(); protected override void Finalize(); protected extern object MemberwiseClone(); }
All types in C# are represented at runtime with an instance of System.Type
. There are two basic ways to get a System.Type
object:
Call GetType
on the instance.
Use the typeof
operator on a type name.
GetType
is evaluated dynamically at runtime;
typeof
is evaluated statically at compile
time.
System.Type
has properties for such things as the
type’s name, assembly, base type, and so on. For example:
int x = 3; Console.Write (x.GetType().Name); // Int32 Console.Write (typeof(int).Name); // Int32 Console.Write (x.GetType().FullName); // System.Int32 Console.Write (x.GetType() == typeof(int)); // True
System.Type
also has methods that act as a gateway
to the runtime’s reflection model. For detailed information,
see Chapter 17 of C# 3.0 in a Nutshell.
The Equals
method is similar to the == operator,
except that Equals
is virtual, whereas == is static.
The following example illustrates the difference:
object x = 3; object y = 3; Console.WriteLine (x == y); // False Console.WriteLine (x.Equals (y)); // True
Because x
and y
have been cast to the object
type, the compiler
statically binds to object's
== operator, which uses
reference-type semantics to compare two instances. (And because
x
and y
are boxed,
they are represented in separate memory locations, and so are unequal.) The virtual
Equals
method, however, defers to the Int32
type’s Equals
method,
which uses value-type semantics in comparing two values.
The static object.Equals
method simply calls the
virtual Equals
method—after checking that the arguments
are not null.
object x = null; object y = 3; bool error = x.Equals (y); // NullReferenceException bool ok = object.Equals (x, y);
ReferenceEquals
forces a reference-type equality
comparison (this is occasionally useful on reference types where the == operator has been
overloaded to do otherwise).
GetHashCode
emits a hash code when the type is used
in a hashtable-based dictionary, namely System.Collections.Generic.Dictionary
and System.Collections.Hashtable
.
To customize a type’s equality semantics, you must at a minimum override Equals
and GetHashCode
. You
would also usually overload the == and != operators. For an example on how to do both, see
the upcoming “Operator Overloading” section.
The ToString
method returns the default textual
representation of a type instance. The ToString
method
is overridden by all built-in types. Here is an example of using the int
type’s ToString
method:
int x = 1; string s = x.ToString(); // s is "1"
You can override the ToString
method on custom
types as follows:
public class Panda { public string Name; public override string ToString() { return Name; } } ... Panda p = new Panda { Name = "Petey" }; Console.WriteLine (p); / Petey