A type defines the blueprint for a value. A
value is a storage location denoted by a variable or a constant. A
variable represents a value that can change, whereas a
constant represents an invariant. We created a local variable named
x
in our first program:
static void Main( ) { int x = 12 * 30; Console.WriteLine (x); }
All values in C# are instances of a
specific type. The meaning of a value, and the set of possible values a variable can have,
is determined by its type. The type of x
is int
.
Predefined types are types that are specially
supported by the compiler. The int
type is a predefined
primitive type for representing the set of integers that fits into 32 bits of memory, from
–231 to 231–1. We can perform
functions such as arithmetic with instances of the int
type, as follows:
int x = 12 * 30;
Another predefined C# type is the string
type. The
string
type represents a sequence of characters, such
as “.NET” or http://oreilly.com. We can manipulate strings by calling
functions on them as follows:
string message = "Hello world"; string upperMessage = message.ToUpper( ); Console.WriteLine (upperMessage); // HELLO WORLD int x = 2007; message = message + x.ToString( ); Console.WriteLine (message); // Hello world2007
The primitive bool
type has exactly two possible
values: true
and false
. The bool
type is commonly used to
conditionally branch execution flow based with an if
statement. For example:
bool simpleVar = false; if (simpleVar) Console.WriteLine ("This will not print"); int x = 5000; bool lessThanAMile = x < 5280; if (lessThanAMile) Console.WriteLine ("This will print");
Just as we can build complex functions from simple functions, we can build complex
types from primitive types. In this example, we will define a custom type named UnitConverter
— a class that serves as a blueprint for unit
conversions:
using System; public class UnitConverter { int ratio; // Field public UnitConverter (int unitRatio) // Constructor { ratio = unitRatio; } public int Convert (int unit) // Method { return unit * ratio; } } class Test { static void Main( ) { UnitConverter feetToInches = new UnitConverter(12); UnitConverter milesToFeet = new UnitConverter(5280); Console.Write (feetToInches.Convert(30)); // 360 Console.Write (feetToInches.Convert(100)); // 1200 Console.Write (feetToInches.Convert (milesToFeet.Convert(1))); // 63360 } }
A type contains data members and function
members. The data member of UnitConverter
is the field called ratio
. The function members of UnitConverter
are the Convert
method and
the UnitConverter's
constructor.
A beautiful aspect of C# is that predefined types and custom types have few differences. The primitive int
type serves as a blueprint for integers. It holds
data—32 bits—and provides function members that use that data, such as ToString
. Similarly, our custom UnitConverter
type acts as a blueprint for unit conversions. It holds
data—the ratio—and provides function members to use that data.
Data is created by instantiating a type. Primitive types can be instantiated simply by using a
literal. For example, the following line instantiates two integers (12
and 30
), which are
used to compute a third instance, x
:
int x = 12 * 30;
The new
operator is needed to create a new
instance of a custom type. We created and declared an instance of the
UnitConverter
type with this statement:
UnitConverter feetToInchesConverter = new UnitConverter(12);
Immediately after the new
operator instantiates
an object, the object’s constructor is called to perform initialization. A constructor
is defined like a method, except that the method name and return type are reduced to the
name of the enclosing type:
public class UnitConverter { ... public UnitConverter (int r) // Constructor { ratio = r; } ... }
The data members and function members that operate on the
instance of the type are called instance members.
The UnitConverter's Convert
method and the int's ToString
method are examples of instance members.
By default, members are instance members.
Data members and function members that don’t operate on the instance of the type,
but rather on the type itself, must be marked as static
. The Test.Main
and Console.WriteLine
methods are static methods. The Console
class is actually a static
class, where all its members are static. You never
actually create instances of a Console
—one console is
shared across the whole application.
To contrast instance versus static members, the instance field Name
pertains to an instance of a particular Panda
, whereas Population
pertains to the set of all Panda
instances:
using System; public class Panda { public string Name; // Instance field public static int Population; // Static field public Panda (string n) // Constructor { Name = n; // Assign instance field Population = Population+1; // Increment static field } }
The following code creates two instances of the Panda
, prints their names and then the total population:
Panda p1 = new Panda ("Pan Dee"); Panda p2 = new Panda ("Pan Dah"); Console.WriteLine (p1.Name); // Pan Dee Console.WriteLine (p2.Name); // Pan Dah Console.WriteLine (Panda.Population); // 2
The public
keyword exposes members to other
classes. In this example, if the Name
field in
Panda
was not public, the Test
class could not access it. Marking a member public
is how a type communicates: “Here is what I want other types to see—everything else is my own private implementation
details.” In object-oriented terms, we say that the public members
encapsulate the private members of the class.
C# can convert between instances of compatible types, through
implicit and explicit conversions. A
conversion always creates a new value from an existing one. Conversions can be either
implicit or explicit; implicit conversions happen
automatically, and explicit conversions require a
cast. In the following example, we implicitly cast an int
to a long
type (which
has twice the capacity of an int
) and
explicitly cast an int
to a
short
type (which has half the capacity of an
int
):
int x = 123456; // int is a 32-bit integer long y = x; // Implicit conversion to 64-bit int short z = (short)x; // Explicit conversion to 16-bit int
Implicit conversions are allowed when:
The compiler can guarantee they will always succeed, and no information is lost in conversion.
Conversely, explicit conversions are required when:
The compiler cannot guarantee they will always succeed, or o information may be lost during conversion.
Most conversions are built into the language, such as the previously shown numeric conversions. Occasionally, it is useful to write custom conversions (see the upcoming “Operator Overloading” section).
All C# types fall into the following categories:
Value types
Reference types
Pointer types
Value types comprise most built-in types (specifically, all
numeric types, the char
type, and the bool
type) as well as custom struct
and enum
types.
Reference types comprise all class, array, delegate, and interface types.
The fundamental difference between value types and reference types is how they are handled in memory. Pointer types fall outside mainstream C# usage (see the upcoming “Unsafe Code and Pointers” section).
The content of a value type variable or constant is simply a
value. For example, the content of the built-in value type int
is 32 bits of data.
You can define a custom value type with the struct
keyword as follows (see Figure 1-1).
public struct Point { public int X, Y; }
The assignment of a value type instance always copies the instance. For example:
Point p1 = new Point( ); p1.X = 7; Point p2 = p1; // Assignment causes copy Console.WriteLine (p1.X); // 7 Console.WriteLine (p2.X); // 7 p1.X = 9; // Change p1.X Console.WriteLine (p1.X); // 9 Console.WriteLine (p2.X); // 7
Figure 1-2 shows that p1
and p2
have
independent storage.
A reference type is more complex than a value type, having two parts: an
object and the reference to that object. The
content of a reference type variable or constant is a reference to an object that
contains the value. Here is the Point
type from our
previous example rewritten as a class, rather than a struct (seeFigure 1-3).
public class Point { public int X, Y; }
Assigning a reference type variable copies the reference, not the object instance.
This allows multiple variables to refer to the same object—something not ordinarily
possible with value types. If we repeat the previous example, but with Point
now a class, an operation to X
affects Y
:
Point p1 = new Point( ); p1.X = 7; Point p2 = p1; // Copies p1 reference Console.WriteLine (p1.X); // 7 Console.WriteLine (p2.X); // 7 p1.X = 9; // Change p1.X Console.WriteLine (p1.X); // 9 Console.WriteLine (p2.X); // 9
Figure 1-4 shows that p1
and p2
are two references that point
to the same object.
A reference can be assigned the literal null
,
indicating that the reference points to no object:
class Point {...} ... Point p = null; Console.WriteLine (p == null); // True // The following line generates a runtime // error (a NullReferenceException is thrown): Console.WriteLine (p.X);
In contrast, a value type cannot ordinarily have a null value:
struct Point {...} ... Point p = null; // Compile-time error int x = null; // Compile-time error
Value type instances occupy precisely the sum of the memory occupied by their fields.
Reference types require separate allocations of memory for the reference and object. The object consumes as many bytes as its fields, plus additional administrative overhead (typically 12 bytes). Each reference to an object requires an extra 4 or 8 bytes, depending on whether the .NET runtime is running on a 32- or 64-bit platform.
The following are the predefined types in C#:
Value types |
Numeric types
— Signed integer (sbyte, short, int,
long ) |
— Unsigned integer (byte, ushort, uint,
ulong ) |
— Real number (float, double,
decimal ) |
Logical (bool
)
Character (char
)
Reference types |
String (string
)
Object (object
)
Predefined types in C# alias Framework types in the System
namespace. There is only a syntactic difference between these two
statements:
int i = 5; System.Int32 i = 5;
The predefined value types are also known as primitive types. Primitive types are so called because they are the atoms, or smallest possible building blocks of data, in a language, and most have a direct representation in machine code.