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

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

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

314 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

Field Access via Pointers (the -> Operator)

Now assume that you have defined a Point structure and wish to declare a pointer to a Point type. Like C(++), when you wish to invoke methods or trigger fields of a pointer type, you will need to make use of the pointer-field access operator (->). As mentioned in Table 9-3, this is the unsafe version of the standard (safe) dot operator (.). In fact, using the pointer indirection operator (*), it is possible to dereference a pointer to (once again) apply the dot operator notation. Check out the following:

struct Point

{

public int x; public int y;

public override string ToString()

{ return string.Format("({0}, {1})", x, y);}

}

static void Main(string[] args)

{

//Access members via pointer. unsafe

{

Point point; Point* p = &point; p->x = 100;

p->y = 200; Console.WriteLine(p->ToString());

}

//Access members via pointer indirection. unsafe

{

Point point; Point* p = &point; (*p).x = 100; (*p).y = 200;

Console.WriteLine((*p).ToString());

}

}

The stackalloc Keyword

In an unsafe context, you may need to declare a local variable that allocates memory directly from the call stack (and is therefore not subject to .NET garbage collection). To do so, C# provides the stackalloc keyword, which is the C# equivalent to the _alloca function of the C runtime library. Here is a simple example:

unsafe

{

char* p = stackalloc char[256]; for (int k = 0; k < 256; k++)

p[k] = (char)k;

}

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

315

Pinning a Type via the fixed Keyword

As you saw in the previous example, allocating a chunk of memory within an unsafe context may be facilitated via the stackalloc keyword. By the very nature of this operation, the allocated memory is cleaned up as soon as the allocating method has returned (as the memory is acquired from the stack). However, assume a more complex example. During our examination of the -> operator, you created a value type named Point. Like all value types, the allocated memory is popped off the stack once the executing scope has terminated. For the sake of argument, assume Point was instead defined as a reference type:

class Point // <= Now a class!

{

public int x; public int y;

public override string ToString()

{ return string.Format("({0}, {1})", x, y);}

}

As you are well aware, if the caller declares a variable of type Point, the memory is allocated on the garbage collected heap. The burning question then becomes, what if an unsafe context wishes to interact with this object (or any object on the heap)? Given that garbage collection can occur at any moment, imagine the pain of accessing the members of Point at the very point in time at which a sweep of the heap is under way. Theoretically, it is possible that the unsafe context is attempting to interact with a member that is no longer accessible or has been repositioned on the heap after surviving a generational sweep (which is an obvious problem).

To lock a reference type variable in memory from an unsafe context, C# provides the fixed keyword. The fixed statement sets a pointer to a managed type and “pins” that variable during the execution of statement. Without fixed, pointers to managed variables would be of little use, since garbage collection could relocate the variables unpredictably. (In fact, the C# compiler will not allow you to set a pointer to a managed variable except in a fixed statement.)

Thus, if you create a Point type (now redesigned as a class) and want to interact with its members, you must write the following code (or receive a compiler error):

unsafe public static void Main()

{

Point pt = new Point(); pt.x = 5;

pt.y = 6;

//pin pt in place so it will not

//be moved or GC-ed.

fixed (int* p = &pt.x)

{

// Use int* variable here!

}

// pt is now unpinned, and ready to be GC-ed.

Console.WriteLine ("Point is: {0}", pt);

}

In a nutshell, the fixed keyword allows you to build a statement that locks a reference variable in memory, such that its address remains constant for the duration of the statement. To be sure, any time you interact with a reference type from within the context of unsafe code, pinning the reference is a must.

316 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

The sizeof Keyword

The final unsafe-centric C# keyword to consider is sizeof. As in C(++), the C# sizeof keyword is used to obtain the size in bytes for a value type (never a reference type), and it may only be used within an unsafe context. As you may imagine, this ability may prove helpful when you’re interacting with unmanaged C-based APIs. Its usage is straightforward:

unsafe

{

Console.WriteLine("The size of short is {0}.", sizeof(short)); Console.WriteLine("The size of int is {0}.", sizeof(int)); Console.WriteLine("The size of long is {0}.", sizeof(long));

}

As sizeof will evaluate the number of bytes for any System.ValueType-derived entity, you are able to obtain the size of custom structures as well. Assume you have defined the following struct:

struct MyValueType

{

public short s; public int i; public long l;

}

You can now obtain its size as follows:

unsafe

{

Console.WriteLine("The size of short is {0}.", sizeof(short)); Console.WriteLine("The size of int is {0}.", sizeof(int)); Console.WriteLine("The size of long is {0}.", sizeof(long)); Console.WriteLine("The size of MyValueType is {0}.",

sizeof(MyValueType));

}

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

C# Preprocessor Directives

Like many other languages in the C family, C# supports the use of various symbols that allow you to interact with the compilation process. Before examining various C# preprocessor directives, let’s get our terminology correct. The term “C# preprocessor directive” is not entirely accurate. In reality, this term is used only for consistency with the C and C++ programming languages. In C#, there is no separate preprocessing step. Rather, preprocessing directives are processed as part of the lexical analysis phase of the compiler.

In any case, the syntax of the C# preprocessor directives is very similar to that of the other members of the C family, in that the directives are always prefixed with the pound sign (#). Table 9-4 defines some of the more commonly used directives (consult the .NET Framework 2.0 SDK documentation for complete details).

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

317

Table 9-4. Common C# Preprocessor Directives

Directives

Meaning in Life

#region, #endregion

Used to mark sections of collapsible source code

#define, #undef

Used to define and undefine conditional compilation symbols

#if, #elif, #else, #endif

Used to conditionally skip sections of source code (based on specified

 

compilation symbols)

 

 

Specifying Code Regions

Perhaps some of the most useful of all preprocessor directives are #region and #endregion. Using these tags, you are able to specify a block of code that may be hidden from view and identified by a friendly textual marker. Use of regions can help keep lengthy *.cs files more manageable. For example, you could create one region for a type’s constructors, another for type properties, and so forth:

class Car

{

private string petName; private int currSp;

#region Constructors public Car()

{ ... }

public Car Car(int currSp, string petName) {...}

#endregion

#region Properties public int Speed { ... }

public string Name {...}

#endregion

}

When you place your mouse cursor over a collapsed region, you are provided with a snapshot of the code lurking behind (see Figure 9-5).

Figure 9-5. Regions at work

318 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

Conditional Code Compilation

The next batch of preprocessor directives (#if, #elif, #else, #endif) allows you to conditionally compile a block of code, based on predefined symbols. The classic use of these directives is to identify a block of code that is compiled only under a debug (rather than a release) build:

class Program

{

static void Main(string[] args)

{

//This code will only execute if the project is

//compiled as a Debug build.

#if DEBUG

Console.WriteLine("App directory: {0}", Environment.CurrentDirectory);

Console.WriteLine("Box: {0}", Environment.MachineName); Console.WriteLine("OS: {0}",

Environment.OSVersion); Console.WriteLine(".NET Version: {0}",

Environment.Version);

#endif

}

}

Here, you are checking for a symbol named DEBUG. If it is present, you dump out a number of interesting statistics using some static members of the System.Environment class. If the DEBUG symbol is not defined, the code placed between #if and #endif will not be compiled into the resulting assembly, and it will be effectively ignored.

By default, Visual Studio 2005 always defines a DEBUG symbol; however, this can be prevented by deselecting the “Define DEBUG constant” check box located under the Build tab of your project’s Properties page. Assuming you did disable this autogenerated DEBUG symbol, you could now define this symbol on a file-by-file basis using the #define preprocessor directive:

#define DEBUG using System;

namespace Preprocessor

{

class ProcessMe

{

static void Main(string[] args)

{

// Same code as before...

}

}

}

Note #define directives must be listed before anything else in a C# code file.

You are also able to define your own custom preprocessor symbols. For example, assume you have authored a C# class that should be compiled a bit differently under the Mono distribution of

.NET (see Chapter 1). Using #define, you can define a symbol named MONO_BUILD on a file-by-file basis:

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

319

#define DEBUG #define MONO_BUILD

using System;

namespace Preprocessor

{

class Program

{

static void Main(string[] args)

{

#if MONO_BUILD

Console.WriteLine("Compiling under Mono!"); #else

Console.WriteLine("Compiling under Microsoft .NET"); #endif

}

}

}

To create a project-wide symbol, make use of the “Conditional compilation symbols” text box located on the Build tab of your project’s Properties page (see Figure 9-6).

Figure 9-6. Defining a projectwide preprocessor symbol

Summary

The purpose of this chapter is to deepen your understanding of the C# programming language. You began by investigating various advanced type construction techniques (indexer methods, overloaded operators, and custom conversion routines). You spent the remainder of this chapter examining a small set of lesser-known keywords (e.g., sizeof, checked, unsafe, and so forth), and during the process came to learn how to work with raw pointer types. As stated throughout the chapter’s examination of pointer types, a vast majority of your C# applications will never need to make use of them.

C H A P T E R 1 0

■ ■ ■

Understanding Generics

With the release of .NET 2.0, the C# programming language has been enhanced to support a new feature of the CTS termed generics. Simply put, generics provide a way for programmers to define “placeholders” (formally termed type parameters) for method arguments and type definitions, which are specified at the time of invoking the generic method or creating the generic type.

To illustrate this new language feature, this chapter begins with an examination of the System.Collections.Generic namespace. Once you’ve seen generic support within the base class libraries, in the remainder of this chapter you’ll examine how you can build your own generic members, classes, structures, interfaces, and delegates.

Revisiting the Boxing, Unboxing, and

System.Object Relationship

To understand the benefits provided by generics, it is helpful to understand the “issues” programmers had without them. As you recall from Chapter 3, the .NET platform supports automatic conversion between stack-allocated and heap-allocated memory through boxing and unboxing. At first glance, this may seem like a rather uneventful language feature that is more academic than practical. In reality, the (un)boxing process is very helpful in that it allows us to assume everything can be treated as a System.Object, while the CLR takes care of the memory-related details on our behalf.

To review the boxing process, assume you have created a System.Collections.ArrayList to hold numeric (stack-allocated) data. Recall that the members of ArrayList are all prototyped to receive and return System.Object types. However, rather than forcing programmers to manually wrap the stack-based integer in a related object wrapper, the runtime will automatically do so via a boxing operation:

static void Main(string[] args)

{

//Value types are automatically boxed when

//passed to a member requesting an object.

ArrayList myInts = new ArrayList(); myInts.Add(10);

Console.ReadLine();

}

If you wish to retrieve this value from the ArrayList object using the type indexer, you must unbox the heap-allocated object into a stack-allocated integer using a casting operation:

static void Main(string[] args)

{

...

// Value is now unboxed...then reboxed!

321

322

C H A P T E R 1 0 U N D E R S TA N D I N G G E N E R I C S

Console.WriteLine("Value of your int: {0}", (int)myInts[0]);

Console.ReadLine();

}

When the C# compiler transforms a boxing operation into terms of CIL code, you find the box opcode is used internally. Likewise, the unboxing operation is transformed into a CIL unbox operation. Here is the relevant CIL code for the previous Main() method (which can be viewed using ildasm.exe):

.method private hidebysig static void Main(string[] args) cil managed

{

...

box [mscorlib]System.Int32

callvirt instance int32 [mscorlib]System.Collections.ArrayList::Add(object) pop

ldstr "Value of your int: {0}" ldloc.0

ldc.i4.0

callvirt instance object [mscorlib] System.Collections.ArrayList::get_Item(int32)

unbox [mscorlib]System.Int32 ldind.i4

box [mscorlib]System.Int32

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

...

}

Note that the stack-allocated System.Int32 is boxed prior to the call to ArrayList.Add() in order to pass in the required System.Object. Also note that the System.Object is unboxed back into a System.Int32 once retrieved from the ArrayList using the type indexer (which maps to the hidden get_Item() method), only to be boxed again when it is passed to the Console.WriteLine() method.

The Problem with (Un)Boxing Operations

Although boxing and unboxing are very convenient from a programmer’s point of view, this simplified approach to stack/heap memory transfer comes with the baggage of performance issues and a lack of type safety. To understand the performance issues, ponder the steps that must occur to box and unbox a simple integer:

1.A new object must be allocated on the managed heap.

2.The value of the stack-based data must be transferred into that memory location.

3.When unboxed, the value stored on the heap-based object must be transferred back to the stack.

4.The now unused object on the heap will (eventually) be garbage collected.

Although the current Main() method won’t cause a major bottleneck in terms of performance, you could certainly feel the impact if an ArrayList contained thousands of integers that are manipulated by your program on a somewhat regular basis.

Now consider the lack of type safety regarding unboxing operations. As you know, to unbox

a value using the syntax of C#, you make use of the casting operator. However, the success or failure of a cast is not known until runtime. Therefore, if you attempt to unbox a value into the wrong data type, you receive an InvalidCastException:

C H A P T E R 1 0 U N D E R S TA N D I N G G E N E R I C S

323

static void Main(string[] args)

{

...

// Ack! Runtime exception!

Console.WriteLine("Value of your int: {0}", (short)myInts[0]);

Console.ReadLine();

}

In an ideal world, the C# compiler would be able to resolve illegal unboxing operations at compile time, rather than at runtime. On a related note, in a really ideal world, we could store sets of value types in a container that did not require boxing in the first place. .NET 2.0 generics are the solution to each of these issues. However, before we dive into the details of generics, let’s see how programmers attempted to contend with these issues under .NET 1.x using strongly typed collections.

Type Safety and Strongly Typed Collections

In the world of .NET prior to version 2.0, programmers attempted to address type safety by building custom strongly typed collections. To illustrate, assume you wish to create a custom collection that can only contain objects of type Person:

public class Person

{

// Made public for simplicity. public int currAge;

public string fName, lName;

public Person(){}

public Person(string firstName, string lastName, int age)

{

currAge = age; fName = firstName; lName = lastName;

}

public override string ToString()

{

return string.Format("{0}, {1} is {2} years old", lName, fName, currAge);

}

}

To build a person collection, you could define a System.Collections.ArrayList member variable within a class named PeopleCollection and configure all members to operate on strongly typed Person objects, rather than on generic System.Objects:

public class PeopleCollection : IEnumerable

{

private ArrayList arPeople = new ArrayList(); public PeopleCollection(){}

// Cast for caller.

public Person GetPerson(int pos) { return (Person)arPeople[pos]; }

// Only insert Person types. public void AddPerson(Person p) { arPeople.Add(p); }