User-defined reference types
The C# language lets us create our own user-defined
classes, as well as arrays, delegates, and interfaces. Indeed, most programming
work will probably involve defining new reference types. User-defined reference
types can be written to do almost anything, and can make use of any combination
of
built-in types.
Determining a type
There are two ways to determine the type of a variable. We can use the GetType()
method defined in the Object class. This method returns a Type
object representing the type of a variable. Alternatively, we can use the typeof
operator that does the same thing. The typeof operator takes as its argument
either the fully qualified name of a type or a type alias.
|
Example: Determining a type
|
|
In this simple example, the TypeDemo class defines a method
named DoStuff().
This method takes an object argument. Inside the DoStuff()
method, the GetType()
method is called to return a Type object corresponding to the
input argument, and the typeof operator is used to return a
Type
object corresponding to the String class. The two Type
objects are compared.
|
|
The DoStuff() method is called twice.
The first time the method is passed a double as an argument. The second
time the method is called it is passed a string:
using System;
public class TypeDemo
{
public static void Main()
{
DoStuff(32.45);
DoStuff("Hello");
}
public static void DoStuff(object obj)
{
if (obj.GetType() ==
typeof(string))
Console.WriteLine("Argument was a string");
else
Console.WriteLine("Argument was not a string");
}
}
Output:
Argument was not a string
Argument was a string
In this example, we used the alias for the String
class. We could also have used the syntax typeof(System.String). Determining
an object's type is an important element of reflection. See Chapter 25 for
more details on reflection, and for other examples of using GetType()
and typeof.
|
Casting
When assembling the classes and methods provided by the
.NET Framework into your own application, it may be necessary to convert one
data type to another. For instance, consider the following code that uses a FileStream
object to read data from a file. The Read() method reads a specified number of bytes from the input stream and places them
into a byte[]
array. The Length
property of the FileStream is used to designate how many
bytes to read:
FileStream fs =
File.Create("data.inp");
byte[] buf2 = new byte[fs.Length];
int k = fs.Read(buf2, 0, fs.Length);
This code will not
compile. The problem is that the Read() method takes an int as its third
argument, but the Length property returns a long. The system
attempts to perform an implicit conversion from long to int, but this is not
allowed because it is a narrowing conversion (64-bit to 32-bit). If we were trying
to convert from an int to a long, this would be a widening conversion, and could
be done implicitly.
The solution is to cast the long
explicitly to an int. This is done by placing the desired cast type
inside parentheses before the value or object to be cast. In our previous
example, it would look like this:
if (fs.Length <= int.MaxValue)
int
k = fs.Read(buf2, 0, (int)fs.Length);
Because narrowing conversions can result in loss of data, we
check that the actual value will fit in an int before making the conversion. An
alternative approach would be to use the checked operator, which will cause an
error to be thrown if the conversion would result in loss of data. The checked
operator is covered in Chapter 3.
Casting can be performed on both value and reference types.
As was previously noted, some casts will be performed implicitly by the
compiler. These are widening conversions, going from a smaller data type to a
larger one. The implicit conversions supported by C#
are:
|
From
|
To
|
|
byte
|
decimal, double, float, int, long, short, uint,
ulong, ushort
|
|
char
|
decimal, double, float, int, long, uint, ulong,
ushort
|
|
float
|
double
|
|
int
|
decimal, double, float, long
|
|
long
|
decimal, double, float
|
|
sbyte
|
decimal, double, float, int, long, short
|
|
short
|
decimal, double, float, int, long
|
|
uint
|
decimal, double, float, long, ulong
|
|
ulong
|
decimal, double, float
|
|
ushort
|
decimal, double, float, int, long, uint, ulong
|
Any conversion not listed in the above table must be performed
explicitly using a cast. Note that we cannot implicitly or explicitly cast a bool
into another type. We must also be careful when performing a narrowing
conversion. For example, if we try to cast a long to an int
and the value of the long is greater than the maximum value of an int,
the statement will compile, but the cast will not be properly performed. Using
the checked
operator can help detect conversion overflows. We will look in more detail at
casting between reference types in Chapter 7, including defining custom casts
for class types.
Boxing and unboxing
Both value types and reference types are derived from the Object
class. This means that any method that takes an object argument, for instance,
can be passed a value type. Similarly, a value type can call an Object
class method:
int j = 4;
string str = j.ToString();
What is happening here is another example of type casting.
If you recall, a value-type variable contains data stored on the stack. You
might wonder how such a variable could call a reference-type method. The answer
is that the value-type variable is implicitly cast into a reference type in a
process called boxing. Conceptually, this is achieved by creating a
temporary reference-type "box" corresponding to the value type
(although the exact process may vary due to compiler optimizations). This is
what happens in IL:
IL_0000: ldc.i4.4 // Load the int 4 onto the stack
IL_0001: stloc.0 // Pop the value off the stack and into V_0
IL_0002: ldloca.s
V_0 // Push the address of variable V_0
// onto the stack
// Call Int32::ToString()
IL_0004: call
instance string [mscorlib]System.Int32::ToString()
The key instruction here is ldloca.sV_0,
which loads a managed pointer to the V_0
variable it is this managed pointer, not the value itself, against which the ToString()
method is called.
We can also explicitly box a value following the normal
casting syntax:
int j = 4;
object obj = (object)j;
We can convert a previously boxed variable back into a value
type using the same casting syntax:
There are several limitations on unboxing. You can only unbox a variable that has previously
been explicitly boxed. The normal casting limitations also apply. For instance,
if we box a long
into an object, we can't unbox the object into an int,
although we can of course explicitly cast the long
to an int
once we've unboxed it:
long l = 1000;
object o = (object)l;
int i = (int)((long)o);
Summary
We've taken a look into some of the core features of C# in
this chapter. The format and behavior of types in .NET is defined by the Common
Type System (CTS). Not all types defined by the CTS are available to all .NET
languages, so the Common Language Infrastructure defines a subset of the CTS,
called the Common Language Specification, which specifies the types that all .NET languages must support. The types available when working with C# can be
either value or reference types, and this distinction points to the memory
allocation system under the hood.
In this chapter we've looked at:
q
The Common Type System
q
The .NET Framework type hierarchy
q
The differences between value and reference types
q
The basic categories present in the type hierarchy, as
well as the predefined value and
reference types
q
How one type can be converted, or cast, into another
type
q How
value types can be "boxed" into reference types
Copyright and Authorship Notice
This chapter extract is taken from "C#
Programmers Reference" by Grant Palmer published by Wrox Press Limited in
April 2002; ISBN 1861006306; copyright © Wrox Press Limited 2002; all rights
reserved. No part of this chapter may be reproduced, stored in a retrieval system or transmitted in any form or by
any means -- electronic, electrostatic, mechanical, photocopying, recording or
otherwise -- without the prior written permission of the publisher, except in
the case of brief quotations embodied in critical articles or reviews.