Tải bản đầy đủ (.pdf) (86 trang)

Programming C# 4.0 phần 4 pdf

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (10.41 MB, 86 trang )

object of type CalendarEvent[] (shown on the left) where each element in the array
refers to one of the event objects.
Figure 7-1. An array with reference type elements
As you saw in Chapter 3,
with
reference types multiple different variables can all refer
to the same object. Since elements in an array behave in a similar way to local variables
of the element type, we could create an array where all the elements refer to the same
object, as shown in Example 7-11.
Example 7-11. Multiple elements referring to the same object
CalendarEvent theOnlyEvent = new CalendarEvent
{
Title = "Swing Dancing at the South Bank",
StartTime = new DateTimeOffset (2009, 7, 11, 15, 00, 00, TimeSpan.Zero),
Duration = TimeSpan.FromHours(4)
};
CalendarEvent[] events =
{
theOnlyEvent,
theOnlyEvent,
theOnlyEvent,
theOnlyEvent,
theOnlyEvent
};
Figure 7-2 illustrates the result. While this particular example is not brilliantly useful,
in some situations it’s helpful for multiple elements to refer to one object. For example,
imagine a feature for booking meeting rooms or other shared facilities—this could
be a useful addition to a calendar program. An array might describe how the room will
be used today, where each element represents a one-hour slot for a particular room. If
Arrays | 227
the same individual had booked the same room for two different slots, the two corre-


sponding array elements would both refer to the same person.
Figure 7-2. An array where all of the elements refer to the same object
Another feature
that reference type array elements have in common with reference type
variables and arguments is support for polymorphism. As you saw in Chapter 4, a
variable declared as some particular reference type can refer to any object of that type,
or of any type derived from the variable’s declared type. This works for arrays too—
using the examples from Chapter 4, if an array’s type is FirefighterBase[], each ele-
ment could refer to a Firefighter, or TraineeFirefighter, or anything else that derives
from FirefighterBase. (And each element is allowed to refer to an object of a different
type, as long as the objects are all compatible with the element type.) Likewise, you can
declare an array of any interface type—for example, INamedPerson[], in which case each
element can refer to any object of any type that implements that interface. Taking this
to extremes, an array of type object[] has elements that can refer to any object of any
reference type, or any boxed value.
As you will remember from Chapter 3, the alternative to a reference type is a value
type. With value types, each variable holds its own copy of the value, rather than a
reference to some potentially shared object. As you would expect, this behavior carries
over to arrays when the element type is a value type. Consider the array shown in
Example 7-12.
Example 7-12. An array of integer values
int[] numbers = { 2, 3, 5, 7, 11 };
Like all the numeric types, int is a value type, so we end up with a rather different
structure. As Figure 7-3 shows, the array elements are the values themselves, rather
than references to values.
Why would you need to care about where exactly the value lives? Well, there’s a sig-
nificant difference in behavior. Given the numbers array in Example 7-12, consider this
code:
228 | Chapter 7: Arrays and Lists
int thirdElementInArray = numbers[2];

thirdElementInArray += 1;
Console.WriteLine("Variable: " + thirdElementInArray);
Console.WriteLine("Array element: " + numbers[2]);
which would print out the following:
Variable: 6
Array element: 5
Figure 7-3. An array with value type elements
Because
we
are dealing with a value type, the thirdElementInArray local variable gets
a copy of the value in the array. This means that the code can change the local variable
without altering the element in the array. Compare that with similar code working on
the array from Example 7-10:
CalendarEvent thirdElementInArray = events[2];
thirdElementInArray.Title = "Modified title";
Console.WriteLine("Variable: " + thirdElementInArray.Title);
Console.WriteLine("Array element: " + events[2].Title);
This would print out the following:
Variable: Modified title
Array element: Modified title
This shows that we’ve modified the event’s title both from the point of view of the local
variable and from the point of view of the array element. That’s because both refer to
the same CalendarEvent object—with a reference type, when the first line gets an ele-
ment from the array we don’t get a copy of the object, we get a copy of the reference
to that object. The object itself is not copied.
The distinction between the reference and the object being referred to means that
there’s sometimes scope for ambiguity—what exactly does it mean to change an ele-
ment in an array? For value types, there’s no ambiguity, because the element is the
value. The only way to change an entry in the numbers array in Example 7-12 is to assign
a new value into an element:

numbers[2] = 42;
Arrays | 229
But as you’ve seen, with reference types the array element is just a reference, and we
may be able to modify the object it refers to without changing the array element itself.
Of course, we can also change the element, it just means something slightly different—
we’re asking to change which object that particular element refers to. For example, this:
events[2] = events[0];
causes the third element to refer to the same object as the first. This doesn’t modify the
object that element previously referenced. (It might cause the object to become inac-
cessible, though—if nothing else has a reference to that object, overwriting the array
element that referred to it means the program no longer has any way of getting hold of
that object, and so the .NET Framework can reclaim the memory it occupies during
the next garbage collection cycle.)
It’s often tempting to talk in terms of “the fourth object in the array,” and in a lot of
cases, that’s a perfectly reasonable approximation in practice. As long as you’re aware
that with reference types, array elements contain references, not objects, and that what
you really mean is “the object referred to by the fourth element in the array” you won’t
get any nasty surprises.
Regardless of what element type you choose for an array, all arrays provide various
useful methods and properties.
Array Members
An array is an object in its own right; distinct from any objects its elements may refer
to. And like any object, it has a type—as you’ve already seen, we write an array type as
SomeType[]. Whatever type SomeType may be, its corresponding array type, Some
Type[], will derive from a standard built-in type called Array, defined in the System
namespace.
The Array base class provides a variety of services for working with arrays. It can help
you find interesting items in an array. It can reorder the elements, or move information
between arrays. And there are methods for working with the array’s size.
Finding elements

Suppose we want to find out if an array of calendar items contains any events that start
on a particular date. An obvious way to do this would be to write a loop that iterates
through all of the elements in the array, looking at each date in turn (see Example 7-13).
Example 7-13. Finding elements with a loop
DateTime dateOfInterest = new DateTime (2009, 7, 12);
foreach (CalendarEvent item in events)
{
if (item.StartTime.Date == dateOfInterest)
{
Console.WriteLine(item.Title + ": " + item.StartTime);
230 | Chapter 7: Arrays and Lists
}
}
Example 7-13 relies on a useful feature of the DateTimeOffset type that
makes it
easy to work out whether two DateTimeOffset values fall on the
same day, regardless of the exact time. The Date property returns a
DateTime in which the year, month, and day are copied over, but the
time of day is set to the default time of midnight.
Although Example 7-13 works just fine, the Array class provides an alternative: its
FindAll method builds a new array containing only those elements in the original array
that match whatever criteria you specify. Example 7-14 uses this method to do the same
job as Example 7-13.
Example 7-14. Finding elements with FindAll
DateTime dateOfInterest = new DateTime (2009, 7, 12);
CalendarEvent[] itemsOnDateOfInterest = Array.FindAll(events,
e => e.StartTime.Date == dateOfInterest);
foreach (CalendarEvent item in itemsOnDateOfInterest)
{
Console.WriteLine(item.Title + ": " + item.StartTime);

}
Notice that we’re using a lambda expression to tell FindAll which items match. That’s
not mandatory—FindAll requires a delegate here, so you can use any of the alternatives
discussed in Chapter 5, including lambda expressions, anonymous methods, method
names, or any expression that returns a suitable delegate. The delegate type here is
Predicate<T>, where T is the array element type (Predicate<CalendarEvent> in this case).
We also discussed predicate delegates in Chapter 5, but in case your memory needs
refreshing, we just need to supply a function that takes a CalendarEvent and returns
true if it matches, and false if it does not. Example 7-14 uses the same expression as
the if statement in Example 7-13.
This may not seem like an improvement on Example 7-13. We’ve not written any less
code, and we’ve ended up using a somewhat more advanced language feature—lambda
expressions—to get the job done. However, notice that in Example 7-14, we’ve already
done all the work of finding the items of interest before we get to the loop. Whereas
the loop in Example 7-13 is a mixture of code that works out what items we need and
code that does something with those items, Example 7-14 keeps those tasks neatly
separated. And if we were doing more complex work with the matching items, that
separation could become a bigger advantage—code tends to be easier to understand
and maintain when it’s not trying to do too many things at once.
The FindAll method becomes even more useful if you want to pass the set of matching
items on to some other piece of code, because you can just pass the array of matches
Arrays | 231
it returns as an argument to some method in your code. But how would you do that
with the approach in Example 7-13, where the match-finding code is intermingled with
the processing code? While the simple foreach loop in Example 7-13 is fine for trivial
examples, FindAll and similar techniques (such as LINQ, which we’ll get to in the next
chapter) are better at managing the more complicated scenarios likely to arise in real
code.
This is an important principle that is not limited to arrays or collections.
In general, you should try to construct your programs by combining

small pieces, each of which does one well-defined job. Code written this
way tends to be easier to maintain and to contain fewer bugs than code
written as one big, sprawling mass of complexity. Separating code that
selects information from code that processes information is just one
example of this idea.
The Array class offers a few variations on the FindAll theme. If you happen to be in-
terested only in finding the first matching item, you can just call Find. Conversely,
FindLast returns the very last matching item.
Sometimes it can be useful to know where in the array a matching item was found. So
as an alternative to Find and FindLast, Array also offers FindIndex and FindLastIndex,
which work in the same way except they return a number indicating the position of the
first or last match, rather than returning the matching item itself.
Finally, one special case for finding the index of an item turns out to crop up fairly
often: the case where you know exactly which object you’re interested in, and just need
to know where it is in the array. You could do this with a suitable predicate, for example:
int index = Array.FindIndex(events, e => e == someParticularEvent);
But Array offers the more specialized IndexOf and LastIndexOf, so you only have to
write this:
int index = Array.IndexOf(events, someParticularEvent);
Ordering elements
Sometimes it’s useful to modify the order in which entries appear in an array. For
example, with a calendar, some events will be planned long in advance while others
may be last-minute additions. Any calendar application will need to be able to ensure
that events are displayed in chronological order, regardless of how they were added, so
we need some way of getting items into the right order.
The Array class makes this easy with its Sort method. We just need to tell it how we
want the events ordered—it can’t really guess, because it doesn’t have any way of
knowing whether we consider our events to be ordered by the Title, StartTime, or
Duration property. This is a perfect job for a delegate: we can provide a tiny bit of code
232 | Chapter 7: Arrays and Lists

that looks at two CalendarEvent objects and says whether one should appear before the
other, and pass that code into the Sort method (see Example 7-15).
Example 7-15. Sorting an array
Array.Sort(events,
(event1, event2) => event1.StartTime.CompareTo(event2.StartTime));
The Sort method’s first argument, events, is just the array we’d like to reorder. (We
defined that back in Example 7-10.) The second argument is a delegate, and for con-
venience we again used the lambda syntax introduced in Chapter 5. The Sort method
wants to be able to know, for any two events, whether one should appear before the
other, It requires a delegate of type Comparison<T>, a function which takes two argu-
ments—we called them event1 and event2 here—and which returns a number. If
event1 is before event2, the number must be negative, and if it’s after, the number must
be positive. We return zero to indicate that the two are equal. Example 7-15 just defers
to the StartTime property—that’s a DateTimeOffset, which provides a handy
CompareTo method that does exactly what we need.
It turns out that Example 7-15 isn’t changing anything here, because the events array
created in Example 7-10 happens to be in ascending order of date and time already. So
just to illustrate that we can sort on any criteria, let’s order them by duration instead:
Array.Sort(events,
(event1, event2) => event1.Duration.CompareTo(event2.Duration));
This illustrates how the use of delegates enables us to plug in any number of different
ordering criteria, leaving the Array class to get on with the tedious job of shuffling the
array contents around to match the specified order.
Some data types such as dates or numbers have an intrinsic ordering. It would be irri-
tating to have to tell Array.Sort how to work out whether one number comes before
or after another. And in fact we don’t have to—we can pass an array of numbers to a
simpler overload of the Sort method, as shown in Example 7-16.
Example 7-16. Sorting intrinsically ordered data
int[] numbers = { 4, 1, 2, 5, 3 };
Array.Sort(numbers);

As you would expect, this arranges the numbers into ascending order. We would pro-
vide a comparison delegate here only if we wanted to sort the numbers into some other
order. You might be wondering what would happen if we tried this simpler method
with an array of CalendarEvent objects:
Array.Sort(events); // Blam!
Arrays | 233
If you try this, you’ll find that the method throws an InvalidOperationException, be-
cause Array.Sort has no way of working out what order we need. It works only for
types that have an intrinsic order. And should we want to, we could make Calen
darEvent self-ordering. We just have to implement an interface called IComparable<Cal
endarEvent>, which provides a single method, CompareTo. Example 7-17 implements
this, and defers to the DateTimeOffset value in StartTime—the DateTimeOffset type
implements IComparable<DateTimeOffset>. So all we’re really doing here is passing the
responsibility on to the property we want to use for ordering, just like we did in Ex-
ample 7-15. The one extra bit of work we do is to check for comparison with null—
the IComparable<T> interface documentation states that a non-null object should always
compare as greater than null, so we return a positive number in that case. Without this
check, our code would crash with a NullReferenceException if null were passed to
CompareTo.
Example 7-17. Making a type comparable
class CalendarEvent : IComparable<CalendarEvent>
{
public string Title { get; set; }
public DateTimeOffset StartTime { get; set; }
public TimeSpan Duration { get; set; }
public int CompareTo(CalendarEvent other)
{
if (other == null) { return 1; }
return StartTime.CompareTo(other.StartTime);
}

}
Now that our CalendarEvent class has declared an intrinsic ordering for itself, we are
free to use the simplest Sort overload:
Array.Sort(events); // Works, now that CalendarEvent is IComparable<T>
Getting your array contents in order isn’t the only reason for relocating elements, so
Array offers some slightly less specialized methods for moving data around.
Moving or copying elements
Suppose you want to build a calendar application that works with multiple sources of
information—maybe you use several different websites with calendar features and
would like to aggregate all the events into a single list. Example 7-18 shows a method
that takes two arrays of CalendarEvent objects, and returns one array containing all the
elements from both.
Example 7-18. Copying elements from two arrays into one big one
static CalendarEvent[] CombineEvents(CalendarEvent[] events1,
CalendarEvent[] events2)
{
234 | Chapter 7: Arrays and Lists
CalendarEvent[] combinedEvents =
new CalendarEvent[events1.Length + events2.Length];
events1.CopyTo(combinedEvents, 0);
events2.CopyTo(combinedEvents, events1.Length);
return combinedEvents;
}
This
example
uses the CopyTo method, which makes a complete copy of all the elements
of the source array into the target passed as the first argument. The second argument
says where to start copying elements into the target—Example 7-18 puts the first array’s
elements at the start (offset zero), and then copies the second array’s elements directly
after that. (So the ordering won’t be very useful—you’d probably want to sort the results

after doing this.)
You might sometimes want to be a bit more selective—you might want to copy only
certain elements from the source into the target. For example, suppose you want to
remove the first event. Arrays cannot be resized in .NET, but you could create a new
array that’s one element shorter, and which contains all but the first element of the
original array. The CopyTo method can’t help here as it copies the whole array, but you
can use the more flexible Array.Copy method instead, as Example 7-19 shows.
Example 7-19. Copying less than the whole array
static CalendarEvent[] RemoveFirstEvent(CalendarEvent[] events)
{
CalendarEvent[] croppedEvents = new CalendarEvent[events.Length - 1];
Array.Copy(
events, // Array from which to copy
1, // Starting point in source array
croppedEvents, // Array into which to copy
0, // Starting point in destination array
events.Length - 1 // Number of elements to copy
);
return croppedEvents;
}
The key here is that we get to specify the index from which we want to start copying—
1 in this case, skipping over the first element, which has an index of 0.
In practice, you would rarely do this—if you need to be able to add or
remove items
from a collection, you would normally use the List<T>
type that we’ll be looking at later in this chapter, rather than a plain
array. And even if you are working with arrays, there’s an
Array.Resize helper function that you would typically use in reality—
it calls Array.Copy for you. However, you often have to copy data be-
tween arrays, even if it might not be strictly necessary in this simple

example. A more complex example would have obscured the essential
simplicity of Array.Copy.
Arrays | 235
The topic of array sizes is a little more complex than it first appears, so let’s look at that
in more detail.
Array Size
Arrays know how many elements they contain—several of the previous examples have
used the Length property to discover the size of an existing array. This read-only prop-
erty is defined by the base Array class, so it’s always present.
*
That may sound like
enough to cover the simple task of knowing an array’s size, but arrays don’t have to be
simple sequential lists. You may need to work with multidimensional data, and .NET
supports two different styles of arrays for that: jagged and rectangular arrays.
Arrays of arrays (or jagged arrays)
As we said earlier, you can make an array using any type as the element type. And since
arrays themselves have types, it follows that you can have an array of arrays. For ex-
ample, suppose we wanted to create a list of forthcoming events over the next five days,
grouped by day. We could represent this as an array with one entry per day, and since
each day may have multiple events, each entry needs to be an array. Example 7-20
creates just such an array.
Example 7-20. Building an array of arrays
static CalendarEvent[][] GetEventsByDay(CalendarEvent[] allEvents,
DateTime firstDay,
int numberOfDays)
{
CalendarEvent[][] eventsByDay = new CalendarEvent[numberOfDays][];
for (int day = 0; day < numberOfDays; ++day)
{
DateTime dateOfInterest = (firstDay + TimeSpan.FromDays(day)).Date;

CalendarEvent[] itemsOnDateOfInterest = Array.FindAll(allEvents,
e => e.StartTime.Date == dateOfInterest);
eventsByDay[day] = itemsOnDateOfInterest;
}
return eventsByDay;
}
* There’s
also a LongLength, which is a 64-bit version of the property, which theoretically allows for larger arrays
than the 32-bit Length property. However, .NET currently imposes an upper limit on the size of any single
array: it cannot use more than 2 GB of memory, even in a 64-bit process. So in practice, LongLength isn’t very
useful in the current version of .NET (4). (You can use a lot more than 2 GB of memory in total in a 64-bit
process—the 2 GB limit applies only to individual arrays.)
236 | Chapter 7: Arrays and Lists
We’ll look at this one piece at a time. First, there’s the method declaration:
static CalendarEvent[][] GetEventsByDay(CalendarEvent[] allEvents,
DateTime firstDay,
int numberOfDays)
{
The
return type—CalendarEvent[][]—is an array of arrays, denoted by two pairs of
square brackets. You’re free to go as deep as you like, by the way—it’s perfectly possible
to have an array of arrays of arrays of arrays of anything.
The method’s arguments are fairly straightforward. This method expects to be passed
a simple array containing an unstructured list of all the events. The method also needs
to know which day we’d like to start from, and how many days we’re interested in.
The very first thing the method does is construct the array that it will eventually return:
CalendarEvent[][] eventsByDay = new CalendarEvent[numberOfDays][];
Just as new CalendarEvent[5] would create an array capable of containing five
CalendarEvent elements, new CalendarEvent[5][] would create an array capable of
containing five arrays of CalendarEvent objects. Since our method lets the caller specify

the number of days, we pass that argument in as the size of the top-level array.
Remember that arrays are reference types, and that whenever you create a new array
whose element type is a reference type, all the elements are initially null. So although
our new eventsByDay array is capable of referring to an array for each day, what it holds
right now is a null for each day. So the next bit of code is a loop that will populate the
array:
for (int day = 0; day < numberOfDays; ++day)
{

}
Inside this loop, the first couple of lines are similar to the start of Example 7-14:
DateTime dateOfInterest = (firstDay + TimeSpan.FromDays(day)).Date;
CalendarEvent[] itemsOnDateOfInterest = Array.FindAll(allEvents,
e => e.StartTime.Date == dateOfInterest);
The only difference is that this example calculates which date to look at as we progress
through the loop. So Array.FindAll will return an array containing all the events that
fall on the day for the current loop iteration. The final piece of code in the loop puts
that into our array of arrays:
eventsByDay[day] = itemsOnDateOfInterest;
Once the loop is complete, we return the array:
return eventsByDay;
}
Each element will contain an array with the events that fall on the relevant day.
Arrays | 237
Code that uses such an array can use the normal element access syntax, for example:
Console.WriteLine("Number of events on first day: " + eventsByDay[0].Length);
Notice that this code uses just a single index—this means we want to retrieve one of
the arrays from our array of arrays. In this case, we’re looking at the size of the first of
those arrays. Or we can dig further by providing multiple indexes:
Console.WriteLine("First day, second event: " + eventsByDay[0][1].Title);

This syntax, with its multiple sets of square brackets, fits right in with the syntax used
to declare and construct the array of arrays.
So why is an array of arrays sometimes called a jagged array? Figure 7-4 shows the
various objects you would end up with if you called the method in Example 7-20,
passing the events from Example 7-10, asking for five days of events starting from July
11. The figure is laid out to show each child array as a row, and as you can see, the rows
are not all the same length—the first couple of days have two items per row, the third
day has one, and the last two are empty (i.e., they are zero-length arrays). So rather
than looking like a neat rectangle of objects, the rows form a shape with a somewhat
uneven or “jagged” righthand edge.
This jaggedness can be either a benefit or a problem, depending on your goals. In this
example, it’s helpful—we used it to handle the fact that the number of events in our
calendar may be different every day, and some days may have no events at all. But if
you’re working with information that naturally fits into a rectangular structure (e.g.,
pixels in an image), rows of differing lengths would constitute an error—it would be
better to use a data structure that doesn’t support such things, so you don’t have to
work out how to handle such an error.
Moreover, jagged arrays end up with a relatively complicated structure—there are a lot
of objects in Figure 7-4. Each array is an object distinct from the objects its element
refers to, so we’ve ended up with 11 objects: the five events, the five per-day arrays
(including two zero-length arrays), and then one array to hold those five arrays. In
situations where you just don’t need this flexibility, there’s a simpler way to represent
multiple rows: a rectangular array.
Rectangular arrays
A rectangular array

lets you store multidimensional data in a single array, rather than
needing to create arrays of arrays. They are more regular in form than jagged arrays—
in a two-dimensional rectangular array, every row has the same width.
† Rectangular arrays are also sometimes called multidimensional arrays, but that’s a slightly confusing name,

because jagged arrays also hold multidimensional data.
238 | Chapter 7: Arrays and Lists
Rectangular arrays are not limited to two dimensions, by the way. Just
as you
can have arrays of arrays of arrays, so you can have any number
of dimensions in a “rectangular” array, although the name starts to
sound a bit wrong. With three dimensions, it’s a cuboid rather than a
rectangle, and more generally the shape of these arrays is always an
orthotope. Presumably the designers of C# and the .NET Framework
felt that this “proper” name was too obscure (as does the spellchecker
in Word) and that rectangular was more usefully descriptive, despite
not being technically correct. Pragmatism beat pedantry here because
C# is fundamentally a practical language.
Figure 7-4. A jagged array
Arrays | 239
Rectangular arrays tend to suit different problems than jagged arrays, so we need to
switch temporarily to a different example. Suppose you were writing a simple game in
which a character runs around a maze. And rather than going for a typical modern 3D
game rendered from the point of view of the player, imagine something a bit more
retro—a basic rendering of a top-down view, and where the walls of the maze all fit
neatly onto a grid. If you’re too young to remember this sort of thing, Figure 7-5 gives
a rough idea of what passed for high-tech entertainment back when your authors were
at school.
Figure 7-5. Retro gaming—3D is for wimps
We don’t
want to get too hung up on the details of the game play, so let’s just assume
that our code needs to know where the walls are in order to work out where the player
can or can’t move next, and whether she has a clean shot to take out the baddies chasing
her through the maze. We could represent this as an array of numbers, where 0 repre-
sents a gap and 1 represents a wall, as Example 7-21 shows. (We could also have used

bool instead of int as the element type, as there are only two possible options: a wall
or no wall. However, using true and false would have prevented each row of data from
fitting on a single row in this book, making it much harder to see how Example 7-21
reflects the map in Figure 7-5. Moreover, using numbers leaves open the option to add
exciting game features such as unlockable doors, squares of instant death, and other
classics.)
240 | Chapter 7: Arrays and Lists
Example 7-21. A multidimensional rectangular array
int[,] walls = new int[,]
{
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1 },
{ 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1 },
{ 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1 },
{ 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0 },
{ 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1 },
{ 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1 },
{ 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1 },
{ 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
};
There
are a couple of differences between this and previous examples. First, notice that
the array type has a comma between the square brackets. The number of commas
indicates how many dimensions we want—no commas at all would mean a one-
dimensional array, which is what we’ve been using so far, but the single comma here
specifies a two-dimensional array. We could represent a cuboid layout with int[,,],
and so on, into as many dimensions as your application requires.

The second thing to notice here is that we’ve not had to use the new keyword for each
row in the initializer list—new appears only once, and that’s because this really is just
a single object despite being multidimensional. As Figure 7-6 illustrates, this kind of
array has a much simpler structure than the two-dimensional jagged array in Figure 7-4.
While Figure
7-6
is accurate in the sense that just one object holds all
the values here, the grid-like layout of the numbers is not a literal rep-
resentation of how the numbers are really stored, any more than the
position of the various objects in Figure 7-4 is a literal representation of
what you’d see if you peered into your computer’s memory chips with
a scanning electron microscope.
In reality, multidimensional arrays store their elements as a sequential
list just like the simple array in Figure 7-3, because computer memory
itself is just a big sequence of storage locations. But the programming
model C# presents makes it look like the array really is
multidimensional.
The syntax for accessing elements in a rectangular array is slightly different from that
of a jagged array. But like a jagged array, the access syntax is consistent with the dec-
laration syntax—as Example 7-22 shows, we use a single pair of square brackets, pass-
ing in an index for each dimension, separated by commas.
Arrays | 241
Figure 7-6. A two-dimensional rectangular array
Example 7-22. Accessing an element in a rectangular array
static bool CanCharacterMoveDown(int x, int y, int[,] walls)
{
int newY = y + 1;
// Can't move off the bottom of the map
if (newY == walls.GetLength(0)) { return false; }
// Can only move down if there's no wall in the way

return walls[newY, x] == 0;
}
If you pass in the wrong number of indexes, the C# compiler will com-
plain. The
number of dimensions (or rank, to use the official term) is
considered to be part of the type: int[,] is a different type than
int[,,], and C# checks that the number of indexes you supply matches
the array type’s rank.
242 | Chapter 7: Arrays and Lists
Example 7-22 performs two checks: before it looks to see if there’s a wall in the way of
the game character, it first checks to see if the character is up against the edge of the
map. To do this, it needs to know how big the map is. And rather than assuming a
fixed-size grid, it asks the array for its size. But it can’t just use the Length property we
saw earlier—that returns the total number of elements. Since this is a 12 × 12 array,
Length will be 144. But we want to know the length in the vertical dimension. So instead,
we use the GetLength method, which takes a single argument indicating which dimen-
sion you want—0 would be the vertical dimension and 1 in this case is horizontal.
Arrays don’t really have any concept of horizontal and vertical. They
simply have as many dimensions as you ask for, and it’s up to your
program to decide what each dimension is for. This particular program
has chosen to use the first dimension to represent the vertical position
in the maze, and the second dimension for the horizontal position.
This rectangular example has used a two-dimensional array of integers, and since int
is a value type, the values get to live inside the array. You can also create multidimen-
sional rectangular arrays with reference type elements. In that case, you’ll still get a
single object containing all the elements of the array in all their dimensions, but these
individual elements will be null references—you’ll need to create objects for them to
refer to, just like you would with a single-dimensional array.
While jagged and rectangular multidimensional arrays give us flexibility in terms of
how to specify the size of an array, we have not yet dealt with an irritating sizing problem

mentioned back at the start of the chapter: an array’s size is fixed. We saw that it’s
possible to work around this by creating new arrays and copying some or all of the old
data across, or by getting the Array.Resize method to do that work for us. But these
are inconvenient solutions, so in practice, we rarely work directly with arrays in C#.
There’s a far easier way to work with changing collection sizes, thanks to the List<T>
class.
List<T>
The List<T> class, defined in the System.Collections.Generic namespace, is effectively
a resizable array. Strictly speaking, it’s just a generic class provided by the .NET Frame-
work class library, and unlike arrays, List<T> does not get any special treatment from
the type system or the CLR. But from a C# developer’s perspective, it feels very
similar—you can do most of the things you could do with an array, but without the
restriction of a fixed size.
List<T> | 243
Generics
List<T> is an example of a generic type. You do not use a generic type directly; you use
it to build new types. For example, List<int> is a list of integers, and List<string> is
a list of strings. These are two types in their own right, built by passing different type
arguments to List<T>. Plugging in type arguments to form a new type is called instan-
tiating the generic type.
Generics were added in C# 2.0 mainly to support collection classes such as List<T>.
Before this, we had to use the ArrayList class (which you should no longer use; it’s not
present in Silverlight, and may eventually be deprecated in the full .NET Framework).
ArrayList was also a resizable array, but it represented all items as object. This meant
it could hold anything, but every time you read an element, you were obliged to cast
to the type you were expecting, which was messy.
With generics, we can write code that has one or more placeholder type names—the
T in List<T>, for example. We call these type parameters. (The distinction between
parameters and arguments is the same here as it is for methods: a parameter is a named
placeholder, whereas an argument is a specific value or type provided for that parameter

at the point at which you use the code.) So you can write code like this:
public class Wrapper<T>
{
public Wrapper(T v) { Value = v; }
public T Value { get; private set; }
}
This code doesn’t need to know what type T is—and in fact T can be any type. If we
want a wrapper for an int, we can write Wrapper<int>, and that generates a class exactly
like the example, except with the T replaced by int throughout.
Some classes take multiple type parameters. Dictionary collections (which are descri-
bed in Chapter 9) require both a key and a value type, so you would specify, say,
Dictionary<string, MyClass>. An instantiated generic type is a type in its own right, so
you can use one as an argument for another generic type, for example, Diction
ary<string, List<int>>.
You can also specify a type parameter list for a method. For example, .NET defines an
extension method for all collections called OfType<TResult>. If you have a
List<object> that happens to contain a mixture of different kinds of objects, you can
retrieve just the items that are of type string by calling myList.OfType<string>().
You may be wondering why .NET offers arrays when List<T> appears
to be
more useful. The answer is that it wouldn’t be possible for
List<T> to exist if there were no arrays: List<T> uses an array internally
to hold its elements. As you add elements, it allocates new, larger arrays
as necessary, copying the old contents over. It employs various tricks to
minimize how often it needs to do this.
244 | Chapter 7: Arrays and Lists
List<T> is one of the most useful types in the .NET Framework. If you’re dealing with
multiple pieces of information, as programs often do, it’s very common to need some
flexibility around the amount of information—fixed-size lists are the exception rather
than the rule. (An individual’s calendar tends to change over time, for example.) So

have we just wasted your time with the first half of this chapter? Not at all—not only
do arrays crop up a lot in APIs, but List<T> collections are very similar in use to arrays.
We could migrate most of the examples seen so far in this chapter from arrays to lists.
Returning to our earlier, nonrectangular example, we would need to modify only the
first line of Example 7-10, which creates an array of CalendarEvent objects. That line
currently reads:
CalendarEvent[] events =
It is followed by the list of objects to add to the array, contained within a pair of braces.
If you change that line to this:
List<CalendarEvent> events = new List<CalendarEvent>
the initializer list can remain the same. Notice that besides changing the variable dec-
laration to use the List<T> type (with the generic type argument T set to the element
type CalendarEvent, of course) we also need an explicit call to the constructor. (Nor-
mally, you’d expect parentheses after the type name when invoking a constructor, but
those are optional when using an initializer list.) As you saw earlier, the use of new is
optional when assigning a value to a newly declared array, but C# does not extend that
courtesy to other collection types.
While we can initialize the list in much the same way as we would an array, the differ-
ence is that we are free to add and remove elements later. To add a new element, we
can use the Add method:
CalendarEvent newEvent = new CalendarEvent
{
Title = "Dean Collins Shim Sham Lesson",
StartTime = new DateTimeOffset (2009, 7, 14, 19, 15, 00, TimeSpan.Zero),
Duration = TimeSpan.FromHours(1)
};
events.Add(newEvent);
This appends the element to the end of the list. If you want to put the new element
somewhere other than at the end, you can use Insert:
events.Insert(2, newEvent);

The first argument indicates the index at which you’d like the new item to appear—
any items at or after this index will be moved down to make space. You can also remove
items, either by index, using the RemoveAt method, or by passing the value you’d like
to remove to the Remove method (which will remove the first element it finds that con-
tains the specified value).
List<T> | 245
List<T> does not have a Length property, and instead offers a Count. This
may seem like pointless inconsistency with arrays, but there’s a reason.
An array’s Length property is guaranteed not to change. A List<T> can-
not make that guarantee, and so the behavior of its Count property is
necessarily different from an array’s Length. The use of different names
signals the fact that the semantics are subtly different.
List<T> also offers AddRange, which lets you add multiple elements in a single step. This
makes it much easier to concatenate lists—remember that with arrays we ended up
writing the CombineEvents method in Example 7-18 to concatenate a couple of arrays.
But with lists, it becomes as simple as the code shown in Example 7-23.
Example 7-23. Adding elements from one list to another
events1.AddRange(events2);
The one possible downside of List<T> is
that this kind of operation
modifies the first list. Example 7-18 built a brand-new array, leaving the
two input arrays unmodified, so if any code happened still to be using
those original arrays, it would carry on working. But Example 7-23
modifies the first list by adding in the events from the second list. You
would need to be confident that nothing in your code was relying on
the first list containing only its original content. Of course, you could
always build a brand-new new List<T> from the contents of two existing
lists. (There are various ways to do this, but one straightforward ap-
proach is to construct a new List<T> and then call AddRange twice, once
for each list.)

You access elements in a List<T> with exactly the same syntax as for an array. For
example:
Console.WriteLine("List element: " + events[2].Title);
As with arrays, a List<T> will
throw an IndexOutOfRangeException if you
use too high an index, or a negative index. This applies for writes as well
as reads—a List<T> will not automatically grow if you write to an index
that does not yet exist.
There is a subtle difference between array element access and list element access that
can cause problems with custom value types (structs). You may recall that Chapter 3
warned that when writing a custom value type, it’s best to make it immutable if you
plan to use it in a collection. To understand why, you need to know how List<T> makes
the square bracket syntax for element access work.
246 | Chapter 7: Arrays and Lists
Custom Indexers
Arrays are an integral part of the .NET type system, so C# knows exactly what to do
when you access an array element using the square bracket syntax. However, as
List<T> demonstrates, it’s also possible to use this same syntax with some objects that
are not arrays. For this to work, the object’s type needs to help C# out by defining the
behavior for this syntax. This takes the form of a slightly unusual-looking property, as
shown in Example 7-24.
Example 7-24. A custom indexer
class Indexable
{
public string this[int index]
{
get
{
return "Item " + index;
}

set
{
Console.WriteLine("You set item " + index + " to " + value);
}
}
}
This has the get and set parts we’d expect in a normal property, but the definition line
is a little unusual: it starts with the accessibility and type as normal, but where we’d
expect to see the property name we instead have this[int index]. The this keyword
signifies that this property won’t be accessed by any name. It is followed by a parameter
list enclosed in square brackets, signifying that this is an indexer property, defining
what should happen if we use the square bracket element access syntax with objects of
this type. For example, look at the code in Example 7-25.
Example 7-25. Using a custom indexer
Indexable ix = new Indexable();
Console.WriteLine(ix[10]);
ix[42] = "Xyzzy";
After constructing the object, the next line uses the same element access syntax you’d
use to read an element from an array. But this is not an array, so the C# compiler will
look for a property of the kind shown in Example 7-24. If you try this on a type that
doesn’t provide an indexer, you’ll get a compiler error, but since this type has one, that
ix[10] expression ends up calling the indexer’s get accessor. Similarly, the third line
has the element access syntax on the lefthand side of an assignment, so C# will use the
indexer’s set accessor.
List<T> | 247
If you want to support the multidimensional rectangular array style of
index (e.g., ix[10, 20]), you can specify multiple parameters between
the square brackets in your indexer. Note that the List<T> class does
not do this—while it covers most of the same ground as the built-in
array types, it does not offer rectangular multidimensional behavior.

You’re free to create a jagged list of lists, though. For example,
List<List<int>> is a list of lists of integers, and is similar in use to an
int[][].
The indexer in Example 7-24 doesn’t really contain any elements at all—it just makes
up a value in the get, and prints out the value passed into set without storing it any-
where. So if you run this code, you’ll see this output:
Item 10
You set item 42 to Xyzzy
It may seem a bit odd to provide array-like syntax but to discard whatever values are
“written,” but this is allowed—there’s no rule that says that indexers are required to
behave in an array-like fashion. In practice, most do—the reason C# supports indexers
is to make it possible to write classes such as List<T> that feel like arrays without nec-
essarily having to be arrays. So while Example 7-24 illustrates that you’re free to do
whatever you like in a custom indexer, it’s not a paragon of good coding style.
What does any of this have to do with value types and immutability, though? Look at
Example 7-26. It has a public field with an array and also an indexer that provides access
to the array.
Example 7-26. Arrays versus indexers
// This class's purpose is to illustrate a difference between
// arrays and indexers. Do not use this in real code!
class ArrayAndIndexer<T>
{
public T[] TheArray = new T[100];
public T this[int index]
{
get
{
return TheArray[index];
}
set

{
TheArray[index] = value;
}
}
}
248 | Chapter 7: Arrays and Lists
You might think that it would make no difference whether we use this class’s indexer,
or go directly for the array. And some of the time that’s true, as it is in this example:
ArrayAndIndexer<int> aai = new ArrayAndIndexer<int>();
aai.TheArray[10] = 42;
Console.WriteLine(aai[10]);
aai[20] = 99;
Console.WriteLine(aai.TheArray[20]);
This
swaps freely between using the array and the indexer, and as the output shows,
items set through one mechanism are visible through the other:
42
99
However, things are a little different if we make this class store a mutable value type.
Here’s a very simple modifiable value type:
struct CanChange
{
public int Number { get; set; }
public string Name { get; set; }
}
The Number and Name properties both have setters, so this is clearly not an immutable
type. This might not seem like a problem—we can do more or less exactly the same
with this type as we did with int just a moment ago:
ArrayAndIndexer<CanChange> aai = new ArrayAndIndexer<CanChange>();
aai.TheArray[10] = new CanChange { Number = 42 };

Console.WriteLine(aai[10].Number);
aai[20] = new CanChange { Number = 99, Name = "My item" };
Console.WriteLine(aai.TheArray[20].Number);
That works fine. The problem arises when we try to modify a property of one of the
values already inside the array. We can do it with the array:
aai.TheArray[10].Number = 123;
Console.WriteLine(aai.TheArray[10].Number);
That works—it prints out 123 as you’d expect. But this does not work:
aai[20].Number = 456;
If you try this, you’ll find that the C# compiler reports the following error:
error CS1612: Cannot modify the return value of
'ArrayAndIndexer<CanChange>.this[int]' because it is not a variable
That’s a slightly cryptic message. But the problem becomes clear when we think about
what we just asked the compiler to do. The intent of this code:
aai[20].Number = 456;
seems clear—we want to modify the Number property of the item whose index is 20.
And remember, this line of code is using our ArrayAndIndexer<T> class’s indexer. Look-
ing at Example 7-26, which of the two accessors would you expect it to use here? Since
List<T> | 249
we’re modifying the value, you might expect set to be used, but a set accessor is an all
or nothing proposition: calling set means you want to replace the whole element. But
we’re not trying to do that here—we just want to modify the Number property of the
value, leaving its Name property unmodified. If you look at the set code in Exam-
ple 7-26, it simply doesn’t offer that as an option—it will completely replace the element
at the specified index in the array. The set accessor can come into play only when we’re
providing a whole new value for the element, as in:
aai[20] = new CanChange { Number = 456 };
That compiles, but we end up losing the Name property that the element in that location
previously had, because we overwrote the entire value of the element.
Since set doesn’t work, that leaves get. The C# compiler could interpret this code:

aai[20].Number = 456;
as being equivalent to the code in Example 7-27.
Example 7-27. What the compiler might have done
CanChange elem = aai[20];
elem.Number = 456;
And in fact, that’s what it would have done if we were using a reference type. However,
it has noticed that CanChange is a value type, and has therefore rejected the code. (The
error message says nothing about value types, but you can verify that this is the heart
of the problem by changing the CanChange type from a struct to a class. That removes
the compiler error, and you’ll find that the code aai[20].Number = 456 works as
expected.)
Why has the compiler rejected this seemingly obvious solution? Well, remember that
the crucial difference between reference types and value types is that values usually
involve copies—if you retrieve a value from an indexer, the indexer returns a copy. So
in Example 7-27 the elem variable holds a copy of the item at index 20. Setting
elem.Number to 456 has an effect on only that copy—the original item in the array
remains unchanged. This makes clear why the compiler rejected our code—the only
thing it can do with this:
aai[20].Number = 456;
is to call the get accessor, and then set the Number property on the copy returned by the
array, leaving the original value unaltered. Since the copy would then immediately be
discarded, the compiler has wisely determined that this is almost certainly not what we
meant. (If we really want that copy-then-modify behavior, we can always write the code
in Example 7-27 ourselves, making the fact that there’s a copy explicit. Putting the copy
into a named variable also gives us the opportunity to go on and do something with
the copy, meaning that setting a property on the copy might no longer be a waste of
effort.)
250 | Chapter 7: Arrays and Lists
You might be thinking that the compiler could read and modify a copy
like Example 7-27, and then write that value back using the set indexer

accessor. However, as Example 7-24 showed, indexer accessors are not
required to work in the obvious way, and more generally, accessors can
have side effects. So the C# compiler cannot assume that such a get-
modify-set sequence is necessarily safe.
This problem doesn’t arise with reference types, because in that case, the get accessor
returns a reference rather than a value—no copying occurs because that reference refers
to the same object that the corresponding array entry refers to.
But why does this work when we use the array directly? Recall that the compiler didn’t
have a problem with this code:
aai.TheArray[10].Number = 123;
It lets that through because it’s able to make that behave like we expect. This will in
fact modify the Number property of the element in the array. And this is the rather subtle
difference between an array and an indexer. With an array you really can work directly
with the element inside the array—no copying occurs in this example. This works
because the C# compiler knows what an array is, and is able to generate code that deals
directly with array elements in situ. But there’s no way to write a custom indexer that
offers the same flexibility. (There are reasons for this, but to explain them would require
an exploration of the .NET Framework’s type safety rules, which would be lengthy and
quite outside the scope of this chapter.)
Having established the root of the problem, let’s look at what this means for List<T>.
Immutability and List<T>
The List<T> class gets no special privileges—it may be part of the .NET Framework
class library, but it is subject to the same restrictions as your code. And so it has the
same problem just described—the following code will produce the same compiler error
you saw in the preceding section:
List<CanChange> numbers = new List<CanChange> { new CanChange() };
numbers[0].Number = 42; // Will not compile
One way of dealing with this would be to avoid using custom value types in a collection
class such as List<T>, preferring custom reference types instead. And that’s not a bad
rule of thumb—reference types are a reasonable default choice for most data types.

However, value types do offer one compelling feature if you happen to be dealing with
very large volumes of data. As Figure 7-1 showed earlier, an array with reference type
elements results in an object for the array itself, and one object for each element in the
array. But when an array has value type elements, you end up with just one object—
the values live inside the array, as Figure 7-3 illustrates. List<T> has similar character-
istics because it uses an array internally.
List<T> | 251

Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Tải bản đầy đủ ngay
×