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

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

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

284C H A P T E R 8 C A L L B A C K I N T E R FA C E S, D E L E G AT E S, A N D E V E N T S

class SomeCaller

{

static void Main(string[] args)

{

SomeType t = new SomeType();

t.SomeEvent += delegate (optionallySpecifiedDelegateArgs) { /* statements */ };

}

}

When handling the first AboutToBlow event within the previous Main() method, notice that you are not defining the arguments passed by the delegate:

c1.AboutToBlow += delegate { Console.WriteLine("Eek! Going too fast!");

};

Strictly speaking, you are not required to receive the incoming arguments sent by a specific event. However, if you wish to make use of the possible incoming arguments, you will need to specify the parameters prototyped by the delegate type (as seen in the second handling of the AboutToBlow and Exploded events). For example:

c1.AboutToBlow += delegate(object sender, CarEventArgs e) { Console.WriteLine("Critical Message from Car: {0}", e.msg);

};

Accessing “Outer”Variables

Anonymous methods are interesting in that they are able to access the local variables of the method that defines them. Formally speaking, such variables are termed “outer variables” of the anonymous method. To illustrate, assume our Main() method defined a local integer named aboutToBlowCounter. Within the anonymous methods that handle the AboutToBlow event, we will increment this counter by 1 and print out the tally before Main() completes:

static void Main(string[] args)

{

...

int aboutToBlowCounter = 0;

// Make a car as usual.

Car c1 = new Car("SlugBug", 100, 10);

// Register event handlers as anonymous methods. c1.AboutToBlow += delegate

{

aboutToBlowCounter++;

Console.WriteLine("Eek! Going too fast!");

};

c1.AboutToBlow += delegate(string msg)

{

aboutToBlowCounter++;

Console.WriteLine("Critical Message from Car: {0}", msg);

};

...

Console.WriteLine("AboutToBlow event was fired {0} times.", aboutToBlowCounter);

Console.ReadLine();

C H A P T E R 8 C A L L B A C K I N T E R FA C E S, D E L E G AT E S, A N D E V E N T S

285

Once you run this updated Main() method, you will find the final Console.WriteLine() reports the AboutToBlow event was fired twice.

Note An anonymous method cannot access ref or out parameters of the defining method.

C# Method Group Conversions

Another delegate-and-event-centric feature of C# is termed method group conversion. This feature allows you to register the “simple” name of an event handler. To illustrate, let’s revisit the SimpleMath type examined earlier in this chapter, which is now updated with a new event named

ComputationFinished:

public class SimpleMath

{

//Not bothering to create a System.EventArgs

//derived type here.

public delegate void MathMessage(string msg); public event MathMessage ComputationFinished;

public int Add(int x, int y)

{

ComputationFinished("Adding complete."); return x + y;

}

public int Subtract(int x, int y)

{

ComputationFinished("Subtracting complete."); return x - y;

}

}

If we are not using anonymous method syntax, you know that the way we would handle the

ComputationComplete event is as follows:

class Program

{

static void Main(string[] args)

{

SimpleMath m = new SimpleMath(); m.ComputationFinished +=

new SimpleMath.MathMessage(ComputationFinishedHandler); Console.WriteLine("10 + 10 is {0}", m.Add(10, 10)); Console.ReadLine();

}

static void ComputationFinishedHandler(string msg) { Console.WriteLine(msg); }

}

However, we can register the event handler with a specific event like this (the remainder of the code is identical):

m.ComputationFinished += ComputationFinishedHandler;

286 C H A P T E R 8 C A L L B A C K I N T E R FA C E S, D E L E G AT E S, A N D E V E N T S

Notice that we are not directly “new-ing” the associated delegate type, but rather simply specifying a method that matches the delegate’s expected signature (a method returning nothing and taking

a single System.String in this case). Understand that the C# compiler is still ensuring type safety. Thus, if the ComputationFinishedHandler() method did not take a System.String and return void, we would be issued a compiler error.

It is also possible to explicitly convert an event hander into an instance of the delegate it relates to. This can be helpful if you need to obtain the underlying delegate using a predefined method. For example:

//.NET 2.0 allows event handlers to be converted into

//their underlying delegate.

SimpleMath.MathMessage mmDelegate = (SimpleMath.MathMessage)ComputationFinishedHandler;

Console.WriteLine(mmDelegate.Method);

If you executed this code, the final Console.WriteLine() prints out the signature of Computation FinishedHandler, as shown in Figure 8-9.

Figure 8-9. You can extract a delegate from the related event handler.

Source Code The AnonymousMethods project is located under the Chapter 8 subdirectory.

Summary

In this chapter, you have examined a number of ways in which multiple objects can partake in a bidirectional conversation. First, you examined the use of callback interfaces, which provide a way to have object B make calls on object A through a common interface type. Do understand

that this design pattern is not specific to .NET, but may be employed in any language or platform that honors the use of interface-based programming techniques.

Next, you examined the C# delegate keyword, which is used to indirectly construct a class derived from System.MulticastDelegate. As you have seen, a delegate is simply an object that maintains a list of methods to call when told to do so. These invocations may be made synchronously (using the Invoke() method) or asynchronously (via the BeginInvoke() and EndInvoke() methods). Again, the asynchronous nature of .NET delegate types will be examined at a later time.

C H A P T E R 8 C A L L B A C K I N T E R FA C E S, D E L E G AT E S, A N D E V E N T S

287

You then examined the C# event keyword which, when used in conjunction with a delegate type, can simplify the process of sending your event notifications to awaiting callers. As shown via the resulting CIL, the .NET event model maps to hidden calls on the System.Delegate/System.MulticastDelegate types. In this light, the C# event keyword is purely optional in that it simply saves you some typing time.

Finally, this chapter examined a new C# 2005 language feature termed anonymous methods. Using this syntactic construct, you are able to directly associate a block of code statements to a given event. As you have seen, anonymous methods are free to ignore the parameters sent by the event and have access to the “outer variables” of the defining method. Last but not least, you examined

a simplified way to register events using method group conversion.

C H A P T E R 9

■ ■ ■

Advanced C# Type Construction

Techniques

In this chapter, you’ll deepen your understanding of the C# programming language by examining a number of advanced (but still quite useful) syntactic constructs. To begin, you’ll learn how to construct and use an indexer method. This C# mechanism enables you to build custom types that provide access to internal subtypes using an array-like syntax. Once you learn how to build an indexer method, you’ll then examine how to overload various operators (+, , <, >, and so forth), and create custom explicit and implicit conversion routines for your types (and you’ll learn why you may wish to do so).

The later half of this chapter examines a small set of lesser used (but nonetheless interesting) C# keywords. For example, you’ll learn how to programmatically account for overflow and underflow conditions using the checked and unchecked keywords, as well as how to create an “unsafe” code context in order to directly manipulate pointer types using C#. The chapter wraps up with an examination of the role of C# preprocessor directives.

Building a Custom Indexer

As programmers, we are very familiar with the process of accessing discrete items contained within a standard array using the index operator, for example:

// Declare an array of integers.

int[] myInts = { 10, 9, 100, 432, 9874};

// Use the [] operator to access each element. for(int j = 0; j < myInts.Length; j++)

Console.WriteLine("Index {0} = {1} ", j, myInts[j]);

The previous code is by no means a major newsflash. However, the C# language provides the capability to build custom classes and structures that may be indexed just like a standard array. It should be no big surprise that the method that provides the capability to access items in this manner is termed an indexer.

Before exploring how to create such a construct, let’s begin by seeing one in action. Assume you have added support for an indexer method to the custom collection (Garage) developed in Chapter 8. Observe the following usage:

289

290C 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

//Indexers allow you to access items in an arraylike fashion. public class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Indexers *****\n");

//Assume the Garage type has an indexer method.

Garage carLot = new Garage();

//Add some cars to the garage using indexer. carLot[0] = new Car("FeeFee", 200); carLot[1] = new Car("Clunker", 90); carLot[2] = new Car("Zippy", 30);

//Now obtain and display each item using indexer. for (int i = 0; i < 3; i++)

{

Console.WriteLine("Car number: {0}", i); Console.WriteLine("Name: {0}", carLot[i].PetName); Console.WriteLine("Max speed: {0}", carLot[i].CurrSpeed); Console.WriteLine();

}

Console.ReadLine();

}

}

As you can see, indexers behave much like a custom collection supporting the IEnumerator and IEnumerable interfaces. The only major difference is that rather than accessing the contents using interface types, you are able to manipulate the internal collection of automobiles just like a standard array.

Now for the big question: How do you configure the Garage class (or any class/structure) to support this functionality? An indexer is represented as a slightly mangled C# property. In its simplest form, an indexer is created using the this[] syntax. Here is the relevant update to the Garage type:

// Add the indexer to the existing class definition. public class Garage : IEnumerable // foreach iteration

{

...

//Use ArrayList to contain the Car types. private ArrayList carArray = new ArrayList();

//The indexer returns a Car based on a numerical index. public Car this[int pos]

{

//Note ArrayList has an indexer as well!

get { return (Car)carArray[pos]; } set {carArray.Add(value);}

}

}

Beyond the use of the this keyword, the indexer looks just like any other C# property declaration. Do be aware that indexers do not provide any array-like functionality beyond the use of the subscript operator. In other words, the object user cannot write code such as the following:

// Use ArrayList.Count property? Nope!

Console.WriteLine("Cars in stock: {0} ", carLot.Count);

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

291

To support this functionality, you would need to add your own Count property to the Garage type, and delegate accordingly:

public class Garage: IEnumerable

{

...

// Containment/delegation in action once again. public int Count { get { return carArray.Count; } }

}

As you can gather, indexers are yet another form of syntactic sugar, given that this functionality can also be achieved using “normal” public methods. For example, if the Garage type did not support an indexer, you would be able to allow the outside world to interact with the internal array list using a named property or traditional accessor/mutator methods. Nevertheless, you when you support indexers on your custom collection types, they integrate well into the fabric of the .NET base class libraries.

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

A Variation of the Garage Indexer

The current Garage type defined an indexer that allowed the caller to identify subitems using a numerical value. Understand, however, that this is not a requirement of an indexer method. Assume you would rather contain the Car objects within a System.Collections.Specialized.ListDictionary rather than an ArrayList. Given that ListDictionary types allow access to the contained types using a key token (such as a string), you could configure the new Garage indexer as follows:

public class Garage : IEnumerable

{

private ListDictionary carDictionary = new ListDictionary();

// This indexer returns a Car based on a string index. public Car this[string name]

{

get { return (Car)carDictionary[name]; } set { carDictionary[name] = value; }

}

public int Length { get { return carDictionary.Count; } }

public IEnumerator GetEnumerator()

{ return carDictionary.GetEnumerator(); }

}

The caller would now be able to interact with the internal cars as shown here:

public class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Indexers *****\n");

Garage carLot = new Garage();

292C 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

//Add named cars to garage. carLot["FeeFee"] = new Car("FeeFee", 200, 0);

carLot["Clunker"] = new Car("Clunker", 90, 0); carLot["Zippy"] = new Car("Zippy", 30, 0);

//Now get Zippy.

Car zippy = carLot["Zippy"]; Console.WriteLine("{0} is going {1} MPH",

zippy.PetName, zippy.CurrSpeed); Console.ReadLine();

}

}

Understand that indexers may be overloaded. Thus, if it made sense to allow the caller to access subitems using a numerical index or a string value, you might define multiple indexers for a single type.

Source Cone The StringIndexer project is located under the Chapter 9 subdirectory.

Internal Representation of Type Indexers

Now that you have seen a few variations on the C# indexer method, you may be wondering how indexers are represented in terms of CIL. If you were to open up the numerical indexer of the Garage type, you would find that the C# compiler has created a property named Item, which maps to the correct getter/setter methods:

property instance class SimpleIndexer.Car Item(int32)

{

.get instance class SimpleIndexer.Car SimpleIndexer.Garage::get_Item(int32)

.set instance void SimpleIndexer.Garage::set_Item(int32, class SimpleIndexer.Car)

} // end of property Garage::Item

The get_Item() and set_Item() methods are implemented like any other .NET property, for example:

method public hidebysig specialname instance class SimpleIndexer.Car get_Item(int32 pos) cil managed

{

 

Code size

22 (0x16)

.maxstack

2

.locals init ([0] class SimpleIndexer.Car CS$1$0000)

IL_0000:

ldarg.0

IL_0001:

ldfld class [mscorlib]System.Collections.ArrayList

SimpleIndexer.Garage::carArray

IL_0006:

ldarg.1

IL_0007:

callvirt instance object [mscorlib]

System.Collections.ArrayList::get_Item(int32)

IL_000c:

castclass SimpleIndexer.Car

IL_0011:

stloc.0

IL_0012:

br.s IL_0014

IL_0014:

ldloc.0

IL_0015:

ret

} // end of

method Garage::get_Item

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

293

Indexers: Final Details

If you want to get really exotic, you can also create an indexer that takes multiple parameters. Assume you have a custom collection that stores subitems in a 2D array. If this is the case, you may configure an indexer method as follows:

public class SomeContainer

{

private int[,] my2DintArray = new int[10, 10];

public int this[int row, int column]

{ /* get or set value from 2D array */ }

}

Finally, understand that indexers can be defined on a given .NET interface type to allow implementing types to provide a custom implementation. Such an interface is as follows:

public interface IEstablishSubObjects

{

//This interface defines an indexer that returns

//strings based on a numerical index.

string this[int index] { get; set; }

}

So much for the topic of C# indexers. Next up, you’ll examine a technique supported by some (but not all) .NET programming languages: operator overloading.

Understanding Operator Overloading

C#, like any programming language, has a canned set of tokens that are used to perform basic operations on intrinsic types. For example, you know that the + operator can be applied to two integers in order to yield a larger integer:

// The + operator with ints. int a = 100;

int b = 240;

int c = a + b; // c is now 340

Again, this is no major news flash, but have you ever stopped and noticed how the same + operator can be applied to most intrinsic C# data types? For example, consider this code:

// + operator with strings. string s1 = "Hello"; string s2 = " world!";

string s3 = s1 + s2; // s3 is now "Hello world!"

In essence, the + operator functions in unique ways based on the supplied data types (strings or integers in this case). When the + operator is applied to numerical types, the result is the summation of the operands. However, when the + operator is applied to string types, the result is string concatenation.

The C# language provides the capability for you to build custom classes and structures that also respond uniquely to the same set of basic tokens (such as the + operator). Be aware that you cannot overload each and every intrinsic C# operator. Table 9-1 outlines the “overloadability” of the core operators.