Creating Lists
You can create list objects by invoking the
default constructor. With the generic class List<T>, you must specify the type for the
values of the list with the declaration. The code shows how to
declare a List<T> with
int and a list with Racer elements. ArrayList
is a nongeneric list that accepts any Object type for its elements.
Using the default constructor creates an empty
list. As soon as elements are added to the list, the capacity of
the list is extended to allow 4 elements. If the 5th element is
added, the list is resized for a place of 8 elements. If 8 elements
are not enough, the list is resized again to contain 16 elements.
With every resize the capacity of the list is doubled.
If the capacity of the list changes the complete
collection is reallocated to a new memory block. With the
implementation of List<T>, an
array of type T is used. With
reallocation a new array is created, and Array.Copy() copies the elements from the old to the
new array. To save time, if you know the number of elements in
advance, that should be in the list; you can define the capacity
with the constructor. Here a collection with a capacity of 10
elements is created. If the capacity is not large enough for the
elements added, the capacity is resized to 20 and 40 elements -
doubled again.
You can get and set the capacity of a collection by
using the Capacity property:
The capacity is not the same as the number of
elements in the collection. The number of elements in the
collection can be read with the Count
property. Of course, the capacity is always larger or equal to the
number of items. As long as no element was added to the list, the
count is 0.
If you are finished adding elements to the list and
don’t want to add any more elements, you can get rid of the
unneeded capacity by invoking the TrimExcess() method. However, because the relocation
takes time, TrimExcess() does nothing if
the item count is more than 90 percent of capacity.
|
|
Tip |
Because with new applications usually you can
use the generic List<T> class
instead of the non-generic ArrayList
class, and also because the methods of ArrayList are very similar, the reminder of this
section just focuses on List<T>.
|
Adding Elements
You can add elements to the list with the
Add() method as shown. The generic
instantiated type defines the type of the first parameter with the
Add() method.
The variable racers is
defined as type List<Racer>. With
the new operator a new object of the
same type is created. Because the class List<T> was instantiated with the concrete
class Racer, now only Racer objects can be added with the Add() method. In the following sample code, four
Formula-1 racers are created and added to the collection:
With the AddRange()
method of the List<T> class, you
can add multiple elements to the collection at once. The method
AddRange() accepts an object of type
IEnumerable<T>, so you can also
pass an array as shown:
If you know the elements of the collection when
instantiating the list, you can also pass any object that
implements IEnumerable<T> to the
constructor of the class. This is very similar to the AddRange() method.
Inserting Elements
You can insert elements at a specified
position with the Insert() method:
The method InsertRange()
offers the capability to insert a number of elements, similarly to
the AddRange() method shown earlier.
If the index set is larger than the number of
elements in the collection, an exception of type ArgumentOutOfRangeException is thrown.
Accessing Elements
All classes that implement the IList and IList<T>
interface offer an indexer, so you can access the elements by using
an indexer and passing the item number. The first item can be
accessed with an index value 0.
Getting the number of elements with the
Count property, you can do a
for loop to iterate through every item
in the collection, and use the indexer to access every item:
|
|
Important |
Indexed access to collection classes is
available with ArrayList, StringCollection, and List<T>.
|
Because List<T>
implements the interface IEnumerable,
you can iterate through the items in the collection using the
foreach statement as well.
|
|
Tip |
How the foreach
statement is resolved by the compiler to make use of the
IEnumerable and IEnumerator interfaces is explained in Chapter
5, “Arrays.”
|
Instead of using the foreach statement, the List<T> class also offers a ForEach() method that is declared with an
Action<T> parameter. ForEach() iterates through every item of the
collection and invokes the method that is passed as parameter for
every item.
For passing a method with ForEach, Action<T>
is declared as a delegate that defines a method with void return
type and parameter T.
With a list of Racer
items, the handler for the ForEach()
method must be declared with a Racer
object as parameter and void return
type.
Because one overload of the Console.WriteLine() method accepts Object as parameter, you can pass the address of
this method to the ForEach method, and
every racer of the collection is written to the console:
You can also write an anonymous method that accepts
a Racer object as parameter. Here, the
format A is used with the ToString() method of the IFormattable interface to display all information of
the racer.
Removing Elements
You can remove elements by index or pass the
item that should be removed. Here, the fourth element is removed by
passing 3 to RemoveAt():
You can also directly pass a Racer object to the Remove() method to remove this element. Removing by
index is faster, as here the collection must be searched for the
item to remove. The Remove() method
first searches in the collection to get the index of the item with
the IndexOf() method and then uses the
index to remove the item. IndexOf()
first checks if the item type implements the interface IEquatable. If it does, the Equals() method of this interface is invoked to find
the item in the collection that is the same as the one passed to
the method. If this interface is not implemented, the Equals() method of the Object class is used to compare the items. The
default implementation of the Equals()
method in the Object class does a
bitwise compare with value types, but compares only references with
reference types.
Here, the racer referenced by the variable
graham is removed from the collection.
The variable graham was created earlier
when the collection was filled. Because the interface IEquatable and the Object.Equals() method are not overridden with the
Racer class, you cannot create a new
object with the same content as the item that should be removed and
pass it to the Remove() method.
The method RemoveRange()
removes a number of items from the collection. The first parameter
specifies the index where the removal of items should begin; the
second parameter specifies the number of items to be removed.
To remove all items with some specific
characteristics from the collection, you can use the RemoveAll() method. This method uses the
Predicate<T> parameter that is
discussed next when searching for elements. For removing all
elements from the collection, use the Clear() method defined with the ICollection<T> interface.
Searching
There are different ways to search for
elements in the collection. You can get the index to the found
item, or the item itself. You can use methods such as IndexOf(), LastIndexOf(),
FindIndex(), FindLastIndex(), Find(),
and FindLast(). And for just checking if
an item exists, the List<T> class
offers the Exists() method.
The method IndexOf()
requires an object as parameter and returns the index of the item
if it is found inside the collection. If the item is not found, -1
is returned. Remember that IndexOf() is
using the IEquatable interface for
comparing the elements.
With the IndexOf()
method, you can also specify that the complete collection should
not be searched, but rather specify an index where the search
should start and the number of elements that should be iterated for
the comparison.
Instead of searching a specific item with the
IndexOf() method, you can search for an
item that has some specific characteristics that you can define
with the FindIndex() method.
FindIndex() requires a parameter of type
Predicate:
The Predicate<T>
type is a delegate that returns a Boolean value and requires type
T as parameter. This delegate can be
used similarly to the Action delegate
shown earlier with the ForEach() method.
If the predicate returns true, there’s a
match and the element is found. If it returns false, the element is not found and the search
continues.
With the List<T>
class that is using Racer objects for
type T, you can pass the address of a
method that returns a bool and defines a
parameter of type Racer to the
FindIndex() method. Finding the first
racer of a specific country, you can create the FindCountry class as shown. The Find() method has the signature and return type
defined by the Predicate<T>
delegate. The Find() method uses the
variable country to search for a country
that you can pass with the constructor of the class.
With the FindIndex()
method you can create a new instance of the FindCountry() class, pass a country string to the
constructor, and pass the address of the Find method. After FindIndex() completes successfully, index2 contains the index of the first item where
the Country property of the racer is set
to Finland.
Instead of creating a class with a handler method,
you can create an anonymous method here as well. The result is
exactly the same as before. Now the delegate defines the
implementation of the anonymous method to search for an item where
the Country property is set to
Finland.
Similarly to the IndexOf() method, with the FindIndex() method you can also specify the index
where the search should start and the count of items that should be
iterated through. To do a search for an index beginning from the
last element in the collection, you can use the FindLastIndex() method.
The method FindIndex()
returns the index of the found item. Instead of getting the index,
you can also get directly to the item in the collection. The
Find() method requires a parameter of
type Predicate<T> similarly to the
FindIndex() method. The Find() method here is searching for the first racer
in the list that has the Firstname
property set to Niki. Of course, you can
also do a FindLast() to find the last
item that fulfills the predicate.
To get not only one, but all, items that fulfill
the requirements of a predicate, you can use the FindAll() method. The FindAll() method uses the same Predicate<T> delegate as the Find() and FindIndex()
methods. The FindAll() method just does
not stop when the first item is found but instead iterates through
every item in the collection and returns all items where the
predicate returns true.
With the FindAll()
method invoked here, all racer items are returned where the
property Wins is set to more than 20.
All racers that won more than 20 races are referenced from the
bigWinners list.
Iterating through the variable bigWinners with a foreach
statement gives the following result:
The result is not sorted, but this is done
next.
Sorting
The List<T>
class allows sorting its elements. The Sort() method has several overloads defined. The
arguments that can be passed are a generic delegate Comparison<T>, the generic interface
IComparer<T>, and a range together
with the generic interface IComparer<T>:
Using the Sort() method
without arguments is possible only if the elements in the
collection implement the interface IComparable.
The class Racer
implements the interface IComparable<T> to sort racers by the last
name:
If you need to do a sort other than the default
supported by the item types, you need to use other techniques, for
example passing an object that implements the IComparer<T> interface.
The class RacerComparer
implements the interface IComparer<T> for Racer types. This class allows you to sort either by
the first name, last name, country, or number of wins. The kind of
sort that should be done is defined with the inner enumeration type
CompareType. The CompareType is set with the constructor of the class
RacerComparer. The interface
IComparer<Racer> defines the
method Compare that is required for
sorting. In the implementation of this method, the CompareTo() method of the string and int types is
used.
An instance of the RacerComparer class can now be used with the
Sort() method. Passing the enumeration
RacerComparer.CompareType.Country sorts
the collection by the property Country.
Another way to do the sort is by using the
overloaded Sort method, which requires a
Comparison<T> delegate:
Comparison<T> is a
delegate to a method that has two parameters of type T and a return type int.
If the parameter values are equal, the method must return 0. If the
first parameter is less than the second, a value less than zero
must be returned; otherwise, a value greater than zero is
returned.
Now you can pass a simple anonymous method to the
Sort() method to do a sort by the number
of wins. The two parameters are of type Racer, and in the implementation the Wins properties are compared by using the
int method CompareTo(). In the implementation, r2 and r1 are used in the
reverse order, so the number of wins is sorted in descending order.
After the method has been invoked, the complete racer list is
sorted based on the name of the racer.
You can also reverse the order of a complete
collection by invoking the Reverse()
method.
Type Conversion
With the List<T> method ConvertAll(), all types of a collection can be
converted to a different type. The ConvertAll() method uses a Converter delegate that is defined like this:
The generic types TInput
and TOutput are used with the
conversion. TInput is the argument of
the delegate method, and TOutput is the
return type.
In this example, all Racer types should be converted to Person types. Whereas the Racer type contains a name and a car, the
Person type just contains a name. For the conversion, the country of the racer
can be ignored, but the name must be converted:
The conversion happens by invoking the racers.ConvertAll<Person>() method. The
argument of this method is defined as an anonymous method with an
argument of type Racer and a
Person type that is returned. In the
implementation of the anonymous method, a new Person object is created and returned. For the
Person object, the Firstname and Lastname
are passed to the constructor:
The result of the conversion is a list
containing the converted Person objects:
persons of type List<Person>.