A type defines the blueprint for a value. A
value is a storage location denoted by a
variable (if it can change) or a
constant (if it cannot). We created a local variable
named x
in our first program.
All values in C# are an instance 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
in our example program is int
.
Predefined types (also called built-in types) are types that are specially
supported by the compiler. The int
type is a
predefined type for representing
the set of integers that fit 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 string
. The string
type represents a sequence of
characters, such as “.NET” or “http://oreilly.com”. We can work with 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 predefined bool
type has exactly
two possible values: true
and
false
. The bool
type is commonly used to conditionally
branch execution flow 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");
The System
namespace in the
.NET Framework contains many important types that are not predefined
by C# (e.g., DateTime
).
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 predefined 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. Predefined 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 started our
Main
method by creating two
instances of the UnitConverter
type. 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 UnitConverter (int unitRatio) // Constructor { ratio = unitRatio; }
The data members and function members that operate on the
instance of the type are called instance members. The Unit
Converter
’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, which means
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:
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 prints
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. A conversion always creates a new value from
an existing one. Conversions can be either implicit
or explicit: implicit conversions happen automatically, whereas explicit conversions require a cast. In the following example, we
implicitly convert an int
to a long
type (which has twice the bitwise
capacity of an int
) and
explicitly cast an int
to a short
type (which has half the bitwise
capacity of an int
):
int x = 12345; // 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
In general, implicit conversions are allowed when the compiler can guarantee they will always succeed without loss of information. Otherwise, you must perform an explicit cast to convert between compatible types.
C# types can be divided into value types and reference 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.
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 (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 (see Figure 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 via p1
affects p2
:
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. Assuming Point
is a
class:
Point p = null; Console.WriteLine (p == null); // True
Accessing a member of a null reference generates a runtime error:
Console.WriteLine (p.X); // NullReferenceException
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
C# has a special construct, called nullable types, for representing value-type nulls (see the section Nullable Types).
The predefined types in C# are:
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 set of predefined value types, excluding
decimal
, are known as primitive types in the Common
Language Runtime (CLR). Primitive types are so called because they are
supported directly via instructions in compiled code, which usually
translates to direct support on the underlying processor.