A type defines the blueprint for a
value. In our example, we used two literals of type int
with values 12 and 30. We also declared a
variable of type int
whose name was x
.
A variable denotes a storage location that can contain different values over time. In contrast, a constant always represents the same value (more on this later).
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.
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 = 2012; message = message + x.ToString(); Console.WriteLine (message); // Hello world2012
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");
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
such as 12
or "Hello, world"
.
The new
operator creates
instances 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 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, 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 with 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 (for more information, see Nullable Types).
The predefined types in C# are:
Numeric
Signed integer (sbyte
, short
, int
, long
)
Unsigned integer (byte
, ushort
, uint
, ulong
)
Real number (float
, double
, decimal
)
Logical (bool
)
Character (char
)
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 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.