Thư viện tri thức trực tuyến
Kho tài liệu với 50,000+ tài liệu học thuật
© 2023 Siêu thị PDF - Kho tài liệu học thuật hàng đầu Việt Nam

Programming C# 4.0 phần 4 pdf
Nội dung xem thử
Mô tả chi tiết
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 corresponding 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 element 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 significant 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 element 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 element 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 inaccessible, 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 interested 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 convenience 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 arguments—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 irritating 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 provide 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, because 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 Example 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