Tải bản đầy đủ (.pdf) (105 trang)

Tìm hiểu về các loại tham khảo và các loại giá trị potx

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (12.95 MB, 105 trang )

This page intentionally left blank
CHAPTER 4
Understanding
Reference Types and
Value Types
IN THIS CHAPTER
. A Quick Introduction to
Reference Types and Value
Types
. The Unified Type System
. Reference Type and Value Type
Memory Allocation
. Reference Type and Value Type
Assignment
. More Differences Between
Reference Types and Value
Types
. C# and .NET Framework Types
. Nullable Types
Deep in the course of coding, you’re often immersed in
logic, solving the problem at hand. Simple actions, such as
assignment and instantiation, are tasks you perform regu-
larly, without much thought. However, when writing C#
programs, or using any language that targets the Common
Language Runtime (CLR), you might want to take a second
look. What appears to be simple can sometimes result in
hard-to-find bugs. This chapter goes into greater depth on
CLR types and shows you a few things about coding in C#
that often catch developers off guard. More specifically,
you learn about the differences between reference types
and value types.


The .NET type system, which C# is built upon, is divided
into reference types and value types. You’ll work with each
of these types all the time, and it’s important to know the
differences between them. This chapter shows you the
differences via memory allocation and assignment behav-
iors. This understanding should translate into helping you
make smart design decisions that improve application
performance and reduce errors.
A Quick Introduction to Reference
Types and Value Types
There is much to be said about reference types and value
types, but this section gives a quick introduction to the
essentials. You learn a little about their behaviors and what
they look like in code.
80
CHAPTER 4 Understanding Reference Types and Value Types
As its name suggests, a reference type has a value that is a reference to an object in
memory. However, a value type has a value that contains the object itself.
Up until now, you’ve been creating custom reference types, which is defined with the
class keyword shown here:
class Customer
{
public string Name;
}
The Customer class is a reference type because it uses the class keyword in its definition.
Value types are similar in syntax but use the
struct keyword instead, as shown here:
struct Money
{
public decimal Amount;

}
The struct keyword classifies the Money type as a value type.
In both of these examples, I used the public modifier on the Name and Amount fields. This
allows code using the
Customer and Money types to access the Name and Amount fields,
respectively. You can learn more about the different access modifiers in Chapter 9,
“Implementing Object-Oriented Principles.”
Later sections of this chapter go into even greater depth on the differences between these
types, but at least you now know the bare minimum to move forward. The next section
starts your journey into understanding the differences between reference types and value
types and how these differences affect you.
The Unified Type System
Before looking at the specific behaviors of reference types and value types, you should
understand the relationship between them and how the C# type system, the Unified Type
System, works. The details described here help you understand the coding practices and
performance issues that you learn later in the chapter.
How the Unified Type System Works
Essentially, the Unified Type System ensures that all C# types derive from a common
ancestor,
System.Object. The C# object type is an alias for System.Object, and further
discussion will use a C# perspective and refer to
System.Object as object. Figure 4.1 illus-
trates this relationship.
81
The Unified Type System
4
System.Object
Reference Type System.ValueType
Reference Type Value Type
FIGURE 4.1 In the Unified Type System, all objects derive from the object type.

WHAT IS INHERITANCE
Inheritance is an object-oriented principle that promotes reuse and helps build hierar-
chical frameworks of objects. In the context of this chapter, you learn that all types
derive from object. This gives you the ability to assign a derived object to a variable of
type object. Also, whatever belongs to object is also a member of a derived class.
In Chapter 8, “Designing Objects,” and Chapter 9, “Implementing Object-Oriented
Principles,” you can learn a lot more about C# syntax supporting inheritance and how
to use it. Throughout the rest of the book, too, you’ll see many examples of how to use
inheritance.
In Figure 4.1, the arrows are Unified Modeling Language (UML) generalization symbols,
showing how one type, a box, derives from the type being pointed to. The direction of
inheritance shows that all types derive either directly or indirectly from
object.
Reference types can either derive directly from System.Object or from another reference
type. However, the relationship between value type objects and
object is indirect. All
value types implicitly derive from the
System.ValueType class, a reference type object,
which inherits
object. For simplicity, further discussion omits the fact of either explicit or
implicit inheritance relationships.
At this point, you might be scratching your head and wondering why you should care (a
natural reaction). The big deal is that your coding experience with treating types in a
generic manner is simplified (the good news), but you must also be aware of performance
penalties that are possible when treating types in a generic manner. In Chapter 17,
“Parameterizing Type with Generics and Writing Iterators,” you can learn about the best
way to manage generic code, but the next two sections explain the implications of the
Unified Type System and how it affects you.
82
CHAPTER 4 Understanding Reference Types and Value Types

Using object for Generic Programming
Because both reference types and value types inherit object, you can assign any type to a
variable of type object as shown here:
decimal amount = 3.50m;
object obj1 = amount;
Customer cust = new Customer();
object obj2 = cust;
The amount variable is a decimal, a value type, and the cust variable is a Customer class, a
reference type.
Any assignment to object is an implicit conversion, which is always safe. However, doing
an assignment from type object to a derived type may or may not be safe. C# forces you
to state your intention with a cast operator, as shown here:
Customer cust2 = (Customer)obj2;
The cast operator is necessary because the C# compiler can’t tell whether obj2 is actually a
Customer type. Chapter 10, “Coding Methods and Custom Operators,” goes into greater
depth on conversions, but the basic idea is that C# is type-safe and has features that
ensure safe assignments of one object to another.
A more concrete example of when you might see a situation where a variable can be
assigned to another variable of type object is with standard collection classes. The first
version of the .NET Framework Class Library (FCL) included a library of collection classes,
one of them being
ArrayList. These collections offered many conveniences that you
don’t have in C# arrays or would have to create yourself.
One of the features of these collections, including
ArrayList, was that they could work
generically with any type. The Unified Type System makes this possible because the collec-
tions operate on the object type, meaning that you can use them with any .NET type.
Here’s an example that uses an
ArrayList collection:
ArrayList customers = new ArrayList();

Customer cust1 = new Customer();
cust1.Name = “John Smith”;
Customer cust2 = new Customer();
cust2.Name = “Jane Doe”;
customers.Add(cust1);
customers.Add(cust2);
foreach (Customer cust in customers)
{
Console.WriteLine(“Customer Name: {0}”, cust.Name);
}
The preceding example creates a new instance of an ArrayList class, named customers. It
creates a couple
Customer objects, sets their Name fields, and then adds them to the
83
The Unified Type System
4
customers ArrayList. Notice that the foreach loop works seamlessly with collections as
well as it does with arrays.
Again, because the
ArrayList operates on type object, it is convenient to use with any
type, whether it is a reference type or value type. The preceding example showed you how
to assign a reference type, the
Customer class, to an ArrayList, which is convenient.
However, there is a hidden cost when assigning value types to object type variables, such
as the elements of an
ArrayList. The next section explains this phenomenon, which is
known as boxing and unboxing.
Performance Implications of Boxing and Unboxing
Boxing occurs when you assign a value type variable to a variable of type object.
Unboxing occurs when you assign a variable of type object to a variable with the same

type as the true type of the object. The following code is a minimal example that causes
boxing and unboxing to occur:
decimal amountIn = 3.50m;
object obj = amountIn; // box
decimal amountOut = (decimal)obj; // unbox
Figures 4.2 to 4.4 illustrate what is happening in the preceding algorithm. Figure 4.2
shows the first line.
Before boxing, as in the declaration of
amountIn, the variable is just a value type that
contains the data directly. However, as soon as you assign that value type variable to an
object, as in Figure 4.3, the value is boxed.
Managed Heap
amountIn
FIGURE 4.2 A value type variable before boxing.
84
CHAPTER 4 Understanding Reference Types and Value Types
Managed Heap
amountIn
obj
(boxed amountIn)
FIGURE 4.3 A boxed value.
Managed Heap
amountIn
amountOut
(unboxed obj)
obj
(boxed amountIn)
FIGURE 4.4 Unboxing a value.
As shown in Figure 4.3, boxing causes a new object to be allocated on the heap and a
copy of the original value to be put into the boxed object. Now, you have two copies of

the original variable: one in
amountIn and another in the boxed decimal, obj, on the
heap. You can pull that value out of the boxed decimal, as shown in Figure 4.4.
In Figure 4.4, the boxed value in
obj is copied into the decimal variable, amountOut. Now,
you have three copies of the original value that was assigned to
amountIn.
Writing code as shown here is pointless because the specific example doesn’t do
anything useful. However, the point of this boxing and unboxing exercise is so that you
can see the mechanics of what is happening and understand the overhead associated with
it. On the other hand, you could write a lot of code similar to the
ArrayList example in
the previous section; that is, unless you understood the information in this section. Here’s
85
Reference Type and Value Type Memory Allocation
4
an example, similar to the ArrayList code in the previous section, that uses value type
variables:
ArrayList prices = new ArrayList();
decimal amount1 = 7.50m;
decimal amount2 = 1.95m;
prices.Add(amount1);
prices.Add(amount2);
foreach (decimal amount in prices)
{
Console.WriteLine(“Amount: {0}”, amount);
}
Because of the Unified Type System, this code is as convenient as the code written for the
Customer class, but beware. If the prices ArrayList held 10, 20, or 100 decimal type vari-
ables, you probably wouldn’t care. However, what if it contains 10,000 or 100,000? In that

case, you should be concerned because this could have a serious impact on the perfor-
mance of your application.
Generally, any time you assign a value type to any object variable, whether a collection or
a method parameter, take a second look to see whether there is potential for performance
problems. In development, you might not notice any performance problem; after deploy-
ment to production, however, you could get slammed by a slow application with a hard-
to-find bug.
From the perspective of collections, you have two choices: arrays or generics. You can
learn more about arrays in Chapter 6, “Using Arrays and Enums.” If you are programming
in C# 1.0, your only choices will be arrays or collections, and you’ll have to design with
tradeoffs between convenience and performance, or type safety and no type safety. If
you’re using C# 2.0 or later, you can have the best of both worlds, performance and type
safety, by using generics, which you can learn more about in Chapter 17.
Now that you know the performance characteristics of boxing and unboxing, let’s dig a
little deeper. The next sections tell you more about what reference types and value types
are, their differences, and what you need to know.
Reference Type and Value Type Memory Allocation
Reference type and value type objects are allocated differently in memory. This can affect
your code in the area of method call parameters and is the basis for understanding assign-
ment behavior in the next section. This section takes a quick look at memory allocation
and the differences between reference types and value types.
86
CHAPTER 4 Understanding Reference Types and Value Types
Managed Heap
cust
FIGURE 4.5 Reference type declaration.
Reference Type Memory Allocation
Reference type objects are always allocated on the heap. The following code is a typical
reference type object declaration and instantiation:
Customer cust = new Customer();

In earlier chapters, I explained that this was how you declare and instantiate a reference
type, but there is much more to the preceding line. By declaring
cust as type Customer,
the variable
cust is strongly typed, meaning that only compatible objects can be assigned
to it. Figure 4.5 shows the declaration of
cust, from the left side of the statement.
In Figure 4.5, the
cust box is in your code, representing the declaration of cust as
Customer. On the right side of the preceding code, the new Customer() is what creates the
new instance of a
Customer object. The assignment puts a reference into cust that refers
to the new
Customer object on the heap, as shown in Figure 4.6.
Figure 4.6 shows how the cust variable holds a reference to the new instance of a
Customer object on the heap. The heap is a portion of memory that the CLR uses to allo-
cate objects. This is what you should remember: A reference type variable will either hold
a reference to an object on the heap or it will be set to the C# value
null.
Next, you learn about value type memory allocation and how it is different from reference
type memory allocation.
Value Type Memory Allocation
The answer to where value type variables are allocated is “It depends.” The two places that
a value type variable can be allocated is either the stack or along with a reference type on
the heap. See the sidebar “What Is the Stack?” if you’re curious about what the stack is.
87
Reference Type and Value Type Memory Allocation
4
Managed Heap
cust

new Customer()
FIGURE 4.6 Reference type object allocated on the heap.
WHAT IS THE STACK?
The CLR has a stack for keeping track of the path from the entry point to the currently
executing method in an application. Just like any other stack, the CLR stack works on a
last-in, first-out fashion. When your program runs,
Main (the entry point) is pushed onto
the stack. Any method that
Main calls is then pushed onto the top of the stack.
Method parameter arguments and local variables are pushed onto the stack, too. When
a method completes, it is popped off the top of the stack, and control returns to the
next method in the stack, which was the caller of the method just popped.
Value type variables passed as arguments to methods, as well as local variables defined
inside a method, are pushed onto the stack with the method. However, if the value type
variable is a field of a reference type object, it will be stored on the heap along with the
reference type object.
Regardless of memory allocation, a value type variable will always hold the object that is
assigned to it. An uninitialized value type field will have a value that defaults to some
form of zero (
bool defaults to false), as described in Chapter 2.
C# 2.0 and later versions have a feature known as nullable types, which also allow value
types to contain the value
null. A later section of this chapter explains how to use
nullable types.
Now you know where reference type and value type variables are allocated in memory, but
more important, you understand the type of data they can hold and why. This opens the
door to understanding the assignment differences between reference types and value
types, which is discussed next.
88
CHAPTER 4 Understanding Reference Types and Value Types

Reference Type and Value Type Assignment
Based on what you know so far about reference types and value types—their relationship
through the Unified Type System and memory allocation—the step to understanding
assignment behavior is easier. This section examines assignment among reference types
and assignment among value types. You’ll see how reference type and value type assign-
ment differs and what happens when assigned values are subsequently modified. We look
at reference type assignment first.
Reference Type Assignment
To understand reference type assignment, it’s helpful to look at previous sections of this
chapter, focusing on reference type features. Because the value of a reference type resides
on the heap, the reference type variable holds a reference (to the object on the heap).
Keeping this in mind, here’s an example of reference type assignment:
Customer cust5 = new Customer();
cust5.Name = “John Smith”;
Customer cust6 = new Customer();
cust6.Name = “Jane Doe”;
Console.WriteLine(“Before Reference Assignment:”);
Console.WriteLine(“cust5: {0}”, cust5.Name);
Console.WriteLine(“cust6: {0}”, cust6.Name);
cust5 = cust6;
Console.WriteLine(“After Reference Assignment:”);
Console.WriteLine(“cust5: {0}”, cust5.Name);
Console.WriteLine(“cust6: {0}”, cust6.Name);
In the preceding example, you can see there are two variables, cust5 and cust6, of type
Customer that are declared and initialized. Between sets of Console.WriteLine statements,
there is an assignment of
cust6 to cust5. The Console.WriteLine statements show the
effect of the assignment, and here’s what they show when the program runs:
Before Reference Assignment:
cust5: John Smith

cust6: Jane Doe
After Reference Assignment:
cust5: Jane Doe
cust6: Jane Doe
You can see from the preceding output that the value of the Name property in cust5 and
cust6 is different. There are no surprises here because that is what the code explicitly did
when declaring and instantiating the variables. What could be misleading is the result,
89
Reference Type and Value Type Assignment
4
after assignment, where both cust5.Name and cust6.Name produce the same results. The
following statement and results show why the preceding results could be misleading:
cust6.Name = “John Smith”;
Console.WriteLine(“After modifying the contents of a Reference type object:”);
Console.WriteLine(“cust5: {0}”, cust5.Name);
Console.WriteLine(“cust6: {0}”, cust6.Name);
And here’s the output:
After modifying the contents of a Reference type object:
cust5: John Smith
cust6: John Smith
In the preceding code, the only assignment was to change the Name field of cust6 to ”John
Smith”, but look at the results. The Name fields of both cust5 and cust6 are set to the
same value. What’s tricky is that the code in this last example didn’t use the
cust5 vari-
able at all.
Now, let’s see what happened. To start off, look at Figure 4.7, which shows what the
memory layout is right after
cust5 and cust6 were declared and initialized.
Because
cust5 and cust6 are reference type variables, they hold a reference (address) of an

object on the heap. Figure 4.7 represents the reference as an arrow coming from the vari-
ables
cust5 and cust6 to Customer objects. These Customer objects were allocated during
runtime when the
new Customer expression ran. Each object contains a different value in
Managed Heap
cust5
new Customer()
{Name = “John Smith”}
cust6
new Customer()
{Name = “John Smith”}
FIGURE 4.7 Two reference type variables declared and initialized separately.
90
CHAPTER 4 Understanding Reference Types and Value Types
Managed Heap
cust5
new Customer()
{Name = “John Smith”}
cust6
new Customer()
{Name = “Jane Doe”}
FIGURE 4.8 Assigning one reference type variable to another.
its
Name field. Next, you see the effects of assigning the cust6 variable to cust5, shown in
Figure 4.8.
The assignment of cust6 to cust5 didn’t copy the object; it actually copied the contents of
the
cust6 variable, which was the reference to the object. So, as shown in Figure 4.8, both
cust5 and cust6 now refer to the same object. Figure 4.9 shows what happens after modi-

fying the
Name field of cust6.
Figure 4.9 shows that changing the
Name field of cust6 actually modified the object
referred to by
cust6. This is important because both cust5 and cust6 refer to the same
object, and any modification to that object will be seen through both the
cust5 and cust6
reference. That’s why the output, after we modified cust6, showed that the Name field of
cust5 and cust6 were the same.
Managed Heap
cust5
new Customer()
{Name = “John Smith”}
cust6
new Customer()
{Name = “Jane Doe”}
FIGURE 4.9 Affects of modifying the contents of an object that has multiple references to it.
91
Reference Type and Value Type Assignment
4
Looking at reference type assignment in a more general perspective, you can assume
that modifications to the contents of an object are visible through any reference to the
same object.
Assignment behavior isn’t the same for reference types and value types. The next section
shows how value type assignment works and how reference type and value type assign-
ment differs.
Value Type Assignment
Value type assignment is a whole lot simpler than reference type assignment. Because a
value type variable holds the entire object, rather than only a reference to the object, no

special actions can be occurring behind the scenes to affect that value. Here’s an example
of a value type assignment, using the Money struct that was created earlier in the chapter:
Money cash1;
Money cash2;
cash1.Amount = 50.00m;
cash2.Amount = 75.00m;
Console.WriteLine(“Before Value Assignment:”);
Console.WriteLine(“cash1.Amount: {0}”, cash1.Amount);
Console.WriteLine(“cash2.Amount: {0}”, cash2.Amount);
cash1 = cash2;
Console.WriteLine(“After Value Assignment:”);
Console.WriteLine(“cash1.Amount: {0}”, cash1.Amount);
Console.WriteLine(“cash2.Amount: {0}”, cash2.Amount);
The pattern used for value type assignment is similar to that used for the reference type
assignment in the previous paragraph. This is intentional so that you can see the differ-
ences. In the preceding code, the results after assigning
cash2 to cash1 is that the Amount
field of both cash1 and cash2 is the same, as shown in the following output:
Before Value Assignment:
cash1.Amount: 50.00
cash2.Amount: 75.00
After Value Assignment:
cash1.Amount: 75.00
cash2.Amount: 75.00
No surprises here, except perhaps if you expect the same behavior as what you saw in the
reference type section, but that isn’t the case because value type assignment is not the
92
CHAPTER 4 Understanding Reference Types and Value Types
same as reference type assignment. The next example demonstrates how value types are
separate entities that hold their own values:

cash2.Amount = 50.00m;
Console.WriteLine(“After modifying contents of Value type object:”);
Console.WriteLine(“cash1.Amount: {0}”, cash1.Amount);
Console.WriteLine(“cash2.Amount: {0}”, cash2.Amount);
After we set the Amount field of cash2 to 50.00m, the output, shown next, demonstrates
that there was no affect on
cash1, which was set to 75.00 during the previous
example:
After modifying contents of Value type object:
cash1.Amount: 75.00
cash2.Amount: 50.00
When you perform value type assignment, the only object affected is the one being
assigned.
Now, you can see that this is different from reference type assignment. Value type assign-
ment affects only the object held by the variable being assigned to, but reference type
assignment will affect an object in the heap that could be referred to by multiple variables.
Therefore, while writing code, be aware of the type of the variable being assigned to
ensure that subsequent modifications produce the results you expect.
In the next section, you learn a few more of the differences between reference types and
value types.
More Differences Between Reference Types and
Value Types
In addition to memory allocation, variable contents, and assignment behavior, there are
other differences between reference types and value types. These differences can be catego-
rized as inheritance, construction, finalization, and size recommendations. These issues
are covered thoroughly in later chapters, so I just give you a quick overview here of what
they mean and let you know where in this book you can get more in-depth information.
Inheritance Differences Between Reference Types and Value Types
Reference types support implementation and interface inheritance. They can derive from
another reference type or have a reference type derive from them. However, value types

can’t derive from other value types. Chapter 9 goes into detail about how implementation
inheritance works in C#, but here’s a quick example:
class Customer
93
More Differences Between Reference Types and Value Types
4
{
public string Name;
}
class PotentialCustomer : Customer
{
public string SalesPerson;
}
class RegularCustomer : Customer
{
public DateTime LastPurchase;
}
In the preceding example, Customer is the base class. PotentialCustomer and
RegularCustomer derive from Customer, that is, they are types of Customer, as indicated
by the
: (colon) on the right side of the class identifier.
SINGLE-IMPLEMENTATION INHERITANCE
Reference types support single-implementation inheritance, meaning that they can
derive only from a single class. However, both reference types and value types support
multiple-interface inheritance where they can implement many interfaces.
Construction and Finalization Differences Between Reference Types
and Value Types
Construction is the process that occurs during the instantiation process to help ensure an
object has the information you want it to have when it starts up. Chapter 15, “Managing
Object Lifetime,” discusses this in detail, but for reference, you should know that you

can’t create a default constructor for a value type. Chapter 3, “Writing C# Expressions and
Statements,” contains the default values of built-in types, which are some form of zero.
Value types are automatically initialized when declared, which is why the
cash1 and cash2
variables in the previous section didn’t need to be instantiated with new Money().
Finalization is a process that occurs when the CLR is performing garbage collection, clean-
ing up objects from memory. Value type objects don’t have a finalizer, but reference types
do. A finalizer is a special class member that could be called by the CLR garbage collector
during cleanup. Value types are either garbage collected with their containing type or
when the method they are associated with ends. Therefore, value types don’t need a final-
izer. Because garbage collection is a process that operates on heap objects, it is possible for
a reference type to have a finalizer. Chapter 15 goes into the garbage collection process in
detail, giving you the information you need to make effective design decisions on imple-
menting finalizers, and other techniques for managing object lifetime effectively.
94
CHAPTER 4 Understanding Reference Types and Value Types
Object Size Considerations for Reference Types and Value Types
Because of the way an object is allocated differently for reference types and value types,
you might need to consider the impact of the size of the object on resources and perfor-
mance. Reference type objects can generally be whatever size you need because the vari-
able just holds a reference, which is 32 bits on a 32-bit CLR and 64 bits on a 64-bit CLR.
However, value type size might need more thought.
If a value type is a field inside of a class or at some level of containment that puts it into a
class, its size shouldn’t be much concern. However, think about scenarios where you might
need to pass a value type to a method. In the case of a reference type argument, it is simply
the reference being passed, but for a value type argument, the entire object is passed.
With local variables and parameters that are value types, the CLR pushes the entire object
onto the stack when calling the associated method. Now, instead of the 4 or 8 bytes that
would have been pushed with a reference type, you have potentially much more informa-
tion to push, which represents overhead. A recommended rule of thumb for value type

size is 16 bytes. I’ve benchmarked this by calling methods that have value type parameters
of differing sizes and verified that performance does tend to deteriorate faster as the size of
the value type increases above 16 bytes. That said, you should also look at how many
times you’ll call the method before the performance implications matter to you; that is,
consider whether the method is called frequently or in a loop.
REFERENCE TYPE OR VALUE TYPE: WHICH TO CHOOSE?
As a rule of thumb, I typically create new types as classes, reference types. The excep-
tion is when I have a type that should behave more like a value type. For example, a
ComplexNumber would probably be better as a struct value type, because of its memo-
ry allocation, assignment behavior, and other capabilities such as math operations that
are similar to built-in value types such as int and double.
Among the many tips you get from this chapter for working with both reference types and
value types, you also have a lot of facts related to tradeoffs. Look at the differences to see
what matters the most in your situation and choose the tradeoffs that are best for you.
The next section looks at specific .NET types, building on what you learned so far in this
chapter.
C# and .NET Framework Types
In Chapter 1, “Introducing the .NET Platform,” you learned about the CTS and in
Chapter 3, you learned how to use the C# built-in types. This section melds these two
features together and builds upon them so that you can see how C# types support the
CTS. We also look at a couple .NET Framework types (specifically,
DateTime and Guid) that
are important but don’t have C# keyword aliases.
95
C# and .NET Framework Types
4
C# Aliases and the CTS
C# types are specified with keywords that alias .NET CLR types. Table 4.1 shows all the
.NET types and which C# keywords alias them.
Some of the types in Table 4.1 are marked as “No alias” because C# doesn’t have a

keyword that aliases that type, but the type is still important. For example,
DBNull is a
value that comes from a database field that is set to
NULL but is not equal to the C# null
value. The following sections show you how to work with the System.Guid and
System.DateTime types, which don’t have C# aliases either.
Using System.Guid
A globally unique identifier (GUID) is a 128-bit string of characters used whenever there is
a need for a unique way to identify something. You can see GUIDs used throughout the
Microsoft operating system. Just look at the registry; all of those long strings of characters
TABLE 4.1 .NET Types with Matching C# Aliases
.NET Type C# Alias
System.Boolean Bool
System.Byte Byte
System.Char Char
System.DateTime
No alias
System.DBNull
No alias
System.Decimal decimal
System.Double double
System.Guid
No alias
System.Int16 short
System.Int32 Int
System.Int64 Long
System.IntPtr
No alias
System.Object object
System.SByte sbyte

System.Single Float
System.String string
System.TimeSpan
No alias
System.TimeZone
No alias
System.UInt16 ushort
System.UInt32 Uint
System.UInt64 ulong
System.UIntPtr
No alias
96
CHAPTER 4 Understanding Reference Types and Value Types
are GUIDs. Another place GUIDs are used is as unique columns in SQL Server for when
you need unique IDs across separate databases. Generally, any time you need a unique
value, you can reliably use a GUID.
GUIDs are Microsoft’s implementation of universally unique identifiers (UUID), an Open
Software Foundation (OSF) standard. You can find more information about UUIDs at
Wikipedia, />.NET implements the GUID as the
System.Guid (Guid) struct. You can use the Guid type to
generate new GUIDs or work with an existing
Guid value. Here’s an example of how you
don’t want to create a new GUID:
Guid uniqueVal1 = new Guid();
Console.WriteLine(“uniqueVal1: {0}”, uniqueVal1.ToString());
The problem here is that Guid is a value type and it is immutable (can’t be modified). If
you recall from a previous section, value types have a default (no parameter) constructor
that you can’t override, and the default value is some form of zero. Therefore, the follow-
ing output from the preceding code makes sense:
uniqueVal1: 00000000-0000-0000-0000-000000000000

Because Guid is immutable, you can’t change this value. Fortunately, if you have a Guid
value already defined, you are still able to work with it because Guid has several overloads
for specifying an existing GUID. Here’s the
Guid constructor overload that takes a string:
uniqueVal1 = new Guid(“89e9f11b-00ee-47dc-be15-01f70eeac3f9”);
Console.WriteLine(“uniqueVal1: {0}”, uniqueVal1.ToString());
The preceding code results in the following output:
uniqueVal1: 89e9f11b-00ee-47dc-be15-01f70eeac3f9
You’ll often want to generate your own GUIDs from scratch, and the instance, uniqueVal1,
doesn’t have methods to accomplish this. In this case, you’ll want to use the
NewGuid
method of Guid to generate a new GUID, like this:
uniqueVal1 = Guid.NewGuid();
Console.WriteLine(“uniqueVal1: {0}”, uniqueVal1.ToString());
As you might already expect, the output is a unique ID, as follows:
uniqueVal1: cabfe0ba-fa72-4c5c-969f-e76821949ff1
In fact, every time you run the preceding code, the output will differ. This is because, for
all practical purposes, the GUID will be unique across space and time. Speaking of time,
the next section covers another important .NET value type that isn’t aliased by a C#
keyword,
System.DateTime.
97
C# and .NET Framework Types
4
Working with System.DateTime
Many programs need to work with dates and times. Fortunately, .NET has the
System.DateTime (DateTime) type to help out. You can use DateTime to hold DateTime
values, extract portions such as the day of a DateTime, and perform arithmetic calcula-
tions. You can also parse strings into
DateTime instances and emit DateTime instances as a

string in the format of your choice.
Creating New DateTime Objects
The default value of
DateTime is Jan 1, 0001 at 12:00 midnight. Here’s how to create the
default
DateTime:
DateTime date = new DateTime();
Console.WriteLine(“date: {0}”, date);
And the output is as follows:
date: 1/1/0001 12:00:00 AM
You can initialize the DateTime through the constructor, which has several overloads.
Here’s an example of how to use one of the more detailed overloads:
date = new DateTime(2008, 7, 4, 21, 35, 15, 777);
Console.WriteLine(“date: {0}”, date);
Here’s the output:
date: 7/4/2008 9:35:15 PM
Quite often, you’ll just need the current date and time, like this:
date = DateTime.Now;
Console.WriteLine(‘date: {0}’, date);
Which produces the following output:
date: 11/4/2007 8:42:39 PM
This section worked with the entire DateTime, but sometimes you only want to have
access to parts of a
DateTime. The next section shows you how to extract different parts of
a
DateTime.
Extracting Parts of a DateTime
You can access any part of a
DateTime instance, including parts of the date, day of the
week, or day of the year. Here’s an example:

Console.WriteLine(
“{0} day {1} of the month is day {2} of the year”,
date.DayOfWeek, date.Day, date.DayOfYear);
98
CHAPTER 4 Understanding Reference Types and Value Types
And here’s the output
Friday day 4 of the month is day 186 of the year
You can also extract other parts of the date (for example, month and hour). If you’re using
VS2008, you can see all of them in IntelliSense.
Next, you learn how to manipulate
DateTime objects.
DateTime Math and TimeSpan
You often need to manipulate
DateTime objects. However, because they are immutable,
you need to create a new instance with a modified value. Here’s an example:
Console.WriteLine(“date before AddDays(1): {0}”, date);
date.AddDays(1);
Console.WriteLine(“date after AddDays(1): {0}”, date);
The preceding code calls the AddDays method, trying to add a day, but the original value,
date, doesn’t change, as shown by the following output:
date before AddDays(1): 11/4/2007 8:48:26 PM
date after AddDays(1): 11/4/2007 8:48:26 PM
This just proves that DateTime is immutable and hopefully saves you from making this
common mistake yourself. Here’s how you can change the
date variable:
Console.WriteLine(“date before AddDays(1): {0}”, date);
date = date.AddDays(1);
Console.WriteLine(“date after AddDays(1): {0}”, date);
If you look at the documentation for AddDays and other methods of DateTime that manip-
ulate dates, you see that they return a

DateTime. Just reassign the return value to the origi-
nal variable, as in the preceding example, and it will work fine. Here’s the output:
date before AddDays(1): 11/4/2007 8:52:08 PM
date after AddDays(1): 11/5/2007 8:52:08 PM
The preceding date shows that date was truly modified because the day was incremented
by one as intended.
You can also use the
DateTime type for quick-and-dirty performance benchmarks. Here’s
some code that does
DateTime math and produces a TimeSpan object to tell how long an
algorithm took. Here’s an example of how you might go about this:
int testIterations = int.MaxValue/4;
DateTime start = DateTime.Now;
99
C# and .NET Framework Types
4
for (int i = 0; i < testIterations; i++)
{
Money cash;
cash.Amount = decimal.MaxValue;
}
DateTime finish = DateTime.Now;
TimeSpan elapsedTime = finish - start;
Console.WriteLine(“Elapsed Time: {0}”, elapsedTime);
The for loop is code that you might change to hold whatever algorithm you need to
benchmark. The example gets the current time before and after the
for loop. Notice how
the mathematical operation, subtracting
start from finish, produced a TimeSpan. A
TimeSpan is used to represent an amount of time, as opposed to an exact time as held by

DateTime. Any time you perform a mathematical operation on DateTime types, the return
value is a
TimeSpan. Here’s the output:
Elapsed Time: 00:00:16.2834144
Here’s an exercise that you might find fun. Create a few methods that take value type
parameters of varying sizes. For example, you could create multiple versions of
Money and
add more decimal fields to make them bigger. Then use the benchmark preceding code to
call each method a specified number of times and compare the
TimeSpan results of each.
Do the same with a reference type. This exercise will let you know at what point the size
of the value type affects performance.
Converting Between DateTime and string Types
If the user inputs a
date and/or time, it will often reach the code in the form of a string.
Alternatively, sometimes a
DateTime needs to be formatted and presented in the form of a
string. This section shows you how to read string types into a
DateTime and how to format
the output of a
DateTime.
The DateTime type has a Parse method you can use to get the value of a string. Here’s
how you can use it:
Console.Write(“Please enter a date (mm/dd/yyyy): “);
string dateStr = Console.ReadLine();
date = DateTime.Parse(dateStr);
Console.WriteLine(“You entered ‘{0}’”, date);
The user’s input, retrieved by the call to Console.ReadLine, came back in the form of a
string,
dateStr. The call to DateTime.Parse converted the string to a DateTime, which can

now be manipulated with
DateTime members as described in previous sections.
100
CHAPTER 4 Understanding Reference Types and Value Types
You could see an exception message after typing in the date on the command line. This
would be because the date was not typed in the correct format. In Chapter 11, “Error and
Exception Handling,” you’ll learn how to handle errors like this, and Chapter 10 shows you
how to use the
TryParse method, which is effective for handling user input. Here’s the
output:
Please enter a date (mm/dd/yyyy): 11/04/2007
You entered ‘11/4/2007 12:00:00 AM’
Notice from the output that the response to the Please enter a date (mm/dd/yyyy)
prompt was 11/04/2007. However, the response included the time, which you may or may
not want. In case you don’t want the time to show or you want the output to appear
differently, you have the option to specify the output format. Here’s what you could do to
remove the time from the preceding output:
Console.WriteLine(“Date Only: {0:d}”, date);
The preceding example used the format specifier in the placeholder of
Console.WriteLine’s format string parameter. You could have also used the DateTime
ToString method like this:
Console.WriteLine(“Date Only: {0}”, date.ToString(“d”));
A lowercase d means to print a short date time. Here’s what it looks like:
Date Only: 11/4/2007
In addition to the d, there are several other predefined format specifiers, shown in Table 4.2.
TABLE 4.2 Standard DateTime Format Strings
Format String Output for date = new DateTime(2008, 7, 4, 21, 35, 15, 777);
D 7/4/2008
D Friday, July 04, 2008
T 9:35 PM

T 9:35:15 PM
F Friday, July 04, 2008 9:35 PM
F Friday, July 04, 2008 9:35:15 PM
G 7/4/2008 9:35 PM
G 7/4/2008 9:35:15 PM
M | M July 04
r | R Fri, 04 Jul 2008 21:35:15 GMT
S 2008-07-04T21:35:15
U 2008-07-04 21:35:15Z
U Saturday, July 05, 2008 3:35:15 AM
Y | Y July, 2008
101
C# and .NET Framework Types
4
Table 4.2 shows a predefined set of strings for formatting dates, but you aren’t limited by
this list. You can also customize
DateTime strings. Here’s an example that ensures two
characters for each part of the date:
Console.WriteLine(“MM/dd/yy: {0:MM/dd/yy}”, date);
Based on the input used for Table 4.2, the output would be this:
MM/dd/yy: 07/04/08
The number of possible custom format strings is much more than is practical for listing
here, but you can find the entire list by opening the .NET Framework Documentation
Help file and searching for “formatting strings, custom date and time format strings” in
the index. Rather than list all the possible format strings, Table 4.3 lists a few that could
be useful; it also includes the ones used in the preceding example.
As with
DateTime, most of the other built-in types are value types whose value is always
defined. The next section discusses nullable types and helps you deal with those situations
where the value you have to work with is not defined, but is

null.
TABLE 4.3 Common Custom DateTime Format Strings
Format String Purpose
D
Day (1–31)
Dd
Two-digit day (01–31)
Ddd
Abbreviated name of day (for example, Fri)
dddd
Full name of day (for example, Friday)
M
Month (1–12)
MM
Two-digit month (01–12)
MMM
Abbreviated name of month (for example, Jul)
MMMM
Full month name (for example, July)
Yy
Two-digit year
yyyy
Four-digit year
H
Hour (1–12)
Hh
Two-digit hour (01–12)
H
24-hour clock (0–23)
HH

Two-digit 24-hour clock (00–23)
M
Minutes (0–59)
Mm
Two-digit minutes (00–59)
S
Seconds (0–59)
Ss
Two-digit seconds (00–59)
102
CHAPTER 4 Understanding Reference Types and Value Types
Nullable Types
As you’ve learned previously, the default value for reference types is null, and the default
value for value types is some form of zero. Sometimes, you receive values from external
sources, such as XML files or databases that don’t have a value—they could be
nil or
null, respectively. For reference types, this is no problem. However, for value types, you
have to find your own solution for working with
null values.
This problem, not being able to assign null to value types, was alleviated in C# 2.0 with
the introduction of a feature called nullable types. It essentially allows you to declare
nullable value types to which, as the name suggests, you can assign the value
null.
Think about how useful this is. SQL Server has column types that map to the C# built-in
types. For example, SQL Server
money and datetime column types map to C# decimal and
DateTime types. In SQL Server, these values can be null. However, that is particularly
problematic when dealing with an application that interfaces with multiple databases. You
could be working with FoxPro, SQL Server, and another database, and they all return a
different default

DateTime value, which makes a mapping between DBNull and the default
DateTime value impractical. This is just one element of complexity you have to deal with
for
null database values, and there are many more. By having nullable types, we can more
quickly write easier-to-maintain code.
An entire part of this book, Chapters 19 to 23, provides extensive coverage of working
with data in .NET, and this material is applicable in the context of those chapters.
However, the examples here assume that there is code that has extracted data from a data
source that contains
null values. The following example assumes there is a value from a
database for the creation date of a record:
DateTime? createDate = null;
The most noticeable part of the preceding statement is the question mark suffix, ?, on the
DateTime type. The proper terminology for this is that the type of createDate is a nullable
DateTime. It is explicitly set to the value null, which is not possible in non-nullable value
type objects.
There are a couple ways to check a nullable type to see whether it has the value
null.
Here’s an example:
bool isNull;
isNull = createDate == null;
isNull = createDate.HasValue;
Using the C# equals operator, you can learn whether createDate is set to null. Calling
HasValue will return true if createDate is not null. The C# not equals operator, !=, is
equivalent in behavior to
HasValue.

×