Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 105 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
105
Dung lượng
12,95 MB
Nội dung
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.