Home Random Page


CATEGORIES:

BiologyChemistryConstructionCultureEcologyEconomyElectronicsFinanceGeographyHistoryInformaticsLawMathematicsMechanicsMedicineOtherPedagogyPhilosophyPhysicsPolicyPsychologySociologySportTourism






Nbsp;   Boxing and Unboxing Value Types

Value types are lighter weight than reference types because they are not allocated as objects in the managed heap, not garbage collected, and not referred to by pointers. However, in many cases, you must get a reference to an instance of a value type. For example, let’s say that you wanted to cre- ate an ArrayList object (a type defined in the System.Collections namespace) to hold a set of Point structures. The code might look like this.

 

// Declare a value type. struct Point {

public Int32 x, y;

}

 

public sealed class Program { public static void Main() {

ArrayList a = new ArrayList();

Point p; // Allocate a Point (not in the heap). for (Int32 i = 0; i < 10; i++) {

p.x = p.y = i; // Initialize the members in the value type. a.Add(p); // Box the value type and add the

// reference to the Arraylist.

}

...

}

}

With each iteration of the loop, a Point’s value type fields are initialized. Then the Point is stored in the ArrayList. But let’s think about this for a moment. What is actually being stored in the ArrayList? Is it the Point structure, the address of the Point structure, or something else entirely?


To get the answer, you must look up ArrayList’s Add method and see what type its parameter is

defined as. In this case, the Add method is prototyped as follows.

 

public virtual Int32 Add(Object value);

 

From this, you can plainly see that Add takes an Object as a parameter, indicating that Add re- quires a reference (or pointer) to an object on the managed heap as a parameter. But in the preced- ing code, I’m passing p, a Point, which is a value type. For this code to work, the Point value type must be converted into a true heap-managed object, and a reference to this object must be obtained.

It’s possible to convert a value type to a reference type by using a mechanism called boxing. Inter- nally, here’s what happens when an instance of a value type is boxed:

1.Memory is allocated from the managed heap. The amount of memory allocated is the size required by the value type’s fields plus the two additional overhead members (the type object pointer and the sync block index) required by all objects on the managed heap.

2.The value type’s fields are copied to the newly allocated heap memory.

3.The address of the object is returned. This address is now a reference to an object; the value type is now a reference type.

The C# compiler automatically produces the IL code necessary to box a value type instance, but you still need to understand what’s going on internally so that you’re aware of code size and perfor- mance issues.

In the preceding code, the C# compiler detected that I was passing a value type to a method that requires a reference type, and it automatically emitted code to box the object. So at run time, the fields currently residing in the Point value type instance p are copied into the newly allocated Point object. The address of the boxed Point object (now a reference type) is returned and is then passed to the Add method. The Point object will remain in the heap until it is garbage collected. The Point value type variable (p) can be reused because the ArrayList never knows anything about it. Note that the lifetime of the boxed value type extends beyond the lifetime of the unboxed value type.



       
   
 
 


Now that you know how boxing works, let’s talk about unboxing. Let’s say that you want to grab

the first element out of the ArrayList by using the following code.

 

Point p = (Point) a[0];

 

Here you’re taking the reference (or pointer) contained in element 0 of the ArrayList and trying to put it into a Point value type instance, p. For this to work, all of the fields contained in the boxed Point object must be copied into the value type variable, p, which is on the thread’s stack. The CLR accomplishes this copying in two steps. First, the address of the Point fields in the boxed Point ob- ject is obtained. This process is called unboxing. Then, the values of these fields are copied from the heap to the stack-based value type instance.

Unboxing is not the exact opposite of boxing. The unboxing operation is much less costly than boxing. Unboxing is really just the operation of obtaining a pointer to the raw value type (data fields) contained within an object. In effect, the pointer refers to the unboxed portion in the boxed instance. So, unlike boxing, unboxing doesn’t involve the copying of any bytes in memory. Having made this important clarification, it is important to note that an unboxing operation is typically followed by copying the fields.

Obviously, boxing and unboxing/copy operations hurt your application’s performance in terms of both speed and memory, so you should be aware of when the compiler generates code to perform these operations automatically and try to write code that minimizes this code generation.

Internally, here’s exactly what happens when a boxed value type instance is unboxed:

1.If the variable containing the reference to the boxed value type instance is null, a Null­ ReferenceException is thrown.

2.If the reference doesn’t refer to an object that is a boxed instance of the desired value type, an InvalidCastException is thrown.1

The second item in the preceding list means that the following code will not work as you might expect.

 

public static void Main() { Int32 x = 5;

Object o = x; // Box x; o refers to the boxed object Int16 y = (Int16) o; // Throws an InvalidCastException

}

 

Logically, it makes sense to take the boxed Int32 that o refers to and cast it to an Int16. How- ever, when unboxing an object, the cast must be to the exact unboxed value type—Int32 in this case. Here’s the correct way to write this code.

 

public static void Main() { Int32 x = 5;

Object o = x; // Box x; o refers to the boxed object Int16 y = (Int16)(Int32) o; // Unbox to the correct type and cast

}

 

 
 

1 The CLR also allows you to unbox a value type into a nullable version of the same value type. This is discussed in Chapter 19.


I mentioned earlier that an unboxing operation is frequently followed immediately by a field copy.

Let’s take a look at some C# code demonstrating that unbox and copy operations work together.

 

public static void Main() { Point p;

p.x = p.y = 1;

Object o = p; // Boxes p; o refers to the boxed instance

 

p = (Point) o; // Unboxes o AND copies fields from boxed

// instance to stack variable

}

 

On the last line, the C# compiler emits an IL instruction to unbox o (get the address of the fields in the boxed instance) and another IL instruction to copy the fields from the heap to the stack-based variable p.

Now look at this code.

 

public static void Main() { Point p;

p.x = p.y = 1;

Object o = p; // Boxes p; o refers to the boxed instance

 

// Change Point's x field to 2

p = (Point) o; // Unboxes o AND copies fields from boxed

// instance to stack variable

p.x = 2; // Changes the state of the stack variable

o = p; // Boxes p; o refers to a new boxed instance

}

 

The code at the bottom of this fragment is intended only to change Point’s x field from 1 to 2. To do this, an unbox operation must be performed, followed by a field copy, followed by changing the field (on the stack), followed by a boxing operation (which creates a whole new boxed instance in the managed heap). Hopefully, you see the impact that boxing and unboxing/copying operations have on your application’s performance.

Some languages, such as C++/CLI, allow you to unbox a boxed value type without copying the fields. Unboxing returns the address of the unboxed portion of a boxed object (ignoring the object’s type object pointer and sync block index overhead). You can now use this pointer to manipulate the unboxed instance’s fields (which happen to be in a boxed object on the heap). For example, the previ- ous code would be much more efficient if written in C++/CLI, because you could change the value

of Point’s x field within the already boxed Point instance. This would avoid both allocating a new

object on the heap and copying all of the fields twice!

       
   
 
 


Let’s look at a few more examples that demonstrate boxing and unboxing.

 

public static void Main() {

Int32 v = 5; // Create an unboxed value type variable. Object o = v; // o refers to a boxed Int32 containing 5. v = 123; // Changes the unboxed value to 123

 

Console.WriteLine(v + ", " + (Int32) o); // Displays "123, 5"

}

 

In this code, can you guess how many boxing operations occur? You might be surprised to discover that the answer is three! Let’s analyze the code carefully to really understand what’s going on. To help you understand, I’ve included the IL code generated for the Main method shown in the preceding code. I’ve commented the code so that you can easily see the individual operations.

 

.method public hidebysig static void Main() cil managed

{

.entrypoint

// Code size 45 (0x2d)

.maxstack 3

.locals init ([0]int32 v,

[1] object o)

// Load 5 into v. IL_0000: ldc.i4.5 IL_0001: stloc.0

 

// Box v and store the reference pointer in o. IL_0002: ldloc.0

IL_0003: box [mscorlib]System.Int32 IL_0008: stloc.1

 

// Load 123 into v. IL_0009: ldc.i4.s 123 IL_000b: stloc.0

 

// Box v and leave the pointer on the stack for Concat. IL_000c: ldloc.0

IL_000d: box [mscorlib]System.Int32

 

// Load the string on the stack for Concat. IL_0012: ldstr ", "

 

// Unbox o: Get the pointer to the In32's field on the stack. IL_0017: ldloc.1

IL_0018: unbox.any [mscorlib]System.Int32

 

// Box the Int32 and leave the pointer on the stack for Concat. IL_001d: box [mscorlib]System.Int32

 

// Call Concat.

IL_0022: call string [mscorlib]System.String::Concat(object,

object, object)


// The string returned from Concat is passed to WriteLine.

IL_0027: call void [mscorlib]System.Console::WriteLine(string)

 

// Return from Main terminating this application. IL_002c: ret

} // end of method App::Main

 

First, an Int32 unboxed value type instance (v) is created on the stack and initialized to 5. Then a variable (o) typed as Object is created, and is initialized to point to v. But because reference type

variables must always point to objects in the heap, C# generated the proper IL code to box and store the address of the boxed copy of v in o. Now the value 123 is placed into the unboxed value type instance v; this has no effect on the boxed Int32 value, which keeps its value of 5.

Next is the call to the WriteLine method. WriteLine wants a String object passed to it, but there is no string object. Instead, these three items are available: an unboxed Int32 value type in- stance (v), a String (which is a reference type), and a reference to a boxed Int32 value type instance

(o) that is being cast to an unboxed Int32. These must somehow be combined to create a String.

 

To create a String, the C# compiler generates code that calls String’s static Concat method. There are several overloaded versions of the Concat method, all of which perform identically—the only difference is in the number of parameters. Because a string is being created from the concatena- tion of three items, the compiler chooses the following version of the Concat method.

 

public static String Concat(Object arg0, Object arg1, Object arg2);

 

For the first parameter, arg0, v is passed. But v is an unboxed value parameter and arg0 is an Object, so v must be boxed and the address to the boxed v is passed for arg0. For the arg1 pa- rameter, the "," string is passed as a reference to a String object. Finally, for the arg2 parameter, o (a reference to an Object) is cast to an Int32. This requires an unboxing operation (but no copy

operation), which retrieves the address of the unboxed Int32 contained inside the boxed Int32. This unboxed Int32 instance must be boxed again and the new boxed instance’s memory address passed for Concat’s arg2 parameter.

The Concat method calls each of the specified objects’ ToString method and concatenates each object’s string representation. The String object returned from Concat is then passed to Write­ Line to show the final result.

I should point out that the generated IL code is more efficient if the call to WriteLine is written as follows.

 

Console.WriteLine(v + ", " + o);// Displays "123, 5"

 

This line is identical to the earlier version except that I’ve removed the (Int32) cast that preceded the variable o. This code is more efficient because o is already a reference type to an Object and its address can simply be passed to the Concat method. So, removing the cast saved two operations:

an unbox and a box. You can easily see this savings by rebuilding the application and examining the generated IL code, as shown in the following code.


.method public hidebysig static void Main() cil managed

{

.entrypoint

// Code size 35 (0x23)

.maxstack 3

.locals init ([0] int32 v,

[1] object o)

 

// Load 5 into v. IL_0000: ldc.i4.5 IL_0001: stloc.0

 

// Box v and store the reference pointer in o. IL_0002: ldloc.0

IL_0003: box [mscorlib]System.Int32 IL_0008: stloc.1

 

// Load 123 into v. IL_0009: ldc.i4.s 123 IL_000b: stloc.0

 

// Box v and leave the pointer on the stack for Concat. IL_000c: ldloc.0

IL_000d: box [mscorlib]System.Int32

 

// Load the string on the stack for Concat. IL_0012: ldstr ", "

 

// Load the address of the boxed Int32 on the stack for Concat. IL_0017: ldloc.1

 

// Call Concat.

IL_0018: call string [mscorlib]System.String::Concat(object,

object, object)

 

// The string returned from Concat is passed to WriteLine.

IL_001d: call void [mscorlib]System.Console::WriteLine(string)

 

// Return from Main terminating this application. IL_0022: ret

} // end of method App::Main

 

A quick comparison of the IL for these two versions of the Main method shows that the version without the (Int32) cast is 10 bytes smaller than the version with the cast. The extra unbox/box steps in the first version are obviously generating more code. An even bigger concern, however, is that the extra boxing step allocates an additional object from the managed heap that must be gar- bage collected in the future. Certainly, both versions give identical results, and the difference in speed isn’t noticeable, but extra, unnecessary boxing operations occurring in a loop cause the performance and memory usage of your application to be seriously degraded.

You can improve the previous code even more by calling WriteLine like this.

 

Console.WriteLine(v.ToString() + ", " + o); // Displays "123, 5"


Now ToString is called on the unboxed value type instance v, and a String is returned. String objects are already reference types and can simply be passed to the Concat method without requir- ing any boxing.

Let’s look at yet another example that demonstrates boxing and unboxing.

 

public static void Main() {

Int32 v = 5; // Create an unboxed value type variable.

Object o = v; // o refers to the boxed version of v.

 

v = 123; // Changes the unboxed value type to 123 Console.WriteLine(v); // Displays "123"

 

v = (Int32) o; // Unboxes and copies o into v Console.WriteLine(v); // Displays "5"

}

 

How many boxing operations do you count in this code? The answer is one. The reason that there is only one boxing operation is that the System.Console class defines a WriteLine method that accepts an Int32 as a parameter.

 

public static void WriteLine(Int32 value);

 

In the two preceding calls to WriteLine, the variable v, an Int32 unboxed value type instance, is passed by value. Now it may be that WriteLine will box this Int32 internally, but you have no

control over that. The important thing is that you’ve done the best you could and have eliminated the boxing from your own code.

If you take a close look at the FCL, you’ll notice many overloaded methods that differ based on their value type parameters. For example, the System.Console type offers several overloaded ver- sions of the WriteLine method.

 

public static void WriteLine(Boolean); public static void WriteLine(Char); public static void WriteLine(Char[]); public static void WriteLine(Int32); public static void WriteLine(UInt32); public static void WriteLine(Int64); public static void WriteLine(UInt64); public static void WriteLine(Single); public static void WriteLine(Double); public static void WriteLine(Decimal); public static void WriteLine(Object); public static void WriteLine(String);

 

You’ll also find a similar set of overloaded methods for System.Console’s Write method, System.IO.BinaryWriter’s Write method, System.IO.TextWriter’s Write and Write­ Line methods, System.Runtime.Serialization.SerializationInfo’s AddValue method, System.Text.StringBuilder’s Append and Insert methods, and so on. Most of these meth- ods offer overloaded versions for the sole purpose of reducing the number of boxing operations for the common value types.


If you define your own value type, these FCL classes will not have overloads of these methods that accept your value type. Furthermore, there are a bunch of value types already defined in the FCL for which overloads of these methods do not exist. If you call a method that does not have an overload for the specific value type that you are passing to it, you will always end up calling the overload that takes an Object. Passing a value type instance as an Object will cause boxing to occur, which will adversely affect performance. If you are defining your own class, you can define the methods in the class to be generic (possibly constraining the type parameters to be value types). Generics give you

a way to define a method that can take any kind of value type without having to box it. Generics are

discussed in Chapter 12.

 

One last point about boxing: if you know that the code that you’re writing is going to cause the compiler to box a single value type repeatedly, your code will be smaller and faster if you manually box the value type. Here’s an example.

 

using System;

 

public sealed class Program { public static void Main() {

Int32 v = 5; // Create an unboxed value type variable.

 

#if INEFFICIENT

// When compiling the following line, v is boxed

// three times, wasting time and memory. Console.WriteLine("{0}, {1}, {2}", v, v, v);


#else


 

// The lines below have the same result, execute

// much faster, and use less memory.

Object o = v; // Manually box v (just once).


 

// No boxing occurs to compile the following line. Console.WriteLine("{0}, {1}, {2}", o, o, o);

#endif

}

}

 

If this code is compiled with the INEFFICIENT symbol defined, the compiler will generate code to box v three times, causing three objects to be allocated from the heap! This is extremely waste- ful because each object will have exactly the same contents: 5. If the code is compiled without the INEFFICIENT symbol defined, v is boxed just once, so only one object is allocated from the heap. Then, in the call to Console.WriteLine, the reference to the single boxed object is passed three times. This second version executes much faster and allocates less memory from the heap.

In these examples, it’s fairly easy to recognize when an instance of a value type requires boxing. Basically, if you want a reference to an instance of a value type, the instance must be boxed. Usu- ally this happens because you have a value type instance and you want to pass it to a method that requires a reference type. However, this situation isn’t the only one in which you’ll need to box an instance of a value type.


Recall that unboxed value types are lighter-weight types than reference types for two reasons:

 

■ They are not allocated on the managed heap.

 

■ They don’t have the additional overhead members that every object on the heap has: a type object pointer and a sync block index.

Because unboxed value types don’t have a sync block index, you can’t have multiple threads syn- chronize their access to the instance by using the methods of the System.Threading.Monitor type (or by using C#’s lock statement).

Even though unboxed value types don’t have a type object pointer, you can still call virtual meth- ods (such as Equals, GetHashCode, or ToString) inherited or overridden by the type. If your value type overrides one of these virtual methods, then the CLR can invoke the method nonvirtually be- cause value types are implicitly sealed and cannot have any types derived from them. In addition, the value type instance being used to invoke the virtual method is not boxed. However, if your override of the virtual method calls into the base type's implementation of the method, then the value type instance does get boxed when calling the base type's implementation so that a reference to a heap object gets passed to the this pointer into the base method.

However, calling a nonvirtual inherited method (such as GetType or MemberwiseClone) always requires the value type to be boxed because these methods are defined by System.Object, so the methods expect the this argument to be a pointer that refers to an object on the heap.

In addition, casting an unboxed instance of a value type to one of the type’s interfaces requires the instance to be boxed, because interface variables must always contain a reference to an object on the heap. (I’ll talk about interfaces in Chapter 13, “Interfaces.”) The following code demonstrates.

 

using System;

 

internal struct Point : IComparable { private readonly Int32 m_x, m_y;

 

// Constructor to easily initialize the fields public Point(Int32 x, Int32 y) {

m_x = x; m_y = y;

}

 

// Override ToString method inherited from System.ValueType public override String ToString() {

// Return the point as a string. Note: calling ToString prevents boxing return String.Format("({0}, {1})", m_x.ToString(), m_y.ToString());

}

 

// Implementation of type­safe CompareTo method public Int32 CompareTo(Point other) {

// Use the Pythagorean Theorem to calculate

// which point is farther from the origin (0, 0) return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y)

­ Math.Sqrt(other.m_x * other.m_x + other.m_y * other.m_y));

}


// Implementation of IComparable's CompareTo method public Int32 CompareTo(Object o) {

if (GetType() != o.GetType()) {

throw new ArgumentException("o is not a Point");

}

// Call type­safe CompareTo method return CompareTo((Point) o);

}

}

 

 

public static class Program { public static void Main() {

// Create two Point instances on the stack. Point p1 = new Point(10, 10);

Point p2 = new Point(20, 20);

 

// p1 does NOT get boxed to call ToString (a virtual method). Console.WriteLine(p1.ToString());// "(10, 10)"

 

// p DOES get boxed to call GetType (a non­virtual method). Console.WriteLine(p1.GetType());// "Point"

 

// p1 does NOT get boxed to call CompareTo.

// p2 does NOT get boxed because CompareTo(Point) is called. Console.WriteLine(p1.CompareTo(p2));// "­1"

 

// p1 DOES get boxed, and the reference is placed in c. IComparable c = p1;

Console.WriteLine(c.GetType());// "Point"

 

// p1 does NOT get boxed to call CompareTo.

// Because CompareTo is not being passed a Point variable,

// CompareTo(Object) is called, which requires a reference to

// a boxed Point.

// c does NOT get boxed because it already refers to a boxed Point. Console.WriteLine(p1.CompareTo(c));// "0"

 

// c does NOT get boxed because it already refers to a boxed Point.

// p2 does get boxed because CompareTo(Object) is called. Console.WriteLine(c.CompareTo(p2));// "­1"

 

// c is unboxed, and fields are copied into p2. p2 = (Point) c;

 

// Proves that the fields got copied into p2. Console.WriteLine(p2.ToString());// "(10, 10)"

}

}

 

This code demonstrates several scenarios related to boxing and unboxing:

 

Calling ToStringIn the call to ToString, p1 doesn’t have to be boxed. At first, you’d think that p1 would have to be boxed because ToString is a virtual method that is inherited from the base type, System.ValueType. Normally, to call a virtual method, the CLR needs to de- termine the object’s type in order to locate the type’s method table. Because p1 is an unboxed


value type, there’s no type object pointer. However, the just-in-time (JIT) compiler sees that Point overrides the ToString method, and it emits code that calls ToString directly (non- virtually) without having to do any boxing. The compiler knows that polymorphism can’t come into play here because Point is a value type, and no type can derive from it to provide another implementation of this virtual method. Note that if Point's ToString method internally calls base.ToString(), then the value type instance would be boxed when calling System.Value­ Type's ToString method.

Calling GetTypeIn the call to the nonvirtual GetType method, p1 does have to be boxed. The reason is that the Point type inherits GetType from System.Object. So to call GetType, the CLR must use a pointer to a type object, which can be obtained only by boxing p1.

Calling CompareTo (first time) In the first call to CompareTo, p1 doesn’t have to be boxed because Point implements the CompareTo method, and the compiler can just call it directly.

Note that a Point variable (p2) is being passed to CompareTo, and therefore the compiler calls the overload of CompareTo that accepts a Point parameter. This means that p2 will be passed by value to CompareTo and no boxing is necessary.

Casting to IComparableWhen casting p1 to a variable (c) that is of an interface type, p1 must be boxed because interfaces are reference types by definition. So p1 is boxed, and the pointer to this boxed object is stored in the variable c. The following call to GetType proves that c does refer to a boxed Point on the heap.

Calling CompareTo (second time)In the second call to CompareTo, p1 doesn’t have to be boxed because Point implements the CompareTo method, and the compiler can just call it directly. Note that an IComparable variable (c) is being passed to CompareTo, and therefore, the compiler calls the overload of CompareTo that accepts an Object parameter. This means that the argument passed must be a pointer that refers to an object on the heap. Fortunately, c does refer to a boxed Point, and therefore, that memory address in c can be passed to CompareTo, and no additional boxing is necessary.

Calling CompareTo (third time)In the third call to CompareTo, c already refers to a boxed Point object on the heap. Because c is of the IComparable interface type, you can call only the interface’s CompareTo method that requires an Object parameter. This means that the argument passed must be a pointer that refers to an object on the heap. So p2 is boxed, and the pointer to this boxed object is passed to CompareTo.

Casting to PointWhen casting c to a Point, the object on the heap referred to by c is unboxed, and its fields are copied from the heap to p2, an instance of the Point type residing on the stack.

I realize that all of this information about reference types, value types, and boxing might be over- whelming at first. However, a solid understanding of these concepts is critical to any .NET Framework developer’s long-term success. Trust me: having a solid grasp of these concepts will allow you to build efficient applications faster and easier.


Changing Fields in a Boxed Value Type by Using Interfaces (and Why You Shouldn’t Do This)

Let’s have some fun and see how well you understand value types, boxing, and unboxing. Examine

the following code, and see whether you can figure out what it displays on the console.

 

using System;

 

// Point is a value type. internal struct Point {

private Int32 m_x, m_y;

 

public Point(Int32 x, Int32 y) { m_x = x;

m_y = y;

}

 

public void Change(Int32 x, Int32 y) { m_x = x; m_y = y;

}

 

public override String ToString() {

return String.Format("({0}, {1})", m_x.ToString(), m_y.ToString());

}

}

 

public sealed class Program { public static void Main() {

Point p = new Point(1, 1); Console.WriteLine(p);

p.Change(2, 2); Console.WriteLine(p);

 

Object o = p; Console.WriteLine(o);

 

((Point) o).Change(3, 3); Console.WriteLine(o);

}

}

 

Very simply, Main creates an instance (p) of a Point value type on the stack and sets its m_x and m_y fields to 1. Then, p is boxed before the first call to WriteLine, which calls ToString on the boxed Point, and (1, 1) is displayed as expected. Then, p is used to call the Change method, which changes the values of p’s m_x and m_y fields on the stack to 2. The second call to WriteLine requires p to be boxed again and displays (2, 2), as expected.

Now, p is boxed a third time, and o refers to the boxed Point object. The third call to WriteLine again shows (2, 2), which is also expected. Finally, I want to call the Change method to update the fields in the boxed Point object. However, Object (the type of the variable o) doesn’t know anything about the Change method, so I must first cast o to a Point. Casting o to a Point unboxes o and


copies the fields in the boxed Point to a temporary Point on the thread’s stack! The m_x and m_y fields of this temporary point are changed to 3 and 3, but the boxed Point isn’t affected by this call to Change. When WriteLine is called the fourth time, (2, 2) is displayed again. Many developers do not expect this.

Some languages, such as C++/CLI, let you change the fields in a boxed value type, but C# does not.

However, you can fool C# into allowing this by using an interface. The following code is a modified

version of the previous code.

 

using System;

 

// Interface defining a Change method internal interface IChangeBoxedPoint {

void Change(Int32 x, Int32 y);

}

 

// Point is a value type.

internal struct Point : IChangeBoxedPoint { private Int32 m_x, m_y;

 

public Point(Int32 x, Int32 y) { m_x = x;

m_y = y;

}

 

public void Change(Int32 x, Int32 y) { m_x = x; m_y = y;

}

 

public override String ToString() {

return String.Format("({0}, {1})", m_x.ToString(), m_y.ToString());

}

}

 

public sealed class Program { public static void Main() {

Point p = new Point(1, 1); Console.WriteLine(p);

p.Change(2, 2); Console.WriteLine(p);

 

Object o = p; Console.WriteLine(o);

 

((Point) o).Change(3, 3); Console.WriteLine(o);

 

// Boxes p, changes the boxed object and discards it ((IChangeBoxedPoint) p).Change(4, 4); Console.WriteLine(p);


// Changes the boxed object and shows it ((IChangeBoxedPoint) o).Change(5, 5); Console.WriteLine(o);

}

}

 

This code is almost identical to the previous version. The main difference is that the Change method is defined by the IChangeBoxedPoint interface, and the Point type now implements this interface. Inside Main, the first four calls to WriteLine are the same and produce the same results I had before (as expected). However, I’ve added two more examples at the end of Main.

In the first example, the unboxed Point, p, is cast to an IChangeBoxedPoint. This cast causes the value in p to be boxed. Change is called on the boxed value, which does change its m_x and m_y fields to 4 and 4, but after Change returns, the boxed object is immediately ready to be garbage collected. So the fifth call to WriteLine displays (2, 2). Many developers won’t expect this result.

In the last example, the boxed Point referred to by o is cast to an IChangeBoxedPoint. No box- ing is necessary here because o is already a boxed Point. Then Change is called, which does change the boxed Point’s m_x and m_y fields. The interface method Change has allowed me to change the fields in a boxed Point object! Now, when WriteLine is called, it displays (5, 5) as expected. The purpose of this whole example is to demonstrate how an interface method is able to modify the fields of a boxed value type. In C#, this isn’t possible without using an interface method.

 

ImportantEarlier in this chapter, I mentioned that value types should be immutable: that

is, they should not define any members that modify any of the type’s instance fields. In

fact, I recommended that value types have their fields marked as readonly so that the compiler will issue errors should you accidentally write a method that attempts to modify a field. The previous example should make it very clear to you why value types should

be immutable. The unexpected behaviors shown in the previous example all occur when attempting to call a method that modifies the value type’s instance fields. If after con- structing a value type, you do not call any methods that modify its state, you will not get confused when all of the boxing and unboxing/field copying occurs. If the value type is im- mutable, you will end up just copying the same state around, and you will not be surprised by any of the behaviors you see.

A number of developers reviewed the chapters of this book. After reading through some of my code samples (such as the preceding one), these reviewers would tell me that they’ve sworn off value types. I must say that these little value type nuances have cost me days of debugging time, which is why I spend time pointing them out in this book. I hope you’ll remember some of these nuances and that you’ll be prepared for them if and when they strike you and your code. Certainly, you shouldn’t be scared of value types. They are use- ful, and they have their place. After all, a program needs a little Int32 love now and then. Just keep in mind that value types and reference types have very different behaviors de- pending on how they’re used. In fact, you should take the preceding code and declare


 

 


Date: 2016-03-03; view: 637


<== previous page | next page ==>
Nbsp;   Programming Language Primitive Types | Object Equality and Identity
doclecture.net - lectures - 2014-2024 year. Copyright infringement or personal data (0.037 sec.)