Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp And The .NET 2.0 Platform (2005) [eng]

.pdf
Скачиваний:
111
Добавлен:
16.08.2013
Размер:
10.35 Mб
Скачать

304 C H A P T E R 9 A D VA N C E D C # T Y P E C O N S T R U C T I O N T E C H N I Q U E S

Additional Explicit Conversions for the Square Type

Now that you can explicitly convert Rectangles into Squares, let’s examine a few additional explicit conversions. Given that a square is symmetrical on each side, it might be helpful to provide an explicit conversion routine that allows the caller to cast from a System.Int32 type into a Square (which, of course, will have a side length equal to the incoming integer). Likewise, what if you were to update Square such that the caller can cast from a Square into a System.Int32? Here is the calling logic:

static void Main(string[] args)

{

...

//Converting a System.Int32 to a Square.

Square sq2 = (Square)90; Console.WriteLine("sq2 = {0}", sq2);

//Converting a Square to a System.Int32. int side = (int)sq2;

Console.WriteLine("Side length of sq2 = {0}", side);

}

And here is the update to the Square type:

public struct Square

{

...

public static explicit operator Square(int sideLength)

{

Square newSq; newSq.Length = sideLength; return newSq;

}

public static explicit operator int (Square s) {return s.Length;}

}

Wild, huh? To be honest, converting from a Square into a System.Int32 may not be the most intuitive (or useful) operation. However, this does point out a very important fact regarding custom conversion routines: the compiler does not care what you convert to or from, as long as you have written syntactically correct code. Thus, as with overloading operators, just because you can create an explicit cast operation for a given type does not mean you should. Typically, this technique will be most helpful when you’re creating .NET structure types, given that they are unable to participate in classical inheritance (where casting comes for free).

Defining Implicit Conversion Routines

Thus far, you have created various custom explicit conversion operations. However, what about the following implicit conversion?

static void Main(string[] args)

{

...

// Attempt to make an implicit cast?

Square s3; s3.Length = 83;

Rectangle rect2 = s3;

}

C H A P T E R 9 A D VA N C E D C # T Y P E C O N S T R U C T I O N T E C H N I Q U E S

305

As you might expect, this code will not compile, given that you have not provided an implicit conversion routine for the Rectangle type. Now here is the catch: it is illegal to define explicit and implicit conversion functions on the same type, if they do not differ by their return type or parameter set. This might seem like a limitation; however, the second catch is that when a type defines an implicit conversion routine, it is legal for the caller to make use of the explicit cast syntax!

Confused? To clear things up, let’s add an implicit conversion routine to the Rectangle structure using the C# implicit keyword (note that the following code assumes the width of the resulting Rectangle is computed by multiplying the side of the Square by 2):

public struct Rectangle

{

...

public static implicit operator Rectangle(Square s)

{

Rectangle r; r.Height = s.Length;

//Assume the length of the new Rectangle with

//(Length x 2)

r.Width = s.Length * 2; return r;

}

}

With this update, you are now able to convert between types as follows:

static void Main(string[] args)

{

...

//Implicit cast OK!

Square s3; s3.Length= 83; Rectangle rect2 = s3;

Console.WriteLine("rect2 = {0}", rect2); DrawSquare(s3);

//Explicit cast syntax still OK!

Square s4; s4.Length = 3;

Rectangle rect3 = (Rectangle)s4; Console.WriteLine("rect3 = {0}", rect3);

...

}

Again, be aware that it is permissible to define explicit and implicit conversion routines for the same type as long as their signatures differ. Thus, you could update the Square as follows:

public struct Square

{

...

//Can call as:

//Square sq2 = (Square)90;

//or as:

//Square sq2 = 90;

public static implicit operator Square(int sideLength)

{

306 C H A P T E R 9 A D VA N C E D C # T Y P E C O N S T R U C T I O N T E C H N I Q U E S

Square newSq; newSq.Length = sideLength; return newSq;

}

//Must call as:

//int side = (Square)mySquare;

public static explicit operator int (Square s) { return s.Length; }

}

The Internal Representation of Custom Conversion

Routines

Like overloaded operators, methods that are qualified with the implicit or explicit keywords have “special” names in terms of CIL: op_Implicit and op_Explicit, respectively (see Figure 9-2).

Figure 9-2. CIL representation of user-defined conversion routines

That wraps up our examination of defining custom conversion routines. As with overloaded operators, remember that this bit of syntax is simply a shorthand notation for “normal” member functions, and in this light it is always optional.

Source Code The CustomConversions project is located under the Chapter 9 subdirectory.

C H A P T E R 9 A D VA N C E D C # T Y P E C O N S T R U C T I O N T E C H N I Q U E S

307

The Advanced Keywords of C#

To close this chapter, you’ll examine some of the more esoteric C# keywords:

checked/unchecked

unsafe/stackalloc/fixed/sizeof

To start, let’s check out how C# provides automatic detection of arithmetic overflow and underflow conditions using the checked and unchecked keywords.

The checked Keyword

As you are no doubt well aware, each numerical data type has a fixed upper and lower limit (which may be obtained programmatically using the MaxValue and MinValue properties). Now, when you are performing arithmetic operations on a specific type, it is very possible that you may accidentally overflow the maximum storage of the type (i.e., assign a value that is greater than the maximum value) or underflow the minimum storage of the type (i.e., assign a value that is less than the minimum value). To keep in step with the CLR, I will refer to both of these possibilities collectively as “overflow.” (As you will see, checked overflow and underflow conditions result in a System.OverflowException type. There is no System.UnderflowException type in the base class libraries.)

To illustrate the issue, assume you have created two System.Byte types (a C# byte), each of which has been assigned a value that is safely below the maximum (255). If you were to add the values of these types (casting the resulting integer as a byte), you would assume that the result would be the exact sum of each member:

namespace CheckedUnchecked

{

class Program

{

static void Main(string[] args)

{

//Overflow the max value of a System.Byte.

Console.WriteLine("Max value of byte is {0}.", byte.MaxValue); Console.WriteLine("Min value of byte is {0}.", byte.MinValue); byte b1 = 100;

byte b2 = 250;

byte sum = (byte)(b1 + b2);

//sum should hold the value 350, however...

Console.WriteLine("sum = {0}", sum); Console.ReadLine();

}

}

}

If you were to view the output of this application, you might be surprised to find that sum contains the value 94 (rather than the expected 350). The reason is simple. Given that a System.Byte can hold a value only between 0 and 255 (inclusive, for a grand total of 256 slots), sum now contains the overflow value (350 – 256 = 94). As you have just seen, if you take no corrective course of action, overflow occurs without exception. At times, this hidden overflow may cause no harm whatsoever in your project. Other times, this loss of data is completely unacceptable.

To handle overflow or underflow conditions in your application, you have two options. Your first choice is to leverage your wits and programming skills to handle all overflow conditions manually. Assuming you were indeed able to find each overflow condition in your program, you could resolve the previous overflow error as follows:

308 C H A P T E R 9 A D VA N C E D C # T Y P E C O N S T R U C T I O N T E C H N I Q U E S

// Store sum in an integer to prevent overflow. byte b1 = 100;

byte b2 = 250; int sum = b1 + b2;

Of course, the problem with this technique is the simple fact that you are human, and even your best attempts may result in errors that have escaped your eyes. Given this, C# provides the checked keyword. When you wrap a statement (or a block of statements) within the scope of the checked keyword, the C# compiler emits specific CIL instructions that test for overflow conditions that may result when adding, multiplying, subtracting, or dividing two numerical data types. If an overflow has occurred, the runtime will throw a System.OverflowException type. Observe the following update:

class Program

{

static void Main(string[] args)

{

// Overflow the max value of a System.Byte.

Console.WriteLine("Max value of byte is {0}.", byte.MaxValue); byte b1 = 100;

byte b2 = 250;

try

{

byte sum = checked((byte)(b1 + b2)); Console.WriteLine("sum = {0}", sum);

}

catch(OverflowException e)

{Console.WriteLine(e.Message); }

}

}

Here, you wrap the addition of b1 and b2 within the scope of the checked keyword. If you wish to force overflow checking to occur over a block of code, you can interact with the checked keyword as follows:

try

{

checked

{

byte sum = (byte)(b1 + b2); Console.WriteLine("sum = {0}", sum);

}

}

catch(OverflowException e)

{

Console.WriteLine(e.Message);

}

In either case, the code in question will be evaluated for possible overflow conditions automatically, which will trigger an overflow exception if encountered.

Setting Projectwide Overflow Checking

Now, if you are creating an application that should never allow silent overflow to occur, you may find yourself in the annoying position of wrapping numerous lines of code within the scope of the checked keyword. As an alternative, the C# compiler supports the /checked flag. When enabled, all of your arithmetic will be evaluated for overflow without the need to make use of the C# checked keyword. If overflow has been discovered, you will still receive a runtime OverflowException.

C H A P T E R 9 A D VA N C E D C # T Y P E C O N S T R U C T I O N T E C H N I Q U E S

309

To enable this flag using Visual Studio 2005, open your project’s property page and click the Advanced button on the Build tab. From the resulting dialog box, select the “Check for arithmetic overflow/underflow” check box (see Figure 9-3).

Figure 9-3. Enabling Visual Studio 2005 overflow checking

As you may guess, this setting can be very helpful when you’re creating a debug build. Once all of the overflow exceptions have been squashed out of the code base, you’re free to disable the /checked flag for subsequent builds (which will increase the runtime execution of your application).

The unchecked Keyword

Now, assuming you have enabled this projectwide setting, what are you to do if you have a block of code where silent overflow is acceptable? Given that the /checked flag will evaluate all arithmetic logic, the C# language provides the unchecked keyword to disable the throwing of System.OverflowException on a case-by-case basis. This keyword’s use is identical to that of the checked keyword in that you can specify a single statement or a block of statements, for example:

//Assuming /checked is enabled,

//this block will not trigger

//a runtime exception. unchecked

{

byte sum = (byte)(b1 + b2); Console.WriteLine("sum = {0}", sum);

}

So, to summarize the C# checked and unchecked keywords, remember that the default behavior of the .NET runtime is to ignore arithmetic overflow. When you want to selectively handle discrete statements, make use of the checked keyword. If you wish to trap overflow errors throughout your application, enable the /checked flag. Finally, the unchecked keyword may be used if you have

a block of code where overflow is acceptable (and thus should not trigger a runtime exception).

Source Code The CheckedUnchecked project can be found under the Chapter 9 subdirectory.

310 C H A P T E R 9 A D VA N C E D C # T Y P E C O N S T R U C T I O N T E C H N I Q U E S

Working with Pointer Types

In Chapter 3, you learned that the .NET platform defines two major categories of data: value types and reference types. Truth be told, however, there is a third category: pointer types. To work with pointer types, we are provided with specific operators and keywords that allow us to bypass the CLR’s memory management scheme and take matters into our own hands (see Table 9-3).

Table 9-3. Pointer-centric C# Operators and Keywords

Operator/Keyword

Meaning in Life

*

This operator is used to create a pointer variable (i.e., a variable that

 

represents a direct location in memory). As in C(++), this same operator

 

is used for pointer indirection.

&

This operator is used to obtain the address of a variable in memory.

–>

This operator is used to access fields of a type that is represented by

 

a pointer (the unsafe version of the C# dot operator).

[]

The [] operator (in an unsafe context) allows you to index the slot

 

pointed to by a pointer variable (recall the interplay between a pointer

 

variable and the [] operator in C(++)!).

++, --

In an unsafe context, the increment and decrement operators can be

 

applied to pointer types.

+, -

In an unsafe context, the addition and subtraction operators can be

 

applied to pointer types.

==, !=, <, >, <=, =>

In an unsafe context, the comparison and equality operators can be

 

applied to pointer types.

stackalloc

In an unsafe context, the stackalloc keyword can be used to allocate

 

C# arrays directly on the stack.

fixed

In an unsafe context, the fixed keyword can be used to temporarily fix

 

a variable so that its address may be found.

 

 

Now, before we dig into the details, let me point out the fact that you will seldom if ever need to make use of pointer types. Although C# does allow you to drop down to the level of pointer manipulations, understand that the .NET runtime has absolutely no clue of your intentions. Thus, if you mismanage a pointer, you are the one in charge of dealing with the consequences. Given these warnings, when exactly would you need to work with pointer types? There are two common situations:

You are looking to optimize select parts of your application by directly manipulating memory outside the management of the CLR.

You are calling methods of a C-based *.dll or COM server that demand pointer types as parameters.

In the event that you do decide to make use of this C# language feature, you will be required to inform csc.exe of your intentions by enabling your project to support “unsafe code.” To do so using the C# command-line compiler (csc.exe), simply supply the /unsafe flag as an argument. From Visual Studio 2005, you will need to access your project’s Properties page and enable the Allow Unsafe Code option from the Build tab (see Figure 9-4).

C H A P T E R 9 A D VA N C E D C # T Y P E C O N S T R U C T I O N T E C H N I Q U E S

311

Figure 9-4. Enabling unsafe code using Visual Studio 2005

The unsafe Keyword

In the examples that follow, I’m assuming that you have some background in C(++) pointer manipulations. If this is not true in your case, don’t sweat it. Again, writing unsafe code will not be a common task for a majority of .NET applications. When you wish to work with pointers in C#, you must specifically declare a block of “unsafe” code using the unsafe keyword (as you might guess, any code that is not marked with the unsafe keyword is considered “safe” automatically):

unsafe

{

// Work with pointer types here!

}

In addition to declaring a scope of unsafe code, you are able to build structures, classes, type members, and parameters that are “unsafe.” Here are a few examples to gnaw on:

//This entire structure is 'unsafe' and can

//be used only in an unsafe context. public unsafe struct Node

{

public int Value; public Node* Left; public Node* Right;

}

//This struct is safe, but the Node* members

//are not. Technically, you may access 'Value' from

//outside an unsafe context, but not 'Left' and 'Right'. public struct Node

{

public int Value;

//These can be accessed only in an unsafe context! public unsafe Node* Left;

public unsafe Node* Right;

}

312 C H A P T E R 9 A D VA N C E D C # T Y P E C O N S T R U C T I O N T E C H N I Q U E S

Methods (static or instance level) may be marked as unsafe as well. For example, assume that you know a given static method will make use of pointer logic. To ensure that this method can be called only from an unsafe context, you could define the method as follows:

unsafe public static void SomeUnsafeCode()

{

// Work with pointer types here!

}

This configuration demands that the caller invoke SomeUnsafeCode() as so:

static void Main(string[] args)

{

unsafe

{

SomeUnsafeCode();

}

}

Conversely, if you would rather not force the caller to wrap the invocation within an unsafe context, you could remove the unsafe keyword from the SomeUnsafeCode() method signature and opt for the following:

public static void SomeUnsafeCode()

{

unsafe

{

// Work with pointers here!

}

}

which would simplify the call to this:

static void Main(string[] args)

{

SomeUnsafeCode();

}

Working with the * and & Operators

Once you have established an unsafe context, you are then free to build pointers to data types using the * operator and obtain the address of said pointer using the & operator. Using C#, the * operator is applied to the underlying type only, not as a prefix to each pointer variable name. For example, the following code declares two variables, both of type int* (a pointer to an integer):

//No! This is incorrect under C#! int *pi, *pj;

//Yes! This is the way of C#. int* pi, pj;

Consider the following example:

unsafe

{

int myInt;

//Define an int pointer, and

//assign it the address of myInt. int* ptrToMyInt = &myInt;

C H A P T E R 9 A D VA N C E D C # T Y P E C O N S T R U C T I O N T E C H N I Q U E S

313

//Assign value of myInt using pointer indirection.

*ptrToMyInt = 123;

//Print some stats.

Console.WriteLine("Value of myInt {0}", myInt); Console.WriteLine("Address of myInt {0:X}", (int)&ptrToMyInt);

}

An Unsafe (and Safe) Swap Function

Of course, declaring pointers to local variables simply to assign their value (as shown in the previous example) is never required and not altogether useful. To illustrate a more practical example of unsafe code, assume you wish to build a swap function using pointer arithmetic:

unsafe public static void UnsafeSwap(int* i, int* j)

{

int temp = *i; *i = *j;

*j = temp;

}

Very C-like, don’t you think? However, given your work in Chapter 3, you should be aware that you could write the following safe version of your swap algorithm using the C# ref keyword:

public static void SafeSwap(ref int i, ref int j)

{

int temp = i; i = j;

j = temp;

}

The functionality of each method is identical, thus reinforcing the point that direct pointer manipulation is not a mandatory task under C#. Here is the calling logic:

static void Main(string[] args)

{

Console.WriteLine("***** Calling method with unsafe code *****");

//Values for swap. int i = 10, j = 20;

//Swap values 'safely'.

Console.WriteLine("\n***** Safe swap *****");

Console.WriteLine("Values before safe swap: i = {0}, j = {1}", i, j); SafeSwap(ref i, ref j);

Console.WriteLine("Values after safe swap: i = {0}, j = {1}", i, j);

//Swap values 'unsafely'.

Console.WriteLine("\n***** Unsafe swap *****");

Console.WriteLine("Values before unsafe swap: i = {0}, j = {1}", i, j); unsafe { UnsafeSwap(&i, &j); }

Console.WriteLine("Values after unsafe swap: i = {0}, j = {1}", i, j); Console.ReadLine();

}