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

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

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

C H A P T E R 1 2

Indexers, Operators, and Pointers

In this chapter, you’ll deepen your understanding of the C# programming language by examining a handful of advanced 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 how to create custom explicit and implicit conversion routines for your types (and you’ll learn why you may wish to

do so).

The remainder of this chapter examines a small set of lesser used (but nonetheless interesting) C# keywords. For example, you’ll learn how to create an “unsafe” code context in order to directly manipulate pointer types using C# and make use of various preprocessor directives.

Understanding Indexer Methods

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

static void Main(string[] args)

{

//Loop over incoming start up params. for(int i = 0; i < args.Length; i++)

Console.WriteLine("Args: {0}", args[i]);

//Declare an array of local 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]); Console.ReadLine();

}

The previous code is by no means a major newsflash. However, the C# language provides the capability to design custom classes and structures that may be indexed just like a standard array, by defining an indexer method. This particular language feature is most useful when you are creating custom collection types (generic or nongeneric).

Before examining 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 PeopleCollection type developed in Chapter 10 (specifically, the CustomNonGenericCollection project). Observe the following usage within a new Console Application named SimpleIndexer:

383

384CHAPTER 12 INDEXERS, OPERATORS, AND POINTERS

// Indexers allow you to access items in an array-like fashion. class Program

{

static void Main(string[] args)

{

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

PeopleCollection myPeople = new PeopleCollection();

// Add objects with indexer syntax.

myPeople[0] = new Person("Homer", "Simpson", 40); myPeople[1] = new Person("Marge", "Simpson", 38); myPeople[2] = new Person("Lisa", "Simpson", 9); myPeople[3] = new Person("Bart", "Simpson", 7); myPeople[4] = new Person("Maggie", "Simpson", 2);

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

{

Console.WriteLine("Person number: {0}", i); Console.WriteLine("Name: {0} {1}",

myPeople[i].FirstName, myPeople[i].LastName); Console.WriteLine("Age: {0}", myPeople[i].Age); Console.WriteLine();

}

}

}

As you can see, indexers behave much like a custom collection supporting the IEnumerator and IEnumerable interfaces in that they provide access to a container’s subitems. The major difference of course is that rather than accessing the contents using the foreach construct, you are able to manipulate the internal collection of sub-objects just like a standard array.

Now for the big question: How do you configure the PeopleCollection 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 required update for the PeopleCollection class:

// Add the indexer to the existing class definition. public class PeopleCollection : IEnumerable

{

private ArrayList arPeople = new ArrayList();

// Custom indexer for this class. public Person this[int index]

{

get { return (Person)arPeople[index]; } set { arPeople.Insert(index, value); }

}

...

}

Beyond the use of the this keyword, the indexer looks just like any other C# property declaration. For example, the role of the get scope is to return the correct object to the caller. Here, we are in fact doing so by using the indexer of the ArrayList object! The set scope is in charge of placing the incoming object into the container at the specified index; in this example, this is achieved by calling the Insert() method of the ArrayList.

CHAPTER 12 INDEXERS, OPERATORS, AND POINTERS

385

As you can see, indexers are yet another form of syntactic sugar, given that this functionality can also be achieved using “normal” public methods such as AddPerson() or GetPerson(). Nevertheless, when you support indexer methods on your custom collection types, they integrate well into the fabric of the .NET base class libraries.

While building indexer methods is quite commonplace when you are building custom collections, do remember that generic types give you this very functionality out of the box. Consider the following method, which makes use of a generic List<T> of Person objects. Note we are able to simply use the indexer of List<T> directly, for example:

static void UseGenericListOfPeople()

{

List<Person> myPeople = new List<Person>(); myPeople.Add(new Person("Lisa", "Simpson", 9)); myPeople.Add(new Person("Bart", "Simpson", 7));

// Change first person with indexer.

myPeople[0] = new Person("Maggie", "Simpson", 2);

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

{

Console.WriteLine("Person number: {0}", i); Console.WriteLine("Name: {0} {1}", myPeople[i].FirstName,

myPeople[i].LastName); Console.WriteLine("Age: {0}", myPeople[i].Age); Console.WriteLine();

}

}

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

Indexing Objects Using String Values

The current PeopleCollection 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 Person objects using a System.Collections.Generic. Dictionary<TKey, TValue> rather than an ArrayList. Given that ListDictionary types allow access to the contained types using a string token (such as a person’s first name), you could define an indexer as follows:

public class PeopleCollection : IEnumerable

{

private Dictionary<string, Person> listPeople = new Dictionary<string, Person>();

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

{

get { return (Person)listPeople[name]; } set { listPeople[name] = value; }

}

386 CHAPTER 12 INDEXERS, OPERATORS, AND POINTERS

public void ClearPeople() { listPeople.Clear(); }

public int Count

{ get { return listPeople.Count; } }

IEnumerator IEnumerable.GetEnumerator() { return listPeople.GetEnumerator(); }

}

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

static void Main(string[] args)

{

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

PeopleCollection myPeople = new PeopleCollection();

myPeople["Homer"] = new Person("Homer", "Simpson", 40); myPeople["Marge"] = new Person("Marge", "Simpson", 38);

// Get "Homer" and print data.

Person homer = myPeople["Homer"]; Console.WriteLine(homer.ToString()); Console.ReadLine();

}

Again, if you were to make use of the generic Dictionary<TKey, TValue> type directly, you could gain the indexer method functionality out of the box.

Source Code The StringIndexer project is located under the Chapter 12 subdirectory.

Overloaded Indexer Methods

Understand that indexer methods 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. By way of example, if you have ever programmed with ADO.NET (.NET’s native database access API), you may recall that the DataSet type supports a property named Tables, which returns to you a strongly typed DataTableCollection type. As it turns out, DataTableCollection defines three indexers to get and set DataTable objects; one by ordinal position, and the others by a friendly string moniker and optional containing namespace:

public sealed class DataTableCollection : InternalDataCollectionBase

{

...

// Overloaded indexers!

public DataTable this[string name] { get; }

public DataTable this[string name, string tableNamespace] { get; } public DataTable this[int index] { get; }

}

To be sure, a number of types in the base class libraries support indexer methods. Therefore, even if your current project does not require you to build custom indexers for your classes and structures, be aware that many types already support this syntax.

CHAPTER 12 INDEXERS, OPERATORS, AND POINTERS

387

Internal Representation of Indexer Methods

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 indexer of the current PeopleCollection 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 StringIndexer.Person Item(string)

{

.get instance class StringIndexer.Person StringIndexer.PeopleCollection::get_Item(string)

.set instance void StringIndexer.PeopleCollection::set_Item(string, class StringIndexer.Person)

} // end of property PeopleCollection::Item

The get_Item() and set_Item() methods are implemented like any other .NET property; for example, consider the following set logic:

.method public hidebysig specialname instance void set_Item(string name,

class StringIndexer.Person 'value') cil managed

{

 

 

// Code size

16 (0x10)

.maxstack

8

 

IL_0000:

nop

 

IL_0001:

ldarg.0

 

IL_0002:

ldfld

class [System]System.Collections.Specialized.ListDictionary

StringIndexer.PeopleCollection::listPeople IL_0007: ldarg.1

IL_0008: ldarg.2

IL_0009: callvirt instance void [System]System.Collections.Specialized.ListDictionary::Add(object, object) IL_000e: nop

IL_000f: ret

} // end of method PeopleCollection::set_Item

Note The .NET Framework SDK 3.5 documentation will list indexer methods of a class or structure as a property named Item. However, the Visual Studio Object Browser will show indexers as properties defined using expected this[] syntax.

Indexers with Multiple Dimensions

It is also permissible to create an indexer method 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 */ }

}

388 CHAPTER 12 INDEXERS, OPERATORS, AND POINTERS

Indexer Definitions on Interface Types

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

public interface IStringContainer

{

//This interface defines an indexer that returns

//strings based on a numerical index.

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

}

With this interface definition, any class or structure that implements this interface must now support a read/write indexer that manipulates subitems using a numerical value. As well, you could design a generic interface where the type indexer allows the implementer to determine what will be used to get or set the subobjects:

public interface IStringContainer<Key>

{

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

}

Here would be an implementation using a numerical indexer:

class MyStrings : IStringContainer<int>

{

string[] strings = { "First", "Second" };

public string this[int Key]

{

get

{

return strings[Key];

}

set

{

strings[Key] = value;

}

}

}

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 newsflash, 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!"

CHAPTER 12 INDEXERS, OPERATORS, AND POINTERS

389

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 12-1 outlines the “overloadability” of the core operators.

Table 12-1. Overloadability of C# Operators

C# Operator

Overloadability

+, -, !, ~, ++, --, true, false +, -, *, /, %, &, |, ^, <<, >> ==, !=, <, >, <=, >=

This set of unary operators can be overloaded.

These binary operators can be overloaded.

The comparison operators can be overloaded. C# will demand that “like” operators (i.e., < and >, <= and >=, == and !=) are overloaded together.

[]

The [] operator cannot be overloaded. As you saw earlier in

 

this chapter, however, the indexer construct provides the

 

same functionality.

()

The () operator cannot be overloaded. As you will see later

 

in this chapter, however, custom conversion methods

 

provide the same functionality.

+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=

Shorthand assignment operators cannot be overloaded;

 

however, you receive them as a freebie when you overload

 

the related binary operator.

 

 

Note In C#, true and false can be used as operators in addition to literals. This functionality can be useful when building custom types that represent true, false, and null (meaning neither true nor false).

Overloading Binary Operators

To illustrate the process of overloading binary operators, assume the following simple Point structure defined in a new Console Application named OverloadedOps:

// Just a simple everyday C# struct. public struct Point

{

private int x, y;

public Point(int xPos, int yPos)

{

x = xPos; y = yPos;

}

public override string ToString()

{

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

}

}

390 CHAPTER 12 INDEXERS, OPERATORS, AND POINTERS

Now, logically speaking, it makes sense to add Points together. For example, if you added together two Point variables, you should receive a new Point that is the summation of the x and y values. On a related note, it may be helpful to subtract one Point from another. Ideally, you would like to be able to author the following code:

// Adding and subtracting two points? static void Main(string[] args)

{

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

// Make two points.

Point ptOne = new Point(100, 100); Point ptTwo = new Point(40, 40); Console.WriteLine("ptOne = {0}", ptOne); Console.WriteLine("ptTwo = {0}", ptTwo);

//Add the points to make a bigger point?

Console.WriteLine("ptOne + ptTwo: {0} ", ptOne + ptTwo);

//Subtract the points to make a smaller point?

Console.WriteLine("ptOne - ptTwo: {0} ", ptOne - ptTwo); Console.ReadLine();

}

However, as our Point now stands, we will receive compile-time errors, as the Point type does not know how to respond to the + or - operators (see Figure 12-1).

Figure 12-1. By default, custom classes/structures do not support custom operators.

To equip a custom type to respond uniquely to intrinsic operators, C# provides the operator keyword, which you can use only in conjunction with static methods. When you are overloading a binary operator (such as + and -), you will most often pass in two arguments that are the same type as the defining class (a Point in this example), as illustrated in the following code update:

// A more intelligent Point type. public struct Point

{

...

// overloaded operator +

public static Point operator + (Point p1, Point p2) { return new Point(p1.x + p2.x, p1.y + p2.y); }

// overloaded operator -

public static Point operator - (Point p1, Point p2) { return new Point(p1.x - p2.x, p1.y - p2.y); }

}

CHAPTER 12 INDEXERS, OPERATORS, AND POINTERS

391

The logic behind operator + is simply to return a brand new Point based on the summation of the fields of the incoming Point parameters. Thus, when you write pt1 + pt2, under the hood you can envision the following hidden call to the static operator + method:

// Point p3 = Point.operator+ (p1, p2)

Point p3 = p1 + p2;

Likewise, p1 p2 maps to the following:

// Point p4 = Point.operator- (p1, p2)

Point p4 = p1 - p2;

With this update, our program now compiles, and we find we are able to add and subtract Point objects (see Figure 12-2).

Figure 12-2. Redefining + and - for the Point type

Strictly speaking, when you are overloading a binary operator, you are not required to pass in two parameters of the same type. If it makes sense to do so, one of the arguments can differ. For example, here is an overloaded operator +, which allows the caller to obtain a new Point that is based on a numerical adjustment:

public struct Point

{

...

public static Point operator +(Point p1, int change)

{

return new Point(p1.x + change, p1.y + change);

}

public static Point operator +(int change, Point p1)

{

return new Point(p1.x + change, p1.y + change);

}

}

We would now be able to use these new versions of operator + as follows:

// Prints [110, 110]

Point biggerPoint = ptOne + 10; Console.WriteLine("ptOne + 10 = {0}", biggerPoint);

// Prints [120, 120]

Console.WriteLine("10 + biggerPoint = {0}", 10 + biggerPoint); Console.WriteLine();