Compiler Design
Compiler Design
Compiler Design
com
www.allitebooks.com
Developer’s Guide to
Collections in Microsoft ®
.NET
Calvin Janes
www.allitebooks.com
Published with the authorization of Microsoft Corporation by:
O’Reilly Media, Inc.
1005 Gravenstein Highway North
Sebastopol, California 95472
ISBN: 978-0-7356-5927-8
1 2 3 4 5 6 7 8 9 LSI 6 5 4 3 2 1
Microsoft Press books are available through booksellers and distributors worldwide. If you need support
related to this book, email Microsoft Press Book Support at mspinput@microsoft.com. Please tell us
what you think of this book at http://www.microsoft.com/learning/booksurvey.
The example companies, organizations, products, domain names, email addresses, logos, people,
places, and events depicted herein are fictitious. No association with any real company, organization,
product, domain name, email address, logo, person, place, or event is intended or should be inferred.
This book expresses the author’s views and opinions. The information contained in this book is provided
without any express, statutory, or implied warranties. Neither the authors, O’Reilly Media, Inc., Microsoft
Corporation, nor its resellers, or distributors will be held liable for any damages caused or alleged to
be caused either directly or indirectly by this book.
www.allitebooks.com
Contents at a Glance
Part I Collection Basics
1 Understanding Collections: Arrays and Linked Lists . . . . . . . . . . . . 3
2 Understanding Collections: Associative Arrays . . . . . . . . . . . . . . . 87
3 Understanding Collections: Queues, Stacks, and
Circular Buffers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
iii
www.allitebooks.com
www.allitebooks.com
Table of Contents
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xiii
microsoft.com/learning/booksurvey
www.allitebooks.com
vi Table of Contents
www.allitebooks.com
Table of Contents vii
www.allitebooks.com
viii Table of Contents
www.allitebooks.com
Table of Contents ix
SynchronizedKeyedCollection(T) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 512
SynchronizedReadOnlyCollection(T) . . . . . . . . . . . . . . . . . . . . . . . . . . . 512
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 512
microsoft.com/learning/booksurvey
xii Table of Contents
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 611
Introduction
Applications that don’t internally use some type of collection are difficult to find. Also
difficult to find are books about collections. Many of the methods and properties of the
Microsoft .NET collections are well documented, but it often seems like you can’t find help
for the particular one you are interested in. At times, it may feel like everyone in the world
should know what a hash table is, how to use it in a multithreaded environment, and when
to use a list instead. But when you happen to be one of the unfortunate developers busily
searching the Internet for information about how to solve a critical collection problem for
tomorrow’s application release, you may find the Internet full of inconsistent information.
Or you may find yourself throwing figurative duct tape and bandages on threading and per-
formance issues to hold to the release schedule. All of this—and more—was my motivation
for creating this book. I wanted to create a one-stop shop for anyone struggling with collec-
tions: from beginners to experts who just need a reference or a few pointers here and there.
Throughout the book are many useful tips and tricks that you can use with collections. Some
you may already know, and others will be new to you. In either case, I hope you enjoy reading
it as much as I enjoyed writing it.
Developers who are not developing in .NET may find the book useful as well.
Assumptions
This book expects that you have at least a minimal understanding of .NET development
and object-oriented programming concepts. Although collections are used in .NET, the
majority of the book provides examples in C# and Visual Basic .NET only. If you have
not yet learned one of those languages, you might consider reading John Sharp’s book
Microsoft Visual C# 2010 Step by Step (Microsoft Press, 2010) or Michael Halvorson’s book
Microsoft Visual Basic 2010 Step by Step (Microsoft Press, 2010).
The first two chapters cover basic collection types and concepts and assume that you may
want to use collections in languages other than .NET. Some care is taken to allow you to do
so. Later chapters focus more on .NET.
xiii
xiv Introduction
In Chapter 6, you learn about the .NET interfaces, such as IEnumerable(T), IEnumerator(T),
ICollection(T), IList(T), and IDictionary(TKey, TValue). You learn how to implement them in
your classes and how to use them.
In Chapter 7, you are introduced to the LINQ. You learn how to write simple and complex
LINQ statements and use them with your custom collection classes and the built-in .NET
collection classes.
In Chapter 8, you learn about threads and the importance of synchronizing data used across
threads. You also learn how to implement synchronization for your custom collection classes
and how to use some of the built-in .NET synchronization classes.
Introduction xv
In Chapter 9, you learn how to serialize collections and how to add serialization support to
your custom collection classes. You also learn how to control what gets serialized in your
custom collection classes.
Most of the book’s chapters include hands-on samples that you can use to try out the con-
cepts just explained. No matter which sections you choose to focus on, be sure to download
and install the sample applications.
xvi Introduction
■ In most cases, the book includes separate examples for Visual Basic programmers and
Visual C# programmers. You can ignore the examples that do not apply to your selected
language.
■ Boxed elements with labels such as “Note” provide additional information.
■ Text that needs to be added to existing code or is being brought to your attention
(apart from code blocks) appears in bold.
■ A vertical bar between two or more menu items (for example, File | Close) means that
you should select the first menu or menu item, then the next, and so on.
Note Throughout Chapter 6, you’re asked to add class files to the C# or Visual Basic project you
work with in Chapters 1 through 3. You create partial class files the same way you create class
files. To create a class file, right-click the project in the Solution Explorer, click Add, and then click
Class. From there, type the name of the class or the class file name. If you type the name of the
class, Microsoft Visual Studio will create a file by using the name of the class and add a .cs or .vb
extension to the file name, depending on whether you are in C# or Visual Basic. If you type a file
name, Visual Studio will create a class named the same as the portion of your file name preced-
ing the first period. For example, if you added a class and typed the file name ArrayEx.List.cs
or ArrayEx.List.vb, Visual Studio would create a file with that name, but the class within the file
would be named ArrayEx (the portion of the name up to the first period).
System Requirements
You need the following hardware and software to complete the practice exercises in this book:
■ One of the following: Windows XP with Service Pack 3 (except Starter Edition), Windows
Vista with Service Pack 2 (except Starter Edition), Windows 7, Windows Server 2003 with
Service Pack 2, Windows Server 2003 R2, Windows Server 2008 with Service Pack 2, or
Windows Server 2008 R2
■ Microsoft Visual Studio 2008 or later, any edition (multiple downloads may be required
if using Express Edition products)
■ The .NET Framework 3.5 or .NET Framework 4.0
Introduction xvii
■ DVD-ROM drive (if installing Visual Studio from DVD)
■ Microsoft Silverlight 4 if you want to run any of the Silverlight examples
■ An Internet connection to download software or chapter examples
Depending on your Windows configuration, you might require Local Administrator rights to
install or configure Visual Studio.
Code Samples
Most of the chapters in this book include exercises that let you interactively try out new
material learned in the main text. All sample projects, in both their pre-exercise and post-
exercise formats, can be downloaded from the following page:
http://go.microsoft.com/FWLink/?Linkid=227007
Note In addition to the code samples, your system should have Visual Studio 2008 or Visual
Studio 2010 installed. Be sure you have installed the latest service packs for Visual Studio.
1. Unzip the Collections_sample_code.zip file that you downloaded from the book’s
website (name a specific directory along with directions to create it, if necessary).
2. If prompted, review the displayed end-user license agreement. If you accept the terms,
select the accept option, and then click Next.
Note If the license agreement doesn’t appear, you can access it from the same webpage
from which you downloaded the Collections_sample_code.zip file.
xviii Introduction
■ The source code for each chapter is located under the appropriate chapter number.
Each chapter folder contains a folder named VB and one named CS for Visual Basic
.NET source code and C# source code respectively.
■ All custom collection classes are present in the DevGuideToCollections project in each
chapter folder under CS for C# or VB for Visual Basic .NET. DevGuideToCollections is a
class library that you can use in your own project.
To view the source code, access the Developer’s Guide to Collections in the main folder. If your
system is configured to display file extensions, the system will display Visual Basic project files
with a .vbproj extension and C# project files with .csproj as the file extension.
Acknowledgments
I would like to thank the team at O’Reilly Media and Microsoft Press for giving me the op-
portunity to write a book to help fellow developers. I would also like to thank all the devel-
opers and companies that have helped me obtain the knowledge to write the contents for
this book. I realized how critical it is for developers like myself to have a book like this after
hearing and seeing first-hand the issues developers have struggled with in critical applications.
Last, but not least, I would like to thank my beautiful wife, friends, neighbors, and coworkers
for helping me through the book-writing process; and my son, who joined my wife and me in
the middle of writing this book.
http://go.microsoft.com/FWLink/?Linkid=227006
If you find an error that is not already listed, you can report it to us through the same page.
mspinput@microsoft.com
Please note that product support for Microsoft software is not offered through these
addresses.
www.allitebooks.com
Introduction xix
http://www.microsoft.com/learning/booksurvey
The survey is short, and we read every one of your comments and ideas. Thanks in advance
for your input!
Stay in Touch
Let’s keep the conversation going! We’re on Twitter: http://twitter.com/MicrosoftPress.
Developer’s Guide to Collections in Microsoft .NET
Part I
Collection Basics
1
Chapter 1
Understanding Collections:
Arrays and Linked Lists
After completing this chapter, you will be able to
■ Identify arrays and singly and doubly linked lists.
■ Design and implement arrays and singly and doubly linked lists.
■ Understand when and when not to use arrays and singly and doubly linked lists.
Array Overview
An array is a collection of elements stored so that you can access each element through an
index. Each element is accessed through a simple mathematical formula (index * element
length).
Uses of Arrays
You can use arrays to create other data types. In Chapter 3, “Understanding Collections:
Queues, Stacks, and Circular Buffers,” you’ll use an array to create a stack, a queue, and a cir-
cular buffer, but you can also use an array to create lists and other collection types, as well as
strings and noncollection data types.
Arrays can also represent mathematical concepts such as vectors and matrices. You can create
mathematical formulas to perform multiplications, additions, dot product, and so on using
arrays that represent matrices and vectors.
You can easily calculate the size and element indices of an array at run time. This makes arrays
useful for streaming values to disk or the network and even for passing between dynamic-
link libraries (DLLs) created by different programming languages. Libraries are available for
quickly copying the data in arrays because the values are stored continuously in memory.
Note There is a difference between the contents of an array that contains references and an
array that contains values. An array of values is equal to the size of the value type times the
number of elements in the array. An array of references is equal to the size of a reference pointer
times the number of elements in the array. The size of a reference pointer in a Microsoft .NET
Framework 32-bit application is 4 bytes; in a 64-bit application, it’s 8 bytes.
3
4 Part I Collection Basics
Consider using an array when the array contents, but not the properties of the array (such as
the size of the array), change frequently. Arrays are very efficient when performing indexing
operations, and can quickly access the elements that need to be changed. That speed is miti-
gated by the overhead required for item removals, insertions, and additions, but some of the
overhead can be reduced by implementing different types of arrays. You’ll implement an ar-
ray later in this chapter so that you will have the knowledge you need to implement different
types of arrays, depending on your project needs. If you need a general array implementation,
you can use the ArrayList or List(T) class that is discussed in Chapter 4, “Generic Collections.”
Advantages of Arrays
The biggest advantage of arrays is that you can access any item in them using the same
mathematical formula instead of having to traverse the collection to find the index you are
looking for. Theoretically, it takes the same amount of time to access item 1 as it does to ac-
cess item 2, 10, 10,000, or 2,300,000. Arrays also require the least amount of memory of all
the collection types.
Disadvantages of Arrays
All items in an array must be laid out in memory so that they can be accessed using a math-
ematical algorithm. When a user decides to add or remove an item from the array, the new
item (or remaining items) must still be accessible using the same mathematical algorithm.
This creates a performance penalty for the following reasons:
■ When you add an item, you must create a new array and copy the old values to the
new array.
■ When you remove or insert an item, you must shift up or down all items after the loca-
tion operated on.
Note Some of the performance penalties of adding items to an array can be reduced by allocat-
ing more space in the array than it actually requires. That way, you don’t have to create a new
array to hold the new and old contents until the extra space is used up. This type of array is typi-
cally called a dynamic array, and it’s the type of array you’ll implement later in this chapter.
You do not have to truncate an array if you remove an item, but you do have to shift all of the
elements that appear after the removed item to fill up the empty space. However, memory will
not be freed until you truncate the array. This would not be an issue if you reduced an array from
ten items to one item, but it might be an issue if you reduced an array from thousands of items
to one item.
Chapter 1 Understanding Collections: Arrays and Linked Lists 5
Array Implementation
Now that you have seen the usefulness of arrays, it is time to learn how to implement them.
The following sections show you how arrays work and how to implement them.
Understanding Indexing
An array is formed by sequences of elements that are arranged so that a mathematical algo-
rithm can access each element, as shown in the following illustration.
0 1 2 3 4 5 6 7 8
The first element is at index 0, the second at index 1, and the next one is assigned one higher
than the previous index until you get to the last index of 8. The indices are from 0 through 8
in the preceding example, which makes this example an array of nine elements. The number
of elements can be calculated by adding 1 to the index of the last element because the ele-
ments are [0–8] or, written another way, [0]+[1–8].
The preceding example has only nine elements, but an array can contain any number of
elements. So how do you refer to the elements in an array that has an unknown number of
elements? You say that it has N elements and refer to each element as shown in the following
illustration.
0 1 2 ... N-1
The numbers of elements in the list can be calculated by adding 1 to last index. The Count
property, shown in the “Adding Helper Methods and Properties” section, returns the number
of elements in the array.
To refer to the N elements’ indices in reverse, you would say the following.
Getting Started
You can create a simple array by using the following code.
C#
// Create an array of 13 ints
int[] ints = new int[13];
Visual Basic
' Create an array of 13 ints
Dim ints As New Integer(12) {}
This simple array works fine for simple situations, particularly for static arrays, but it does not
help you handle deletions, insertions, notifications, synchronization, removals, or developer
security. You will create an array class that you can use instead of a simple array when such
issues matter. Microsoft has a class with similar functionality called ArrayList. This class is
named ArrayEx(T) to eliminate any possible confusion with the built-in ArrayList class. You
can find the fully implemented code in the DevGuideToCollections project, in either the
Chapter 1\CS\DevGuideToCollections or Chapter 1\VB\DevGuideToCollections folder.
Note The ArrayList class in the .NET Framework accomplishes the same objectives as this example
class. The purpose of this exercise is to show how you can create a custom implementation of
the class that fits the needs of your application. The reasons you might want to do this will be
discussed later.
First, you need to create a class library to hold the ArrayEx(T) class so that you can use it in
other projects. In Microsoft Visual Studio, create a Microsoft Visual Basic or Microsoft Visual
C# class library project. You can name the project DevGuideToCollections to make it easier
to follow along with the provided samples in the book. After the project is created, create a
blank class for the ArrayEx(T) class and remove the Class1 class from the project (if the class
exists). ArrayEx(T) is a generic class, so its elements are type-safe. The top-level view of the
class is as follows.
C#
namespace DevGuideToCollections
{
[DebuggerDisplay("Count={Count}")]
[DebuggerTypeProxy(typeof(ArrayDebugView))]
public class ArrayEx<T>
{
// Fields
private const int GROW_BY = 10;
private int m_count;
private T[] m_data;
private int m_updateCode;
// Constructors
public ArrayEx();
public ArrayEx(IEnumerable<T> items);
public ArrayEx(int capacity);
Chapter 1 Understanding Collections: Arrays and Linked Lists 7
// Methods
public void Add(T item);
public void Clear();
public bool Contains(T item);
public int IndexOf(T item);
private void Initialize(int capacity);
public void Insert(int index, T item);
public bool Remove(T item);
public bool Remove(T item, bool allOccurrences);
public void RemoveAt(int index);
public T[] ToArray();
// Properties
public int Capacity { get; set; }
public int Count { get; }
public bool IsEmpty { get; }
public T this[int index] { get; set; }
}
}
Visual Basic
<DebuggerDisplay("Count={Count}")] _
<DebuggerTypeProxy(typeof(ArrayDebugView))] _
Public Class ArrayEx(Of T)
' Fields
Private Const GROW_BY As Integer = 10
Private m_count As Integer
Private m_data As T()
Private m_updateCode As Integer
' Constructors
Public Sub New()
Public Sub New(ByVal items As IEnumerable(Of T))
Public Sub New(ByVal capacity As Integer)
' Methods
Public Sub Add(ByVal item As T)
Public Sub Clear()
Public Function Contains(ByVal item As T) As Boolean
Public Function IndexOf(ByVal item As T) As Integer
Public Sub Initialize(ByVal capacity As Integer)
Public Sub Insert(ByVal index As Integer, ByVal item As T)
Public Function Remove(ByVal item As T) As Boolean
Public Function Remove(ByVal item As T, ByVal allOccurrences As Boolean) As Boolean
Public Sub RemoveAt(ByVal index As Integer)
Public Function ToArray() As T()
' Properties
Public Property Capacity As Integer
Public ReadOnly Property Count As Integer
Public ReadOnly Property IsEmpty As Boolean
Public Default Property Item(ByVal index As Integer) As T
End Class
8 Part I Collection Basics
All elements will be stored in the simple array m_data. The m_count field will track how many
of the elements in m_data are actually being used. The GROW_BY constant will be explained
later in this section.
The m_updateCode field will be incremented each time the user modifies the list. You’ll use the
m_updateCode field in Chapter 6, “.NET Collection Interfaces,” to determine if the collection has
changed while the user is iterating over it. It is easier to add it to the code now rather than
change the code in Chapter 6.
Note The DebuggerDisplayAttribute is used to control how the custom collection classes are
displayed in the debugger. The specified parameters tell the debugger to use “Count=[{Count}]”
instead of the ToString method. The DebuggerTypeProxyAttribute is used to specify a proxy for
the object being displayed in the debugger. The ArrayDebugView class returns items stored in
the collection to the debugger. The source code for the proxy is located in the Chapter 1
\CS\DevGuideToCollections\ArrayDebugView.cs file for C# and in the Chapter 1\VB
\DevGuideToCollections\ArrayDebugView.vb file for Visual Basic.
Creating Constructors
The ArrayEx(T) class will contain three constructors. One constructor is for creating an empty
class, one is for creating a class with an initial capacity, and the other is for creating a class
with default values. Rather than implement the same functionality in all three constructors,
you use a method named Initialize at the beginning of all three constructors.
C#
void Initialize(int capacity)
{
m_data = new T[capacity];
}
Visual Basic
Sub Initialize(ByVal capacity As Integer)
m_data = New T(capacity - 1) {}
End Sub
Note In the statement, New T(X), Visual Basic creates an array of Ts from 0 through X, because
it considers the X to be an upper bound instead of a count. To correct this, you must state (X-1)
instead.
The Initialize method creates a simple array of the size passed in. You could add to this method
other initializations that are common among all constructors as well.
Next, create a default constructor for the ArrayEx(T) class so that the user can create an
empty array.
www.allitebooks.com
Chapter 1 Understanding Collections: Arrays and Linked Lists 9
C#
/// <summary>
/// Initializes a new instance of the ArrayEx(T) class that is empty./// </summary>
public ArrayEx()
{
Initialize(GROW_BY);
}
Visual Basic
''' <summary>
''' Initializes a new instance of the ArrayEx(T) class that is empty.
''' </summary>
Public Sub New()
Initialize(GROW_BY)
End Sub
The default constructor creates an internal array of size GROW_BY and a count of 0. In the
“Disadvantages of Arrays” section, you saw how adding items can have a performance pen-
alty. This performance penalty can be reduced by adding what is called a grow-by value.
Instead of increasing the internal array by one each time you need to add a value, you in-
crease the array by the GROW_BY size. That way, you won’t have to increase the size again
until all the elements added by the grow-by are used up. Bear in mind that there is a trade-
off: If the grow-by size is too large, you use memory unnecessarily; if it’s too small, you will
still spend a lot of time reallocating the array. You can eliminate some of these problems by
introducing a capacity property, which would allow the user to change the capacity before
adding multiple items. You can allow the user to create an array with an initial capacity by
using the following constructor. You will learn more about capacity later in this section.
C#
/// <summary>
/// Initializes a new instance of the ArrayEx(T) class that is empty and has the
/// specified initial capacity.
/// </summary>
/// <param name="capacity">
/// The number of elements that the new array can initially store.
/// </param>
public ArrayEx(int capacity)
{
Initialize(capacity);
}
Visual Basic
''' <summary>
''' Initializes a new instance of the ArrayEx(T) class that is empty and has the
''' specified initial capacity.
''' </summary>
''' <param name="capacity">
''' The number of elements that the new array can initially store.
''' </param>
Public Sub New(ByVal capacity As Integer)
Initialize(capacity)
End Sub
10 Part I Collection Basics
With this constructor, you can create an array with an initial internal size. If you know you will
be adding a large number of items to the array, you can create one with the size you need.
Creating an array that already contains items is also useful. You can use the following con-
structor to do so.
C#
/// <summary>
/// Initializes a new instance of the ArrayEx(T) class that contains the items in the array.
/// </summary>
/// <param name="items">Adds the items to the ArrayEx(T).</param>
public ArrayEx(IEnumerable<T> items)
{
Initialize(GROW_BY);
Visual Basic
''' <summary>
''' Initializes a new instance of the ArrayEx(T) class that contains the items in the array.
''' </summary>
''' <param name="items">Adds the items to the end of the list.</param>
Public Sub New(ByVal items As IEnumerable(Of T))
Initialize(GROW_BY)
Each item in items will be added to the end of the array in the order it was passed in.
You can use the following method to add items to the end of the array.
C#
/// <summary>
/// Adds an object to the end of the ArrayEx(T).
/// </summary>
/// <param name="item">The item to add to the end of the ArrayEx(T).</param>
Chapter 1 Understanding Collections: Arrays and Linked Lists 11
public void Add(T item)
{
if (m_data.Length <= m_count)
{
Capacity += GROW_BY;
}
// We will need to assign the item to the last element and then increment
// the count variable
m_data[m_count++] = item;
++m_updateCode;
}
Visual Basic
''' <summary>
''' Adds an object to the end of the ArrayEx(T).
''' </summary>
''' <param name="item">The item to add to the end of the ArrayEx(T).</param>
Public Sub Add(ByVal item As T)
If (m_data.Length <= m_count) Then
Capacity += GROW_BY
End If
' We will need to assign the item to the last element and then increment
' the count variable
m_data(m_count) = item
m_count += 1
m_updateCode += 1
End Sub
When the method is called, it will add an item to the end of the array and increment m_count.
If there is no room in m_data for the item, it will increment the capacity of the m_data by
GROW_BY as explained earlier in this chapter.
Sometimes you need to add an item to the array in a position other than the end. You can
use the following insertion method to insert an item into the array at any position.
C#
/// <summary>
/// Inserts an item into the ArrayEx(T) at the specified index.
/// </summary>
/// <param name="index">The zero-based index at which item should be inserted.</param>
/// <param name="item">
/// The item to insert. A value of null will cause an exception later.
/// </param>
m_data[index] = item;
++m_count;
++m_updateCode;
}
Visual Basic
''' <summary>
''' Inserts an item into the ArrayEx(T) at the specified index.
''' </summary>
''' <param name="index">The zero-based index at which item should be inserted.</param>
''' <param name="item">
''' The item to insert. A value of Nothing will cause an exception later.
''' </param>
Public Sub Insert(ByVal index As Integer, ByVal item As T)
m_data(index) = item
m_count += 1
m_updateCode += 1
End Sub
When you insert an item, the item located at the insertion point and all items after need to
be shifted up one index position to make room for the new item. Note that you perform that
shift starting with the last item in the array, to avoid overwriting any items. As with the Add
method, this method adjusts the capacity of the array to hold the item.
Chapter 1 Understanding Collections: Arrays and Linked Lists 13
If you have only the index of the item you need to remove, you can use the following method
to remove an item by its index only.
C#
/// <summary>
/// Removes the item located at the specified index.
/// </summary>
/// <param name="index">The index of the item to remove</param>
public void RemoveAt(int index)
{
if (index < 0 || index >= m_count)
{
// Item has already been removed.
return;
}
// Shift all of the elements after the specified index down one.
for (int i = index + 1; i < count; ++i)
{
m_data[i - 1] = m_data[i];
}
m_data[m_count] = default(T);
}
Visual Basic
''' <summary>
''' Removes the item located at the specified index.
''' </summary>
''' <param name="index">The index of the item to remove</param>
Public Sub RemoveAt(ByVal index As Integer)
' Shift all of the elements after the specified index down one.
For i As Integer = index + 1 To count - 1
m_data(i - 1) = m_data(i)
Next
m_data(m_count) = CType(Nothing,T)
End Sub
The RemoveAt method removes the item located at the specified index and adjusts the count
to reflect the change. All items after the item are shifted down one index to fill the slot created
from the removed item. This leaves the last slot empty, which is then set to the default value.
Setting a slot to the default value removes the reference to the object instance so that it can
be garbage collected.
Removing by the index is the fastest approach, but the index is not always known. For those
cases, you need to use the following methods.
C#
/// <summary>
/// Removes the first occurrence of the specified item from the ArrayEx(T).
/// </summary>
/// <param name="item">The item to remove from the ArrayEx(T).</param>
/// <returns>True if an item was removed, false otherwise.</returns>
public bool Remove(T item)
{
return Remove(item, false);
}
/// <summary>
/// Removes the first or all occurrences of the specified item from the ArrayEx(T).
/// </summary>
/// <param name="item">The item to remove from the ArrayEx(T).</param>
/// <param name="allOccurrences">
/// True if all occurrences of the item should be removed, False if only the first
/// should be removed.
/// </param>
/// <returns>True if an item was removed, false otherwise.</returns>
public bool Remove(T item, bool allOccurrences)
{
int shiftto = 0;
bool shiftmode = false;
bool removed = false;
continue;
}
if (shiftmode)
{
// Since we are shifting elements we need to shift the element
// down and then update the shiftto index to the next element.
m_data[shiftto++] = m_data[i];
}
}
if (removed)
{
++m_updateCode;
}
return removed;
}
Visual Basic
''' <summary>
''' Removes the first occurrence of the specified item from the ArrayEx(T).
''' </summary>
''' <param name="item">The item to remove from the ArrayEx(T).</param>
''' <returns>True if an item was removed, false otherwise.</returns>
Public Function Remove(ByVal item As T) As Boolean
Return Remove(item, False)
End Function
''' <summary>
''' Removes the first or all occurrences of the specified item from the ArrayEx(T).
''' </summary>
16 Part I Collection Basics
Continue For
End If
If (shiftmode) Then
Next
If (removed) Then
m_updateCode += 1
End If
Return removed
End Function
Chapter 1 Understanding Collections: Arrays and Linked Lists 17
The Remove method linearly searches the list for the item that needs to be removed. After
it removes the item, it shifts the items after it down one index to fill the slot created by the
removed item. The allOccurrences flag lets the user remove all occurrences of the item with-
out having to call Remove repeatedly until a false is returned.
Rather than calling the RemoveAt method repeatedly until the count is 0 to remove all items
from the array, you can take a simpler approach by using the following method to accomplish
that task.
C#
/// <summary>
/// Clears all values from the ArrayEx(T).
/// </summary>
public void Clear()
{
Array.Clear(m_data,0,m_count);
m_count = 0;
++m_updateCode;
}
Visual Basic
''' <summary>
''' Clears all values from the ArrayEx(T).
''' </summary>
Public Sub Clear()
Array.Clear(m_data,0,m_count)
m_count = 0
m_updateCode += 1
End Sub
The Clear method changes the count to 0. The Array.Clear method sets all values to the
default value so that garbage collection can remove any items that are no longer being
referenced.
Your users may find it necessary to get the index of an item in the array or simply check to
see if an item is in the array. This information can stop them from having to unnecessarily
traverse or operate on the array. The following methods allow them to do so.
C#
/// <summary>
/// Checks to see if the item is present in the ArrayEx(T).
/// </summary>
18 Part I Collection Basics
/// <summary>
/// Gets the index of the specified item.
/// </summary>
/// <param name="item">The item to get the index of.</param>
/// <returns>
/// -1 if the item isn't found in the array, the index of the first instance of the
/// item otherwise.
/// </returns>
public int IndexOf(T item)
{
return Array.IndexOf<T>(m_data, item, 0, m_count);
}
Visual Basic
''' <summary>
''' Checks to see if the item is present in the ArrayEx(T).
''' </summary>
''' <param name="item">The item to see if the array contains.</param>
''' <returns>True if the item is in the array, false if it is not.</returns>
Public Function Contains(ByVal item As T) As Boolean
Dim comparer As EqualityComparer(Of T) = EqualityComparer(Of T).Default
For i As Integer = 0 To m_count - 1
If (comparer.Equals(m_data(i), item)) Then
Return True
End If
Next
Return False
End Function
''' <summary>
''' Gets the index of the specified item.
''' </summary>
''' <param name="item">The item to get the index of.</param>
''' <returns>
''' -1 if the item isn't found in the array, the index of the found item otherwise.
''' </returns>
Public Function IndexOf(ByVal item As T) As Integer
Return Array.IndexOf(Of T)(m_data, item, 0, m_count)
End Function
www.allitebooks.com
Chapter 1 Understanding Collections: Arrays and Linked Lists 19
The Contains method allows users to see whether an item is in the array, and the IndexOf
method allows them to get an index of the item they specified. The Contains method uses
the equality comparer to find the specified item in the array. You learn more about the
equality comparer in Chapter 4. The IndexOf method uses the Array.IndexOf method to find
the index of the item in the array.
But what if your users want to check the contents at an index or even change its value? The
following property allows just that.
C#
/// <summary>
/// Gets or sets an element in the ArrayEx(T).
/// </summary>
/// <param name="index">The index of the element.</param>
/// <returns>The value of the element.</returns>
public T this[int index]
{
get
{
if (index < 0 || index >= m_count)
{
throw new ArgumentOutOfRangeException("index");
}
return m_data[index];
}
set
{
if (index < 0 || index >= m_count)
{
throw new ArgumentOutOfRangeException("index");
}
m_data[index] = value;
++m_updateCode;
}
}
Visual Basic
''' <summary>
''' Gets or sets an element in the ArrayEx(T).
''' </summary>
''' <param name="index">The index of the element.</param>
''' <returns>The value of the element.</returns>
Default Public Property Item(ByVal index As Integer) As T
Get
If (index < 0 Or index >= m_count) Then
Throw New ArgumentOutOfRangeException("index")
End If
Return m_data(index)
End Get
20 Part I Collection Basics
Set(ByVal value As T)
If (index < 0 Or index >= m_count) Then
Throw New ArgumentOutOfRangeException("index")
End If
m_data(index) = value
m_updateCode += 1
End Set
End Property
The Item property allows the user to directly set or get the contents at a specific index. The
array index value is any value that satisfies the condition 0 <= index < Count. However, with-
out a Count property, the user would never know the bounds of the array. The following
properties will help you to know when to do an operation and the range of the array.
Note C# uses the this keyword to implement the Item property. Visual Basic actually imple-
ments the Item property. Both implement the same indexing functionality.
C#
/// <summary>
/// Gets the number of elements actually contained in the ArrayEx(T).
/// </summary>
public int Count
{
get { return m_count; }
}
/// <summary>
/// States if the ArrayEx(T) is empty.
/// </summary>
public bool IsEmpty
{
get { return m_count <= 0; }
}
Visual Basic
''' <summary>
''' Gets the number of elements actually contained in the ArrayEx(T).
''' </summary>
Public ReadOnly Property Count() As Integer
Get
Return m_count
End Get
End Property
''' <summary>
''' States if the ArrayEx(T) is empty.
''' </summary>
Chapter 1 Understanding Collections: Arrays and Linked Lists 21
Public ReadOnly Property IsEmpty() As Boolean
Get
Return m_count <= 0
End Get
End Property
The Count property returns the number of items in the array, and the IsEmpty property states
whether there are any items in the array.
As you saw earlier in the chapter, the capacity of the array can be used to help with perfor-
mance issues. A user can use the capacity to eliminate constant resizing when doing multiple
adds by changing the capacity of the array. The following property allows the user to change
the capacity.
C#
/// <summary>
/// Gets or sets the size of the internal data array.
/// </summary>
public int Capacity
{
get { return m_data.Length; }
set
{
// We do not support truncating the stored array.
// So throw an exception if the array is less than Count.
if (value < Count)
{
throw new ArgumentOutOfRangeException("value", "The value is less than Count");
}
m_data = tmp;
++m_updateCode;
}
}
22 Part I Collection Basics
Visual Basic
''' <summary>
''' Gets or sets the size of the internal data array.
''' </summary>
Public Property Capacity() As Integer
Get
Return m_data.Length
End Get
Set(ByVal value As Integer)
' We do not support truncating the stored array.
' So throw an exception if the array is less than Count.
If (value < m_count) Then
Throw New ArgumentOutOfRangeException("value", "The value is less than Count")
End If
' We will need to create a new array and move all of the values
' in the old array to the new one
Dim tmp As T() = New T(value - 1) {}
m_data = tmp
m_updateCode += 1
End Set
End Property
Finally, the following helper function copies the contents of the internal array into a new ar-
ray. The new array will be useful when code requires a basic array rather than an instance of
the ArrayEx(T) class.
C#
/// <summary>
/// Copies the elements of the ArrayEx<T> to a new array.
/// </summary>
/// <returns>An array containing copies of the elements of the ArrayEx<T>.</returns>
public T[] ToArray()
{
T[] tmp = new T[Count];
return tmp;
}
Chapter 1 Understanding Collections: Arrays and Linked Lists 23
Visual Basic
''' <summary>
''' Copies the elements of the ArrayEx(T) to a new array.
''' </summary>
''' <returns>An array containing copies of the elements of the ArrayEx(T).</returns>
Public Function ToArray() As T()
Dim tmp As New T(m_count - 1) {}
Return tmp
End Function
Note You can see the finished example for this section, and all sections in this chapter, in the
Chapter 1\CS\Driver folder for C# or the Chapter 1\VB\Driver folder for Visual Basic.
First, create a C# or Visual Basic console application named Driver in Visual Studio. Right-
click the solution in the Solution Explorer, and select Add | Existing Project. Browse for the
DevGuideToCollections project that you just created, and then click Add. Right-click the
Driver project in the Solution Explorer, and select Add Reference. Click the Projects tab, and
select the DevGuideToCollections project. Next, open Program.cs in C# or Module1.vb in
Visual Basic in the Driver project by using the Solution Explorer. At the top of the file, add the
following lines.
C#
using DevGuideToCollections;
using System.Text;
Visual Basic
Imports DevGuideToCollections
Imports System.Text
For C#, in the class Program, create a method called Lesson1a as follows.
C#
static void Lesson1A()
{
}
24 Part I Collection Basics
For Visual Basic, in the module Module1, create a method called Lesson1a as follows.
Visual Basic
Sub Lesson1A()
End Sub
C#
static string ArrayToString(Array array)
{
StringBuilder sb = new StringBuilder();
sb.Append("[");
if (array.Length > 0)
{
sb.Append(array.GetValue(0));
}
for (int i = 1; i < array.Length; ++i)
{
sb.AppendFormat(",{0}", array.GetValue(i));
}
sb.Append("]");
return sb.ToString();
}
Visual Basic
Function ArrayToString(ByVal array As Array) As String
Dim sb As New StringBuilder()
sb.Append("[")
If (array.Length > 0) Then
sb.Append(array.GetValue(0))
End If
For i As Integer = 1 To array.Length - 1
sb.AppendFormat(",{0}", array.GetValue(i))
Next
sb.Append("]")
Return sb.ToString()
End Function
More Info In Chapter 6, you see how to traverse the ArrayEx(T) with a foreach statement.
Chapter 1 Understanding Collections: Arrays and Linked Lists 25
Add the following lines of code to the method Lesson1a. These lines of code will create a ran-
dom array of integers from 0 through 100.
C#
Random rnd = new Random();
ArrayEx<int> array = new ArrayEx<int>();
Visual Basic
Dim rnd As Random = New Random()
Dim array As ArrayEx(Of Integer) = New ArrayEx(Of Integer)()
For i As Integer = 0 To 19
array.Add(rnd.Next(100))
Next
Random rnd = new Random() creates an instance of the class Random, which is used to cre-
ate random numbers. The rnd.Next(100) call returns a random integer from 0 through 100.
The following lines of code will write the unsorted list to the console.
C#
Console.WriteLine("Sorting the following list");
Console.WriteLine(ArrayToString(array.ToArray()));
Visual Basic
Console.WriteLine("Sorting the following list")
Console.WriteLine(ArrayToString(array.ToArray()))
The following lines of code will perform a selection sort on the array that you created.
C#
for (int i = 0; i < array.Count; ++i)
{
for (int j = i + 1; j < array.Count; ++j)
{
if (array[i] > array[j])
{
int tmp = array[j];
array[j] = array[i];
array[i] = tmp;
}
}
}
26 Part I Collection Basics
Visual Basic
For i As Integer = 0 To array.Count - 1
For j As Integer = i + 1 To array.Count - 1
If (array(i) > array(j)) Then
Dim tmp As Integer = array(j)
array(j) = array(i)
array(i) = tmp
End If
Next
Next
The following lines of code will write the sorted list to the console.
C#
Console.WriteLine("The sorted list is");
Console.WriteLine(ArrayToString(array.ToArray()));
Visual Basic
Console.WriteLine("The sorted list is")
Console.WriteLine(ArrayToString(array.ToArray()))
Scroll down to the Main method and add the following to the method.
C#
Lesson1A();
Console.WriteLine("press enter to continue");
Console.ReadLine();
Visual Basic
Lesson1A()
Console.WriteLine("press enter to continue")
Console.ReadLine()
With the Console.Readline, you can see the console output before being returned to Visual
Studio when in debug mode. You need to press Enter on the keyboard to exit the console
application.
Output
Sorting the following list
[17,17,54,65,74,54,9,58,36,54,30,59,21,31,81,0,71,68,23,75,87]
The sorted list is
[0,0,9,17,21,23,30,31,36,54,54,54,58,59,65,68,71,74,75,81,87]
press enter to continue
Chapter 1 Understanding Collections: Arrays and Linked Lists 27
Consider using a linked list if you plan to do a lot of insertions, removals, or additions. Linked
lists are not very efficient at indexing operations, but they perform pretty well when traversing
the list.
Note Always remember to weigh the advantages of indexing over inserting, removing, or add-
ing elements. A linked list may not be your best choice if you are constantly indexing and adding
to a collection of thousands of elements. An array with a large grow size may be your best bet
in that case. However, if you are constantly inserting and removing from the collection, a link list
may be your best choice. Keep in mind that walking from the beginning of a linked list to the end
can be almost (or as) efficient as doing the same with an array, depending on the implementation
of the linked list. Also, you can use the previous and next pointers of the linked list node if you
are traversing the sequence from the previous access node.
Note There is no rule that states you must create your node structure like the one shown in the
preceding illustration. You could have the data contain the node information instead. This may
be useful if you are implementing custom objects that are passed around in your application and
need to be constantly removed and inserted. It may save you the performance hit of having to
traverse the collection to find the node. It may also be helpful if several values in the list equal
each other but you want to interact with the exact one you are handed.
C#
/// <summary>
/// Represents a node in a SingleLinkedList(T).
/// </summary>
/// <typeparam name="T">Specifies the type of data in the node.</typeparam>
www.allitebooks.com
Chapter 1 Understanding Collections: Arrays and Linked Lists 29
[DebuggerDisplay("Data={Data}")]
public class SingleLinkedListNode<T>
{
SingleLinkedList<T> m_owner;
SingleLinkedListNode<T> m_next;
T m_data;
/// <summary>
/// Initializes a new instance of the SingleLinkedList(T) class with the specified data.
/// </summary>
/// <param name="data">The data that this node will contain.</param>
public SingleLinkedListNode(T data)
{
m_data = data;
}
/// <summary>
/// Initializes a new instance of the SingleLinkedList(T) class
/// with the specified data and owner.
/// </summary>
/// <param name="data">The data that this node will contain.</param>
internal SingleLinkedListNode(SingleLinkedList<T> owner, T data)
{
m_data = data;
m_owner = owner;
}
/// <summary>
/// Returns the next node.
/// </summary>
public SingleLinkedListNode<T> Next
{
get { return m_next; }
internal set { m_next = value; }
}
/// <summary>
/// Gets or sets the owner of the node.
/// </summary>
internal SingleLinkedList<T> Owner
{
get { return m_owner; }
set { m_owner = value; }
}
/// <summary>
/// Gets the data contained in the node.
/// </summary>
public T Data
{
get { return m_data; }
internal set { m_data = value; }
}
}
30 Part I Collection Basics
Visual Basic
''' <summary>
''' Represents a node in a SingleLinkedList(T).
''' </summary>
''' <typeparam name="T">Specifies the type of data in the node.</typeparam>
<DebuggerDisplay("Data={Data}")> _
Public Class SingleLinkedListNode(Of T)
Private m_owner As SingleLinkedList(Of T)
Private m_next As SingleLinkedListNode(Of T)
Private m_data As T
''' <summary>
''' Initializes a new instance of the SingleLinkedList(T) class with the specified data.
''' </summary>
''' <param name="data">The data that this node will contain.</param>
Public Sub New(ByVal data As T)
m_data = data
m_owner = Nothing
End Sub
''' <summary>
''' Initializes a new instance of the SingleLinkedList(T) class
''' with the specified data and owner.
''' </summary>
''' <param name="data">The data that this node will contain.</param>
Friend Sub New(ByVal owner As SingleLinkedList(Of T), ByVal data As T)
m_data = data
m_owner = owner
End Sub
''' <summary>
''' Returns the next node.
''' </summary>
Public Property [Next]() As SingleLinkedListNode(Of T)
Get
Return m_next
End Get
Friend Set(ByVal value As SingleLinkedListNode(Of T))
m_next = value
End Set
End Property
''' <summary>
''' Gets or sets the owner of the node.
''' </summary>
Friend Property Owner() As SingleLinkedList(Of T)
Get
Return m_owner
End Get
Set(ByVal value As SingleLinkedList(Of T))
m_owner = value
End Set
End Property
Chapter 1 Understanding Collections: Arrays and Linked Lists 31
''' <summary>
''' Gets the data contained in the node.
''' </summary>
Public Property Data() As T
Get
Return m_data
End Get
Set(ByVal value As T)
m_data = value
End Set
End Property
End Class
The class is defined as a generic class, so the contained data will remain type-safe. The field
m_data contains the data that you want to store. The field m_next references the next node
in the list. The property that wraps m_next is internal so that users can’t change the “next”
pointer and bypass the bookkeeping. The field m_owner is used to verify that the node be-
longs to the list that is being operated on.
C#
[DebuggerDisplay("Count={Count}")]
[DebuggerTypeProxy(typeof(ArrayDebugView))]
public class SingleLinkedList<T>
{
// Fields
private int m_count;
private SingleLinkedListNode<T> m_head;
private SingleLinkedListNode<T> m_tail;
private int m_updateCode;
// Constructors
public SingleLinkedList();
public SingleLinkedList(IEnumerable<T> items);
// Methods
public SingleLinkedListNode<T> AddAfter(SingleLinkedListNode<T> node, T value);
public void AddAfter(SingleLinkedListNode<T> node, SingleLinkedListNode<T> newNode);
public SingleLinkedListNode<T> AddBefore(SingleLinkedListNode<T> node, T value);
public void AddBefore(SingleLinkedListNode<T> node, SingleLinkedListNode<T> newNode);
public SingleLinkedListNode<T> AddToBeginning(T value);
public SingleLinkedListNode<T> AddToEnd(T value);
public void Clear();
public bool Contains(T data);
public SingleLinkedListNode<T> Find(T data);
public bool Remove(T item);
public void Remove(SingleLinkedListNode<T> node);
public bool Remove(T item, bool allOccurrences);
public T[] ToArray();
32 Part I Collection Basics
// Properties
public int Count { get; }
public SingleLinkedListNode<T> Head { get; private set; }
public bool IsEmpty { get; }
public SingleLinkedListNode<T> Tail { get; private set; }
}
Visual Basic
Public Class SingleLinkedList(Of T)
' Constructors
Public Sub New()
Public Sub New(ByVal items As IEnumerable(Of T))
' Methods
Public Function AddAfter(ByVal node As SingleLinkedListNode(Of T), ByVal value As T) _
As SingleLinkedListNode(Of T)
Public Sub AddAfter(ByVal node As SingleLinkedListNode(Of T), ByVal newNode _
As SingleLinkedListNode(Of T))
Public Function AddBefore(ByVal node As SingleLinkedListNode(Of T), ByVal value As T) _
As SingleLinkedListNode(Of T)
Public Sub AddBefore(ByVal node As SingleLinkedListNode(Of T), ByVal newNode _
As SingleLinkedListNode(Of T))
Public Function AddToBeginning(ByVal value As T) As SingleLinkedListNode(Of T)
Public Function AddToEnd(ByVal value As T) As SingleLinkedListNode(Of T)
Public Sub Clear()
Public Function Contains(ByVal data As T) As Boolean
Public Function Find(ByVal data As T) As SingleLinkedListNode(Of T)
Public Function Remove(ByVal item As T) As Boolean
Public Sub Remove(ByVal node As SingleLinkedListNode(Of T))
Public Function Remove(ByVal item As T, ByVal allOccurrences As Boolean) As Boolean
Public Function ToArray() As T()
' Properties
Public ReadOnly Property Count As Integer
Property Head As SingleLinkedListNode(Of T)
Public ReadOnly Property IsEmpty As Boolean
Property Tail As SingleLinkedListNode(Of T)
' Fields
Private m_count As Integer
Private m_head As SingleLinkedListNode(Of T)
Private m_tail As SingleLinkedListNode(Of T)
Private m_updateCode As Integer
End Class
For performance reasons, SingleLinkedList(T) maintains a reference to the last node in the field
m_tail. The m_tail field provides quick access to the last item when a user wants to add a node
to the list. This eliminates the need to traverse the list to find the last node. The field m_head
references the first node, and the field m_count tracks the number of nodes in the list.
The m_updateCode field will be incremented each time the user modifies the list. The
m_updateCode field will be used in Chapter 6 to determine if the collection has changed
while the user is iterating over it. It is easier to add it to the code now instead of changing
the code in Chapter 6.
Chapter 1 Understanding Collections: Arrays and Linked Lists 33
Creating Constructors
The SingleLinkedList(T) class will contain two constructors. One constructor is for creating an
empty class, and the other is for creating a class with default values. These constructors are
defined as follows.
C#
/// <summary>
/// Initializes a new instance of the SingleLinkedList(T) class that is empty.
/// </summary>
public SingleLinkedList()
{
}
/// <summary>
/// Initializes a new instance of the SingleLinkedList(T) class that
/// contains the items in the array.
/// </summary>
/// <param name="items">Adds the items to the end of the SingleLinkedList(T).</param>
public SingleLinkedList(IEnumerable<T> items)
{
foreach (T item in items)
{
AddToEnd(item);
}
}
Visual Basic
''' <summary>
''' Initializes a new instance of the SingleLinkedList(T) class that is empty.
''' </summary>
Public Sub New()
End Sub
''' <summary>
''' Initializes a new instance of the SingleLinkedList(T) class that
''' contains the items in the list.
''' </summary>
''' <param name="items">Adds the items to the end of the SingleLinkedList(T).</param>
Public Sub New(ByVal items As IEnumerable(Of T))
For Each item As T In items
AddToEnd(item)
Next
End Sub
The second constructor adds the specified items to the end of the list in the order they are
in items.
34 Part I Collection Basics
First, let’s look at adding to the beginning. This condition happens if the user adds to an
empty list, calls the AddBefore with the head node specified, or calls the AddToBeginning
method. To accomplish this type of add, you need to set the newly added node’s Next prop-
erty to the previous head. You then need to assign the Head property to the newly added
node. The Count must then be incremented to reflect the newly added node. The dashed line
in the following illustration shows the link to the previous head.
Head
DATA
NEXT
Previous
DATA head
NEXT
New node
Next, let’s look at adding to the middle. This condition happens if the user calls AddAfter
or AddBefore without the head or tail node. When this happens, the node before the newly
added node must be updated as well. You will have a reference to that node in the AddAfter
method but not in the AddBefore method. For the AddBefore method, you need to traverse
the list, while maintaining the node before the current node as the variable prev, until you
reach the node you are adding before. You then need to assign the newly added node’s Next
property to the Next property of prev. Finally, you need to set the Next property of prev to
the newly added node. The dashed line represents the references before the operation. The
node you are adding before is denoted as node, and the new node is denoted as newNode in
the following illustration and in the source code.
DATA DATA
NEXT NEXT
prev node
DATA
NEXT
newNode
For the AddAfter method, you need to assign the newly added node’s Next property to the
Next property value of the node you are adding after. You then need to assign the Next
property of the node you are adding after to the newly added node. The Count must then
be incremented to reflect the newly added node. The dashed line represents the references
Chapter 1 Understanding Collections: Arrays and Linked Lists 35
before the operation. The node you are adding after is denoted as node, and the new node
is denoted as newNode in the following illustration and in the source code. The original node
set as the Next property of node is denoted as Next in the following illustration.
DATA DATA
NEXT NEXT
node Next
DATA
NEXT
newNode
As you can see, AddAfter is a lot more efficient than AddBefore because you do not have to
traverse the list for an AddAfter. For both methods, you need to increment the Count.
Finally, look at adding to the end. This condition happens if the user calls the AddToEnd
method or the AddAfter method with the tail node. To accomplish this type of add, set the
previous tail’s Next property to the newly added node. You then need to assign the Tail
property to the newly added node. The Count must then be incremented to reflect the newly
added node. The dashed line in the following illustration shows the link to the previous tail.
Tail
DATA
NEXT
New node
DATA
NEXT
Old tail
Now let’s look at the implementation of these methods. Adding to the beginning and adding
to the end can be executed using the following methods.
C#
/// <summary>
/// Adds the value to the beginning of the SingleLinkedList(T).
/// </summary>
/// <param name="value">
/// The value to add to the beginning of the SingleLinkedList(T).
/// </param>
/// <returns>The newly created node that is holding the value.</returns>
public SingleLinkedListNode<T> AddToBeginning(T value)
{
SingleLinkedListNode<T> newNode = new SingleLinkedListNode<T>(this, value);
if (IsEmpty)
36 Part I Collection Basics
{
m_head = newNode;
m_tail = newNode;
}
else
{
newNode.Next = m_head;
m_head = newNode;
}
++m_count;
++m_updateCode;
return newNode;
}
/// <summary>
/// Adds the value to the end of the SingleLinkedList(T).
/// </summary>
/// <param name="value">The value to add to the end of the SingleLinkedList(T).</param>
/// <returns>The newly created node that is holding the value.</returns>
public SingleLinkedListNode<T> AddToEnd(T value)
{
SingleLinkedListNode<T> newNode = new SingleLinkedListNode<T>(this, value);
if (IsEmpty)
{
m_head = newNode;
m_tail = newNode;
}
else
{
m_tail.Next = newNode;
m_tail = newNode;
}
++m_count;
++m_updateCode;
return newNode;
}
Visual Basic
''' <summary>
''' Adds the value to the beginning of the SingleLinkedListNode(T).
''' </summary>
''' <param name="value">
''' The value to add to the beginning of the SingleLinkedListNode(T).
''' </param>
''' <returns>The newly created node that is holding the value.</returns>
Public Function AddToBeginning(ByVal value As T) As SingleLinkedListNode(Of T)
Dim newNode As New SingleLinkedListNode(Of T)(Me, value)
Chapter 1 Understanding Collections: Arrays and Linked Lists 37
If (IsEmpty) Then
m_head = newNode
m_tail = newNode
Else
newNode.Next = m_head
m_head = newNode
End If
m_count += 1
m_updateCode += 1
Return newNode
End Function
''' <summary>
''' Adds the value to the end of the SingleLinkedListNode(T).
''' </summary>
''' <param name="value">The value to add to the end of the SingleLinkedListNode(T).</param>
''' <returns>The newly created node that is holding the value.</returns>
Public Function AddToEnd(ByVal value As T) As SingleLinkedListNode(Of T)
Dim newNode As New SingleLinkedListNode(Of T)(Me, value)
If (IsEmpty) Then
m_head = newNode
m_tail = newNode
Else
m_tail.Next = newNode
m_tail = newNode
End If
m_count += 1
m_updateCode += 1
Return newNode
End Function
Note Using the field m_tail eliminates the need to traverse the complete list to find the last
node. Adding an item increases the memory size of the list but eliminates the constant need to
traverse the collection to add the item. Traversing the complete list to do an add may not be a
big deal with a one-time add to a list with 10 items, but it is very beneficial in applications that
add items frequently.
When you are adding to the middle of the list, you use the AddAfter and AddBefore methods.
Each method will have an overload that allows the user to specify a value or a node to add. If
the user specifies a value, a node will be returned that holds the value the user passed in. The
implementation of these methods is as follows.
C#
/// <summary>
/// Adds the specified value to the SingleLinkedList(T) after the specified node.
/// </summary>
38 Part I Collection Basics
/// <summary>
/// Adds the specified newNode to the SingleLinkedList(T) after the specified node.
/// </summary>
/// <param name="node">The node to add the newNode after.</param>
/// <param name="newNode">The node to add.</param>
public void AddAfter(SingleLinkedListNode<T> node, SingleLinkedListNode<T> newNode)
{
if (node == null)
{
throw new ArgumentNullException("node");
}
if (newNode == null)
{
throw new ArgumentNullException("newNode");
}
if (node.Owner != this)
{
throw new InvalidOperationException("node is not owned by this list");
}
if (newNode.Owner != this)
{
throw new InvalidOperationException("newNode is not owned by this list");
}
// The newly added node becomes the tail if you are adding after the tail
if (m_tail == node)
{
m_tail = newNode;
}
newNode.Next = node.Next;
node.Next = newNode;
++m_count;
++m_updateCode;
}
/// <summary>
/// Adds the specified value to the SingleLinkedList(T) before the specified node.
/// </summary>
/// <param name="node">The node to add the value before.</param>
/// <param name="value">The value to add.</param>
/// <returns>The newly created node that holds the value.</returns>
www.allitebooks.com
Chapter 1 Understanding Collections: Arrays and Linked Lists 39
public SingleLinkedListNode<T> AddBefore(SingleLinkedListNode<T> node, T value)
{
SingleLinkedListNode<T> newNode = new SingleLinkedListNode<T>(this, value);
AddBefore(node, newNode);
return newNode;
}
/// <summary>
/// Adds the specified newNode to the SingleLinkedList(T) before the specified node.
/// </summary>
/// <param name="node">The node to add the newNode before.</param>
/// <param name="newNode">The node to add.</param>
public void AddBefore(SingleLinkedListNode<T> node, SingleLinkedListNode<T> newNode)
{
if (node == null)
{
throw new ArgumentNullException("node");
}
if (newNode == null)
{
throw new ArgumentNullException("newNode");
}
if (node.Owner != this)
{
throw new InvalidOperationException("node is not owned by this list");
}
if (newNode.Owner != this)
{
throw new InvalidOperationException("newNode is not owned by this list");
}
if (m_head == node)
{
newNode.Next = m_head;
m_head = newNode;
}
else
{
// We have to find the node before the one we are inserting in front of
newNode.Next = node;
beforeNode.Next = newNode;
}
++m_count;
++m_updateCode;
}
Visual Basic
''' <summary>
''' Adds the specified value to the SingleLinkedList(T) after the specified node.
''' </summary>
''' <param name="node">The node to add the value after.</param>
''' <param name="value">The value to add.</param>
''' <returns>The newly created node that holds the value.</returns>
Public Function AddAfter(ByVal node As SingleLinkedListNode(Of T), ByVal value As T) _
As SingleLinkedListNode(Of T)
Dim newNode As New SingleLinkedListNode(Of T)(Me, value)
AddAfter(node, newNode)
Return newNode
End Function
''' <summary>
''' Adds the specified newNode to the SingleLinkedList(T) after the specified node.
''' </summary>
''' <param name="node">The node to add the newNode after.</param>
''' <param name="newNode">The node to add.</param>
Public Sub AddAfter(ByVal node As SingleLinkedListNode(Of T), ByVal newNode _
As SingleLinkedListNode(Of T))
If (node Is Nothing) Then
Throw New ArgumentNullException("node")
End If
If (newNode Is Nothing) Then
Throw New ArgumentNullException("newNode")
End If
If (node.Owner IsNot Me) Then
Throw New InvalidOperationException("node is not owned by this list")
End If
If (newNode.Owner IsNot Me) Then
Throw New InvalidOperationException("newNode is not owned by this list")
End If
‘ The newly added node becomes the tail if you are adding after the tail
If (node is m_tail) Then
m_tail = newNode
End If
newNode.Next = node.Next
node.Next = newNode
m_count += 1
m_updateCode += 1
End Sub
Chapter 1 Understanding Collections: Arrays and Linked Lists 41
''' <summary>
''' Adds the specified value to the SingleLinkedList(T) before the specified node.
''' </summary>
''' <param name="node">The node to add the value before.</param>
''' <param name="value">The value to add.</param>
''' <returns>The newly created node that holds the value.</returns>
Public Function AddBefore(ByVal node As SingleLinkedListNode(Of T), ByVal value As T) _
As SingleLinkedListNode(Of T)
Dim newNode As New SingleLinkedListNode(Of T)(Me, value)
AddBefore(node, newNode)
Return newNode
End Function
''' <summary>
''' Adds the specified newNode to the SingleLinkedList(T) before the specified node.
''' </summary>
''' <param name="node">The node to add the newNode before.</param>
''' <param name="newNode">The node to add.</param>
Public Sub AddBefore(ByVal node As SingleLinkedListNode(Of T), ByVal newNode _
As SingleLinkedListNode(Of T))
If (node Is Nothing) Then
Throw New ArgumentNullException("node")
End If
If (newNode Is Nothing) Then
Throw New ArgumentNullException("newNode")
End If
If (node.Owner IsNot Me) Then
Throw New InvalidOperationException("node is not owned by this list")
End If
If (newNode.Owner IsNot Me) Then
Throw New InvalidOperationException("newNode is not owned by this list")
End If
newNode.Next = node
beforeNode.Next = newNode
End If
m_count += 1
m_updateCode += 1
End Sub
DATA DATA
NEXT NEXT
Node(m-1) Node(m+1)
DATA
NEXT
Nodem
C#
/// <summary>
/// Removes the first occurrence of the specified item from the SingleLinkedList(T).
/// </summary>
/// <param name="item">The item to remove from the SingleLinkedList(T).</param>
/// <returns>True if an item was removed, false otherwise.</returns>
public bool Remove(T item)
{
return Remove(item, false);
}
/// <summary>
/// Removes the first or all occurrences of the specified item from the SingleLinkedList(T).
/// </summary>
/// <param name="item">The item to remove from the SingleLinkedList(T).</param>
/// <param name="allOccurrences">
/// True if all nodes should be removed that contain the specified item,
/// False otherwise
/// </param>
/// <returns>True if an item was removed, false otherwise.</returns>
Chapter 1 Understanding Collections: Arrays and Linked Lists 43
public bool Remove(T item, bool allOccurrences)
{
if (IsEmpty)
{
return false;
}
if (curr == Head)
{
// If the current node is the head we will have to assign
// the next node as the head
Head = curr.Next;
}
if (curr == Tail)
{
// If the current node is the tail we will have to assign
// the previous node as the tail
Tail = prev;
}
removed = true;
if (!allOccurrences)
{
break;
}
}
if (removed)
{
++m_updateCode;
}
return removed;
}
/// <summary>
/// Removes the specified node from the SingleLinkedList(T).
/// </summary>
/// <param name="node">The node to remove from the SingleLinkedList(T).</param>
public void Remove(SingleLinkedListNode<T> node)
{
if (IsEmpty)
{
return;
}
if (node == null)
{
throw new ArgumentNullException("node");
}
if (node.Owner != this)
{
throw new InvalidOperationException("The node doesn't belong to this list.");
}
// Find the node located before the specified node by traversing the list.
while (curr != null && curr != node)
{
prev = curr;
curr = curr.Next;
}
// The node has been found if the current node equals the node we are looking for
if (curr == node)
{
Chapter 1 Understanding Collections: Arrays and Linked Lists 45
// Assign the head to the next node if the specified node is the head
if (m_head == node)
{
m_head = node.Next;
}
// Assign the tail to the previous node if the specified node is the tail
if (m_tail == node)
{
m_tail = prev;
}
// Set the previous node next reference to the removed node's next reference.
if (prev != null)
{
prev.Next = curr.Next;
}
--m_count;
++m_updateCode;
}
}
Visual Basic
''' <summary>
''' Removes the first occurrence of the specified item from the SingleLinkedList(T).
''' </summary>
''' <param name="item">The item to remove from the SingleLinkedList(T).</param>
''' <returns>True if an item was removed, false otherwise.</returns>
Public Function Remove(ByVal item As T) As Boolean
Return Remove(item, False)
End Function
''' <summary>
''' Removes the first or all occurrences of the specified item from the SingleLinkedList(T).
''' </summary>
''' <param name="item">The item to remove from the SingleLinkedList(T).</param>
''' <param name="allOccurrences">
''' True if all nodes should be removed that contain the specified item, False otherwise
''' </param>
''' <returns>True if an item was removed, false otherwise.</returns>
Public Function Remove(ByVal item As T, ByVal allOccurrences As Boolean) As Boolean
If (IsEmpty) Then
Return False
End If
' Since the node will no longer be used clean up the pointers in it
tmp.Next = Nothing
tmp.Owner = Nothing
removed = True
If (removed) Then
m_updateCode += 1
End If
Return removed
End Function
Chapter 1 Understanding Collections: Arrays and Linked Lists 47
''' <summary>
''' Removes the specified node from the SingleLinkedList(T).
''' </summary>
''' <param name="node">The node to remove from the SingleLinkedList(T).</param>
Public Sub Remove(ByVal node As SingleLinkedListNode(Of T))
If (IsEmpty) Then
Return
End If
' Find the node located before the specified node by traversing the list.
While (Not curr Is Nothing And curr IsNot node)
prev = curr
curr = curr.Next
End While
' The node has been found if the current node equals the node we are looking for
If (curr Is node) Then
' Assign the head to the next node if the specified node is the head
If (m_head Is node) Then
m_head = node.Next
End If
' Assign the tail to the previous node if the specified node is the tail
If (m_tail Is node) Then
m_tail = prev
End If
' Set the previous node next reference to the removed node's next reference.
If (Not prev Is Nothing) Then
prev.Next = curr.Next
End If
m_count -= 1
m_updateCode += 1
End If
End Sub
48 Part I Collection Basics
With the Remove methods, you can remove an item by using its node or value. If the value
is specified, the method traverses the list until it finds the node that contains the value. The
implementation for removing it is done as just described.
With the Clear method, the user can remove all nodes in the list without having to repeatedly
call the Remove method with the Head pointer until the Count is 0.
C#
/// <summary>
/// Removes all items from the SingleLinkedList(T).
/// </summary>
public void Clear()
{
SingleLinkedListNode<T> tmp;
if (m_count <= 0)
{
m_head = null;
m_tail = null;
}
++m_updateCode;
}
Visual Basic
''' <summary>
''' Removes all items from the SingleLinkedList(T).
''' </summary>
Public Sub Clear()
Dim tmp As SingleLinkedListNode(Of T)
tmp = node.Next
www.allitebooks.com
Chapter 1 Understanding Collections: Arrays and Linked Lists 49
' Change the count and head pointer in case we throw an exception.
' this way the node is removed before we clear the data
m_head = tmp
m_count -= 1
m_updateCode += 1
End Sub
It helps to null out all of the nodes during a clear so that if any of the nodes are passed to
you later, you can determine that the node no longer belongs to the collection. The preced-
ing code updates the count and moves the head to the next node while it clears the current
node. This is done because, if an error occurs during the clear, hopefully the collection will
still point to somewhat valid data when accessed again. For example, if Count was equal to 12
and m_head and m_tail pointed to null, unpredictable things would happen while accessing
some of the functions. Hopefully, the preceding example will point you in the right direction
because it is not 100 percent bulletproof.
Your users may find it necessary to find the node of an item in the list or check to see if a node
is present in the list. This information can stop them from having to unnecessarily traverse or
operate on the list. The following method and properties allow users to find or check for the
presence of a node.
The Find method allows users to find or check for the presence of a node. It locates a node in
the list by searching for the value it contains, as follows.
C#
/// <summary>
/// Locates the first node that contains the specified data.
/// </summary>
/// <param name="data">The data to find.</param>
/// <returns>The node that contains the specified data, null otherwise.</returns>
50 Part I Collection Basics
return null;
}
Visual Basic
''' <summary>
''' Locates the first node that contains the specified data.
''' </summary>
''' <param name="data">The data to find.</param>
''' <returns>The node that contains the specified data, null otherwise.</returns>
Public Function Find(ByVal data As T) As SingleLinkedListNode(Of T)
If (IsEmpty) Then
Return Nothing
End If
The method traverses the list from the beginning until it finds the node that contains the
data you are looking for. A null is returned if no node was found.
The Contains method is used to check to see if an item is present in the list.
Chapter 1 Understanding Collections: Arrays and Linked Lists 51
C#
/// <summary>
/// Checks if the specified data is present in the SingleLinkedList(T).
/// </summary>
/// <param name="data">The data to look for.</param>
/// <returns>True if the data is found, false otherwise.</returns>
public bool Contains(T data)
{
return Find(data) != null;
}
Visual Basic
''' <summary>
''' Checks if the specified data is present in the SingleLinkedList(T).
''' </summary>
''' <param name="data">The data to look for.</param>
''' <returns>True if the data is found, false otherwise.</returns>
Public Function Contains(ByVal data As T) As Boolean
Return Not Find(data) Is Nothing
End Function
The method works by checking to see if the Find method found the specified node.
Users may also need to traverse the list for other reasons as well. To do this, they will need
access to the head and tail.
C#
/// <summary>
/// Gets the head node in the SingleLinkedList(T).
/// </summary>
public SingleLinkedListNode<T> Head
{
get { return m_head; }
private set { m_head = value; }
}
/// <summary>
/// Gets the tail node in the SingleLinkedList(T).
/// </summary>
public SingleLinkedListNode<T> Tail
{
get { return m_tail; }
private set { m_tail = value; }
}
Visual Basic
''' <summary>
''' Gets the head node in the SingleLinkedList(T).
''' </summary>
Public Property Head() As SingleLinkedListNode(Of T)
Get
Return m_head
End Get
52 Part I Collection Basics
''' <summary>
''' Gets the tail node in the SingleLinkedList(T).
''' </summary>
Public Property Tail() As SingleLinkedListNode(Of T)
Get
Return m_tail
End Get
The Head and Tail methods return the head and tail of the list respectively. Both properties
block the setting of the values by the user.
Knowing how many items are in the list and if it is empty eliminates the need to perform op-
erations on the list. The following properties help users determine what needs to be done.
C#
/// <summary>
/// States if the SingleLinkedList(T) is empty.
/// </summary>
public bool IsEmpty
{
get { return m_count <= 0; }
}
/// <summary>
/// Gets the number of nodes actually contained in the SingleLinkedList(T).
/// </summary>
public int Count
{
get { return m_count; }
}
Visual Basic
''' <summary>
''' States if the SingleLinkedList(T) is empty.
''' </summary>
Public ReadOnly Property IsEmpty() As Boolean
Get
Return m_count <= 0
End Get
End Property
''' <summary>
''' Gets the number of nodes actually contained in the SingleLinkedList(T).
''' </summary>
Chapter 1 Understanding Collections: Arrays and Linked Lists 53
Public ReadOnly Property Count() As Integer
Get
Return m_count
End Get
End Property
The following helper function helps copy the contents of the linked list into a new array. The
new array is useful when code requires an array instead of our class.
C#
/// <summary>
/// Copies the elements of the SingleLinkedList<T> to a new array.
/// </summary>
/// <returns>
/// An array containing copies of the elements of the SingleLinkedList<T>.
/// </returns>
public T[] ToArray()
{
T[] retval = new T[m_count];
int index = 0;
for (SingleLinkedListNode<T> i = Head; i != null; i = i.Next)
{
retval[index] = i.Data;
++index;
}
return retval;
}
Visual Basic
''' <summary>
''' Copies the elements of the SingleLinkedList(T) to a new array.
''' </summary>
''' <returns>
''' An array containing copies of the elements of the SingleLinkedList(T).
''' </returns>
Public Function ToArray() As T()
Dim retval As T() = New T(m_count - 1) {}
A doubly linked list is a sequence of nodes that reference the next and previous node. Each
node contains data and a reference to the previous and next node.
C#
/// <summary>
/// Represents a node in a DoubleLinkedList(T).
/// </summary>
/// <typeparam name="T">Specifies the type of data in the node.</typeparam>
[DebuggerDisplay("Data={Data}")]
public class DoubleLinkedListNode<T>
{
DoubleLinkedList<T> m_owner;
DoubleLinkedListNode<T> m_prev;
DoubleLinkedListNode<T> m_next;
T m_data;
/// <summary>
/// Initializes a new instance of the DoubleLinkedListNode(T) class
/// with the specified data.
/// </summary>
/// <param name="data">The data that this node will contain.</param>
public DoubleLinkedListNode(T data)
{
m_data = data;
m_owner = null;
}
Chapter 1 Understanding Collections: Arrays and Linked Lists 55
/// <summary>
/// Initializes a new instance of the DoubleLinkedListNode(T) class
/// with the specified data and owner.
/// </summary>
/// <param name="data">The data that this node will contain.</param>
internal DoubleLinkedListNode(DoubleLinkedList<T> owner, T data)
{
m_data = data;
m_owner = owner;
}
/// <summary>
/// Gets the next node.
/// </summary>
public DoubleLinkedListNode<T> Next
{
get { return m_next; }
internal set { m_next = value; }
}
/// <summary>
/// Gets or sets the owner of the node.
/// </summary>
internal DoubleLinkedList<T> Owner
{
get { return m_owner; }
set { m_owner = value; }
}
/// <summary>
/// Gets the previous node.
/// </summary>
public DoubleLinkedListNode<T> Previous
{
get { return m_prev; }
internal set { m_prev = value; }
}
/// <summary>
/// Gets the data contained in the node.
/// </summary>
public T Data
{
get { return m_data; }
internal set { m_data = value; }
}
}
Visual Basic
''' <summary>
''' Represents a node in a DoubleLinkedList(T).
56 Part I Collection Basics
''' </summary>
''' <typeparam name="T">Specifies the type of data in the node.</typeparam>
<DebuggerDisplay("Data={Data}")> _
Public Class DoubleLinkedListNode(Of T)
Private m_owner As DoubleLinkedList(Of T)
Private m_prev As DoubleLinkedListNode(Of T)
Private m_next As DoubleLinkedListNode(Of T)
Private m_data As T
''' <summary>
''' Initializes a new instance of the DoubleLinkedListNode(T) class
''' with the specified data.
''' </summary>
''' <param name="data">The data that this node will contain.</param>
Public Sub New(ByVal data As T)
m_data = data
m_owner = Nothing
End Sub
''' <summary>
''' Initializes a new instance of the DoubleLinkedListNode(T) class
''' with the specified data and owner.
''' </summary>
''' <param name="data">The data that this node will contain.</param>
Friend Sub New(ByVal owner As DoubleLinkedList(Of T), ByVal data As T)
m_data = data
m_owner = owner
End Sub
''' <summary>
''' Gets the next node.
''' </summary>
Public Property [Next]() As DoubleLinkedListNode(Of T)
Get
Return m_next
End Get
Friend Set(ByVal value As DoubleLinkedListNode(Of T))
m_next = value
End Set
End Property
''' <summary>
''' Gets or sets the owner of the node.
''' </summary>
Friend Property Owner() As DoubleLinkedList(Of T)
Get
Return m_owner
End Get
Set(ByVal value As DoubleLinkedList(Of T))
m_owner = value
End Set
End Property
Chapter 1 Understanding Collections: Arrays and Linked Lists 57
''' <summary>
''' Gets the previous node.
''' </summary>
Public Property Previous() As DoubleLinkedListNode(Of T)
Get
Return m_prev
End Get
Friend Set(ByVal value As DoubleLinkedListNode(Of T))
m_prev = value
End Set
End Property
''' <summary>
''' Gets the data contained in the node.
''' </summary>
Public Property Data() As T
Get
Return m_data
End Get
Friend Set(ByVal value As T)
m_data = value
End Set
End Property
End Class
The class is defined as a generic class, so the contained data will remain type-safe. The field
m_data contains the data that you want to store. The field m_next references the next node in
the list whereas the field m_prev references the previous node in the list. The set property that
wraps m_next and m_prev is internal so that users can’t change the next and prev pointers and
bypass the bookkeeping. The field m_owner is used to verify that the node belongs to the list
that is being operated on.
C#
[DebuggerDisplay("Count={Count}")]
[DebuggerTypeProxy(typeof(ArrayDebugView))]
public class DoubleLinkedList<T>
{
// Fields
private int m_count;
private DoubleLinkedListNode<T> m_head;
private DoubleLinkedListNode<T> m_tail;
private int m_updateCode;
// Constructors
public DoubleLinkedList();
public DoubleLinkedList(IEnumerable<T> items);
58 Part I Collection Basics
// Methods
public void AddAfter(DoubleLinkedListNode<T> node, DoubleLinkedListNode<T> newNode);
public DoubleLinkedListNode<T> AddAfter(DoubleLinkedListNode<T> node, T value);
public void AddBefore(DoubleLinkedListNode<T> node, DoubleLinkedListNode<T> newNode);
public DoubleLinkedListNode<T> AddBefore(DoubleLinkedListNode<T> node, T value);
public DoubleLinkedListNode<T> AddToBeginning(T value);
public DoubleLinkedListNode<T> AddToEnd(T value);
public void Clear();
public bool Contains(T data);
public DoubleLinkedListNode<T> Find(T data);
public bool Remove(T item);
public void Remove(DoubleLinkedListNode<T> node);
public bool Remove(T item, bool allOccurrences);
public T[] ToArray();
public T[] ToArrayReversed();
// Properties
public int Count { get; }
public DoubleLinkedListNode<T> Head { get; private set; }
public bool IsEmpty { get; }
public DoubleLinkedListNode<T> Tail { get; private set; }
}
Visual Basic
<DebuggerTypeProxy(GetType(ArrayDebugView))> _
<DebuggerDisplay("Count={Count}")> _
Public Class DoubleLinkedList(Of T)
' Fields
Private m_count As Integer
Private m_head As DoubleLinkedListNode(Of T)
Private m_tail As DoubleLinkedListNode(Of T)
Private m_updateCode As Integer
' Constructors
Public Sub New()
Public Sub New(ByVal items As IEnumerable(Of T))
' Methods
Public Function AddAfter(ByVal node As DoubleLinkedListNode(Of T), ByVal value As T) _
As DoubleLinkedListNode(Of T)
Public Sub AddAfter(ByVal node As DoubleLinkedListNode(Of T), ByVal newNode _
As DoubleLinkedListNode(Of T))
Public Sub AddBefore(ByVal node As DoubleLinkedListNode(Of T), ByVal newNode _
As DoubleLinkedListNode(Of T))
Public Function AddBefore(ByVal node As DoubleLinkedListNode(Of T), ByVal value As T) _
As DoubleLinkedListNode(Of T)
Public Function AddToBeginning(ByVal value As T) As DoubleLinkedListNode(Of T)
Public Function AddToEnd(ByVal value As T) As DoubleLinkedListNode(Of T)
Public Sub Clear()
Public Function Contains(ByVal data As T) As Boolean
Public Function Find(ByVal data As T) As DoubleLinkedListNode(Of T)
Public Function Remove(ByVal item As T) As Boolean
Public Sub Remove(ByVal node As DoubleLinkedListNode(Of T))
Chapter 1 Understanding Collections: Arrays and Linked Lists 59
Public Function Remove(ByVal item As T, ByVal allOccurrences As Boolean) As Boolean
Public Function ToArray() As T()
Public Function ToArrayReversed() As T()
' Properties
Public ReadOnly Property Count As Integer
Property Head As DoubleLinkedListNode(Of T)
Public ReadOnly Property IsEmpty As Boolean
Property Tail As DoubleLinkedListNode(Of T)
End Class
For performance reasons, the class maintains a reference to the last node in the field m_tail.
The m_tail field provides quick access to the last item when a user wants to add a node to
the list. This eliminates the need to traverse the list to find the last node. The field m_head
references the first node, and the field m_count tracks the number of nodes in the list.
The m_updateCode field will be incremented each time the user modifies the list. The
m_updateCode field will be used in Chapter 6 to determine if the collection has changed
while the user is iterating over it. It is easier to add it to the code now instead of changing
the code in Chapter 6.
Creating Constructors
The DoubleLinkedList(T) class will contain two constructors. One constructor is for creating an
empty class, and the other is for creating a class with default values. These constructors are
defined as follows.
C#
/// <summary>
/// Initializes a new instance of the DoubleLinkedList (T) class that is empty.
/// </summary>
public DoubleLinkedList()
{
}
/// <summary>
/// Initializes a new instance of the DoubleLinkedList (T) class
/// that contains the items in the list.
/// </summary>
/// <param name="items">Adds the items to the end of the DoubleLinkedList (T).</param>
public DoubleLinkedList (IEnumerable<T> items)
{
foreach (T item in items)
{
AddToEnd(item);
}
}
60 Part I Collection Basics
Visual Basic
''' <summary>
''' Initializes a new instance of the DoubleLinkedList(T) class that is empty.
''' </summary>
Public Sub New()
End Sub
''' <summary>
''' Initializes a new instance of the DoubleLinkedList(T) class
''' that contains the items in the list.
''' </summary>
''' <param name="items">Adds the items to the end of the DoubleLinkedList(T).</param>
Public Sub New(ByVal items As IEnumerable(Of T))
For Each item As T In items
AddToEnd(item)
Next
End Sub
The second constructor adds the specified items to the end of the list in the order they are
in items.
First, let’s look at adding to the beginning. This condition happens if the user adds to an empty
list, calls the AddBefore method with the head node specified, or calls the AddToBeginning
method. To accomplish this type of add, you need to set the newly added node’s Next prop-
erty to the previous head. Then, assign the previous head’s Previous property to the newly
added node. Also, you need to assign the Head property to the newly added node. The
Count property must then be incremented to reflect the newly added node. The dashed line
in the following illustration shows the link to the previous head.
Head
DATA
NEXT
DATA PREV
NEXT Previous
head
PREV
New
node
Next, let’s look at adding to the middle. This condition happens if the user calls AddAfter or
AddBefore without the head or tail node. First, assign the newly added node’s Next property
Chapter 1 Understanding Collections: Arrays and Linked Lists 61
to the node you are adding before. The newly added node’s Prev property should be set to
the node you are adding after. The Prev property of the node you are adding before and the
Next property of the node you are adding after needs to be assigned to the newly added
node. The dashed line represents the references before the operation. The node you are
adding before is denoted as node and the new node is denoted as newNode in the following
illustration and in the source code. The original node set as the Prev property of node is de-
noted as prev in the following illustration.
DATA DATA
NEXT NEXT
PREV
newNode
For the AddAfter method, you need to assign the newly added node’s Next property to
the Next property of the node you are adding after. Then you need to assign the new node’s
Prev property to the node you are adding after. The original node after the node you are
adding needs to have its Prev property set to the newly added node. You then need to assign
the Next property of the node you are adding after to the newly added node. The dashed
line represents the references before the operation. The node you are adding after is de-
noted as node and the new node is denoted as newNode in the following illustration and in
the source code. The original node set as the Next property of node is denoted as next in the
following illustration.
DATA DATA
NEXT NEXT
PREV
newNode
The AddBefore in a doubly linked list is far more efficient than the one in a singly linked list,
because you already have the previous node in a doubly linked list.
Last, let’s look at adding to the end. This condition happens if the user calls the AddToEnd
method or the AddAfter method with the tail node. To accomplish this type of add, you need
to set the previous tail’s Next property to the newly added node. Next, you need to assign
the new node’s Previous property to the newly added node. You then need to assign the Tail
62 Part I Collection Basics
property to the newly added node. The Count must then be incremented to reflect the newly
added node. The dashed line in the following illustration shows the link to the previous tail.
Tail
DATA
NEXT
DATA PREV
NEXT New node
PREV
Old tail
Now let’s look at the implementation of these methods. Adding to the beginning and adding
to the end can be executed using the following methods.
C#
/// <summary>
/// Adds the value to the beginning of the DoubleLinkedList(T).
/// </summary>
/// <param name="value">
/// The value to add to the beginning of the DoubleLinkedList(T).
/// </param>
/// <returns>The newly created node that is holding the value.</returns>
public DoubleLinkedListNode<T> AddToBeginning(T value)
{
DoubleLinkedListNode<T> newNode = new DoubleLinkedListNode<T>(this, value);
if (IsEmpty)
{
m_head = newNode;
m_tail = newNode;
}
else
{
newNode.Next = m_head;
m_head.Previous = newNode;
m_head = newNode;
}
++m_count;
++m_updateCode;
return newNode;
}
/// <summary>
/// Adds the value to the end of the DoubleLinkedList(T).
/// </summary>
/// <param name="value">The value to add to the end of the DoubleLinkedList(T).</param>
/// <returns>The newly created node that is holding the value.</returns>
Chapter 1 Understanding Collections: Arrays and Linked Lists 63
public DoubleLinkedListNode<T> AddToEnd(T value)
{
DoubleLinkedListNode<T> newNode = new DoubleLinkedListNode<T>(this, value);
if (IsEmpty)
{
m_head = newNode;
m_tail = newNode;
}
else
{
newNode.Previous = m_tail;
m_tail.Next = newNode;
m_tail = newNode;
}
++m_count;
++m_updateCode;
return newNode;
}
Visual Basic
''' <summary>
''' Adds the value to the beginning of the DoubleLinkedList(T).
''' </summary>
''' <param name="value">
''' The value to add to the beginning of the DoubleLinkedList(T).
''' </param>
''' <returns>The newly created node that is holding the value.</returns>
Public Function AddToBeginning(ByVal value As T) As DoubleLinkedListNode(Of T)
Dim newNode As New DoubleLinkedListNode(Of T)(Me, value)
If (IsEmpty) Then
m_head = newNode
m_tail = newNode
Else
newNode.Next = m_head
m_head.Previous = newNode
m_head = newNode
End If
m_count += 1
m_updateCode += 1
Return newNode
End Function
''' <summary>
''' Adds the value to the end of the DoubleLinkedList(T).
''' </summary>
''' <param name="value">The value to add to the end of the DoubleLinkedList(T).</param>
''' <returns>The newly created node that is holding the value.</returns>
64 Part I Collection Basics
If (IsEmpty) Then
m_head = newNode
m_tail = newNode
Else
newNode.Previous = m_tail
m_tail.Next = newNode
m_tail = newNode
End If
m_count += 1
m_updateCode += 1
Return newNode
End Function
Note Using the field m_tail eliminates the need to traverse the complete list to find the last
node. Adding a field increases the memory size of the list but eliminates the constant need to
traverse the collection to add the item. This may not be a big deal with a one-time add to a list
with 10 items, but is very beneficial in applications that add items frequently.
When you are adding to the middle of the list, you will use the AddAfter and AddBefore
methods. Each method will have an overload that allows the user to specify a value or a node
to add. If the user specifies a value, a node will be returned that holds the value passed in.
The implementation of these methods is as follows.
C#
/// <summary>
/// Adds the specified value to the DoubleLinkedList(T) after the specified node.
/// </summary>
/// <param name="node">The node to add the value after.</param>
/// <param name="value">The value to add.</param>
/// <returns>The newly created node that holds the value.</returns>
public DoubleLinkedListNode<T> AddAfter(DoubleLinkedListNode<T> node, T value)
{
DoubleLinkedListNode<T> newNode = new DoubleLinkedListNode<T>(this, value);
AddAfter(node, newNode);
return newNode;
}
/// <summary>
/// Adds the specified newNode to the DoubleLinkedList(T) after the specified node.
/// </summary>
/// <param name="node">The node to add the newNode after.</param>
/// <param name="newNode">The node to add.</param>
public void AddAfter(DoubleLinkedListNode<T> node, DoubleLinkedListNode<T> newNode)
Chapter 1 Understanding Collections: Arrays and Linked Lists 65
{
if (node == null)
{
throw new ArgumentNullException("node");
}
if (newNode == null)
{
throw new ArgumentNullException("newNode");
}
if (node.Owner != this)
{
throw new InvalidOperationException("node is not owned by this list");
}
if (newNode.Owner != this)
{
throw new InvalidOperationException("newNode is not owned by this list");
}
if (node == m_tail)
{
m_tail = newNode;
}
if (node.Next != null)
{
node.Next.Previous = newNode;
}
newNode.Next = node.Next;
newNode.Previous = node;
node.Next = newNode;
++m_count;
++m_updateCode;
}
/// <summary>
/// Adds the specified value to the DoubleLinkedList(T) before the specified node.
/// </summary>
/// <param name="node">The node to add the value before.</param>
/// <param name="value">The value to add.</param>
/// <returns>The newly created node that holds the value.</returns>
public DoubleLinkedListNode<T> AddBefore(DoubleLinkedListNode<T> node, T value)
{
DoubleLinkedListNode<T> newNode = new DoubleLinkedListNode<T>(this, value);
AddBefore(node, newNode);
return newNode;
}
/// <summary>
/// Adds the specified newNode to the DoubleLinkedList(T) before the specified node.
/// </summary>
/// <param name="node">The node to add the newNode before.</param>
/// <param name="newNode">The node to add.</param>
66 Part I Collection Basics
newNode.Previous = node.Previous;
newNode.Next = node;
node.Previous = newNode;
}
++m_count;
++m_updateCode;
}
Visual Basic
''' <summary>
''' Adds the specified value to the DoubleLinkedList(T) after the specified node.
''' </summary>
''' <param name="node">The node to add the value after.</param>
''' <param name="value">The value to add.</param>
''' <returns>The newly created node that holds the value.</returns>
Public Function AddAfter(ByVal node As DoubleLinkedListNode(Of T), ByVal value As T) _
As DoubleLinkedListNode(Of T)
Dim newNode As DoubleLinkedListNode(Of T) = New DoubleLinkedListNode(Of T)(Me, value)
Chapter 1 Understanding Collections: Arrays and Linked Lists 67
AddAfter(node, newNode)
Return newNode
End Function
''' <summary>
''' Adds the specified newNode to the DoubleLinkedList(T) after the specified node.
''' </summary>
''' <param name="node">The node to add the newNode after.</param>
''' <param name="newNode">The node to add.</param>
Public Sub AddAfter(ByVal node As DoubleLinkedListNode(Of T), ByVal newNode _
As DoubleLinkedListNode(Of T))
If (node Is Nothing) Then
Throw New ArgumentNullException("node")
End If
If (newNode Is Nothing) Then
Throw New ArgumentNullException("newNode")
End If
If (node.Owner IsNot Me) Then
Throw New InvalidOperationException("node is not owned by this list")
End If
If (newNode.Owner IsNot Me) Then
Throw New InvalidOperationException("newNode is not owned by this list")
End If
newNode.Next = node.Next
newNode.Previous = node
node.Next = newNode
m_count += 1
m_updateCode += 1
End Sub
''' <summary>
''' Adds the specified value to the DoubleLinkedList(T) before the specified node.
''' </summary>
''' <param name="node">The node to add the value before.</param>
''' <param name="value">The value to add.</param>
''' <returns>The newly created node that holds the value.</returns>
Public Function AddBefore(ByVal node As DoubleLinkedListNode(Of T), ByVal value As T) _
As DoubleLinkedListNode(Of T)
Dim newNode As DoubleLinkedListNode(Of T) = New DoubleLinkedListNode(Of T)(Me, value)
AddBefore(node, newNode)
Return newNode
End Function
68 Part I Collection Basics
''' <summary>
''' Adds the specified newNode to the DoubleLinkedList(T) before the specified node.
''' </summary>
''' <param name="node">The node to add the newNode before.</param>
''' <param name="newNode">The node to add.</param>
Public Sub AddBefore(ByVal node As DoubleLinkedListNode(Of T), ByVal newNode _
As DoubleLinkedListNode(Of T))
If (node Is Nothing) Then
Throw New ArgumentNullException("node")
End If
If (newNode Is Nothing) Then
Throw New ArgumentNullException("newNode")
End If
If (node.Owner IsNot Me) Then
Throw New InvalidOperationException("node is not owned by this list")
End If
If (newNode.Owner IsNot Me) Then
Throw New InvalidOperationException("newNode is not owned by this list")
End If
newNode.Previous = node.Previous
newNode.Next = node
node.Previous = newNode
End If
m_count += 1
m_updateCode += 1
End Sub
DATA DATA
NEXT NEXT
PREV
Nodem
C#
/// <summary>
/// Removes the first occurrence of the specified item from the DoubleLinkedList(T).
/// </summary>
/// <param name="item">The item to remove from the DoubleLinkedList(T).</param>
/// <returns>True if an item was removed, false otherwise.</returns>
public bool Remove(T item)
{
return Remove(item, false);
}
/// <summary>
/// Removes the first or all occurrences of the specified item from the DoubleLinkedList(T).
/// </summary>
/// <param name="item">The item to remove from the DoubleLinkedList(T).</param>
/// <param name="allOccurrences">
/// True if all nodes should be removed that contain the specified item, False otherwise
/// </param>
/// <returns>True if an item was removed, false otherwise.</returns>
public bool Remove(T item, bool allOccurrences)
{
if (IsEmpty)
{
return false;
}
{
curr.Previous.Next = curr.Next;
}
if (curr == Head)
{
// If the current node is the head we will have to
// assign the next node as the head
Head = curr.Next;
}
if (curr == Tail)
{
// If the current node is the tail we will have to
// assign the previous node as the tail
Tail = curr.Previous;
}
if (!allOccurrences)
{
break;
}
}
if (removed)
{
++m_updateCode;
}
return removed;
}
Chapter 1 Understanding Collections: Arrays and Linked Lists 71
/// <summary>
/// Removes the specified node from the DoubleLinkedList(T).
/// </summary>
/// <param name="node">The node to remove from the DoubleLinkedList(T).</param>
public void Remove(DoubleLinkedListNode<T> node)
{
if (IsEmpty)
{
return;
}
if (node == null)
{
throw new ArgumentNullException("node");
}
if (node.Owner != this)
{
throw new InvalidOperationException("The node doesn't belong to this list.");
}
// Assign the head to the next node if the specified node is the head
if (m_head == node)
{
m_head = next;
}
// Assign the tail to the previous node if the specified node is the tail
if (m_tail == node)
{
m_tail = prev;
}
// Set the previous node next reference to the removed nodes next reference.
if (prev != null)
{
prev.Next = next;
}
// Set the next node prev reference to the removed nodes prev reference.
if (next != null)
{
next.Previous = prev;
}
// Null out the removed nodes next and prev pointer to be safe.
node.Previous = null;
node.Next = null;
node.Owner = null;
--m_count;
++m_updateCode;
}
72 Part I Collection Basics
Visual Basic
''' <summary>
''' Removes the first occurrence of the specified item from the DoubleLinkedList(T).
''' </summary>
''' <param name="item">The item to remove from the DoubleLinkedList(T).</param>
''' <returns>True if an item was removed, false otherwise.</returns>
Public Function Remove(ByVal item As T) As Boolean
Return Remove(item, False)
End Function
''' <summary>
''' Removes the first or all occurrences of the specified item from the DoubleLinkedList(T).
''' </summary>
''' <param name="item">The item to remove from the DoubleLinkedList(T).</param>
''' <param name="allOccurrences">
''' True if all nodes should be removed that contain the specified item, False otherwise
''' </param>
''' <returns>True if an item was removed, false otherwise.</returns>
Public Function Remove(ByVal item As T, ByVal allOccurrences As Boolean) As Boolean
If (IsEmpty) Then
Return False
End If
' Since the node will no longer be used clean up the pointers in it
tmp.Next = Nothing
tmp.Previous = Nothing
tmp.Owner = Nothing
If (removed) Then
m_updateCode += 1
End If
Return removed
End Function
''' <summary>
''' Removes the specified node from the DoubleLinkedList(T).
''' </summary>
''' <param name="node">The node to remove from the DoubleLinkedList(T).</param>
Public Sub Remove(ByVal node As DoubleLinkedListNode(Of T))
If (IsEmpty) Then
Return
End If
' Assign the head to the next node if the specified node is the head
If (m_head Is node) Then
m_head = pnext
End If
' Assign the tail to the previous node if the specified node is the tail
If (m_tail Is node) Then
m_tail = prev
End If
' Set the previous node next reference to the removed nodes next reference.
If (prev IsNot Nothing) Then
prev.Next = pnext
End If
' Set the next node prev reference to the removed nodes prev reference.
If (pnext IsNot Nothing) Then
pnext.Previous = prev
End If
' nothing out the removed nodes next and prev pointer to be safe.
node.Previous = Nothing
node.Next = Nothing
node.Owner = Nothing
m_count -= 1
m_updateCode += 1
End Sub
With the Remove method, you can remove an item by using its node or value. If the value
is specified, the method traverses the list until it finds the node that contains the value. The
implementation for removing it is done as previously described.
With the Clear method, users can remove all nodes in the list without having to repeatedly
call the Remove method with the Head pointer until the Count is 0.
C#
/// <summary>
/// Removes all items from the DoubleLinkedList(T).
/// </summary>
public void Clear()
{
DoubleLinkedListNode<T> tmp;
if (m_count <= 0)
{
m_head = null;
m_tail = null;
}
++m_updateCode;
}
Visual Basic
''' <summary>
''' Removes all items from the DoubleLinkedList(T).
''' </summary>
Public Sub Clear()
Dim tmp As DoubleLinkedListNode(Of T)
' Change the count and head pointer in case we throw an exception.
' this way the node is removed before we clear the data
m_head = tmp
If (tmp IsNot Nothing) Then
tmp.Previous = Nothing
End If
m_count -= 1
m_updateCode += 1
End Sub
Your users may find it necessary to find the node of an item in the list or check to see if a node
is present in the list. This information can stop them from having to unnecessarily traverse or
operate on the list. The following method and properties allow users to find or check for the
presence of a node.
The Find method allows users to find or check for the presence of a node. It locates a node in
the list by searching for the value it contains, as follows.
C#
/// <summary>
/// Locates the first node that contains the specified data.
/// </summary>
/// <param name="data">The data to find.</param>
/// <returns>The node that contains the specified data, null otherwise.</returns>
public DoubleLinkedListNode<T> Find(T data)
{
if (IsEmpty)
{
return null;
}
return null;
}
Chapter 1 Understanding Collections: Arrays and Linked Lists 77
Visual Basic
''' <summary>
''' Locates the first node that contains the specified data.
''' </summary>
''' <param name="data">The data to find.</param>
''' <returns>The node that contains the specified data, nothing otherwise.</returns>
Public Function Find(ByVal data As T) As DoubleLinkedListNode(Of T)
If (IsEmpty) Then
Return Nothing
End If
curr = curr.Next
End While
Return Nothing
End Function
The method traverses the list from the beginning until it finds the node that contains the
data you are looking for. A null is returned if no node was found.
The Contains method is used to check to see if an item is present in the list.
C#
/// <summary>
/// Checks if the specified data is present in the DoubleLinkedList(T).
/// </summary>
/// <param name="data">The data to look for.</param>
/// <returns>True if the data is found, false otherwise.</returns>
public bool Contains(T data)
{
return Find(data) != null;
}
Visual Basic
''' <summary>
''' Checks if the specified data is present in the DoubleLinkedList(T).
''' </summary>
''' <param name="data">The data to look for.</param>
''' <returns>True if the data is found, false otherwise.</returns>
Public Function Contains(ByVal data As T) As Boolean
Return Find(data) IsNot Nothing
End Function
78 Part I Collection Basics
The method works by checking to see if the Find method found the specified node.
Users may also need to traverse the list for other reasons as well. To do this, they will need
access to the head and tail.
C#
/// <summary>
/// Gets the head node of the DoubleLinkedList(T).
/// </summary>
public DoubleLinkedListNode<T> Head
{
get { return m_head; }
private set { m_head = value; }
}
/// <summary>
/// Gets the tail node of the DoubleLinkedList(T).
/// </summary>
public DoubleLinkedListNode<T> Tail
{
get { return m_tail; }
private set { m_tail = value; }
}
Visual Basic
''' <summary>
''' Gets the head node of the DoubleLinkedList(T).
''' </summary>
Public Property Head() As DoubleLinkedListNode(Of T)
Get
Return m_head
End Get
Private Set(ByVal value As DoubleLinkedListNode(Of T))
m_head = value
End Set
End Property
''' <summary>
''' Gets the tail node of the DoubleLinkedList(T).
''' </summary>
Public Property Tail() As DoubleLinkedListNode(Of T)
Get
Return m_tail
End Get
Private Set(ByVal value As DoubleLinkedListNode(Of T))
m_tail = value
End Set
End Property
The Head and Tail methods return the head and tail of the list respectively. Both properties
block the setting of the values by the user.
Chapter 1 Understanding Collections: Arrays and Linked Lists 79
Knowing how many items are in the list and if it is empty eliminates the need to do some op-
erations on the list. The following properties help users determine what needs to be done.
C#
/// <summary>
/// States if the DoubleLinkedList(T) is empty.
/// </summary>
public bool IsEmpty
{
get { return m_count <= 0; }
}
/// <summary>
/// Gets the number of elements actually contained in the DoubleLinkedList(T).
/// </summary>
public int Count
{
get { return m_count; }
}
Visual Basic
''' <summary>
''' States if the DoubleLinkedList(T) is empty.
''' </summary>
Public ReadOnly Property IsEmpty() As Boolean
Get
Return m_count <= 0
End Get
End Property
''' <summary>
''' Gets the number of elements actually contained in the DoubleLinkedList(T).
''' </summary>
Public ReadOnly Property Count() As Integer
Get
Return m_count
End Get
End Property
The following helper function helps copy the contents of the linked list into a new array. The
new array is useful when code requires an array instead of a DoubleLinked(T) class.
C#
/// <summary>
/// Copies the elements of the DoubleLinkedList(T) to a new array.
/// </summary>
/// <returns>
/// An array containing copies of the elements of the DoubleLinkedList(T).
/// </returns>
80 Part I Collection Basics
int index = 0;
for (DoubleLinkedListNode<T> i = Head; i != null; i = i.Next)
{
retval[index] = i.Data;
++index;
}
return retval;
}
Visual Basic
''' <summary>
''' Copies the elements of the DoubleLinkedList(T) to a new array.
''' </summary>
''' <returns>
''' An array containing copies of the elements of the DoubleLinkedList(T).
''' </returns>
Public Function ToArray() As T()
Dim retval As T() = New T(m_count - 1) {}
Return retval
End Function
Because the list is a doubly linked list, the same amount of time is required to traverse the list
from head to tail and from tail to head. The following method puts the contents in an array
in reverse order.
C#
/// <summary>
/// Copies the elements of the DoubleLinkedList(T) from back to front to a new array.
/// </summary>
/// <returns>
/// An array containing copies of the elements of the DoubleLinkedList<T>.
/// </returns>
public T[] ToArrayReversed()
{
T[] retval = new T[m_count];
int index = 0;
for (DoubleLinkedListNode<T> i = Tail; i != null; i = i.Previous)
{
retval[index] = i.Data;
Chapter 1 Understanding Collections: Arrays and Linked Lists 81
++index;
}
return retval;
}
Visual Basic
''' <summary>
''' Copies the elements of the DoubleLinkedList(T) from back to front to a new array.
''' </summary>
''' <returns>
''' An array containing copies of the elements of the DoubleLinkedList(T).
''' </returns>
Public Function ToArrayReversed() As T()
Dim retval As T() = New T(m_count - 1) {}
Return retval
End Function
C#
using DevGuideToCollections;
using System.Text;
Visual Basic
Imports DevGuideToCollections
Imports System.Text
82 Part I Collection Basics
For C#, in the Program class, create a method called Lesson2a as follows.
C#
static void Lesson2A()
{
}
For Visual Basic, in the Module1 module, create a method called Lesson2a as follows.
Visual Basic
Sub Lesson2A()
End Sub
C#
static string ArrayToString(Array array)
{
StringBuilder sb = new StringBuilder();
sb.Append("[");
if (array.Length > 0)
{
sb.Append(array.GetValue(0));
}
for (int i = 1; i < array.Length; ++i)
{
sb.AppendFormat(",{0}", array.GetValue(i));
}
sb.Append("]");
return sb.ToString();
}
Visual Basic
Function ArrayToString(ByVal array As Array) As String
Dim sb As StringBuilder = New StringBuilder()
sb.Append("[")
If (array.Length > 0) Then
sb.Append(array.GetValue(0))
End If
For i As Integer = 1 To array.Length - 1
sb.AppendFormat(",{0}", array.GetValue(i))
Next
sb.Append("]")
Return sb.ToString()
End Function
This helper method converts the elements of an array to a string. In Chapter 6, you will learn
how to traverse the DoubleLinkedList(T) with a foreach statement.
Chapter 1 Understanding Collections: Arrays and Linked Lists 83
C#
Random rnd = new Random();
DoubleLinkedList<int> list = new DoubleLinkedList<int>();
. . .
}
Console.WriteLine();
Visual Basic
Dim rnd As Random = New Random()
Dim list As DoubleLinkedList(Of Integer) = New DoubleLinkedList(Of Integer)()
For i As Integer = 0 To 9
' Get the value to add
Dim nextValue As Integer = rnd.Next(100)
. . .
Next
Console.WriteLine()
The preceding driver creates 10 random numbers. To sort the numbers as you get them, you
can traverse your sorted list until you find a value that is greater than the number you are
adding to the list. After you have added the item to the list, you should set a flag that states
that the item is added to the list, as follows.
84 Part I Collection Basics
C#
// Traverse the list until you find the item greater than nextValue.
for (DoubleLinkedListNode<int> curr = list.Head; curr != null; curr = curr.Next)
{
// If the item is less than the current value, you need to insert item before the
// current node.
if (nextValue < curr.Data)
{
list.AddBefore(curr, nextValue);
Visual Basic
' Traverse the list until you find the item greater than nextValue.
Dim curr As DoubleLinkedListNode(Of Integer) = list.Head
While (curr IsNot Nothing)
' If the item is less than the current value, you need to insert item before the
' current node.
If (nextValue < curr.Data) Then
list.AddBefore(curr, nextValue)
Exit While
End If
curr = curr.Next
End While
If the item hasn’t been added to the list, you need to add it to the end of the list by checking
the added flag as follows.
C#
// If the item has not been added to the list, the item is either greater than
// all items in the list or the list is empty. In either case, the item should be
// added to the end of the list.
if (!added)
{
list.AddToEnd(nextValue);
}
Chapter 1 Understanding Collections: Arrays and Linked Lists 85
Visual Basic
' If the item has not been added to the list, the item is either greater than
' all items in the list or the list is empty. In either case, the item should be
' added to the end of the list.
If (Not added) Then
list.AddToEnd(nextValue)
End If
Output
Adding to the list...
10 5 40 5 29 41 35 33 29 94
The sorted list is
[5,5,10,29,29,33,35,40,41,94]
The executed code will display the items being sorted and then the sorted list.
Summary
In this chapter, you began your journey through collections by learning about arrays and
linked lists. You saw how arrays are accessed through indexes and how linked lists are ac-
cessed through nodes. You also learned of the advantages and disadvantages of both and
when to use one instead of the other.
Chapter 2
Understanding Collections:
Associative Arrays
After completing this chapter, you will be able to
■ Identify associative arrays.
■ Design and implement associative arrays.
■ Understand when and when not to use associative arrays.
87
88 Part I Collection Basics
More Info Other methods for creating associative arrays, such as trees and skip lists, are beyond
the scope of this book. You can find information about those methods in other books or on the
Internet.
Getting Started
You can create an associative array class called AssociativeArrayAL(T) by using an association
list, as demonstrated in the following code.
C#
[DebuggerDisplay("Count={Count}")]
[DebuggerTypeProxy(typeof(AssociativeArrayDebugView))]
public class AssociativeArrayAL<TKey, TValue>
{
// Fields
private IEqualityComparer<TKey> m_comparer;
private DoubleLinkedList<KVPair<TKey, TValue>> m_list;
private int m_updateCode;
// Constructor
public AssociativeArrayAL();
public AssociativeArrayAL(IEqualityComparer<TKey> comparer);
// Methods
public void Add(TKey key, TValue value);
private void Add(TKey key, TValue value, bool overwrite);
public void Clear();
public bool ContainsKey(TKey key);
public bool ContainsValue(TValue value);
private DoubleLinkedListNode<KVPair<TKey, TValue>> FindKey(TKey key);
private DoubleLinkedListNode<KVPair<TKey, TValue>> FindValue(TValue value);
public bool Remove(TKey key);
private bool Remove(DoubleLinkedListNode<KVPair<TKey, TValue>> node);
public bool RemoveValue(TValue value);
public bool RemoveValue(TValue value, bool allOccurrences);
public bool TryGetValue(TKey key, out TValue value);
// Properties
public int Count { get; }
public bool IsEmpty { get; }
public TValue this[TKey key] { get; set; }
public TKey[] Keys { get; }
public TValue[] Values { get; }
// Nested Types
private struct KVPair
{
private TKey m_key;
private TValue m_value;
public KVPair(TKey key, TValue value);
public TKey Key { get; }
public TValue Value { get; set; }
}
}
90 Part I Collection Basics
Visual Basic
<DefaultMember("Item")> _
<DebuggerDisplay("Count={Count}")> _
<DebuggerTypeProxy(GetType(AssociativeArrayDebugView))> _
Public Class AssociativeArrayAL(Of TKey, TValue)
' Fields
Private m_comparer As IEqualityComparer(Of TKey)
Private m_list As DoubleLinkedList(Of KVPair(Of TKey, TValue))
Private m_updateCode As Integer
' Constructors
Public Sub New()
Public Sub New(ByVal comparer As IEqualityComparer(Of TKey))
' Methods
Public Sub Add(ByVal key As TKey, ByVal value As TValue)
Private Sub Add(ByVal key As TKey, ByVal value As TValue, ByVal overwrite As Boolean)
Public Sub Clear()
Public Function ContainsKey(ByVal key As TKey) As Boolean
Public Function ContainsValue(ByVal value As TValue) As Boolean
Private Function FindKey(ByVal key As TKey) _
As DoubleLinkedListNode(Of KVPair(Of TKey, TValue))
Private Function FindValue(ByVal value As TValue) _
As DoubleLinkedListNode(Of KVPair(Of TKey, TValue))
Public Function Remove(ByVal key As TKey) As Boolean
Private Function Remove(ByVal node As DoubleLinkedListNode(Of KVPair(Of TKey, TValue))) _
As Boolean
Public Function RemoveValue(ByVal value As TValue) As Boolean
Public Function RemoveValue(ByVal value As TValue, ByVal allOccurrences As Boolean) _
As Boolean
Public Function TryGetValue(ByVal key As TKey, ByRef value As TValue) As Boolean
' Properties
Public ReadOnly Property Count As Integer
Public ReadOnly Property IsEmpty As Boolean
Public Default Property Item(ByVal key As TKey) As TValue
Public ReadOnly Property Keys As TKey()
Public ReadOnly Property Values As TValue()
The internal storage will be implemented as a doubly linked list stored in the field m_list.
The comparer that is used for comparing keys will be stored in m_comparer. The m_comparer
field will be used to check whether a key matches a key stored in m_list.
Chapter 2 Understanding Collections: Associative Arrays 91
The m_updateCode field will be incremented each time the user modifies the list. The
m_updateCode field will be used in Chapter 6, “.NET Collection Interfaces,” to determine
whether the collection has changed while the user is iterating over it. It is easier to add it to
the code now instead of changing the code in Chapter 6.
An association list works by storing the key/value pair in a linked list. To accomplish this, you
need a data type to store the key and value. The KVPair struct will be used to store the key
and value of each association.
Creating Constructors
The AssociativeArrayAL(T) class will contain two constructors. One constructor is for cre-
ating an empty class, and the other is for specifying a comparer to be used for the key
comparisons.
With the default constructor for the AssociativeArrayAL(T) class, users can create an empty
associative array.
C#
/// <summary>
/// Initializes a new instance of the AssociativeArrayAL(TKey,TValue) class
/// that is empty.
/// </summary>
public AssociativeArrayAL()
{
m_comparer = EqualityComparer<TKey>.Default;
m_list = new DoubleLinkedList<KVPair>();
}
Visual Basic
''' <summary>
''' Initializes a new instance of the AssociativeArrayAL(TKey,TValue) class
''' that is empty.
''' </summary>
Public Sub New()
m_comparer = EqualityComparer(Of TKey).Default
m_list = New DoubleLinkedList(Of KVPair)()
End Sub
With the next constructor, users can specify the comparer that will be used to do key
comparisons.
C#
/// <summary>
/// Initializes a new instance of the AssociativeArrayAL(TKey,TValue) class
/// that is empty and uses the specified comparer.
/// </summary>
/// <param name="comparer">The comparer to use for the keys.</param>
public AssociativeArrayAL(IEqualityComparer<TKey> comparer)
{
92 Part I Collection Basics
if (comparer == null)
{
throw new ArgumentNullException("comparer");
}
m_comparer = comparer;
m_list = new DoubleLinkedList<KVPair>();
}
Visual Basic
''' <summary>
''' Initializes a new instance of the AssociativeArrayAL(TKey,TValue) class
''' that is empty and uses the specified comparer.
''' </summary>
''' <param name="comparer">The comparer to use for the keys.</param>
Public Sub New(ByVal comparer As IEqualityComparer(Of TKey))
If (comparer Is Nothing) Then
Throw New ArgumentNullException("comparer")
End If
m_comparer = comparer
m_list = New DoubleLinkedList(Of KVPair)()
End Sub
C#
/// <summary>
/// Adds the key value pair to the AssociativeArrayAL(TKey,TValue).
/// </summary>
/// <param name="key">The key to associate with the value.</param>
/// <param name="value">The value to add.</param>
public void Add(TKey key, TValue value)
{
Add(key, value, false);
}
/// <summary>
/// Gets or sets the value at the specified key.
/// </summary>
/// <param name="key">The key to use for finding the value.</param>
/// <returns>The value associated with the specified key.</returns>
public TValue this[TKey key]
{
set
{
Add(key, value, true);
Chapter 2 Understanding Collections: Associative Arrays 93
}
}
Visual Basic
''' <summary>
''' Adds the key value pair to the AssociativeArrayAL(TKey,TValue).
''' </summary>
''' <param name="key">The key to associate with the value.</param>
''' <param name="value">The value to add.</param>
Public Sub Add(ByVal key As TKey, ByVal value As TValue)
Add(key, value, False)
End Sub
''' <summary>
''' Gets or sets the value at the specified key.
''' </summary>
''' <param name="key">The key to use for finding the value.</param>
''' <returns>The value associated with the specified key.</returns>
Default Public Property Item(ByVal key As TKey) As TValue
set
Add(key, Value, True)
End Set
End Property
C#
/// <summary>
/// Adds the key value pair to the AssociativeArrayAL(TKey,TValue)
/// </summary>
/// <param name="key">The key to associate with the specified value.</param>
/// <param name="value">The value to add to the AssociativeArrayAL(TKey,TValue).</param>
/// <param name="overwrite">
/// True if the value should be overwritten if it exist, false if an error should be thrown.
/// </param>
void Add(TKey key, TValue value, bool overwrite)
{
DoubleLinkedListNode<KVPair> node = FindKey(key);
if (node != null)
{
if (!overwrite)
{
throw new InvalidOperationException("The specified key is already present");
}
else
{
KVPair tmp = node.Data;
tmp.Value = value;
node.Data = tmp;
}
94 Part I Collection Basics
return;
}
m_list.AddToBeginning(kvp);
++m_updateCode;
}
Visual Basic
''' <summary>
''' Adds the key value pair to the AssociativeArrayAL(TKey,TValue)
''' </summary>
''' <param name="key">The key to associate with the specified value.</param>
''' <param name="value">The value to add to the AssociativeArrayAL(TKey,TValue).</param>
''' <param name="overwrite">
''' True if the value should be overwritten if it exist, false if an error should be thrown.
''' </param>
Sub Add(ByVal key As TKey, ByVal value As TValue, ByVal overwrite As Boolean)
Dim node As DoubleLinkedListNode(Of KVPair) = FindKey(key)
If (node IsNot Nothing) Then
If (Not overwrite) Then
Throw New InvalidOperationException("The specified key is already present")
Else
Dim tmp As KVPair = node.Data
tmp.Value = value
node.Data = tmp
End If
Return
End If
m_list.AddToBeginning(kvp)
m_updateCode += 1
End Sub
The Add(TKey,TValue,bool) method searches through the internal linked list to see whether
the key is already present in the list. If the key is already present, an InvalidOperationE xception
will be thrown if the user wasn’t trying to reassign the key. If the key exists and the user was
trying to reassign the key, the method will reassign the key and return. If the key doesn’t
exist, the method will add the key/value pair to the beginning of the list and increment the
update code.
Chapter 2 Understanding Collections: Associative Arrays 95
The Clear method removes all key/value pairs from the collection and is defined as follows.
C#
/// <summary>
/// Removes all items from the AssociativeArrayAL(TKey,TValue).
/// </summary>
public void Clear()
{
m_list.Clear();
++m_updateCode;
}
Visual Basic
''' <summary>
''' Removes all items from the AssociativeArrayAL(TKey,TValue).
''' </summary>
Public Sub Clear()
m_list.Clear()
m_updateCode += 1
End Sub
The Remove method removes the key/value pair associated with the specified key and is
defined as follows.
C#
/// <summary>
/// Removes the specified key from the AssociativeArrayAL(TKey,TValue).
/// </summary>
/// <param name="key">The key to remove from the AssociativeArrayAL(TKey,TValue).</param>
/// <returns>True if the key was removed, false otherwise.</returns>
public bool Remove(TKey key)
{
DoubleLinkedListNode<KVPair> node = FindKey(key);
if (node == null)
{
return false;
}
return Remove(node);
}
96 Part I Collection Basics
Visual Basic
''' <summary>
''' Removes the specified key from the AssociativeArrayAL(TKey,TValue).
''' </summary>
''' <param name="key">The key to remove from the AssociativeArrayAL(TKey,TValue).</param>
''' <returns>True if the key was removed, false otherwise.</returns>
Public Function Remove(ByVal key As TKey) As Boolean
Dim node As DoubleLinkedListNode(Of KVPair) = FindKey(key)
Return Remove(node)
End Function
The RemoveValue method removes the key/value pair that contains the specified value and is
defined as follows.
C#
/// <summary>
/// Removes the first occurrence of the specified value.
/// </summary>
/// <param name="value">The value to remove.</param>
/// <returns>
/// True if the value was removed,
/// false if it wasn't present in the AssociativeArrayAL(TKey,TValue).
/// </returns>
public bool RemoveValue(TValue value)
{
return RemoveValue(value, false);
}
/// <summary>
/// Removes the specified value.
/// </summary>
/// <param name="value">The value to remove.</param>
/// <param name="allOccurrences">
/// True if all occurrences of the value should be removed, false if not.
/// </param>
/// <returns>
/// True if the value was removed,
/// false if it wasn't present in the AssociativeArrayAL(TKey,TValue).
/// </returns>
public bool RemoveValue(TValue value, bool allOccurrences)
{
bool removed = false;
if (!allOccurrences)
{
return removed;
}
node = FindValue(value);
}
return removed;
}
Visual Basic
''' <summary>
''' Removes the first occurrence of the specified value.
''' </summary>
''' <param name="value">The value to remove.</param>
''' <returns>
''' True if the value was removed,
''' false if it wasn't present in the AssociativeArrayAL(TKey,TValue).
''' </returns>
Public Function RemoveValue(ByVal value As TValue) As Boolean
Return RemoveValue(value, False)
End Function
''' <summary>
''' Removes the specified value.
''' </summary>
''' <param name="value">The value to remove.</param>
''' <param name="allOccurrences">
''' True if all occurrences of the value should be removed, false if not.
''' </param>
''' <returns>
''' True if the value was removed,
''' false if it wasn't present in the AssociativeArrayAL(TKey,TValue).
''' </returns>
Public Function RemoveValue(ByVal value As TValue, ByVal allOccurrences As Boolean) _
As Boolean
Dim removed As Boolean = False
node = FindValue(value)
End While
Return removed
End Function
98 Part I Collection Basics
C#
bool Remove(DoubleLinkedListNode<KVPair> node)
{
if (node == null)
{
return false;
}
m_list.Remove(node);
++m_updateCode;
return true;
}
Visual Basic
Private Function Remove(ByVal node As DoubleLinkedListNode(Of KVPair)) As Boolean
If (node Is Nothing) Then
Return False
End If
m_list.Remove(node)
m_updateCode += 1
Return True
End Function
C#
/// <summary>
/// Gets the number of items in the AssociativeArrayAL(TKey,TValue).
/// </summary>
public int Count
{
get { return m_list.Count; }
}
Visual Basic
''' <summary>
''' Gets the number of items in the AssociativeArrayAL(TKey,TValue).
''' </summary>
Public ReadOnly Property Count() As Integer
Get
Return m_list.Count
End Get
End Property
Chapter 2 Understanding Collections: Associative Arrays 99
With the IsEmpty property, users can check whether the collection is empty before doing
some operations.
C#
/// <summary>
/// States if the AssociativeArrayAL(TKey,TValue) is empty.
/// </summary>
public bool IsEmpty
{
get { return Count <= 0; }
}
Visual Basic
''' <summary>
''' States if the AssociativeArrayAL(TKey,TValue) is empty.
''' </summary>
Public ReadOnly Property IsEmpty() As Boolean
Get
Return Count <= 0
End Get
End Property
Users can also get the list of keys and values in the collection by calling the Keys and Values
properties respectively.
C#
/// <summary>
/// Gets an array of current keys.
/// </summary>
public TKey[] Keys
{
get
{
int index = 0;
TKey[] keys = new TKey[Count];
return keys;
}
}
/// <summary>
/// Gets an array of current values.
/// </summary>
public TValue[] Values
{
get
{
100 Part I Collection Basics
return values;
}
}
Visual Basic
''' <summary>
''' Gets an array of current keys.
''' </summary>
Public ReadOnly Property Keys() As TKey()
Get
Dim index As Integer = 0
Dim rkeys As TKey() = New TKey(Count - 1) {}
Return rkeys
End Get
End Property
''' <summary>
''' Gets an array of current values.
''' </summary>
Public ReadOnly Property Values() As TValue()
Get
Dim rvalues As TValue() = New TValue(Count - 1) {}
Dim index As Integer = 0
Return rvalues
End Get
End Property
Chapter 2 Understanding Collections: Associative Arrays 101
Both properties traverse the list from the head to the tail and add the values to an array as
they traverse. This provides the users with a snapshot of the current Keys and Values. For now,
the Keys and Values properties are just placeholders. In Chapter 6, you learn how to return an
enumerator for the Keys and Values properties that will stay in sync with the collection.
Sometimes users may want to know if a particular key or value is present in the collection.
The ContainsKey and ContainsValue methods can be used to do so.
C#
/// <summary>
/// Checks to see if the AssociativeArrayAL(TKey,TValue) contains the specified value.
/// </summary>
/// <param name="value">The value to look for.</param>
/// <returns>True if the value was found, false otherwise.</returns>
public bool ContainsValue(TValue value)
{
return FindValue(value) != null;
}
/// <summary>
/// Checks to see if the specified key is present in the AssociativeArrayAL(TKey,TValue).
/// </summary>
/// <param name="key">The key to look for.</param>
/// <returns>True if the key was found, false otherwise.</returns>
public bool ContainsKey(TKey key)
{
return FindKey(key) != null;
}
Visual Basic
''' <summary>
''' Checks to see if the specified key is present in the AssociativeArrayAL(TKey,TValue).
''' </summary>
''' <param name="key">The key to look for.</param>
''' <returns>True if the key was found, false otherwise.</returns>
Public Function ContainsKey(ByVal key As TKey) As Boolean
Return FindKey(key) IsNot Nothing
End Function
''' <summary>
''' Checks to see if the AssociativeArrayAL(TKey,TValue) contains the specified value.
''' </summary>
''' <param name="value">The value to look for.</param>
''' <returns>True if the value was found, false otherwise.</returns>
Public Function ContainsValue(ByVal value As TValue) As Boolean
Return FindValue(value) IsNot Nothing
End Function
102 Part I Collection Basics
The ContainsValue and ContainsKey methods use the FindValue and FindKey methods to tell
whether an item is in the collection. They are defined as follows.
C#
/// <summary>
/// Finds the node that contains the specified key.
/// </summary>
/// <param name="key">The key to look for.</param>
/// <returns>The node that contains the specified key, otherwise null</returns>
DoubleLinkedListNode<KVPair> FindKey(TKey key)
{
if (IsEmpty)
{
return null;
}
return null;
}
/// <summary>
/// Finds the node that contains the specified value.
/// </summary>
/// <param name="value">The value to look for.</param>
/// <returns>The first node that contains the specified value, otherwise null.</returns>
DoubleLinkedListNode<KVPair> FindValue(TValue value)
{
if (IsEmpty)
{
return null;
}
return null;
}
Chapter 2 Understanding Collections: Associative Arrays 103
Visual Basic
''' <summary>
''' Finds the node that contains the specified key.
''' </summary>
''' <param name="key">The key to look for.</param>
''' <returns>The node that contains the specified key, otherwise null</returns>
Private Function FindKey(ByVal key As TKey) As DoubleLinkedListNode(Of KVPair)
If (IsEmpty) Then
Return Nothing
End If
Return node
End If
node = node.Next
End While
Return Nothing
End Function
''' <summary>
''' Finds the node that contains the specified value.
''' </summary>
''' <param name="value">The value to look for.</param>
''' <returns>The first node that contains the specified value, otherwise null.</returns>
Private Function FindValue(ByVal value As TValue) As DoubleLinkedListNode(Of KVPair)
If (IsEmpty) Then
Return Nothing
End If
Dim comparer As EqualityComparer(Of TValue) = EqualityComparer(Of TValue).Default
Return Nothing
End Function
Both methods traverse the collection from the head to the tail. The FindValue and FindKey
methods check each node by using a comparer to see whether the specified key or value is
present at the current node.
Users need the ability to retrieve a value from the collection by using a key. The Item prop-
erty allows them to do so.
104 Part I Collection Basics
C#
/// <summary>
/// Gets the value at the specified key.
/// </summary>
/// <param name="key">The key to use for finding the value.</param>
/// <returns>The value associated with the specified key.</returns>
public TValue this[TKey key]
{
get
{
DoubleLinkedListNode<KVPair> node = FindKey(key);
if (node == null)
{
throw new KeyNotFoundException("The specified key couldn't be located");
}
return node.Data.Value;
}
}
Visual Basic
''' <summary>
''' Gets or sets the value at the specified key.
''' </summary>
''' <param name="key">The key to use for finding the value.</param>
''' <returns>The value associated with the specified key.</returns>
Default Public Property Item(ByVal key As TKey) As TValue
Get
Dim node As DoubleLinkedListNode(Of KVPair) = FindKey(key)
Return node.Data.Value
End Get
End Property
The Item property uses the FindKey method to locate the node that contains the specified
key. If the node doesn’t exist, the Item property throws an exception. This may not always be
what the user wants. The TryGetValue method allows a user to try and retrieve a value with-
out throwing an exception if the key doesn’t exist in the collection.
C#
/// <summary>
/// Tries to get the value at the specified key without throwing an exception.
/// </summary>
/// <param name="key">The key to get the value for.</param>
/// <param name="value">
/// The value that is associated with the specified key or the default value for the type.
/// </param>
/// <returns>True if the key was found, false otherwise.</returns>
Chapter 2 Understanding Collections: Associative Arrays 105
public bool TryGetValue(TKey key, out TValue value)
{
DoubleLinkedListNode<KVPair> node = FindKey(key);
if (node != null)
{
value = node.Data.Value;
return true;
}
value = default(TValue);
return false;
}
Visual Basic
''' <summary>
''' Tries to get the value at the specified key without throwing an exception.
''' </summary>
''' <param name="key">The key to get the value for.</param>
''' <param name="value">
''' The value that is associated with the specified key or the default value for the type.
''' </param>
''' <returns>True if the key was found, false otherwise.</returns>
Public Function TryGetValue(ByVal key As TKey, ByRef value As TValue) As Boolean
Dim node As DoubleLinkedListNode(Of KVPair) = FindKey(key)
Depending on the implementation of the hashing function, other associative array types
might have better performance than a hash table implementation. Make sure you read the
documentation of the implementation you plan to use to see if it will perform well in your
scenario.
A B C ... Y Z
Now you put each name in the bucket that is labeled with the first letter in the last name.
A B C ... Y Z
Cao, Jun
Abercrombie, Kim
Zhang, Larry
Alberts, Amy E.
Your search has now decreased from the entire list to only the name that begins with the first
letter of the last name you are looking for. This is the concept behind hash table buckets.
Buckets are a way of breaking up a large set of data into a smaller set. The process of mapping
a key—in this case, a name—to an index is done by what is called a hashing function. An ex-
ample hashing function from the preceding example would be like the following.
Chapter 2 Understanding Collections: Associative Arrays 107
C#
static int NameToIndex(string name)
{
// Trim the name and convert it to lower case
name = name.Trim().ToLower();
Visual Basic
Function NameToIndex(ByVal name As String) As Integer
' Trim the name and convert it to lower case
name = name.Trim().ToLower()
' Subtract 'a' from the last initial to create an index from
' 0 to 25
Return Asc(name(0)) - Asc("a"c)
End Function
Note The NameToIndex method is provided to demonstrate the concept of hash functions. You
should research other hashing functions if you want to hash names.
C#
string[] buckets = new string[26];
buckets[NameToIndex("Akin, Cigdem")] = "(901) 555-0166";
buckets[NameToIndex("Zimprich, Karin")] = "(901) 555-0177";
Visual Basic
Dim buckets As String() = New String(25) {}
buckets(NameToIndex("Akin, Cigdem")) = "(901) 555-0166"
buckets(NameToIndex("Zimprich, Karin")) = "(901) 555-0177"
A B C ... Y Z
A B C ... Y Z
Akin, Cigdem
Alberts, Amy E.
AA AB AC ... ZY ZZ
Akin, Cigdem
Alberts, Amy E.
So bucket AA would be used for a person whose last name and first name begin with the
letter A. Bucket AB would be used for a person whose last name begins with A and first name
begins with B. To convert the name to an index, you can use the following method.
C#
static int NameToIndex(string name)
{
// Trim the name and convert it to lower case
name = name.Trim().ToLower();
int lastbucket = 0;
int indexOfLastName = name.IndexOf(", ");
Visual Basic
Function NameToIndex(ByVal name As String) As Integer
' Trim the name and convert it to lower case
name = name.Trim().ToLower()
Chapter 2 Understanding Collections: Associative Arrays 109
' Subtract 'a' from the last initial to create an index from
' 0 to 25
Dim firstbucket As Integer = Asc(name(0)) - Asc("a"c)
End Function
Note The NameToIndex method is provided to demonstrate the concept of hash functions. You
should research other hashing functions if you want to hash names.
The hashing function now uses the first initial of the first and last name to compute the index.
To use this method, you would do as follows.
C#
string[] buckets = new string[26 * 26];
buckets[NameToIndex("Akin, Cigdem")] = "(901) 555-0166";
buckets[NameToIndex("Zimprich, Karin")] = "(901) 555-0177";
buckets[NameToIndex("Zhang, Larry")] = "(901) 555-0133" ;
Visual Basic
Dim buckets As String() = New String(26 * 26 - 1) {}
buckets(NameToIndex("Akin, Cigdem")) = "(901) 555-0166"
buckets(NameToIndex("Zimprich, Karin")) = "(901) 555-0177"
buckets(NameToIndex("Zhang, Larry")) = "(901) 555-0133"
You may have realized that you still have an issue. If you associate the name “Zakardissnehf,
Kerstin” with the phone number “(901) 555-0122”, you would overwrite “Zimprich, Karin”. You
could pick the next initial and then the next and so on, but you would eventually run out of
memory or negate your reason for picking a hash table. So at some point, you have collision
unless you can implement a hashing function that puts only one name in a bucket. Because
first and last names are not unique in the world, you may find it impossible to find a hashing
function that doesn’t contain collisions for a dynamic list. However, there are multiple ways to
solve the problem of collision. The next section explains how to solve it using chaining.
110 Part I Collection Basics
Understanding Chaining
At some point, two different keys will have the same hash value. To solve this, you can use
chaining. Chaining solves the hash collision problem by implementing a linked list at each
bucket. You can then search using the method described earlier for association lists but at the
bucket level instead of the complete list. To implement this, you need to create a data type
to store your key value pair, as in the following.
C#
struct KVPair
{
public string Name;
public string PhoneNumber;
public KVPair(string name, string phoneNumber)
{
Name = name;
PhoneNumber = phoneNumber;
}
}
Visual Basic
Structure KVPair
Public Name As String
Public PhoneNumber As String
Public Sub New(ByVal name As String, ByVal phoneNumber As String)
Me.Name = name
Me.PhoneNumber = phoneNumber
End Sub
End Structure
Each bucket will be a SingleLinkedList(KVPair) instead of a string. So you need a method such
as the one that follows for adding values to the bucket.
C#
static void Associate(SingleLinkedList<KVPair>[] buckets, string name, string phoneNumber)
{
int index = NameToIndex(name);
if (buckets[index] == null)
{
buckets[index] = new SingleLinkedList<KVPair>();
buckets[index].AddToBeginning(new KVPair(name,phoneNumber));
return;
}
Visual Basic
Sub Associate(ByVal buckets As SingleLinkedList(Of KVPair)(), _
ByVal name As String, _
ByVal phoneNumber As String)
Dim index As Integer = NameToIndex(name)
The Associate method creates a SingleLinkedList(KVPair) for a bucket if one doesn’t already
exist. The Associate method then checks to see if the key has already been added to the list.
If it hasn’t, it will add the key value pair to the list, otherwise it will throw an exception. The
following code demonstrates this approach.
C#
SingleLinkedList<KVPair> []buckets = new SingleLinkedList<KVPair>[26 * 26];
Visual Basic
Dim buckets() As New SingleLinkedList(Of KVPair)(26 * 26 - 1) {}
Understanding Object.GetHashCode
The Microsoft .NET Framework already implements a GetHashCode method in every object
that returns a hash code. This method can be used to create a hash code, but you should
keep in mind the following:
■ The hash code from GetHashCode isn’t guaranteed to be unique. That means that two
objects that are not equal could share the same hash code. However, two objects that
are equal should share the same hash code.
■ An object could have a different hash code with different versions of the .NET
Framework if the application were restarted. In other words, if you persist an object
with its hash code, there is no guarantee that it will have the same hash code when the
application restarts and loads the object from persistent storage.
■ Implementers of an object that is going to be used as a key should override the default
behavior of the GetHashCode method. If they do not, or if their implementation isn’t
sufficient enough for your hash table implementation, you should create a custom one
using the System.Collections.IEqualityComparer interface.
■ When implementing GetHashCode, you should base the hash code off of at least one of
the fields of the instance. A checksum of all of the fields used for comparing it against
other objects of the same type is a good choice. That way, two instances that are equal
would return the same hash code.
This is known as the division method. The division method is not the only method available
and is not considered the best for all cases, however, it is easy to follow and you can find a lot
of documentation about it.
In the division method, c is a method that converts the key to a numeric value. You’ll use
Object.GetHashCode for method c. You then use the modulus operation to uniformly distrib-
ute the keys over n buckets. The modulus operator always returns a number from 0–(n-1).
This is because the modulus operator returns the remainder of the division of one number by
another. However, if c(k) returns a negative number, the modulus operator returns a nega-
tive number. The value returned from Object.GetHashCode will be AND with the 0x7FFFFFFF.
(The AND operation is discussed in the BitArray sections in Chapter 5, “Generic and Support
Collections.”) This removes the sign bit from the value returned from the Object.GetHashCode
method, which guarantees the number is a positive number.
Chapter 2 Understanding Collections: Associative Arrays 113
C#
class MyStringComparer : IEqualityComparer<string>
{
const int CONSTANT = 31;
int hashCode = 0;
for (int i = 0; i < obj.Length; ++i)
{
hashCode = CONSTANT * hashCode + (int)obj[i];
}
return hashCode;
}
}
Visual Basic
Class MyStringComparer
Implements IEqualityComparer(Of String)
Return hashCode
End Function
End Class
Note This is also how you would override the default GetHashCode method used in the
Dictionary(TKey,TValue) and AssociativeArrayHT(TKey,TValue) classes.
In the preceding example, m is CONSTANT or 31. So the n you pick needs to coprime with m.
To understand why, look at the following simplified example that doesn’t follow the rule. The
code multiplies m by 0 to samples, takes the modulus of n, and then increments the number
of items in that bucket.
C#
int m = 31;
int hashCode = 0;
int n = 62;
int samples = 1000;
int[] buckets = new int[n];
for (int i = 0; i < samples; ++i)
{
hashCode = m * i;
++buckets[hashCode % n];
}
for (int i = 0; i < n; ++i)
{
if (buckets[i] != 0)
{
Console.WriteLine("buckets[{0}] = {1}%", i, buckets[i] * 100 / samples);
}
}
Visual Basic
Dim m As Integer = 31
Dim hashCode As Integer = 0
Dim n As Integer = 62
Dim samples As Integer = 1000
Dim buckets As Integer() = New Integer(n - 1) {}
For i As Integer = 0 To samples - 1
hashCode = m * i
buckets(hashCode Mod n) += 1
Next
For i As Integer = 0 To n - 1
If (buckets(i) <> 0) Then
Console.WriteLine("buckets[{0}] = {1}%", i, buckets(i) * 100 / samples)
End If
Next
Chapter 2 Understanding Collections: Associative Arrays 115
Output
buckets[0] = 50%
buckets[31] = 50%
You can see how out of 1,000 simulated samples, buckets 0 and 31 received all of the items.
This is because both numbers are divisible by 31, which makes them not coprime. So how can
you guarantee that both numbers are coprime to each other? You can have m and n both be
prime numbers. Prime numbers are numbers that are only divisible by 1 and themselves. Even
numbers except 2 are never prime because they can be divisible by 2. Numbers that end with
0 and 5 except 5 are never prime because they are divisible by 5. So two prime numbers can-
not have any common factors. This makes them perfect for n and m. The only problem is that
you can control n but not necessarily m because you normally will not write the GetHashCode
method for it. You can still pick a prime number for n that will work, assuming the writer of
GetHashCode used a prime number or a method that doesn’t use a multiplier. The same ex-
ample follows, but it has a prime number for n and m.
C#
int m = 31;
int hashCode = 0;
int n = 11;
int samples = 1000;
int[] buckets = new int[n];
for (int i = 0; i < samples; ++i)
{
hashCode = m * i;
++buckets[hashCode % n];
}
for (int i = 0; i < n; ++i)
{
if (buckets[i] != 0)
{
Console.WriteLine("buckets[{0}] = {1}%", i, buckets[i] * 100 / samples);
}
}
Visual Basic
Dim m As Integer = 31
Dim hashCode As Integer = 0
Dim n As Integer = 11
Dim samples As Integer = 1000
Dim buckets As Integer() = New Integer(n - 1) {}
For i As Integer = 0 To samples - 1
hashCode = m * i
buckets(hashCode Mod n) += 1
Next
For i As Integer = 0 To n - 1
If (buckets(i) <> 0) Then
Console.WriteLine("buckets[{0}] = {1}%", i, buckets(i) * 100 / samples)
End If
Next
116 Part I Collection Basics
Output
buckets[0] = 9%
buckets[1] = 9%
buckets[2] = 9%
buckets[3] = 9%
buckets[4] = 9%
buckets[5] = 9%
buckets[6] = 9%
buckets[7] = 9%
buckets[8] = 9%
buckets[9] = 9%
buckets[10] = 9%
Now the items are evenly distributed across n buckets because n and m are now coprimes.
So as long as MyStringComparer.GetHashCode returns a uniform distribution of hash codes,
the items will be uniformly distributed across our buckets.
One problem with using prime numbers for n is that they are very expensive to compute at
run time. You could create a lookup table to contain the prime numbers instead of computing
them at run time. You can also find a good predefined list of prime numbers by searching the
Internet and then running stats on your implementation to see which one performs best for
your scenario.
If m is a prime number, you can also use the even rule we talked about earlier in this sec-
tion. A prime number can never have an even number as a factor, but an even number can
have a prime number as a factor; for example, the even number 14 has the prime numbers
7 and 2 as factors. All 2i, when i is greater than 2, has the prime number 2 as a factor and
another number that isn’t prime. An even number times an odd number is always even, and
an odd number times an odd number is always odd. Using this information, you could let n
be a power of 2, assuming that the writer of the GetHashCode method didn’t use 2 for m or
an even number. Because all even numbers are divisible by 2, n and m would at least have a
common factor of 2 if m was even and n was a power of 2. So, 2i will never be a prime num-
ber or an odd number when i is greater than 1 and can be used for n when m isn’t even.
The other issue with using the linked list classes is that they are not written with high per-
formance in mind, which is why you went through the process of implementing a linked list
and other collection classes as well. Implementing them helps you to understand where the
bottlenecks of an implementation are and what to do to improve them.
If you are presented with n items to add to your hash table, the best case scenario would be
n buckets with one item in each bucket. The worst case scenario would be one bucket with
n items in it. More than likely, you will be somewhere between the two scenarios unless you
know ahead of time all of the items you will be adding—or the guy who is quitting next
week creates your hashing function.
The number of buckets will also be used in the hash function. So, every time you increase or
decrease the number of buckets, you need to traverse all items and calculate their new hash
positions. Traversing all items and calculating their hash positions is an expensive operation.
Also, removing the item from one list involves fixing the node links, so the potential problems
identified are as follows:
C#
Entry<TKey, TValue>[] m_entries;
// Nested Types
private struct Entry
{
public TKey Key;
public TValue Value;
public int HashCode;
public int Next;
}
118 Part I Collection Basics
Visual Basic
Private Structure Entry
Public Key As TKey
Public Value As TValue
Public HashCode As Integer
Public [Next] As Integer
End Structure
The Entry struct represents a node. The Next field references the next node. Each reference is
stored as an index to m_entries. This gives you a maximum of Int32.Max nodes. If you needed
more, you could use an UInt32, or if you needed fewer, you could use an Int16, an UIn16, or
even a Byte. When you need a new node, it takes a while to traverse m_entries and find a
node that isn’t being used. You would also have to store a flag that states that the node is
free. To solve this, the following fields are added to AssociativeArrayHT(T).
C#
int m_nextUnusedEntry;
int m_unusedCount;
Visual Basic
Private m_nextUnusedEntry As Integer
Private m_unusedCount As Integer
The m_nextUnusedEntry points to the first unused node. That node then points to the next
unused one and so on. When it is time to retrieve an unused node, you only have to obtain
the node pointed to by m_nextUnusedEntry and then assign m_nextUnusedEntry to the next
unused one.
To share the node across buckets, you can create the following fields.
C#
int[] m_buckets;
int m_capacity;
int m_count;
public const int NULL_REFERENCE = -1;
Visual Basic
Private m_buckets As Integer()
Private m_count As Integer
Private m_capacity As Integer
Public Const NULL_REFERENCE As Integer = -1
Each element in m_buckets references the first node in that bucket. The constant
NULL_REFERENCE is used to denote an empty list. If a node’s Next field is equal to NULL_
REFERENCE, that node will be the tail of the linked list. The m_capacity field states how many
nodes and buckets are present. The m_count field states how many nodes are currently used.
Chapter 2 Understanding Collections: Associative Arrays 119
Getting Started
You can create a class called AssociativeArrayHT(T) by using the following.
C#
[DebuggerDisplay("Count={Count}")]
[DebuggerTypeProxy(typeof(AssociativeArrayDebugView))]
public class AssociativeArrayHT<TKey, TValue>
{
// Fields
private int[] m_buckets;
private int m_capacity;
private IEqualityComparer<TKey> m_comparer;
private int m_count;
private Entry<TKey, TValue>[] m_entries;
private int m_nextUnusedEntry;
private int m_unusedCount;
private int m_updateCode;
public const int NULL_REFERENCE = -1;
private readonly int[] PRIME_NUMBERS;
// Constructors
public AssociativeArrayHT();
public AssociativeArrayHT(IEqualityComparer<TKey> comparer);
// Methods
public void Add(TKey key, TValue value);
private void Add(TKey key, TValue value, bool overwrite);
private int CalculateCapacity(int size);
public void Clear();
public bool ContainsKey(TKey key);
public bool ContainsValue(TValue value);
private EntryData<TKey, TValue> FindKey(TKey key);
private EntryData<TKey, TValue> FindValue(TValue value);
private int CalculateHashCode(TKey key);
internal bool MoveNext(ref int bucketIndex, ref int entryIndex);
private void Rehash();
private bool Remove(EntryData<TKey, TValue> entry);
public bool Remove(TKey key);
public bool RemoveValue(TValue value);
public bool RemoveValue(TValue value, bool allOccurrences);
public bool TryGetValue(TKey key, out TValue value);
// Properties
public int Count { get; }
public bool IsEmpty { get; }
public TValue this[TKey key] { get; set; }
public TKey[] Keys { get; }
public TValue[] Values { get; }
120 Part I Collection Basics
// Nested Types
private struct Entry
{
public TKey Key;
public TValue Value;
public int HashCode;
public int Next;
}
Visual Basic
<DefaultMember("Item")> _
<DebuggerDisplay("Count={Count}")> _
<DebuggerTypeProxy(GetType(AssociativeArrayDebugView))> _
Public Class AssociativeArrayHT(Of TKey, TValue)
' Fields
Private m_buckets As Integer()
Private m_capacity As Integer
Private m_comparer As IEqualityComparer(Of TKey)
Private m_count As Integer
Private m_entries As Entry(Of TKey, TValue)()
Private m_nextUnusedEntry As Integer
Private m_unusedCount As Integer
Private m_updateCode As Integer
Public Const NULL_REFERENCE As Integer = -1
Private ReadOnly PRIME_NUMBERS As Integer()
' Constructors
Public Sub New()
Public Sub New(ByVal comparer As IEqualityComparer(Of TKey))
' Methods
Public Sub Add(ByVal key As TKey, ByVal value As TValue)
Public Sub Add(ByVal key As TKey, ByVal value As TValue, ByVal overwrite As Boolean)
Private Function CalculateCapacity(ByVal size As Integer) As Integer
Chapter 2 Understanding Collections: Associative Arrays 121
Private Function CalculateHashCode(ByVal key As TKey) As Integer
Public Sub Clear()
Public Function ContainsKey(ByVal key As TKey) As Boolean
Public Function ContainsValue(ByVal value As TValue) As Boolean
Private Function FindKey(ByVal key As TKey) As EntryData(Of TKey, TValue)
Private Function FindValue(ByVal value As TValue) As EntryData(Of TKey, TValue)
Private Function HashFunction(ByVal hashcode As Integer) As Integer
Friend Function MoveNext(ByRef bucketIndex As Integer, ByRef entryIndex As Integer) _
As Boolean
Private Sub Rehash()
Private Function Remove(ByVal entry As EntryData(Of TKey, TValue)) As Boolean
Public Function Remove(ByVal key As TKey) As Boolean
Public Function RemoveValue(ByVal value As TValue) As Boolean
Public Function RemoveValue(ByVal value As TValue, ByVal allOccurrences As Boolean) _
As Boolean
Public Function TryGetValue(ByVal key As TKey, ByRef value As TValue) As Boolean
' Properties
Public ReadOnly Property Count As Integer
Public ReadOnly Property IsEmpty As Boolean
Public Default Property Item(ByVal key As TKey) As TValue
Public ReadOnly Property Keys As TKey()
Public ReadOnly Property Values As TValue()
<StructLayout(LayoutKind.Sequential)> _
Private Structure EntryData
private m_hashtable As AssociativeArrayHT(Of TKey, TValue)
private m_index As Integer
private m_bucketIndex As Integer
private m_previous As Integer
Public Shared ReadOnly EMPTY As EntryData(Of TKey, TValue)
Shared Sub New()
Public ReadOnly Property Index As Integer
Public ReadOnly Property BucketIndex As Integer
Public ReadOnly Property IsEmpty As Boolean
Public ReadOnly Property Previous As Integer
Public ReadOnly Property [Next] As Integer
Public ReadOnly Property Key As TKey
Public Property Value As TValue
Public Sub New(ByVal hashtable As AssociativeArrayHT(Of TKey, TValue), _
ByVal index As Integer, _
ByVal previous As Integer, _
ByVal bucketIndex As Integer)
End Structure
End Class
122 Part I Collection Basics
The m_updateCode field is incremented each time the user modifies the list. The m_updateCode
field is used in Chapter 6 to determine whether the collection has changed while the user is
iterating over it. It is easier to add it to the code now than change the code in Chapter 6.
The comparer that is used for comparing keys is stored in m_comparer. The m_comparer field
is used to check whether a key matches a key stored in the hash table. PRIME_NUMBERS
contains a predefined list of prime numbers that can be used so that they do not have to be
calculated during run time.
The EntryData struct is used to store detailed information about an entry, such as the bucket
it belongs to and the previous bucket. Performance-wise, this helps when the hash table
needs to remove an entry it has found.
The default constructor for the AssociativeArrayHT(T) class allows users to create an empty
associative array.
C#
/// <summary>
/// Initializes a new instance of the AssociativeArrayHT(TKey,TValue) class that is empty.
/// </summary>
public AssociativeArrayHT()
{
m_comparer = EqualityComparer<TKey>.Default;
}
Visual Basic
''' <summary>
''' Initializes a new instance of the AssociativeArrayHT(TKey,TValue) class that is empty.
''' </summary>
Public Sub New()
m_comparer = EqualityComparer(Of TKey).Default
End Sub
The next constructor allows the user to specify the comparer that will be used to do key
comparisons.
C#
/// <summary>
/// Initializes a new instance of the AssociativeArrayHT(TKey,TValue) class
/// that is empty and uses the specified comparer.
/// </summary>
/// <param name="comparer">The comparer to use for the keys.</param>
Chapter 2 Understanding Collections: Associative Arrays 123
public AssociativeArrayHT(IEqualityComparer<TKey> comparer)
{
if (comparer == null)
{
throw new ArgumentNullException("comparer");
}
m_comparer = comparer;
}
Visual Basic
''' <summary>
''' Initializes a new instance of the AssociativeArrayHT(TKey,TValue) class
''' that is empty and uses the specified comparer.
''' </summary>
''' <param name="comparer">The comparer to use for the keys.</param>
Public Sub New(ByVal comparer As IEqualityComparer(Of TKey))
If (comparer Is Nothing) Then
Throw New ArgumentNullException("comparer")
End If
m_comparer = comparer
End Sub
C#
/// <summary>
/// Adds the key value pair to the AssociativeArrayAL(TKey,TValue).
/// </summary>
/// <param name="key">The key to associate with the value.</param>
/// <param name="value">The value to add.</param>
public void Add(TKey key, TValue value)
{
Add(key, value, false);
}
/// <summary>
/// Sets the value at the specified key.
/// </summary>
/// <param name="key">The key to use for finding the value.</param>
/// <returns>The value associated with the specified key.</returns>
public TValue this[TKey key]
{
set
{
Add(key, value, true);
}
}
124 Part I Collection Basics
Visual Basic
''' <summary>
''' Adds the key value pair to the AssociativeArrayHT(TKey,TValue).
''' </summary>
''' <param name="key">The key to associate with the value.</param>
''' <param name="value">The value to add.</param>
Public Sub Add(ByVal key As TKey, ByVal value As TValue)
Add(key, value, False)
End Sub
''' <summary>
''' Gets or sets the value at the specified key.
''' </summary>
''' <param name="key">The key to use for finding the value.</param>
''' <returns>The value associated with the specified key.</returns>
Default Public Property Item(ByVal key As TKey) As TValue
Set(ByVal value As TValue)
Add(key, Value, True)
End Set
End Property
C#
/// <summary>
/// Adds the key value pair to the AssociativeArrayHT(TKey,TValue)
/// </summary>
/// <param name="key">The key to associate with the specified value.</param>
/// <param name="value">The value to add to the AssociativeArrayHT(TKey,TValue).</param>
/// <param name="overwrite">
/// True if the value should be overwritten if it exist, false if an error should be thrown.
/// </param>
void Add(TKey key, TValue value, bool overwrite)
{
EntryData entry = FindKey(key);
if (!entry.IsEmpty)
{
if (!overwrite)
{
throw new InvalidOperationException
("The specified key is already present in the array");
}
else
{
entry.Value = value;
}
return;
}
if (m_unusedCount <= 0)
{
Rehash();
}
Chapter 2 Understanding Collections: Associative Arrays 125
int uHashcode = CalculateHashCode(key);
++m_count;
m_entries[thisEntry].Key = key;
m_entries[thisEntry].Value = value;
m_entries[thisEntry].HashCode = uHashcode;
m_entries[thisEntry].Next = m_buckets[bucketIndex];
m_buckets[bucketIndex] = thisEntry;
++m_updateCode;
}
Visual Basic
''' <summary>
''' Adds the key value pair to the AssociativeArrayHT(TKey,TValue)
''' </summary>
''' <param name="key">The key to associate with the specified value.</param>
''' <param name="value">The value to add to the AssociativeArrayHT(TKey,TValue).</param>
''' <param name="overwrite">
''' True if the value should be overwritten if it exist, false if an error should be thrown.
''' </param>
Sub Add(ByVal key As TKey, ByVal value As TValue, ByVal overwrite As Boolean)
Dim entry As EntryData = FindKey(key)
If (Not entry.IsEmpty) Then
If (Not overwrite) Then
Throw New InvalidOperationException _
("The specified key is already present in the array")
Else
entry.Value = value
End If
Return
End If
' Pop the first unused entry off of the unused bucket
m_nextUnusedEntry = m_entries(m_nextUnusedEntry).Next
m_unusedCount -= 1
m_count += 1
m_entries(thisEntry).Key = key
m_entries(thisEntry).Value = value
m_entries(thisEntry).HashCode = uHashcode
m_entries(thisEntry).Next = m_buckets(bucketIndex)
m_buckets(bucketIndex) = thisEntry
m_updateCode += 1
End Sub
The Add(TKey,TValue,bool) method uses the FindKey method to see whether the key has
already been added. If the key is already present and the user wasn't trying to reassign the
key, an InvalidOperationException will be thrown. If the key exists and the user was trying to
reassign the key, the method reassigns the key and then returns. If there isn’t enough room
to add the key value pair, the method rehashes the table as described in the following code.
The method then adds the key/value pair to the beginning of the list and increments the
update code.
C#
void Rehash()
{
m_capacity = CalculateCapacity(m_capacity + 1);
int unusedStart = 0;
if (oldData != null)
{
// Copy the old bucket to the new one
Array.Copy(oldData, m_entries, oldData.Length);
// Set the next pointer of all unused entries to the next unused entry
for (int i = unusedStart; i < m_capacity - 1; ++i)
{
m_entries[i].Next = i + 1;
}
m_entries[m_capacity - 1].Next = NULL_REFERENCE;
// Set the next of the current entry equal to the first entry in the bucket
m_entries[index].Next = m_buckets[newBucketIndex];
128 Part I Collection Basics
index = nextIndex;
}
}
++m_updateCode;
}
Visual Basic
Private Sub Rehash()
m_capacity = CalculateCapacity(m_capacity + 1)
' Add all of the newly created entry to the unused count
m_unusedCount += (m_capacity - oldData.Length)
Else
m_unusedCount = m_capacity
m_nextUnusedEntry = 0
End If
' Set the next pointer of all unused entries to the next unused entry
For i As Integer = unusedStart To m_capacity - 2
m_entries(i).Next = i + 1
Next
m_entries(m_capacity - 1).Next = NULL_REFERENCE
' Set the next of the current entry equal to the first entry in the bucket
m_entries(index).Next = m_buckets(newBucketIndex)
index = nextIndex
End While
Next
End If
m_updateCode += 1
End Sub
130 Part I Collection Basics
The method uses the CalculateCapacity method defined in the following code to calculate
the next capacity. After the capacity has been calculated, the old buckets and entries are
saved for the rehashing operation later. The new buckets and entries are then created, and
each old entry is traversed using the old buckets so that it can be added to the new hash
table.
C#
/// <summary>
/// Locates the next prime number that should be used for the capacity.
/// </summary>
/// <param name="size">The minimum size needed.</param>
/// <returns>A prime number large enough to hold the specified number of items.</returns>
int CalculateCapacity(int size)
{
for (int i = 0; i < PRIME_NUMBERS.Length; ++i)
{
if (size <= PRIME_NUMBERS[i])
{
return PRIME_NUMBERS[i];
}
}
return int.MaxValue;
}
Visual Basic
''' <summary>
''' Locates the next prime number that should be used for the capacity.
''' </summary>
''' <param name="size">The minimum size needed.</param>
''' <returns>A prime number large enough to hold the specified number of items.</returns>
Private Function CalculateCapacity(ByVal size As Integer) As Integer
For i As Integer = 0 To PRIME_NUMBERS.Length - 1
If (size <= PRIME_NUMBERS(i)) Then
Return PRIME_NUMBERS(i)
End If
Next
Return Integer.MaxValue
End Function
The CalculateCapacity method searches PRIME_NUMBERS for the next prime number that
can hold the specified size.
The Clear method removes all key/value pairs from the collection and is defined as follows.
C#
/// <summary>
/// Removes all items from the AssociativeArrayHT(TKey,TValue).
/// </summary>
public void Clear()
{
// Set each bucket to empty
for (int i = 0; i < m_buckets.Length; ++i)
{
m_buckets[i] = NULL_REFERENCE;
}
m_unusedCount = m_capacity;
m_count = 0;
++m_updateCode;
}
Visual Basic
''' <summary>
''' Removes all items from the AssociativeArrayHT(TKey,TValue).
''' </summary>
Public Sub Clear()
' Set each bucket to empty
For i As Integer = 0 To m_buckets.Length - 1
m_buckets(i) = NULL_REFERENCE
Next
m_unusedCount = m_capacity
m_count = 0
m_updateCode += 1
End Sub
The Clear method empties all buckets by assigning them to NULL_REFERENCE. It then links
all nodes to each other and assigns m_nextUnusedEntry to the first node.
The Remove method removes the key/value pair associated with the specified key and is
defined as follows.
C#
/// <summary>
/// Removes the specified key from the AssociativeArrayHT(TKey,TValue).
/// </summary>
/// <param name="key">The key to remove from the AssociativeArrayHT(TKey,TValue).</param>
/// <returns>True if the key was removed, false otherwise.</returns>
public bool Remove(TKey key)
{
EntryData entry = FindKey(key);
if (entry.IsEmpty)
{
return false;
}
return Remove(entry);
}
Visual Basic
''' <summary>
''' Removes the specified key from the AssociativeArrayHT(TKey,TValue).
''' </summary>
''' <param name="key">The key to remove from the AssociativeArrayHT(TKey,TValue).</param>
''' <returns>True if the key was removed, false otherwise.</returns>
Public Function Remove(ByVal key As TKey) As Boolean
Dim entry As EntryData = FindKey(key)
If (entry.IsEmpty) Then
Return False
End If
Return Remove(entry)
End Function
The Remove(EntryData) and FindKey methods are discussed later in this chapter.
Chapter 2 Understanding Collections: Associative Arrays 133
The RemoveValue method removes the key/value pair that contains the specified value and is
defined as follows.
C#
/// <summary>
/// Removes the first occurrence of the specified value.
/// </summary>
/// <param name="value">The value to remove.</param>
/// <returns>
/// True if the value was removed, false if it wasn't present
/// in the AssociativeArrayHT(TKey,TValue).
/// </returns>
public bool RemoveValue(TValue value)
{
return RemoveValue(value, false);
}
/// <summary>
/// Removes the specified value.
/// </summary>
/// <param name="value">The value to remove.</param>
/// <param name="allOccurrences">
/// True if all occurrences of the value should be removed, false if not.
/// </param>
/// <returns>
/// True if the value was removed, false if it wasn't present
/// in the AssociativeArrayHT(TKey,TValue).
/// </returns>
public bool RemoveValue(TValue value, bool allOccurrences)
{
bool removed = false;
if (!allOccurrences)
{
return removed;
}
entry = FindValue(value);
}
return removed;
}
134 Part I Collection Basics
Visual Basic
''' <summary>
''' Removes the first occurrence of the specified value.
''' </summary>
''' <param name="value">The value to remove.</param>
''' <returns>
''' True if the value was removed, false if it wasn't present
''' in the AssociativeArrayHT(TKey,TValue).
''' </returns>
Public Function RemoveValue(ByVal value As TValue) As Boolean
Return RemoveValue(value, False)
End Function
''' <summary>
''' Removes the specified value.
''' </summary>
''' <param name="value">The value to remove.</param>
''' <param name="allOccurrences">
''' True if all occurrences of the value should be removed, false if not.
''' </param>
''' <returns>
''' True if the value was removed, false if it wasn't present
''' in the AssociativeArrayHT(TKey,TValue).
''' </returns>
Public Function RemoveValue(ByVal value As TValue, ByVal allOccurrences As Boolean) _
As Boolean
Dim removed As Boolean = False
Entry = FindValue(value)
End While
Return removed
End Function
The Remove and RemoveAll methods both use the Remove(EntryData) method, which is
defined as follows.
C#
/// <summary>
/// Removes the entry specified by the user.
/// </summary>
/// <param name="entry">The entry to remove.</param>
/// <returns>True if the entry is removed, false if not.</returns>
bool Remove(EntryData entry)
Chapter 2 Understanding Collections: Associative Arrays 135
{
if (entry.IsEmpty)
{
return false;
}
++m_updateCode;
return true;
}
Visual Basic
''' <summary>
''' Removes the entry associated with the key.
''' </summary>
''' <param name="entry">The entry to remove.</param>
''' <returns>True if the entry is removed, false if not.</returns>
Private Function Remove(ByVal entry As EntryData) As Boolean
If (entry.IsEmpty) Then
Return False
End If
m_buckets(data.BucketIndex) = m_entries(data.Index).Next
End If
' Add the removed entry to the front of the unused bucket.
m_entries(data.Index).Next = m_nextUnusedEntry
m_nextUnusedEntry = data.Index
m_unusedCount += 1
m_count -= 1
m_updateCode += 1
Return True
End Function
The Remove(EntryData) method removes the node from the linked list and fixes the sur-
rounding links. The removed node is then added to the linked list at m_nextUnusedEntry.
C#
/// <summary>
/// Gets the number of items in the AssociativeArrayHT(TKey,TValue).
/// </summary>
public int Count
{
get { return m_count; }
}
Visual Basic
''' <summary>
''' Gets the number of items in the AssociativeArrayHT(TKey,TValue).
''' </summary>
Public ReadOnly Property Count() As Integer
Get
Return m_count
End Get
End Property
The CalculateHashCode method is used to make sure the hash code retrieved from the
comparer is positive.
Chapter 2 Understanding Collections: Associative Arrays 137
C#
int CalculateHashCode(TKey key)
{
// Make sure the hash code is positive.
return m_comparer.GetHashCode(key) & 0x7fffffff;
}
Visual Basic
Private Function CalculateHashCode(ByVal key As TKey) As Integer
' Make sure the hash code is positive.
Return m_comparer.GetHashCode(key) And &H7FFFFFFF
End Function
With the IsEmpty property, users can check whether the collection is empty before doing
some operations.
C#
/// <summary>
/// States if the AssociativeArrayHT(TKey,TValue) is empty.
/// </summary>
public bool IsEmpty
{
get { return m_count <= 0; }
}
Visual Basic
''' <summary>
''' States if the AssociativeArrayHT(TKey,TValue) empty.
''' </summary>
Public ReadOnly Property IsEmpty() As Boolean
Get
Return m_count <= 0
End Get
End Property
Users can also get the list of keys and values in the collection by calling the Keys and Values
properties respectively.
C#
/// <summary>
/// Gets an array of current keys.
/// </summary>
public TKey[] Keys
{
get
{
int index = 0;
TKey[] keys = new TKey[Count];
138 Part I Collection Basics
return keys;
}
}
/// <summary>
/// Gets an array of current values.
/// </summary>
public TValue[] Values
{
get
{
TValue[] values = new TValue[Count];
int index = 0;
return values;
}
}
Visual Basic
''' <summary>
''' Gets an array of current keys.
''' </summary>
Public ReadOnly Property Keys() As TKey()
Get
Dim index As Integer = 0
Dim rkeys As TKey() = New TKey(Count - 1) {}
Return rkeys
End Get
End Property
''' <summary>
''' Gets an array of current values.
''' </summary>
Chapter 2 Understanding Collections: Associative Arrays 139
Public ReadOnly Property Values() As TValue()
Get
Dim rvalues As TValue() = New TValue(Count - 1) {}
Dim index As Integer = 0
Return rvalues
End Get
End Property
Both properties traverse using the MoveNext method described in the following code. This
method provides the users with a snapshot of the current Keys and Values. For now, this is
just a placeholder. In Chapter 6, you learn how to return an enumerator for the Keys and
Values properties that will stay in sync with the collection.
The MoveNext method is used for traversing the collection and is described as follows.
C#
/// <summary>
/// Traverses the entries from the beginning to the end.
/// </summary>
/// <param name="bucketIndex">
/// The index of the bucket to start at or NULL_REFERENCE to start from the beginning.
/// </param>
/// <param name="entryIndex">
/// The index of the entry to start at or NULL_REFERENCE to start from the beginning.
/// </param>
/// <returns>False if the end of the bucket is reached, false otherwise.</returns>
internal bool MoveNext(ref int bucketIndex, ref int entryIndex)
{
if (entryIndex == NULL_REFERENCE)
{
++bucketIndex;
return false;
}
}
// Recursively call this method if we have moved past the end of the current bucket
if (entryIndex == NULL_REFERENCE)
{
return MoveNext(ref bucketIndex, ref entryIndex);
}
}
return true;
}
Visual Basic
''' <summary>
''' Traverses the entries from the beginning to the end.
''' </summary>
''' <param name="bucketIndex">
''' The index of the bucket to start at or NULL_REFERENCE to start from the beginning.
''' </param>
''' <param name="entryIndex">
''' The index of the entry to start at or NULL_REFERENCE to start from the beginning.
''' </param>
''' <returns>False if the end of the bucket is reached, false otherwise.</returns>
Friend Function MoveNext(ByRef bucketIndex As Integer, ByRef entryIndex As Integer) _
As Boolean
If (entryIndex = NULL_REFERENCE) Then
bucketIndex += 1
' Keep checking bucket until we find one that isn't empty.
While (m_buckets(bucketIndex) = NULL_REFERENCE)
' Set the entryIndex to the first entry in the nonempty bucket
entryIndex = m_buckets(bucketIndex)
Chapter 2 Understanding Collections: Associative Arrays 141
Else
entryIndex = m_entries(entryIndex).Next
' Recursively call this method if we have moved past the end of the current bucket
If (entryIndex = NULL_REFERENCE) Then
Return MoveNext(bucketIndex, entryIndex)
End If
End If
Return True
End Function
The MoveNext method traverses the nodes by incrementing the values passed in through
bucketIndex and entryIndex. If both values are NULL_REFERENCE, it is assumed that the caller
is starting from the beginning.
Sometimes users may want to know whether a particular key or value is present in the collec-
tion. The ContainsKey and ContainsValue methods can be used to do so.
C#
/// <summary>
/// Checks to see if the specified key is present in the AssociativeArrayHT(TKey,TValue).
/// </summary>
/// <param name="key">The key to look for.</param>
/// <returns>True if the key was found, false otherwise.</returns>
public bool ContainsKey(TKey key)
{
return !(FindKey(key).IsEmpty);
}
/// <summary>
/// Checks to see if the AssociativeArrayHT(TKey,TValue) contains the specified value.
/// </summary>
/// <param name="value">The value to look for.</param>
/// <returns>True if the value was found, false otherwise.</returns>
public bool ContainsValue(TValue value)
{
return !(FindValue(value).IsEmpty);
}
Visual Basic
''' <summary>
''' Checks to see if the specified key is present in the AssociativeArrayHT(TKey,TValue).
''' </summary>
''' <param name="key">The key to look for.</param>
''' <returns>True if the key was found, false otherwise.</returns>
Public Function ContainsKey(ByVal key As TKey) As Boolean
Return Not (FindKey(key).IsEmpty)
End Function
142 Part I Collection Basics
''' <summary>
''' Checks to see if the AssociativeArrayHT(TKey,TValue) contains the specified value.
''' </summary>
''' <param name="value">The value to look for.</param>
''' <returns>True if the value was found, false otherwise.</returns>
Public Function ContainsValue(ByVal value As TValue) As Boolean
Return Not (FindValue(value).IsEmpty)
End Function
The ContainsValue and ContainsKey methods use the FindValue and FindKey methods to tell
whether an item is in the collection. They are defined as follows.
C#
/// <summary>
/// Finds the entry that contains the specified key.
/// </summary>
/// <param name="key">The key to look for.</param>
/// <returns>The entry that contains the specified key, otherwise EntryData.EMPTY</returns>
EntryData FindKey(TKey key)
{
if (IsEmpty)
{
return EntryData.EMPTY;
}
if (m_comparer.Equals(m_entries[entryIndex].Key, key))
{
return new EntryData(this, entryIndex, previous, bucketIndex);
}
previous = entryIndex;
}
return EntryData.EMPTY;
}
Chapter 2 Understanding Collections: Associative Arrays 143
/// <summary>
/// Finds the entry that contains the specified value.
/// </summary>
/// <param name="value">The value to look for.</param>
/// <returns>
/// The first entry that contains the specified value, otherwise EntryData.EMPTY.
/// </returns>
EntryData FindValue(TValue value)
{
if (IsEmpty)
{
return EntryData.EMPTY;
}
previous = entryIndex;
}
}
return EntryData.EMPTY;
}
Visual Basic
''' <summary>
''' Finds the entry that contains the specified key.
''' </summary>
''' <param name="key">The key to look for.</param>
''' <returns>The entry that contains the specified key, otherwise EntryData.EMPTY</returns>
Private Function FindKey(ByVal key As TKey) As EntryData
If (IsEmpty) Then
Return EntryData.EMPTY
End If
144 Part I Collection Basics
' Calculate which bucket the key belongs in by doing a mod operation
Dim bucketIndex As Integer = HashFunction(uHashcode)
Return EntryData.EMPTY
End Function
''' <summary>
''' Finds the entry that contains the specified value.
''' </summary>
''' <param name="value">The value to look for.</param>
''' <returns>
''' The first entry that contains the specified value, otherwise EntryData.EMPTY.
''' </returns>
Private Function FindValue(ByVal value As TValue) As EntryData
If (IsEmpty) Then
Return EntryData.EMPTY
End If
Return EntryData.EMPTY
End Function
FindValue traverses the collection from the first bucket to the last bucket. The method checks
each node by using a comparer to see whether the specified key or value is present at the
current node.
Users need the ability to retrieve a value from the collection by using a key. The Item prop-
erty allows them to do so.
C#
/// <summary>
/// Gets or sets the value at the specified key.
/// </summary>
/// <param name="key">The key to use for finding the value.</param>
/// <returns>The value associated with the specified key.</returns>
public TValue this[TKey key]
{
get
{
EntryData entry = FindKey(key);
if (entry.IsEmpty)
{
throw new KeyNotFoundException("The specified key couldn't be located");
}
return entry.Value;
}
}
Visual Basic
''' <summary>
''' Gets or sets the value at the specified key.
''' </summary>
''' <param name="key">The key to use for finding the value.</param>
''' <returns>The value associated with the specified key.</returns>
Default Public Property Item(ByVal key As TKey) As TValue
Get
Dim entry As EntryData = FindKey(key)
If (entry.IsEmpty) Then
Throw New KeyNotFoundException("The specified key couldn't be located")
End If
Return entry.Value
End Get
End Property
146 Part I Collection Basics
The Item property uses the FindKey method to locate the node that contains the specified
key. If the node doesn’t exist, the Item property throws an exception. This may not always
be what users want. With the TryGetValue method, users can try to retrieve a value without
throwing an exception if the key doesn’t exist in the collection.
C#
/// <summary>
/// Tries to get the value at the specified key without throwing an exception.
/// </summary>
/// <param name="key">The key to get the value for.</param>
/// <param name="value">The value that is associated with the specified key.</param>
/// <returns>True if the key was found, false otherwise.</returns>
public bool TryGetValue(TKey key, out TValue value)
{
EntryData entry = FindKey(key);
if (!entry.IsEmpty)
{
value = entry.Value;
return true;
}
value = default(TValue);
return false;
}
Visual Basic
''' <summary>
''' Tries to get the value at the specified key without throwing an exception.
''' </summary>
''' <param name="key">The key to get the value for.</param>
''' <param name="value">The value that is associated with the specified key.</param>
''' <returns>True if the key was found, false otherwise.</returns>
Public Function TryGetValue(ByVal key As TKey, ByRef value As TValue) As Boolean
Dim entry As EntryData = FindKey(key)
The application runs in a continuous loop until the user selects the menu option to exit. It
uses the following code in the Lesson3A method.
C#
static void Lesson3A()
{
AssociativeArrayHT<string,string> phoneBook =
new AssociativeArrayHT<string,string>(StringComparer.CurrentCultureIgnoreCase);
string line;
for (; ; )
{
PrintMenu();
line = Console.ReadLine();
line = line.Trim();
if (string.IsNullOrEmpty(line))
{
continue;
}
switch (line[0])
{
case '1':
AddNumber(phoneBook);
break;
case '2':
LookupNumber(phoneBook);
break;
case '3':
Show(phoneBook);
break;
case '4':
return;
}
}
}
148 Part I Collection Basics
Visual Basic
Sub Lesson3A()
Dim phoneBook As AssociativeArrayHT(Of String, String) = _
New AssociativeArrayHT(Of String, String)(StringComparer.CurrentCultureIgnoreCase)
Dim line As String
While (True)
PrintMenu()
line = Console.ReadLine()
line = line.Trim()
If (String.IsNullOrEmpty(line)) Then
Continue While
End If
Select (line(0))
Case "1"c
AddNumber(phoneBook)
Case "2"c
LookupNumber(phoneBook)
Case "3"c
Show(phoneBook)
Case "4"c
Return
End Select
End While
End Sub
The following method is called at the beginning of the loop to display the menu options to
the user.
C#
static void PrintMenu()
{
Console.WriteLine("1. Add Name");
Console.WriteLine("2. Lookup Number");
Console.WriteLine("3. Show Phonebook");
Console.WriteLine("4. Exit");
Console.Write("> ");
}
Visual Basic
Sub PrintMenu()
Console.WriteLine("1. Add Name")
Console.WriteLine("2. Lookup Number")
Console.WriteLine("3. Show Phonebook")
Console.WriteLine("4. Exit")
Console.Write("> ")
End Sub
Chapter 2 Understanding Collections: Associative Arrays 149
Option 1 allows the user to add a person to the phone book by calling the following method.
C#
static void AddNumber(AssociativeArrayHT<string, string> phoneBook)
{
Console.WriteLine("Enter the name to add");
Console.Write("> ");
string name = Console.ReadLine();
if (string.IsNullOrEmpty(name))
{
return;
}
if (string.IsNullOrEmpty(number))
{
return;
}
phoneBook[name] = number;
Visual Basic
Sub AddNumber(ByVal phoneBook As AssociativeArrayHT(Of String, String))
Console.WriteLine("Enter the name to add")
Console.Write("> ")
Dim name As String = Console.ReadLine()
If (String.IsNullOrEmpty(name)) Then
Return
End If
If (String.IsNullOrEmpty(number)) Then
Return
End If
phoneBook(name) = number
Output
1. Add Name
2. Lookup Number
3. Show Phonebook
4. Exit
> 1
Enter the name to add
> Akin, Cigdem
Enter their phone number
> (901) 555-0166
'Akin, Cigdem' phone number is (901) 555-0166
1. Add Name
2. Lookup Number
3. Show Phonebook
4. Exit
> 1
Enter the name to add
> Zimprich, Karin
Enter their phone number
> (901) 555-0177
'Zimprich, Karin' phone number is (901) 555-0177
1. Add Name
2. Lookup Number
3. Show Phonebook
4. Exit
> 1
Enter the name to add
> Zhang, Larry
Enter their phone number
> (901) 555-0133
'Zhang, Larry' phone number is (901) 555-0133
The method asks the user for the name of the person and phone number to associate with
that person by using console input and output. The name is then added or overwritten using
phoneBook[name] = number.
Option 2 allows the user to look up a person in the phone book by calling the following
method.
C#
static void LookupNumber(AssociativeArrayHT<string, string> phoneBook)
{
Console.WriteLine("Enter name to lookup");
Console.Write("> ");
string name = Console.ReadLine();
if (string.IsNullOrEmpty(name))
{
return;
}
Chapter 2 Understanding Collections: Associative Arrays 151
if (phoneBook.ContainsKey(name))
{
Console.WriteLine("'{0}' phone number is {1}", name, phoneBook[name]);
}
else
{
Console.WriteLine("Couldn't find '{0}'", name);
}
}
Visual Basic
Sub LookupNumber(ByVal phoneBook As AssociativeArrayHT(Of String, String))
Console.WriteLine("Enter name to lookup")
Console.Write("> ")
Dim name As String = Console.ReadLine()
If (String.IsNullOrEmpty(name)) Then
Return
End If
If (phoneBook.ContainsKey(name)) Then
Console.WriteLine("'{0}' phone number is {1}", name, phoneBook(name))
Else
Console.WriteLine("Couldn't find '{0}'", name)
End If
End Sub
Output
1. Add Name
2. Lookup Number
3. Show Phonebook
4. Exit
> 2
Enter name to lookup
> Zimprich, Karin
'Zimprich, Karin' phone number is (901) 555-0177
1. Add Name
2. Lookup Number
3. Show Phonebook
4. Exit
> 2
Enter name to lookup
> Zakardissnehf, Kerstin
Couldn't find 'Zakardissnehf, Kerstin'
The method asks for the name of the person the user is looking up by using console input
and output. The name is then checked to see if it is present in the phone book by calling
phoneBook.ContainsKey(name). If the name is present in the phone book, phoneBook[name]
is then used to retrieve it.
152 Part I Collection Basics
Option 3 allows the user to display all numbers in the phone book.
C#
static void Show(AssociativeArrayHT<string, string> phoneBook)
{
string[] names = phoneBook.Keys;
Visual Basic
Sub Show(ByVal phoneBook As AssociativeArrayHT(Of String, String))
Dim names As String() = phoneBook.Keys
Output
1. Add Name
2. Lookup Number
3. Show Phonebook
4. Exit
> 3
'Zhang, Larry' phone number is (901) 555-0133
'Zimprich, Karin' phone number is (901) 555-0177
'Akin, Cigdem' phone number is (901) 555-0166
Names are displayed by retrieving an array of keys from the phone book. Each key is then
traversed and displayed on the screen along with its value.
Summary
In this chapter, you learned how to use and implement associative arrays. You also learned
that associative arrays have many different names among developers and different program-
ming languages. They might be called maps, dictionaries, hash tables, and so on in different
languages. Whatever the language may call them, associative arrays are useful when you
have data that needs to be accessed with a key. Different implementations give you different
performance gains and benefits.
Chapter 3
Understanding Collections: Queues,
Stacks, and Circular Buffers
After completing this chapter, you will be able to
■ Identify queues, stacks, and circular buffers.
■ Design and implement queues, stacks, and circular buffers.
■ Understand when and when not to use queues, stacks, and circular buffers.
Queue Overview
A queue is a First In, First Out (FIFO) collection. A FIFO is a collection that resembles people
waiting in a straight line. The first person to enter the line is the first person to leave the line.
When the first people leave the line, they have been dequeued, or popped off the front of the
queue, and as additional people join the line, they are enqueued, or pushed onto the back of
the queue.
Uses of Queues
Queues are normally used when items need to be processed in the order they are received.
For example, say you have a thread that reads an ordered list of instructions from a file and
another thread that processes those instructions. The reader thread needs to hold the infor-
mation that it reads until the processing thread processes the items in the order they were
read. A queue is the perfect collection for holding the data. The reader thread pushes the
information onto the queue and the processing thread pops it off the queue.
Queue Implementation
A queue may be implemented using a linked list or an array for the internal data storage.
For information about when to use either a linked list or an array, see the advantages and
disadvantages sections for each type in Chapter 1, “Understanding Collections: Arrays and
Linked Lists.”
153
154 Part I Collection Basics
Note The Microsoft .NET Framework contains a class called Queue(T) that can be used for
queues. The implementation uses an array for its internal data. The following code shows you
how to implement from scratch a queue that uses an array, and later, one that uses a linked list
for its internal storage.
C#
[DebuggerDisplay("Count={Count}")]
[DebuggerTypeProxy(typeof(ArrayDebugView))]
public class QueuedArray<T>
{
// Fields
private const int GROW_BY = 10;
private int m_count;
private T[] m_data;
private int m_head;
private int m_tail;
private int m_updateCode;
// Constructors
public QueuedArray();
public QueuedArray(IEnumerable<T> items);
public QueuedArray(int capacity);
// Methods
public void Clear();
public bool Contains(T item);
private void Initialize(int capacity);
public T Peek();
public T Pop();
public void Push(T item);
public T[] ToArray();
// Properties
public int Capacity { get; set; }
public int Count { get; }
public bool IsEmpty { get; }
}
Visual Basic
<DebuggerTypeProxy(GetType(ArrayDebugView)),
<DebuggerDisplay("Count={Count}")> _
Public Class QueuedArray(Of T)
' Fields
Private Const GROW_BY As Integer = 10
Private m_count As Integer
Private m_data As T()
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 155
Private m_head As Integer
Private m_tail As Integer
Private m_updateCode as Integer
' Constructors
Public Sub New()
Public Sub New(ByVal items As IEnumerable(Of T))
Public Sub New(ByVal capacity As Integer)
‘ Methods
Public Sub Clear()
Public Function Contains(ByVal item As T) As Boolean
Public Sub Initialize(ByVal capacity As Integer)
Public Function Peek() As T
Public Function Pop() As T
Public Sub Push(ByVal item As T)
Public Function ToArray() As T()
' Properties
Public Property Capacity As Integer
Public ReadOnly Property Count As Integer
Public ReadOnly Property IsEmpty As Boolean
End Class
The m_data field is used as our internal data storage. The internal data storage is implement-
ed as an array.
The m_updateCode field is incremented each time the user modifies the list. The m_updateCode
field is used in Chapter 6, “.NET Collection Interfaces,” to determine whether the collection
has changed while the user is iterating over it. It is easier to add it to the code now instead of
changing the code in Chapter 6.
Creating Constructors
The QueuedArray(T) class contains three constructors. One constructor is for creating an
empty class, the next one is for creating a class with default values, and the last one is used
to create a queue with a specified internal array size. These constructors call the Initialize
method to create the internal data storage object.
C#
void Initialize(int capacity)
{
m_data = new T[capacity];
}
Visual Basic
Sub Initialize(ByVal capacity As Integer)
m_data = New T(capacity - 1) {}
End Sub
156 Part I Collection Basics
C#
/// <summary>
/// Initializes a new instance of the QueuedArray(T) class.
/// </summary>
public QueuedArray()
{
Initialize(GROW_BY);
}
Visual Basic
''' <summary>
''' Initializes a new instance of the QueuedArray(T) class.
''' </summary>
Public Sub New()
Initialize(GROW_BY)
End Sub
The following constructor creates a queue and adds the specified items to the end of the
queue.
C#
/// <summary>
/// Initializes a new instance of the QueuedArray(T) class.
/// </summary>
public QueuedArray(IEnumerable<T> items)
{
Initialize(GROW_BY);
foreach (T item in items)
{
Push(item);
}
}
Visual Basic
''' <summary>
''' Initializes a new instance of the QueuedArray(T) class.
''' </summary>
Public Sub New(ByVal items As IEnumerable(Of T))
Initialize(GROW_BY)
For Each item As T In items
Push(item)
Next
End Sub
The items are added to the queue by traversing the specified items and adding them to the
queue.
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 157
The next constructor allows users to specify the initial size of the internal array.
C#
/// <summary>
/// Initializes a new instance of the QueuedArray(T) class
/// that is empty and has the specified initial capacity.
/// </summary>
/// <param name="capacity">
/// The number of elements that the new array can initially store.
/// </param>
public QueuedArray(int capacity)
{
Initialize(capacity);
}
Visual Basic
''' <summary>
''' Initializes a new instance of the QueuedArray(T) class
''' that is empty and has the specified initial capacity.
''' </summary>
''' <param name="capacity">
''' The number of elements that the new array can initially store.
''' </param>
Public Sub New(ByVal capacity As Integer)
Initialize(capacity)
End Sub
C#
/// <summary>
/// Adds the item to the end of the QueuedArray(T).
/// </summary>
/// <param name="item">The item to add to the end of the QueuedArray(T).</param>
public void Push(T item)
{
if (m_count >= m_data.Length)
{
Capacity += GROW_BY;
}
m_data[m_tail] = item;
m_tail = (m_tail + 1) % m_data.Length;
++m_count;
++m_updateCode;
}
158 Part I Collection Basics
Visual Basic
''' <summary>
''' Adds the item to the end of the QueuedArray(T).
''' </summary>
''' <param name="item">The item to add to the end of the QueuedArray(T).</param>
Public Sub Push(ByVal item As T)
If (m_count >= m_data.Length) Then
Capacity += GROW_BY
End If
m_data(m_tail) = item
m_tail = (m_tail + 1) Mod m_data.Length
m_count += 1
m_updateCode += 1
End Sub
The Push method adds the specified item to the end of the queue.
C#
/// <summary>
/// Removes the first item from the QueuedArray(T).
/// </summary>
/// <returns></returns>
public T Pop()
{
if (IsEmpty)
{
throw new InvalidOperationException("You cannot pop from an empty queue.");
}
T retval = m_data[m_head];
--m_count;
++m_updateCode;
return retval;
}
Visual Basic
''' <summary>
''' Removes the first item from the QueuedArray(T).
''' </summary>
''' <returns></returns>
Public Function Pop() As T
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 159
If (IsEmpty) Then
Throw New InvalidOperationException("You cannot pop from an empty queue.")
End If
m_count -= 1
m_updateCode += 1
Return retval
End Function
The Pop method removes the first item from the queue and returns it to the user.
It is also useful to remove all items from the queue without repeatedly calling the Pop method.
This is accomplished by calling the following method.
C#
/// <summary>
/// Removes all items from the QueuedArray(T).
/// </summary>
public void Clear()
{
m_count = 0;
m_head = 0;
m_tail = 0;
++m_updateCode;
}
Visual Basic
''' <summary>
''' Removes all items from the QueuedArray(T).
''' </summary>
Public Sub Clear()
m_count = 0
m_head = 0
m_tail = 0
m_updateCode += 1
End Sub
The Contains method can be used to check to see whether an item is present in the queue.
This may be useful if you are waiting for something to appear in the queue before you start
popping things off it.
160 Part I Collection Basics
C#
/// <summary>
/// Checks if the specified data is present in the QueuedLinkedList(T).
/// </summary>
/// <param name="data">The data to look for.</param>
/// <returns>True if the data is found, false otherwise.</returns>
public bool Contains(T item)
{
EqualityComparer<T> comparer = EqualityComparer<T>.Default;
for (int i = 0; i < m_count; i++)
{
if (comparer.Equals(m_data[i], item))
{
return true;
}
}
return false;
}
Visual Basic
''' <summary>
''' Checks if the specified data is present in the QueuedLinkedList(T).
''' </summary>
''' <param name="item">The data to look for.</param>
''' <returns>True if the data is found, false otherwise.</returns>
Public Function Contains(ByVal item As T) As Boolean
Dim comparer As EqualityComparer(Of T) = EqualityComparer(Of T).Default
For i As Integer = 0 To m_count - 1
If (comparer.Equals(m_data(i), item)) Then
Return True
End If
Next
Return False
End Function
The Contains method walks each item in the queue and uses a comparer to see whether the
items are equal.
Users may want to see what the first item in the queue is without actually removing it from
the queue. With the following method, they can do that by looking at the Head element of
the internal data storage.
C#
/// <summary>
/// Gets the first item in the QueuedArray(T) without removing it.
/// </summary>
/// <returns>The item at the front of the QueuedArray(T)</returns>
public T Peek()
{
if (IsEmpty)
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 161
{
throw new InvalidOperationException("You cannot peek at an empty queue.");
}
return m_data[m_head];
}
Visual Basic
''' <summary>
''' Gets the first item in the QueuedArray(T) without removing it.
''' </summary>
''' <returns>The item at the front of the QueuedArray(T)</returns>
Public Function Peek() As T
If (IsEmpty) Then
Throw New InvalidOperationException("You cannot peek at an empty queue.")
End If
Return m_data(m_head)
End Function
For some operations, the user needs to know whether the queue is empty. The following
method provides this information.
C#
/// <summary>
/// States if the QueuedArray(T) is empty.
/// </summary>
public bool IsEmpty
{
get { return m_count == 0; }
}
Visual Basic
''' <summary>
''' States if the QueuedArray(T) is empty.
''' </summary>
Public ReadOnly Property IsEmpty() As Boolean
Get
Return m_count = 0
End Get
End Property
The following property can be used to get the number of items in the queue.
C#
/// <summary>
/// Gets the number of elements actually contained in the QueuedArray(T).
/// </summary>
public int Count
{
get { return m_count; }
}
162 Part I Collection Basics
Visual Basic
''' <summary>
''' Gets the number of elements actually contained in the QueuedArray(T).
''' </summary>
Public ReadOnly Property Count() As Integer
Get
Return m_count
End Get
End Property
Finally, the following helper function copies the contents of the internal array into a new
array. The new array is useful when code requires a basic array rather than an instance of the
QueueArray(T) class.
C#
/// <summary>
/// Copies the elements of the QueuedArray(T) to a new array.
/// </summary>
/// <returns>An array containing copies of the elements of the QueuedArray(T).</returns>
public T[] ToArray()
{
T[] retval = new T[m_count];
if (IsEmpty)
{
return retval;
}
return retval;
}
Visual Basic
''' <summary>
''' Copies the elements of the QueuedArray(T) to a new array.
''' </summary>
''' <returns>An array containing copies of the elements of the QueuedArray(T).</returns>
Public Function ToArray() As T()
Dim retval As New T(m_count - 1) {}
If (IsEmpty) Then
Return retval
End If
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 163
If (m_head < m_tail) Then
Array.Copy(m_data, m_head, retval, 0, m_count)
Else
Array.Copy(m_data, m_head, retval, 0, m_data.Length - m_head)
Array.Copy(m_data, 0, retval, m_data.Length - m_head, m_tail)
End If
Return retval
End Function
C#
[DebuggerDisplay("Count={Count}")]
[DebuggerTypeProxy(typeof(ArrayDebugView))]
public class QueuedLinkedList<T>
{
// Fields
private DoubleLinkedList<T> m_data;
// Constructors
public QueuedLinkedList();
public QueuedLinkedList(IEnumerable<T> items);
// Methods
public void Clear();
public bool Contains(T item);
public T Peek();
public T Pop();
public void Push(T item);
public T[] ToArray();
// Properties
public int Count { get; }
public bool IsEmpty { get; }
}
Visual Basic
<DebuggerTypeProxy(GetType(ArrayDebugView)),
<DebuggerDisplay("Count={Count}")> _
Public Class QueuedLinkedList(Of T)
' Fields
Private m_data As DoubleLinkedList(Of T)
' Constructors
Public Sub New()
Public Sub New(ByVal items As IEnumerable(Of T))
164 Part I Collection Basics
' Methods
Public Sub Clear()
Public Function Contains(ByVal item As T) As Boolean
Public Function Peek() As T
Public Function Pop() As T
Public Sub Push(ByVal item As T)
Public Function ToArray() As T()
' Properties
Public ReadOnly Property Count As Integer
Public ReadOnly Property IsEmpty As Boolean
End Class
The m_data field is used as the internal data storage. The internal data storage is imple-
mented using the doubly linked list that was defined in Chapter 1. Each time the user calls
the Push method, you pass the specified item to the AddtoEnd method of the linked list. The
doubly and singly linked lists you implemented in Chapter 1 store the tail in the field m_tail.
So in this type of scenario, the doubly and singly linked lists perform the same except for the
additional memory that the doubly linked list stores for the previous reference. You could
just as easily make the internal storage a singly linked list if you like.
The m_updateCode from m_data is used in Chapter 6 to determine whether the collection
has changed while the user is iterating over it.
Creating Constructors
The QueuedArray(T) class contains two constructors. One constructor is for creating an empty
class and the other is for creating a class with default values.
C#
/// <summary>
/// Initializes a new instance of the QueuedLinkedList(T) class.
/// </summary>
public QueuedLinkedList()
{
m_data = new DoubleLinkedList<T>();
}
Visual Basic
''' <summary>
''' Initializes a new instance of the QueuedLinkedList(T) class.
''' </summary>
Public Sub New()
m_data = New DoubleLinkedList(Of T)()
End Sub
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 165
The following constructor creates a queue and adds the specified items to the end of the
queue.
C#
/// <summary>
/// Initializes a new instance of the QueuedLinkedList(T) class.
/// </summary>
public QueuedLinkedList(IEnumerable<T> items)
{
m_data = new DoubleLinkedList<T>(items);
}
Visual Basic
''' <summary>
''' Initializes a new instance of the QueuedLinkedList(T) class.
''' </summary>
Public Sub New(ByVal items As IEnumerable(Of T))
m_data = New DoubleLinkedList(Of T)(items)
End Sub
The items are added to the queue by creating the internal linked list with the specified items.
C#
/// <summary>
/// Adds the item to the end of the QueuedLinkedList(T).
/// </summary>
/// <param name="item">The item to add to the end of the QueuedLinkedList(T).</param>
public void Push(T item)
{
m_data.AddToEnd(item);
}
Visual Basic
''' <summary>
''' Adds the item to the end of the QueuedLinkedList(T).
''' </summary>
''' <param name="item">The item to add to the end of the QueuedLinkedList(T).</param>
Public Sub Push(ByVal item As T)
m_data.AddToEnd(item)
End Sub
The Push method adds the specified item to the end of the queue.
166 Part I Collection Basics
C#
/// <summary>
/// Removes the first item from the QueuedLinkedList(T).
/// </summary>
/// <returns></returns>
public T Pop()
{
if (IsEmpty)
{
throw new InvalidOperationException("You cannot pop from an empty queue.");
}
T retval = m_data.Head.Data;
m_data.Remove(m_data.Head);
return retval;
}
Visual Basic
''' <summary>
''' Removes the first item from the QueuedLinkedList(T).
''' </summary>
''' <returns></returns>
Public Function Pop() As T
If (IsEmpty) Then
Throw New InvalidOperationException("You cannot pop from an empty queue.")
End If
m_data.Remove(m_data.Head)
Return retval
End Function
The Pop method removes the first item from the queue and returns it to the user.
It is also useful to remove all items from the queue without repeatedly calling the Pop
method. This can be accomplished by calling the following method.
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 167
C#
/// <summary>
/// Removes all items from the QueuedLinkedList(T).
/// </summary>
public void Clear()
{
m_data.Clear();
}
Visual Basic
''' <summary>
''' Removes all items from the QueuedLinkedList(T).
''' </summary>
Public Sub Clear()
m_data.Clear()
End Sub
The Clear method calls the Clear method of the internal data storage.
The Contains method can be used to check to see whether an item is present in the queue.
This may be useful if you are waiting for something to appear in the queue before you start
popping things off it.
C#
/// <summary>
/// Checks if the specified data is present in the QueuedLinkedList(T).
/// </summary>
/// <param name="data">The data to look for.</param>
/// <returns>True if the data is found, false otherwise.</returns>
public bool Contains(T item)
{
return m_data.Contains(item);
}
Visual Basic
''' <summary>
''' Checks if the specified data is present in the QueuedLinkedList(T).
''' </summary>
''' <param name="item">The data to look for.</param>
''' <returns>True if the data is found, false otherwise.</returns>
Public Function Contains(ByVal item As T) As Boolean
Return m_data.Contains(item)
End Function
168 Part I Collection Basics
Users may want to see what the first item in a queue is without actually removing it. The
following method allows them to do that by looking at the Head element of the internal data
storage.
C#
/// <summary>
/// Gets the first item in the QueuedLinkedList(T) without removing it.
/// </summary>
/// <returns>The item at the front of the QueuedLinkedList(T)</returns>
public T Peek()
{
if (IsEmpty)
{
throw new InvalidOperationException("You cannot peek at an empty queue.");
}
return m_data.Head.Data;
}
Visual Basic
''' <summary>
''' Gets the first item in the QueuedLinkedList(T) without removing it.
''' </summary>
''' <returns>The item at the front of the QueuedLinkedList(T)</returns>
Public Function Peek() As T
If (IsEmpty) Then
Throw New InvalidOperationException("You cannot peek at an empty queue.")
End If
Return m_data.Head.Data
End Function
For some operations, the user needs to know whether the queue is empty. The following
property provides this information.
C#
/// <summary>
/// States if the QueuedLinkedList(T) is empty.
/// </summary>
public bool IsEmpty
{
get { return m_data.IsEmpty; }
}
Visual Basic
''' <summary>
''' States if the QueuedLinkedList(T) is empty.
''' </summary>
Public ReadOnly Property IsEmpty() As Boolean
Get
Return m_data.IsEmpty
End Get
End Property
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 169
The following property can be used to get the number of items in the queue.
C#
/// <summary>
/// Gets the number of elements actually contained in the QueuedLinkedList(T).
/// </summary>
public int Count
{
get { return m_data.Count; }
}
Visual Basic
''' <summary>
''' Gets the number of elements actually contained in the QueuedLinkedList(T).
''' </summary>
Public ReadOnly Property Count() As Integer
Get
Return m_data.Count
End Get
End Property
Finally, the following helper function copies the contents of the internal linked list into a new
array. The new array is useful when code requires a basic array rather than an instance of the
QueuedLinkedList(T) class.
C#
/// <summary>
/// Copies the elements of the QueuedLinkedList(T) to a new array.
/// </summary>
/// <returns>
/// An array containing copies of the elements of the QueuedLinkedList(T).
/// </returns>
public T[] ToArray()
{
return m_data.ToArray();
}
Visual Basic
''' <summary>
''' Copies the elements of the QueuedLinkedList(T) to a new array.
''' </summary>
''' <returns>
''' An array containing copies of the elements of the QueuedLinkedList(T).
''' </returns>
Public Function ToArray() As T()
Return m_data.ToArray()
End Function
170 Part I Collection Basics
More Info For the complete code, refer to the Lesson4A method in the Samples\Chapter 3\CS\
Driver\Program.cs file for Microsoft Visual C# or in the Samples\Chapter 3\VB\Driver\Module1.vb
file for Microsoft Visual Basic.
You can begin with the variable declarations and the clearing of the console.
C#
Request nowPlaying = null;
QueueArray<Request> requests = new QueueArray<Request>();
TimeSpan nextCallIn = TimeSpan.Zero;
TimeSpan currentTime = TimeSpan.Zero;
TimeSpan step = TimeSpan.FromSeconds(1);
Random rnd = new Random();
double playRate = 100;
int nextRow = 0;
int playRow = Console.WindowHeight - 2;
ArrayEx<Song> availableSongs = new ArrayEx<Song>(20);
Console.Clear();
Visual Basic
Dim nowPlaying As Request = Nothing
Dim requests As Queue(Of Request) = New Queue(Of Request)()
Dim nextCallIn As TimeSpan = TimeSpan.Zero
Dim currentTime As TimeSpan = TimeSpan.Zero
Dim [step] As TimeSpan = TimeSpan.FromSeconds(1)
Dim rnd As Random = New Random()
Dim playRate As Double = 100
Dim nextRow As Integer = 0
Dim playRow As Integer = Console.WindowHeight - 2
Dim availableSongs As ArrayEx(Of Song) = New ArrayEx(Of Song)(20)
Console.Clear()
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 171
C#
// Create a random list of songs
for (int i = 0; i < 20; ++i)
{
availableSongs.Add(new Song()
{
Name = string.Format("Song #{0}", i + 1),
// Each song is from 3 minutes to 4 minutes
Duration = TimeSpan.FromSeconds(3 * 60 + rnd.Next(121))
});
}
Visual Basic
' Create a random list of songs
For i As Integer = 0 To 19
availableSongs.Add(New Song() With _
{ _
.Name = String.Format("Song #{0}", i + 1), _
.Duration = TimeSpan.FromSeconds(3 * 60 + rnd.Next(120)) _
})
Next
Next, a separator is drawn on the screen to separate the request from the now playing
section.
C#
WriteSeperator(Console.WindowHeight - 3);
Visual Basic
WriteSeperator(Console.WindowHeight - 3)
The code then enters a loop until you press a key on the keyboard.
C#
while (!Console.KeyAvailable)
{
if (nowPlaying != null)
{
// Check to see if the current request is finished
if (currentTime >= nowPlaying.FinishTime)
{
nowPlaying = null;
}
}
else
{
// Check to see if there are any requests waiting
if (requests.Count > 0)
172 Part I Collection Basics
{
nowPlaying = requests.Pop();
nowPlaying.StartTime = currentTime;
}
}
if (nowPlaying != null)
{
WriteCurrentPlaying(nowPlaying, playRow, currentTime);
}
if (playRow == Console.BufferHeight - 2)
{
// Shifts all of the text up
AdvanceLine();
++nextRow;
}
WriteSeperator(playRow - 1);
}
currentTime += step;
}
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 173
Visual Basic
While (Not Console.KeyAvailable)
If (nowPlaying IsNot Nothing) Then
' Check to see if the current request is finished
If (currentTime >= nowPlaying.FinishTime) Then
nowPlaying = Nothing
End If
Else
' Check to see if there are any requests waiting
If (requests.Count > 0) Then
nowPlaying = requests.Pop()
nowPlaying.StartTime = currentTime
End If
End If
WriteNextRequest(request, nextRow)
nextRow += 1
End If
WriteSeperator(playRow - 1)
End If
currentTime += [step]
End While
174 Part I Collection Basics
Output
Song #2: User request at 00:00:00
Song #8: User request at 00:04:48
Song #10: User request at 00:09:03
Song #7: User request at 00:11:56
Song #18: User request at 00:16:11
Song #11: User request at 00:18:30
Song #7: User request at 00:21:02
Song #16: User request at 00:24:49
Song #2: User request at 00:25:30
Song #12: User request at 00:27:55
Song #12: User request at 00:28:57
Song #18: User request at 00:32:07
Song #16: User request at 00:34:16
Song #18: User request at 00:35:46
Song #12: User request at 00:40:35
Song #6: User request at 00:41:31
-------------------------------------------------------------------------------
Song #12: Playing -00:04:06
\........
The loop checks to see whether a song is currently playing. If a song is playing, the code
checks to see whether it has ended; otherwise, the code checks to see whether another song
is available to play by running the following.
C#
if (requests.Count > 0)
{
nowPlaying = requests.Dequeue();
nowPlaying.StartTime = currentTime;
}
Visual Basic
If (requests.Count > 0) Then
nowPlaying = requests.Dequeue()
nowPlaying.StartTime = currentTime
End If
The code removes the next available request from requests if it contains any items. The code
then checks to see whether someone has called in. If so, the code adds a new request to the
queue by doing the following.
C#
requests.Enqueue(request);
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 175
Visual Basic
requests.Enqueue(request)
The code then randomly picks the next call-in time, pauses for a period of time, and then
restarts the loop.
Stack Overview
A stack is a Last In, First Out (LIFO) collection. A LIFO is a collection that resembles a stack of
plates. The last plate added is the first plate that is removed.
Uses of Stacks
A stack is normally used when you want to process first the last item received. Stacks can be
used for variables in a function. When a function is called, each local variable is pushed onto
the stack. After the function returns, each variable is popped off the stack, leaving the variables
for the current function at the top of the stack. They are useful in some types of parsing. If
you are interested in seeing a parsing example, take a look at Reverse Polish notation (RPN).
Stack Implementation
A stack can be implemented using a linked list or an array for the internal data storage. For
information about when to use either a linked list or an array, see the advantages and disad-
vantages sections for each type in Chapter 1.
Note The .NET Framework contains a class called Stack(T) that can be used for stacks. The im-
plementation uses an array for its internal data. The following code shows you how to implement
from scratch a stack that uses an array and one that uses a linked list for its internal storage.
176 Part I Collection Basics
C#
/// <summary>
/// States if the StackedArray(T) is empty.
/// </summary>
public bool IsEmpty
{
get { return m_data.IsEmpty; }
}
/// <summary>
/// Gets the number of elements in the StackedArray(T).
/// </summary>
public int Count
{
get { return m_data.Count; }
}
/// <summary>
/// Removes all items from the StackedArray(T).
/// </summary>
public void Clear()
{
m_data.Clear();
}
/// <summary>
/// Checks if the specified data is present in the StackedArray(T).
/// </summary>
/// <param name="data">The data to look for.</param>
/// <returns>True if the data is found, false otherwise.</returns>
public bool Contains(T item)
{
return m_data.Contains(item);
}
Visual Basic
''' <summary>
''' States if the StackedArray(T) is empty.
''' </summary>
Public ReadOnly Property IsEmpty()
Get
Return m_data.IsEmpty
End Get
End Property
''' <summary>
''' Gets the number of elements in the StackedArray(T).
''' </summary>
Public ReadOnly Property Count() As Integer
Get
Return m_data.Count
End Get
End Property
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 177
''' <summary>
''' Removes all items from the StackedArray(T).
''' </summary>
Public Sub Clear()
m_data.Clear()
End Sub
''' <summary>
''' Checks if the specified data is present in the StackedArray(T).
''' </summary>
''' <param name="item">The data to look for.</param>
''' <returns>True if the data is found, false otherwise.</returns>
Public Function Contains(ByVal item As T) As Boolean
Return m_data.Contains(item)
End Function
C#
[DebuggerDisplay("Count={Count}")]
[DebuggerTypeProxy(typeof(ArrayDebugView))]
public class StackedArray<T>
{
// Fields
private ArrayEx<T> m_data;
// Constructors
public StackedArray();
public StackedArray(IEnumerable<T> items);
// Methods
public void Clear();
public bool Contains(T item);
public T Peek();
public T Pop();
public void Push(T item);
public T[] ToArray();
// Properties
public int Count { get; }
public bool IsEmpty { get; }
}
Visual Basic
<DebuggerDisplay("Count={Count}")> _
<DebuggerTypeProxy(GetType(ArrayDebugView))> _
Public Class StackedArray(Of T)
' Fields
Private m_data As ArrayEx(Of T)
178 Part I Collection Basics
' Constructors
Public Sub New()
Public Sub New(ByVal items As IEnumerable(Of T))
' Methods
Public Sub Clear()
Public Function Contains(ByVal item As T) As Boolean
Public Function Peek() As T
Public Function Pop() As T
Public Sub Push(ByVal item As T)
Public Function ToArray() As T()
' Properties
Public ReadOnly Property Count As Integer
Public ReadOnly Property IsEmpty As Object
End Class
The m_data field is used for the internal data storage and implemented as an ArrayEx(T)
class.
The m_updateCode from m_data is used in Chapter 6 to determine whether the collection
has changed while the user is iterating over it.
Creating Constructors
The StackedArray(T) class contains two constructors. One constructor is for creating an
empty class and the other is for creating a class with default values.
C#
/// <summary>
/// Initializes a new instance of the StackedArray(T) class that is empty.
/// </summary>
public StackedArray()
{
m_data = new ArrayEx<T>();
}
Visual Basic
''' <summary>
''' Initializes a new instance of the StackedArray(T) class that is empty.
''' </summary>
Public Sub New()
m_data = New ArrayEx(Of T)()
End Sub
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 179
The following constructor creates a stack and adds the specified items to the stack.
C#
/// <summary>
/// Initializes a new instance of the StackedArray(T) class with the specified items.
/// </summary>
/// <param name=”items”>The items to add to the stack.</param>
public StackedArray(IEnumerable<T> items)
{
m_data = new ArrayEx<T>(items);
}
Visual Basic
''' <summary>
''' Initializes a new instance of the StackedArray(T) class with the specified items.
''' </summary>
''' <param name="items">The items to add to the stack.</param>
Public Sub New(ByVal items As IEnumerable(Of T))
m_data = New ArrayEx(Of T)(items)
End Sub
The items are added to the stack by creating the internal data storage with the specified
items.
C#
/// <summary>
/// Adds the item to the top of the StackedArray(T).
/// </summary>
/// <param name="item">The item to add to the top of the StackedArray(T).</param>
public void Push(T item)
{
m_data.Add(item);
}
Visual Basic
''' <summary>
''' Adds the item to the top of the StackedArray(T).
''' </summary>
''' <param name="item">The item to add to the top of the StackedArray(T).</param>
Public Sub Push(ByVal item As T)
m_data.Add(item)
End Sub
The method adds the specified item to the end of the array.
180 Part I Collection Basics
C#
/// <summary>
/// Removes the an item from the top of the StackedArray(T).
/// </summary>
/// <returns>The item at the top of the StackedArray(T).</returns>
public T Pop()
{
if (IsEmpty)
{
throw new InvalidOperationException("You cannot pop from an empty stack.");
}
m_data.RemoveAt(m_data.Count - 1);
return retval;
}
Visual Basic
''' <summary>
''' Removes the an item from the top of the StackedArray(T).
''' </summary>
''' <returns>The item at the top of the StackedArray(T).</returns>
Public Function Pop() As T
If (IsEmpty) Then
Throw New InvalidOperationException("You cannot pop from an empty stack.")
End If
m_data.RemoveAt(m_data.Count - 1)
Return retval
End Function
C#
/// <summary>
/// Gets the item at the top of the StackedArray(T) without removing it.
/// </summary>
/// <returns>
/// The item at the top of the StackedArray(T)
/// </returns>
public T Peek()
{
if (IsEmpty)
{
throw new InvalidOperationException("You cannot peek at an empty stack.");
}
Visual Basic
''' <summary>
''' Gets the item at the top of the StackedArray(T) without removing it.
''' </summary>
''' <returns>The item at the top of the StackedArray(T)</returns>
Public Function Peek() As T
If (IsEmpty) Then
Throw New InvalidOperationException("You cannot peek at an empty stack.")
End If
Return m_data(m_data.Count - 1)
End Function
The Peek method returns the last item added to the stack.
The next method returns an array that contains all of the values in the stack. The items are
added to the array in LIFO order. That is, the first item in the array would be the last one
added to the stack.
C#
/// <summary>
/// Copies the elements of the StackedArray(T) to a new array.
/// </summary>
/// <returns>
/// An array containing copies of the elements of the StackedArray(T).
/// The data in the array will be in LIFO order.
/// </returns>
public T[] ToArray()
{
T[] tmp = new T[Count];
{
tmp[i] = m_data[Count - i - 1];
}
return tmp;
}
Visual Basic
''' <summary>
''' Copies the elements of the StackedArray(T) to a new array.
''' </summary>
''' <returns>
''' An array containing copies of the elements of the StackedArray(T).
''' The data in the array will be in LIFO order.
''' </returns>
Public Function ToArray() As T()
Dim tmp As T() = New T(Count - 1) {}
Return tmp
End Function
C#
[DebuggerDisplay("Count={Count}")]
[DebuggerTypeProxy(typeof(ArrayDebugView))]
public class StackedLinkedList<T>
{
// Fields
private DoubleLinkedList<T> m_data;
// Methods
public StackedLinkedList();
public StackedLinkedList(IEnumerable<T> items);
public void Clear();
public bool Contains(T item);
public T Peek();
public T Pop();
public void Push(T item);
public T[] ToArray();
// Properties
public int Count { get; }
public bool IsEmpty { get; }
}
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 183
Visual Basic
<DebuggerDisplay("Count={Count}"), DebuggerTypeProxy(GetType(ArrayDebugView))> _
Public Class StackedLinkedList(Of T)
' Fields
Private m_data As DoubleLinkedList(Of T)
' Constructors
Public Sub New()
Public Sub New(ByVal items As IEnumerable(Of T))
' Methods
Public Sub Clear()
Public Function Contains(ByVal item As T) As Boolean
Public Function Peek() As T
Public Function Pop() As T
Public Sub Push(ByVal item As T)
Public Function ToArray() As T()
' Properties
Public ReadOnly Property Count As Integer
Public ReadOnly Property IsEmpty As Boolean
End Class
The m_data field is used for the internal data storage and is implemented as a
DoubleLinkedList(T).
The m_updateCode from m_data is used in Chapter 6 to determine whether the collection
has changed while the user is iterating over it.
Creating Constructors
The StackedLinkedList(T) class contains two constructors. One constructor is for creating an
empty class and the other is for creating a class with default values.
C#
/// <summary>
/// Initializes a new instance of the StackedLinkedList(T) class that is empty.
/// </summary>
public StackedLinkedList()
{
m_data = new DoubleLinkedList<T>();
}
Visual Basic
''' <summary>
''' Initializes a new instance of the StackedLinkedList(T) class that is empty.
''' </summary>
Public Sub New()
m_data = New DoubleLinkedList(Of T)()
End Sub
184 Part I Collection Basics
The following constructor creates a stack and adds the specified items to the stack.
C#
/// <summary>
/// Initializes a new instance of the StackedLinkedList(T) class with the specified items.
/// </summary>
/// <param name=”items”>The items to add to the stack.</param>
public StackedLinkedList(IEnumerable<T> items)
{
m_data = new DoubleLinkedList<T>(items);
}
Visual Basic
''' <summary>
''' Initializes a new instance of the StackedLinkedList(T) class with the specified items.
''' </summary>
''' <param name="items">The items to add to the stack.</param>
Public Sub New(ByVal items As IEnumerable(Of T))
m_data = New DoubleLinkedList(Of T)(items)
End Sub
The items are added to the stack by creating the internal data storage with the specified
items.
C#
/// <summary>
/// Adds the item to the top of the StackedLinkedList(T).
/// </summary>
/// <param name="item">The item to add to the top of the StackedLinkedList(T).</param>
public void Push(T item)
{
m_data.AddToEnd(item);
}
Visual Basic
''' <summary>
''' Adds the item to the top of the StackedLinkedList(T).
''' </summary>
''' <param name="item">The item to add to the top of the StackedLinkedList(T).</param>
Public Sub Push(ByVal item As T)
m_data.AddToEnd(item)
End Sub
The method adds the specified item to the end of the linked list.
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 185
C#
/// <summary>
/// Removes the an item from the top of the StackedLinkedList(T).
/// </summary>
/// <returns>The item at the top of the StackedLinkedList(T).</returns>
public T Pop()
{
if (IsEmpty)
{
throw new InvalidOperationException("You cannot pop from an empty stack.");
}
T retval = m_data.Tail.Data;
m_data.Remove(m_data.Tail);
return retval;
}
Visual Basic
''' <summary>
''' Removes the an item from the top of the StackedLinkedList(T).
''' </summary>
''' <returns>The item at the top of the StackedLinkedList(T).</returns>
Public Function Pop() As T
If (IsEmpty) Then
Throw New InvalidOperationException("You cannot pop from an empty stack.")
End If
m_data.Remove(m_data.Tail)
Return retval
End Function
C#
/// <summary>
/// Gets the item at the top of the StackedLinkedList(T) without removing it.
/// </summary>
/// <returns>The item at the top of the StackedLinkedList(T)</returns>
public T Peek()
{
if (IsEmpty)
{
throw new InvalidOperationException
("You cannot get the top item of an empty stack.");
}
return m_data.Tail.Data;
}
Visual Basic
''' <summary>
''' Gets the item at the top of the StackedLinkedList(T) without removing it.
''' </summary>
''' <returns>The item at the top of the StackedLinkedList(T)</returns>
Public Function Peek() As T
If (IsEmpty) Then
Throw New InvalidOperationException _
("You cannot get the top item of an empty stack.")
End If
Return m_data.Tail.Data
End Function
The Peek method will return the last item added to the list.
The next method returns an array that contains all of the values in the stack. The items are
added to the array in LIFO order. That is, the first item in the array would be the last one
added to the stack.
C#
/// <summary>
/// Copies the elements of the StackedLinkedList(T) to a new array.
/// </summary>
/// <returns>
/// An array containing copies of the elements of the StackedLinkedList(T).
/// The data in the array will be in LIFO order.
/// </returns>
public T[] ToArray()
{
T[] tmp = new T[Count];
int i = 0;
for(DoubleLinkedListNode<T> curr = m_data.Tail; curr != null; curr = curr.Previous)
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 187
{
tmp[i++] = curr.Data;
}
return tmp;
}
Visual Basic
''' <summary>
''' Copies the elements of the StackedLinkedList(T) to a new array.
''' </summary>
''' <returns>
''' An array containing copies of the elements of the StackedLinkedList(T).
''' The data in the array will be in LIFO order.
''' </returns>
Public Function ToArray() As T()
Dim tmp As T() = New T(Count - 1) {}
Dim i As Integer = 0
Dim curr As DoubleLinkedListNode(Of T) = m_data.Tail
While (curr IsNot Nothing)
tmp(i) = curr.Data
i = i + 1
curr = curr.Previous
End While
Return tmp
End Function
More Info For the complete code, you can refer to the Lesson6A method in the Samples\
Chapter 3\CS\Driver\Program.cs file for C# or the Samples\Chapter 3\VB\Driver\Module1.vb file
for Visual Basic.
C#
class Plate
{
public int Number;
public TimeSpan NextOperation;
}
188 Part I Collection Basics
Visual Basic
Class Plate
Public Number As Integer
Public NextOperation As TimeSpan
End Class
Number is a unique number assigned to each plate that you can use to track the plate as it
goes through the simulation. NextOperation is used internally by the simulator to determine
when the next operation should happen, such as cleaning the plate, using the plate, and so on.
Next, you need to create the variables and the initial stack of plates.
C#
StackedArray<Plate> cleanPlates = new StackedArray<Plate>();
SingleLinkedList<Plate> usingPlates = new SingleLinkedList<Plate>();
QueuedArray<Plate> dirtyPlates = new QueuedArray<Plate>();
Random rnd = new Random();
TimeSpan currentTime = TimeSpan.Zero;
TimeSpan step = TimeSpan.FromSeconds(1);
double playRate = 100.0;
TimeSpan nextCustomer = TimeSpan.Zero;
bool hasWarned = false;
Visual Basic
Dim cleanPlates As New StackedArray(Of Plate)()
Dim usingPlates As New SingleLinkedList(Of Plate)()
Dim dirtyPlates As New QueuedArray(Of Plate)()
Dim rnd As New Random()
Dim currentTime As TimeSpan = TimeSpan.Zero
Dim [step] As TimeSpan = TimeSpan.FromSeconds(1)
Dim playRate As Double = 100.0
Dim nextCustomer As TimeSpan = TimeSpan.Zero
Dim hasWarned As Boolean = False
For i As Integer = 0 To 19
cleanPlates.Push(New Plate() With {.Number = i})
Next
The variable cleanPlates represents the stack of clean plates that customers can get. The vari-
able usingPlates represents the collection of plates that customers are using. The variable
dirtyPlates represents the plates that are currently on the conveyer belt to be cleaned. The for
statement adds 20 plates to the stack.
C#
while (!Console.KeyAvailable)
{
// Simulates the person eating and carrying the plate to the conveyer belt to be cleaned
. . .
currentTime += step;
}
Visual Basic
While (Not Console.KeyAvailable)
' Simulates the person eating and carrying the plate to the conveyer belt to be cleaned
. . .
currentTime += [step]
End While
The following code is used to simulate a person eating and carrying his or her plate to the
conveyer belt to be cleaned.
C#
if (usingPlates.Count > 0)
{
SingleLinkedListNode<Plate> node = usingPlates.Head;
while (node != null)
{
if (currentTime >= node.Data.NextOperation)
{
Plate plate = node.Data;
Visual Basic
If (usingPlates.Count > 0) Then
Dim node As SingleLinkedListNode(Of Plate) = usingPlates.Head
While (node IsNot Nothing)
If (currentTime >= node.Data.NextOperation) Then
Dim plate As Plate = node.Data
The NextOperation field is used for stating when the customer will finish eating. Customers do
not finish eating in the order that they start, so a linked list is used to simulate the eating cus-
tomers. The collection is traversed with each loop to check whether the customer has finished
eating. After the customer has finished, the plate is then added to the dirtyPlates collection.
C#
if (dirtyPlates.Count > 0)
{
Plate plate = dirtyPlates.Peek();
Visual Basic
If (dirtyPlates.Count > 0) Then
Dim plate As Plate = dirtyPlates.Peek()
The NextOperation field is used to state when a plate is finished being cleaned. Plates are
cleaned in the order they are received, so a queue is used for the conveyer belt.
The following code simulates a customer standing in line and getting a plate.
C#
If (currentTime >= nextCustomer) Then
If (Not cleanPlates.IsEmpty) Then
Dim plate As Plate = cleanPlates.Pop()
hasWarned = False
Else
If (Not hasWarned) Then
Console.WriteLine("Waiting for plates to be cleaned!")
hasWarned = True
End If
End If
End If
Visual Basic
if (currentTime >= nextCustomer)
{
if (!cleanPlates.IsEmpty)
192 Part I Collection Basics
{
Plate plate = cleanPlates.Pop();
hasWarned = false;
}
else
{
if (!hasWarned)
{
Console.WriteLine("Waiting for plates to be cleaned!");
hasWarned = true;
}
}
}
The NextOperation field is used to state when a customer will finish eating. The next cus-
tomer gets a plate off the stack and uses it. If no plates are present, the customer waits until
one is available.
Output
Plate 19 has been taken
Plate 18 has been taken
Plate 17 has been taken
Plate 16 has been taken
Plate 17 is being taken to the cleaners
Plate 15 has been taken
Plate 14 has been taken
Plate 17 has been cleaned
Plate 17 has been taken
Plate 13 has been taken
Plate 17 is being taken to the cleaners
Plate 12 has been taken
Plate 14 is being taken to the cleaners
Plate 17 has been cleaned
Plate 17 has been taken
Plate 11 has been taken
Plate 10 has been taken
Plate 9 has been taken
Plate 14 has been cleaned
Plate 14 has been taken
Plate 8 has been taken
Plate 7 has been taken
Plate 13 is being taken to the cleaners
Plate 6 has been taken
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 193
Plate 15 is being taken to the cleaners
Plate 13 has been cleaned
Plate 13 has been taken
Plate 14 is being taken to the cleaners
Plate 19 is being taken to the cleaners
Plate 15 has been cleaned
Plate 15 has been taken
Plate 18 is being taken to the cleaners
Plate 14 has been cleaned
Plate 19 has been cleaned
Plate 16 is being taken to the cleaners
Plate 9 is being taken to the cleaners
Plate 19 has been taken
Plate 18 has been cleaned
Plate 18 has been taken
Plate 16 has been cleaned
Plate 9 has been cleaned
Plate 9 has been taken
Plate 10 is being taken to the cleaners
Plate 12 is being taken to the cleaners
Plate 16 has been taken
Plate 10 has been cleaned
Plate 9 is being taken to the cleaners
Plate 11 is being taken to the cleaners
Plate 8 is being taken to the cleaners
Plate 12 has been cleaned
Older data will be overwritten if the user attempts to add data to a full buffer. That function-
ality is helpful if, say for example, you have a large number of messages coming in that you
need to process but old messages are not very important. If your buffer size is 1000, you will
start to drop messages that are more than a 1000 messages old when the buffer fills up.
194 Part I Collection Basics
Getting Started
To illustrate using an array to create a circular buffer, you will create a class called
CircularBuffer(T).
C#
[DebuggerDisplay("Count={Count}")]
[DebuggerTypeProxy(typeof(ArrayDebugView))]
public class CircularBuffer<T>
{
// Fields
private int m_capacity;
private int m_count;
private T[] m_data;
private int m_end;
private FullOperations m_fullOperation;
private int m_start;
private int m_updateCode;
// Methods
public CircularBuffer(int size);
public CircularBuffer(int size, IEnumerable<T> items);
public void Clear();
public bool Contains(T item);
private void Initialize(int size);
public T Peek();
public T Pop();
public void Push(T item);
public T[] ToArray();
// Properties
public int Capacity { get; set; }
public int Count { get; }
public FullOperations FullOperation { get; set; }
public bool IsEmpty { get; }
public bool IsFull { get; }
}
Visual Basic
<DebuggerDisplay("Count={Count}") _
<DebuggerTypeProxy(GetType(ArrayDebugView))> _
Public Class CircularBuffer(Of T)
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 195
' Fields
Private m_capacity As Integer
Private m_count As Integer
Private m_data As T()
Private m_end As Integer
Private m_fullOperation As FullOperations
Private m_start As Integer
Private m_updateCode as Integer
' Constructors
Public Sub New(ByVal size As Integer)
Public Sub New(ByVal size As Integer, ByVal items As IEnumerable(Of T))
' Methods
Public Sub Clear()
Public Function Contains(ByVal item As T) As Boolean
Public Sub Initialize(ByVal size As Integer)
Public Function Peek() As T
Public Function Pop() As T
Public Sub Push(ByVal item As T)
Public Function ToArray() As T()
' Properties
Public Property Capacity As Integer
Public ReadOnly Property Count As Integer
Public Property FullOperation As FullOperations
Public ReadOnly Property IsEmpty As Boolean
Public ReadOnly Property IsFull As Boolean
End Class
0 1 2 3 4 5 6 7 8 ... N
Start End
When you add a value, it is placed at the location of the end pointer. The end pointer is then
incremented so that the process can be repeated on the next add. The lightly shaded area in
the following illustration denotes a filled element.
0 1 2 3 4 5 6 7 8 ... N
Start End
196 Part I Collection Basics
When you remove a value, it is removed from the start location. Start is then incremented so
that the process can be repeated with the next removal.
0 1 2 3 4 5 6 7 8 ... N
Start End
At some point, the start or end values will be at the end of the list. To make the collection a
circular buffer, you move the pointer to the beginning of the array and start the remove or
add process from there.
Here the end pointer is pointing to the end of the array before an add.
0 1 2 3 4 5 6 7 8 ... N
Start End
When that happens, end then points to the beginning of the array after the add.
0 1 2 3 4 5 6 7 8 ... N
End Start
You can accomplish this in two ways. You can increment the index and then check to see
whether it has reached the capacity of the array, as shown in the following code.
C#
++index;
if (index >= capacity)
{
index = 0;
}
Visual Basic
index += 1
If (index >= capacity) Then
index = 0
End If
Or you can use the modulus (mod) operation to keep the value from 0 to (capacity – 1).
C#
index = (index + 1) % capacity;
Visual Basic
index = (index + 1) Mod capacity
You may have noticed that the number of elements in the array can be calculated by a
mathematical formula.
C#
if (start < end)
{
count = end - start;
}
else
{
count = end + (capacity - start);
}
Visual Basic
If (start < [end]) Then
count = [end] - start
Else
count = [end] + (capacity - start)
End If
What you may not have noticed yet is that there are two times when start and end can be
equal to each other. Both pointers can be equal to each other when the buffer is full and
when the buffer is empty.
0 1 2 3 4 5 6 7 8 ... N
Start End
0 1 2 3 4 5 6 7 8 ... N
Start End
To distinguish the two, you need an additional variable called a count. The count variable
needs to be updated whenever a value is removed or added to the list. This variable also
eliminates the calculation that is needed whenever the count is requested. Using the count
variable, you can create three status properties for the user.
C#
/// <summary>
/// Gets the number of elements actually contained in the CircularBuffer(T).
/// </summary>
public int Count
{
get { return m_count; }
}
/// <summary>
/// States if the CircularBuffer(T) is empty.
/// </summary>
public bool IsEmpty
198 Part I Collection Basics
{
get { return m_count <= 0; }
}
/// <summary>
/// States if the CircularBuffer(T) is full.
/// </summary>
public bool IsFull
{
get { return m_count == m_capacity; }
}
Visual Basic
''' <summary>
''' Gets the number of elements actually contained in the CircularBuffer(T).
''' </summary>
Public ReadOnly Property Count() As Integer
Get
Return m_count
End Get
End Property
''' <summary>
''' States if the CircularBuffer(T) is empty.
''' </summary>
Public ReadOnly Property IsEmpty() As Boolean
Get
Return m_count <= 0
End Get
End Property
''' <summary>
''' States if the CircularBuffer(T) is full.
''' </summary>
Public ReadOnly Property IsFull() As Boolean
Get
Return m_count = m_capacity
End Get
End Property
Creating Constructors
The CircularBuffer(T) class contains two constructors. One constructor is for creating an
empty class and the other is for creating a class with default values. Both require the user to
specify the size of the circular buffer.
Both constructors call the Initialize method so that duplicate code does not have to be
maintained.
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 199
C#
void Initialize(int size)
{
if (size <= 0)
{
throw new ArgumentOutOfRangeException("size");
}
m_data = new T[size];
m_capacity = size;
FullOperation = FullOperations.Ignore;
}
Visual Basic
Sub Initialize(ByVal size As Integer)
If (size <= 0) Then
Throw New ArgumentOutOfRangeException("size")
End If
m_data = New T(size - 1) {}
m_capacity = size
FullOperation = FullOperations.Ignore
End Sub
The method simply creates an array of the size passed in and sets the FullOperation property
to a default operation that it performs when the user adds to a full circular buffer.
C#
/// <summary>
/// Initializes a new instance of the CircularBuffer(T) class
/// with the specified size as the capacity.
/// </summary>
/// <param name="size">The capacity of the buffer</param>
public CircularBuffer(int size)
{
Initialize(size);
}
Visual Basic
''' <summary>
''' Initializes a new instance of the CircularBuffer(T) class
''' with the specified size as the capacity.
''' </summary>
''' <param name="size">The capacity of the buffer</param>
Public Sub New(ByVal size As Integer)
Initialize(size)
End Sub
The following constructor creates a circular buffer of the specified size and adds the specified
items to the end of the circular buffer.
200 Part I Collection Basics
C#
/// <summary>
/// Initializes a new instance of the CircularBuffer(T) class
/// with the specified items and capacity.
/// </summary>
/// <param name="size">The capacity of the buffer</param>
/// <param name="items">The collection of items you want to add to the buffer.</param>
public CircularBuffer(int size, IEnumerable<T> items)
{
Initialize(size);
Visual Basic
''' <summary>
''' Initializes a new instance of the CircularBuffer(T) class
''' with the specified items and capacity.
''' </summary>
''' <param name="size">The capacity of the buffer</param>
''' <param name="items">The collection of items you want to add to the buffer.</param>
Public Sub New(ByVal size As Integer, ByVal items As IEnumerable(Of T))
Initialize(size)
The constructor adds the specified items to the internal array by walking each item and
adding it to the current end of the array.
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 201
C#
/// <summary>
/// Enumeration of what should happen if the circular buffer is full.
/// </summary>
public enum FullOperations
{
/// <summary>
/// The value being pushed should be ignored.
/// </summary>
Ignore,
/// <summary>
/// The first value should be popped off.
/// </summary>
Pop,
/// <summary>
/// An exception should be thrown.
/// </summary>
Error
}
/// <summary>
/// Gets or sets what to do when a user pushes to a full buffer.
/// </summary>
public FullOperations FullOperation
{
get { return m_fullOperation; }
set { m_fullOperation = value; }
}
202 Part I Collection Basics
Visual Basic
''' <summary>
''' Enumeration of what should happen if the circular buffer is full.
''' </summary>
Public Enum FullOperations
''' <summary>
''' The value being pushed should be ignored.
''' </summary>
Ignore
''' <summary>
''' The first value should be popped off.
''' </summary>
Pop
''' <summary>
''' An exception should be thrown.
''' </summary>
[Error]
End Enum
''' <summary>
''' Gets or sets what to do when a user pushes to a full buffer.
''' </summary>
Public Property FullOperation() As FullOperations
Get
Return m_fullOperation
End Get
Set(ByVal value As FullOperations)
m_fullOperation = value
End Set
End Property
Next, the Push method adds an item to the buffer and does what the user requested on a full
buffer.
C#
/// <summary>
/// Adds the item to the end of the CircularBuffer(T).
/// </summary>
/// <param name="item">The item to add to the end of the buffer.</param>
public void Push(T item)
{
if (IsFull)
{
switch (m_fullOperation)
{
case FullOperations.Ignore:
// Do not do anything
return;
case FullOperations.Pop:
Pop();
break;
case FullOperations.Error:
default:
throw new InvalidOperationException("You cannot add to a full buffer");
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 203
}
}
m_data[m_end] = item;
++m_count;
++m_updateCode;
m_end = (m_end + 1) % m_capacity;
}
Visual Basic
''' <summary>
''' Adds the item to the end of the CircularBuffer(T).
''' </summary>
''' <param name="item">The item to add to the end of the buffer.</param>
Public Sub Push(ByVal item As T)
If (IsFull) Then
Select Case (m_fullOperation)
Case FullOperations.Ignore
' Do not do anything
Exit Sub
Case FullOperations.Pop
Pop()
Exit Select
Case FullOperations.Error
Throw New InvalidOperationException("You cannot add to a full buffer")
Exit Select
Case Else
Throw New InvalidOperationException("You cannot add to a full buffer")
Exit Select
End Select
End If
m_data(m_end) = item
m_count += 1
m_updateCode += 1
m_end = (m_end + 1) Mod m_capacity
End Sub
C#
/// <summary>
/// Removes the first item from the CircularBuffer(T).
/// </summary>
/// <returns>The first item in the CircularBuffer(T).</returns>
public T Pop()
{
if (IsEmpty)
204 Part I Collection Basics
{
throw new InvalidOperationException
("You cannot remove an item from an empty collection.");
}
T value = m_data[m_start];
m_start = (m_start + 1) % m_capacity;
--m_count;
++m_updateCode;
return value;
}
Visual Basic
''' <summary>
''' Removes the first item from the CircularBuffer(T).
''' </summary>
''' <returns>The first item in the CircularBuffer(T).</returns>
Public Function Pop() As T
If (IsEmpty) Then
Throw New InvalidOperationException _
("You cannot remove an item from an empty collection.")
End If
With the Clear method, users can remove all items from the buffer without having to repeat-
edly call Pop until the buffer is empty.
C#
/// <summary>
/// Removes all items from the CircularBuffer(T).
/// </summary>
public void Clear()
{
m_count = 0;
m_start = 0;
m_end = 0;
++m_updateCode;
}
Visual Basic
''' <summary>
''' Removes all items from the CircularBuffer(T).
''' </summary>
Public Sub Clear()
m_count = 0
m_start = 0
m_end = 0
m_updateCode += 1
End Sub
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 205
C#
/// <summary>
/// Gets the item the first item in the CircularBuffer(T) without removing it.
/// </summary>
/// <returns>The first item in the CircularBuffer(T).</returns>
public T Peek()
{
if (IsEmpty)
{
throw new InvalidOperationException
("You cannot view an item in an empty collection.");
}
return m_data[m_start];
}
Visual Basic
''' <summary>
''' Gets the item the first item in the CircularBuffer(T) without removing it.
''' </summary>
''' <returns>The first item in the CircularBuffer(T).</returns>
Public Function Peek() As T
If (IsEmpty) Then
Throw New InvalidOperationException _
("You cannot view an item in an empty collection.")
End If
Return m_data(m_start)
End Function
With the following method, users can check and see if an item is present in the buffer. This
may be useful if a user wants to interact only with a collection that contains a specific item.
C#
/// <summary>
/// Checks if the specified data is present in the CircularBuffer(T).
/// </summary>
/// <param name="data">The data to look for.</param>
/// <returns>True if the data is found, false otherwise.</returns>
public bool Contains(T item)
{
EqualityComparer<T> comparer = EqualityComparer<T>.Default;
int i = m_start;
int index = 0;
while (index < m_count)
{
if (comparer.Equals(m_data[i], item))
206 Part I Collection Basics
{
return true;
}
i = (i + 1) % m_capacity;
++index;
}
return false;
}
Visual Basic
''' <summary>
''' Checks if the specified data is present in the CircularBuffer(T).
''' </summary>
''' <param name="item">The data to look for.</param>
''' <returns>True if the data is found, false otherwise.</returns>
Public Function Contains(ByVal item As T) As Boolean
Dim comparer As EqualityComparer(Of T) = EqualityComparer(Of T).Default
Dim i As Integer = m_start
Dim index As Integer = 0
While (index < m_count)
If (comparer.Equals(m_data(i), item)) Then
Return True
End If
i = (i + 1) Mod m_capacity
index += 1
End While
Return False
End Function
The following method copies the contents of the buffer into an array. This is useful when the
user wants to interact with the data in array form.
C#
/// <summary>
/// Copies the elements of the CircularBuffer(T) to a new array.
/// </summary>
/// <returns>An array containing copies of the elements of the CircularBuffer(T).</returns>
public T[] ToArray()
{
T[] retval = new T[m_count];
if (IsEmpty)
{
return retval;
}
return retval;
}
Visual Basic
''' <summary>
''' Copies the elements of the CircularBuffer(T) to a new array.
''' </summary>
''' <returns>An array containing copies of the elements of the CircularBuffer(T).</returns>
Public Function ToArray() As T()
Dim retval As New T(m_count - 1) {}
If (IsEmpty) Then
Return retval
End If
Return retval
End Function
Changing Capacity
If it is determined during run time that the initial buffer size is too small, the user may want
to increase the size of the buffer. There are two cases that you need to be aware of when
reallocating a buffer that is not empty.
0 1 2 3 4 5 6 7 8 ... N
Start End
0 1 2 3 4 5 6 7 8 ... N
Start End
208 Part I Collection Basics
0 1 2 3 4 5 6 7 8 ... N
End Start
0 1 2 3 4 5 6 7 8 9 10 ... M
Start End
You need to create the following property for users to change the capacity of the buffer.
C#
/// <summary>
/// Gets or sets the capacity of the CircularBuffer(T).
/// </summary>
public int Capacity
{
get { return m_capacity; }
set
{
if (value < Count)
{
throw new NotSupportedException
("The capacity has to be larger than the current count.");
}
// We can simply copy the data if the end hasn't wrapped around yet.
if (m_start < m_end)
{
// We will need to copy the data from the old array to the new one
// All data will be copied to the beginning of the new array
Array.Copy(m_data, m_start, tmp, 0, m_count);
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 209
m_data = tmp;
m_capacity = value;
m_start = 0;
m_end = m_count;
return;
}
// First, we will copy all data from the start to the physical end of the buffer
Array.Copy(m_data, m_start, tmp, 0, m_data.Length - m_start);
// Next, we will copy the items from the physical start of the buffer to the end
Array.Copy(m_data, 0, tmp, m_data.Length - m_start, m_end);
m_data = tmp;
m_start = 0;
m_end = m_count;
m_capacity = value;
}
}
Visual Basic
''' <summary>
''' Gets or sets the capacity of the CircularBuffer(T).
''' </summary>
Public Property Capacity() As Integer
Get
Return m_capacity
End Get
Set(ByVal value As Integer)
If (value < Count) Then
Throw New NotSupportedException _
("The capacity has to be larger than the current count.")
End If
' We can simply copy the data if the end hasn't wrapped around yet.
If (m_start < m_end) Then
' We will need to copy the data from the old array to the new one
' All data will be copied to the beginning of the new array
Array.Copy(m_data, m_start, tmp, 0, m_count)
210 Part I Collection Basics
m_data = tmp
m_capacity = value
m_start = 0
m_end = m_count
Exit Property
End If
' First, we will copy all data from the start to the physical end of the buffer
Array.Copy(m_data, m_start, tmp, 0, m_data.Length - m_start)
' Next, we will copy the items from the physical start of the buffer to the end
Array.Copy(m_data, 0, tmp, m_data.Length - m_start, m_end)
m_data = tmp
m_start = 0
m_end = m_count
m_capacity = value
End Set
End Property
More Info For the complete code, refer to the Lesson5A method in the Samples\Chapter 3\
CS\Driver\Program.cs file for C# or the Samples\Chapter 3\VB\Driver\Module1.vb file for Visual
Basic.
Note the following code from the Lesson5A method., which is modified from Lesson4A.
C#
QueueArray<Request> requests = new QueueArray<Request>();
Visual Basic
Dim requests As QueueArray(Of Request) = New QueueArray(Of Request)()
C#
CircularBuffer<Request> requests = new CircularBuffer<Request>(10);
Visual Basic
Dim requests As CircularBuffer(Of Request) = New CircularBuffer(Of Request)(10)
Chapter 3 Understanding Collections: Queues, Stacks, and Circular Buffers 211
The constructor now takes an integer that states the size of the circular buffer. You might
notice that there is a possibility that the request line that uses the circular buffer may get
more requests than it can play. When using a queue, this could cause the application to
eventually run out of memory. A circular buffer doesn’t grow in size, so the buffer cannot
become larger than the value specified in the constructor.
Output
Song #2: User request at 00:00:00
Song #8: User request at 00:04:48
Song #10: User request at 00:09:03
Song #7: User request at 00:11:56
Song #18: User request at 00:16:11
Song #11: User request at 00:18:30
Song #7: User request at 00:21:02
Song #16: User request at 00:24:49
Song #2: User request at 00:25:30
Song #12: User request at 00:27:55
Song #12: User request at 00:28:57
Song #18: User request at 00:32:07
Song #16: User request at 00:34:16
Song #18: User request at 00:35:46
Song #12: User request at 00:40:35
Song #6: User request at 00:41:31
-------------------------------------------------------------------------------
Song #12: Playing -00:04:06
\........
Summary
In this chapter, you saw how to use and implement queues, stacks, and circular buffers. You
also saw that queues and circular buffers are FIFO collections and stacks are LIFO collections.
Circular buffers are good for when you have a fixed size collection that needs to be accessed
in a FIFO manner. Remember that adding to a full circular buffer requires a performance
penalty if you resize the buffer or loss of data if you decide not to resize the buffer. Queues,
on the other hand, should be used when the buffer size is not fixed. Of course, these are just
general guidelines; you may sometimes find it useful to use a circular buffer with a collection
that isn’t a fixed size.
Developer’s Guide to Collections in Microsoft .NET
Part II
.NET Built-in Collections
213
Chapter 4
Generic Collections
After completing this chapter, you will be able to
■ Understand the equality and ordering comparers.
■ Understand delegates, anonymous methods, and lambda expressions.
■ Understand when and how to use the List(T) class.
■ Understand when and how to use the LinkedList(T) class.
C#
// Define an empty class named ClassA
class ClassA { }
215
216 Part II .NET Built-in Collections
Visual Basic
Public Class ClassA
End Class
Dim refx = x
The following code would display false because x and y reference two different instances of
the class ClassA.
C#
Console.WriteLine(x.Equals(y));
Visual Basic
Console.WriteLine(x.Equals(y))
The following code would display true because x and refx both reference the same instance
of the class ClassA.
C#
Console.WriteLine(x.Equals(refx));
Visual Basic
Console.WriteLine(x.Equals(refx))
C#
int x = 1;
int y = 2;
Console.WriteLine(x.Equals(y));
Visual Basic
Dim x As Integer = 1
Dim y As Integer = 2
Console.WriteLine(x.Equals(y))
The variables x and y are set to two different values, so the console would display false.
But the following two lines of code would both return true because x and y are now binary
equivalents.
Chapter 4 Generic Collections 217
C#
int x = 1;
int y = 1;
Console.WriteLine(x.Equals(y));
y = x;
Console.WriteLine(x.Equals(y));
Visual Basic
Dim x As Integer = 1
Dim y As Integer = 1
Console.WriteLine(x.Equals(y))
y = x
Console.WriteLine(x.Equals(y))
Structures are value types, so they use bitwise comparison as well. Look at the following
structure declaration.
C#
struct StructA
{
public StructA(int a)
{
FieldA = a;
}
public int FieldA;
}
Visual Basic
Public Structure StructA
Public Sub New(ByVal a As Integer)
FieldA = a
End Sub
Public FieldA As Integer
End Structure
C#
StructA x = new StructA(1);
StructA y = new StructA(2);
Console.WriteLine(x.Equals(y));
Visual Basic
Dim x As StructA = New StructA(1)
Dim y As StructA = New StructA(2)
Console.WriteLine(x.Equals(y))
218 Part II .NET Built-in Collections
The variables x and y are not binary-compatible because one contains a 1 and the other
contains a 2. The following would be binary compatible, and would display true:
C#
StructA x = new StructA(1);
StructA z = new StructA(1);
Console.WriteLine(x.Equals(z));
StructA y = x;
Console.WriteLine(x.Equals(y));
Visual Basic
Dim x As StructA = New StructA(1)
Dim z As StructA = New StructA(1)
Console.WriteLine(x.Equals(z))
Dim y As StructA = x
Console.WriteLine(x.Equals(y))
Note An object is free to override the Object.Equals(Object) method. That is the case with the
String class, which overrides the Object.Equals(Object) method so that it compares the internal
strings themselves instead of the object references.
Note to Implementers
The class or structure you are using for T should implement IEquatable (or preferably
IEquatable(T)) interface and override the Object.Equals(object) method. Both should re-
turn the same result for the same object instance. Methods that need to compare in-
stances of your value type need to box your value type to use the Object.Equals(object)
method or IEquatable interface. They do not need to box if your implementation imple-
ments the IEquatable(T) interface and the code that does the comparison checks for
the IEquatable(T) interface first.
Note to Implementers
The class or structure you are using for T should implement the IComparer or prefer-
ably IComparer(T) interface—especially if you want your objects to be sorted or used
in other comparison operations.
C#
class MethodDelegateClass
{
int Square(int x) { return x * x; }
Visual Basic
Public Class MethodDelegateClass
Function Square(ByVal x As Integer)
Return x * x
End Function
Output
The square of 2 is 4
220 Part II .NET Built-in Collections
However, using the anonymous method feature, you can now define the method referenced
by the variable Operation “anonymously” directly in your code block as follows.
C#
Func<int, int> Operation = delegate(int x) { return x * x; };
Note Anonymous methods are not supported in Microsoft Visual Basic, but lambda expressions
are, as you’ll see in the section “Lambda Expressions in Visual Basic.”
Output
The square of 2 is 4
Developers can use this to define simple methods in code without having to create a named
method such as the following.
C#
System.Windows.Forms.Form form = (System.Windows.Forms.Form)obj;
form.Deactivate += delegate(object sender, EventArgs e)
{
m_active.Remove(form);
};
In the .NET Framework 3.0, Microsoft added support for lambda expressions as an easier way
to read and express an inline method. Lambda expressions are now preferred over anonymous
methods as the best way to write an inline method. Here’s an example of how to write a
lambda expression.
C#
Func<int, int> Operation = x => x * x;
Note See the following section for information about lambda expressions in Visual Basic.
Output
The square of 2 is 4
The preceding example uses a lambda expression to define a square function inline and assigns
it to the variable Operation. Operation can then be called to square an integer.
Chapter 4 Generic Collections 221
However, you cannot write the following code in Visual Basic 2008 because it uses a sub.
Only functions and one-line lambda expressions are allowed. With Visual Basic 2010, you can
write lambda subs and functions as well as multiline lambda statements.
Operation(2)
Operation(2)
Output
The square of 2 is 4
Note This book marks examples that can be compiled in only Visual Basic 2010 by using the
header “Visual Basic 2010,” but it uses only “Visual Basic” when you can compile the example in
both Visual Basic 2008 and Visual Basic 2010.
222 Part II .NET Built-in Collections
List(T) Overview
The List(T) class is a generic implementation of a dynamic array. (Dynamic arrays are discussed
briefly in Chapter 1, “Understanding Collections: Arrays and Linked Lists.”) In addition to be-
ing a dynamic array, the class also lets you search, traverse, and sort elements. The Contains,
IndexOf, LastIndexOf, and Remove methods use the default equality comparer, and the
BinarySearch and Sort methods use an ordering comparer. Both comparers are discussed in
the “Understanding the Equality and Ordering Comparers” section earlier in this chapter.
The List(T) class does not automatically sort. You must sort the list manually by calling the
Sort method. If you want the list to sort the items for you automatically, you should look at
the SortedList(TKey,TValue) and SortedDictionary(TKey,TValue) classes, which are discussed in
Chapter 5, “Generic and Support Collections.”
Adding an item to a list that has Count equal to Capacity takes O(n) operations; otherwise,
it takes only O(1) operations. Depending on the location where you’re inserting the item,
inserting is similar to an Add and takes up to O(n) operations. Removing an item from the list
takes up to O(n) operations.
The List(T) class is the generic version of the ArrayList class, so it does not have to box value
types like the ArrayList class has to. According to MSDN, the amount of space saved by not
boxing approximately 500 elements is greater than the memory used when generating a
List(T) to handle the value type you specified for T.
Creating a List(T)
A List(T) can be created using one of three constructors: List(T)(), List(T)(IEnumerable(T)
collection), and List(T)(int size).
List(T)()
You can create an empty List(T) instance by writing the following.
C#
List<int> a = new List<int>();
Chapter 4 Generic Collections 223
Visual Basic
Dim a As New List(Of Integer)
List(T)(IEnumerable(T) collection)
You can create a list with default values by passing an object that implements the
IEnumerable(T) interface.
C#
List<int> a = new List<int>( new int [] { 2 , 4, 5 } );
Visual Basic
Dim a As New List(Of Integer)(New Integer() {2, 4, 5})
List(T)(int size)
You can also create a list that has an initial internal array size, which is useful if you know in
advance how many items you plan on adding to the list.
C#
List<int> a = new List<int>(100);
Visual Basic
Dim a As New List(Of Integer)(100)
C#
List<int> list = new List<int>();
list.Add(2);
list.Add(4);
224 Part II .NET Built-in Collections
Visual Basic
Dim list As New List(Of Integer)()
list.Add(2)
list.Add(4)
C#
List<int> list = new List<int>();
list.AddRange( new int []{ 2, 4 });
Visual Basic
Dim list As New List(Of Integer)()
Traversing a List(T)
You traverse a List(T) by either calling the For Each method (Visual Basic) or the foreach
statement (C#).
More Info Chapter 6, “.NET Collection Interfaces,” provides more information about the
IEnumerable interface. Also, in Chapter 7, “Introduction to LINQ,” you will learn about LINQ, a
query mechanism that you can use to make SQL-like queries on collections.
Chapter 4 Generic Collections 225
The following code shows an example of using the foreach statement with a list.
C#
List<int> list = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7 });
foreach (int value in list)
{
Console.WriteLine(value);
}
Visual Basic
Dim list As New List(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7})
The code writes each element of the list on a separate line, as follows.
Output
1
2
3
4
5
6
7
C#
public delegate void Action<T>(T obj);
Visual Basic
Public Delegate Sub Action(Of T)(ByVal obj As T)
Using a Method for the Action(T) Argument Suppose you have a group of tasks that need
to be processed by an external method called ProcessTask. You can pass the ProcessTask
method as an Action(T) using the following code.
226 Part II .NET Built-in Collections
C#
class Task
{
public Task(string name)
{
Name = name;
}
public string Name;
public bool IsProcessed;
}
Visual Basic
Public Class Task
Public Sub New(ByVal name As String)
Me.Name = name
End Sub
Public Name As String
Public IsProcessed As Boolean
End Class
C#
static void ProcessTask(Task task)
{
// Nothing to process if the task is null or has already been processed
if (task == null || task.IsProcessed)
{
return;
}
Visual Basic
Public Sub ProcessTask(ByVal task As Task)
' Nothing to process if the task is null or has already been processed
If (task Is Nothing) Then
Exit Sub
End If
If (task.IsProcessed) Then
Exit Sub
End If
Chapter 4 Generic Collections 227
' Process the task
' ...
Now you can use the following code to process the task.
C#
List<Task> list = new List<Task>();
list.Add(new Task("BACKUP"));
list.Add(new Task("VERFIY"));
list.Add(new Task("MARK_ARCHIVED"));
list.ForEach(ProcessTask);
Visual Basic
Dim list As New List(Of Task)
list.Add(New Task("BACKUP"))
list.Add(New Task("VERFIY"))
list.Add(New Task("MARK_ARCHIVED"))
list.ForEach(AddressOf ProcessTask)
The code calls the ProcessTask method on each element in the list and marks its IsProcessed
field as true.
Using a Lambda Expression for the Action(T) Method For simple operations, it’s not always
convenient to go through the trouble of creating a separate method. Fortunately, you can
use a lambda expression to perform the action instead. The following code uses a lambda
expression to sum all the elements in a list.
C#
List<int> list = new List<int>(new int[] { 16, 8, 44, 5 });
int sum = 0;
list.ForEach(value =>
{
sum += value;
});
Console.WriteLine(sum);
aList.ForEach(Sub(value As Integer)
sum = sum + value
End Sub)
Console.WriteLine(sum)
Output
73
228 Part II .NET Built-in Collections
For each element, the method calls the lambda expression, which adds that element’s value
to the sum variable. In this case, the sum variable contains the value 73 after the ForEach
method completes—the sum of all elements in list. You could, of course, write a lambda ex-
pression to do other things, such as count all the even numbers.
void Clear()
The Clear method removes all items from the list.
C#
List<int> list = new List<int>(new int[] { 1, 6, 2, 3 });
list.Clear();
Visual Basic
Dim list As New List(Of Integer)(New Integer() {1, 6, 2, 3})
list.Clear()
The list will be empty, [], after the Clear method is called. (But just as a reminder, the list will
still have the same capacity.)
C#
List<int> list = new List<int>(new int[] { 1, 6, 2, 3 });
list.Remove(6);
Visual Basic
Dim list As New List(Of Integer)(New Integer() {1, 6, 2, 3})
list.Remove(6)
The preceding code changes the list from [ 1, 6, 2, 3 ] to [ 1, 2, 3 ] because a 6 was passed in
to the Remove method.
Chapter 4 Generic Collections 229
C#
delegate bool Predicate<T>(T obj);
Visual Basic
Dim instance As New Predicate(Of T)(AddressOf HandlerMethod) as Boolean
Predicate(T) returns true when an object matches the criteria, meaning that the object should
be removed. The RemoveAll method returns the number of items removed.
Using a Method with RemoveAll The following code removes all even values by using a
method for the Predicate(T).
C#
static bool IsEven(int value)
{
return (value % 2) == 0;
}
Visual Basic
Function IsEven(ByVal value As Integer) As Boolean
Return ((value Mod 2) = 0)
End Function
Using a Lambda Expression with RemoveAll The following code removes all even values
by using a lambda expression for the Predicate(T).
C#
List<int> list = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
list.RemoveAll(item => { return (item % 2) == 0; });
Visual Basic
Dim list As New List(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9})
list.RemoveAll(Function(x) x Mod 2 = 0)
C#
List<string> list = new List<string>(new string[] { "red", "blue", "green" });
list.RemoveAt(0);
Visual Basic
Dim list As New List(Of String)(New String() {"red", "blue", "green"})
list.RemoveAt(0)
The list contains only [ “blue”, “green” ] after calling RemoveAt in the preceding code, because
the index that was passed in, 0, is the position of element “red”.
C#
List<string> list = new List<string>(new string[] { "red", "please", "remove", "me", "blue",
"green" });
list.RemoveRange(1,3);
Visual Basic
Dim list As New List(Of String) _
(New String() {"red", "please", "remove", "me", "blue", "green"})
list.RemoveRange(1, 3)
The list contains only [ “red”, “blue”, “green” ] after calling RemoveRange in the preceding
code, because “please”, “remove”, and “me” occupied indices 1, 2, and 3 respectively.
C#
List<string> list = new List<string>(new string[] { "Adam", "Charlie", "David" });
list.Insert(1, "Bob");
Visual Basic
Dim list As New List(Of String)(New String() {"Adam", "Charlie", "David"})
list.Insert(1, "Bob")
After running the preceding code, the list contains [“Adam”, “Bob”, “Charlie”, “David”], because
the code inserts “Bob” at the element 1 position, which formerly contained the value “Charlie”.
C#
List<string> list = new List<string>(new string[] { "Adam", "Charlie", "David" });
List<string> listToAppend = new List<string>(new string[] { "Bob", "Bobbie", "Bobby" });
list.InsertRange(1, listToAppend);
Visual Basic
Dim list As List(Of String) = New List(Of String)(New String() {"Adam", "Charlie", "David"})
Dim listToAppend As New List(Of String)(New String() {"Bob", "Bobbie", "Bobby"})
list.InsertRange(1, listToAppend)
The list contains [“Adam”, “Bob”, “Bobbie”, “Bobby”, “Charlie”, “David”] after the InsertRange
call in the preceding code completes, because the list [“Bob”, “Bobbie”, “Bobby”] was inserted
at element 1, which formerly contained the value “Charlie”.
Sorting a List(T)
You can sort a list by calling one of the four overloaded Sort methods. In fact, you must call
one of the Sort methods manually whenever you want the list to be sorted, because List(T)
doesn’t support automatic sorting.
232 Part II .NET Built-in Collections
Before starting, the QuickSort example uses this ListToString method, which writes the con-
tents of a list to a string. This is handy for showing the contents of a list on the screen.
C#
static StringBuilder ListToString(System.Collections.IList array)
{
StringBuilder sb = new StringBuilder();
sb.Append("[");
for (int i = 0; i < array.Count - 1;++i)
{
sb.Append(array[i]);
sb.Append(",");
}
sb.Append(array[array.Count - 1]);
sb.Append("]");
return sb;
}
Visual Basic
Function ListToString(ByVal array As System.Collections.IList) As System.Text.StringBuilder
Dim sb As New System.Text.StringBuilder()
sb.Append("[")
For i As Integer = 0 To array.Count - 2
sb.Append(array(i))
sb.Append(",")
Next
sb.Append(array(array.Count - 1))
sb.Append("]")
Return sb
End Function
This function writes an object that implements IList to a StringBuilder in the format
“[E0,E1,E2,E3…EN]”, where E0 is element 0, E1 is element 1, and so on.
Chapter 4 Generic Collections 233
C#
static void QuickSort(List<int> items)
{
// There isn't a need to sort an array that contains one element.
if (items.Count <= 1)
{
return;
}
// Create a list that holds the values that are less than
// and greater than the pivot point
List<int> less = new List<int>();
List<int> greater = new List<int>();
// Traverse the list and set the pivot point to the lesser than or greater than list
// There isn't a need to check the last one since it is the pivot point
for (int i = 0; i < items.Count - 1; ++i)
{
if (items[i] < pivot)
{
less.Add(items[i]);
}
else
{
greater.Add(items[i]);
}
}
int index = 0;
// Add the items that are less than the pivot point to the beginning
foreach (int value in less)
{
items[index++] = value;
}
// Add the items that are greater than the pivot point to the end
foreach (int value in greater)
{
items[index++] = value;
}
}
234 Part II .NET Built-in Collections
Visual Basic
Sub QuickSort(ByVal items As List(Of Integer))
' There isn't a need to sort an array that contains one element.
If (items.Count <= 1) Then
Return
End If
' Create a list that holds the values that are less than
' and greater than the pivot point
Dim less As New List(Of Integer)
Dim greater As New List(Of Integer)
' Traverse the list and set the pivot point to the lesser than or greater than list
' There isn't a need to check the last one since it is the pivot point
For i As Integer = 0 To items.Count - 2
If (items(i) < pivot) Then
less.Add(items(i))
Else
greater.Add(items(i))
End If
Next
' Add the items that are less than the pivot point to the beginning
For Each value As Integer In less
items(index) = value
index = index + 1
Next
' Add the items that are greater than the pivot point to the end
For Each value As Integer In greater
items(index) = value
index = index + 1
Next
End Sub
Chapter 4 Generic Collections 235
The QuickSort method obtains a pivot point by picking the last item in the list to be sorted.
Next, it traverses the list and adds the element’s value to either the greater or less list by
comparing the value to the pivot point. The method then recursively calls the QuickSort
method on each list, and finally, combines the list.
Note This example function is designed to show you how QuickSort works; it isn’t optimized or
designed to win any coder-of-the-year awards.
C#
// Create a random list of 20 integers
Random rnd = new Random();
List<int> items = Enumerable.Repeat(0, 20).Select(i => rnd.Next(100)).ToList();
Visual Basic
' Create a random list of 20 integers
Dim rnd As Random = New Random()
Dim items As Enumerable.Repeat(0, 20).Select(Function(i) rnd.Next(100)).ToList()
Output
items=[36,96,35,60,85,53,29,5,62,22,84,80,23,91,22,5,90,17,59,24]
QuickSort(items)=[5,5,17,22,22,23,24,29,35,36,53,59,60,62,80,84,85,90,91,96]
The first line of the output is the unsorted list of random numbers. The second line is the list
after it has been quick-sorted.
236 Part II .NET Built-in Collections
void Sort()
The Sort method uses the default comparer as described in the “Understanding the Equality
and Ordering Comparers” section earlier in this chapter.
C#
List<int> list = new List<int>(new int[] { 16, 8, 44, 5 });
list.Sort();
Visual Basic
Dim list As New List(Of Integer)(New Integer() {16, 8, 44, 5})
list.Sort()
C#
struct Person
{
/// <summary>
/// The age of the person.
/// </summary>
public int Age;
/// <summary>
/// The first name of the person.
/// </summary>
public string FirstName;
/// <summary>
/// The last name of the person.
/// </summary>
public string LastName;
/// <summary>
/// Initializes a new instance of the Person structure with the specified contents.
/// </summary>
/// <param name="firstName">The first name of the person.</param>
/// <param name="lastName">The last name of the person.</param>
/// <param name="age">The age of the person.</param>
public Person(string firstName, string lastName, int age)
{
FirstName = firstName;
LastName = lastName;
Age = age;
}
Chapter 4 Generic Collections 237
/// <summary>
/// Override the toString method for display purposes.
/// </summary>
/// <returns></returns>
public override string ToString()
{
return string.Format("[{0},{1},{2}]",FirstName,LastName,Age);
}
}
Visual Basic
Structure Person
''' <summary>
''' The age of the person.
''' </summary>
Public Age As Integer
''' <summary>
''' The first name of the person.
''' </summary>
Public FirstName As String
''' <summary>
''' The last name of the person.
''' </summary>
Public LastName As String
''' <summary>
''' Initializes a new instance of the Person structure with the specified contents.
''' </summary>
''' <param name="firstName">The first name of the person.</param>
''' <param name="lastName">The last name of the person.</param>
''' <param name="age">The age of the person.</param>
Public Sub New(ByVal firstName As String, ByVal lastName As String, _
ByVal age As Integer)
Me.firstName = firstName
Me.lastName = lastName
Me.Age = age
End Sub
''' <summary>
''' Override the toString method for display purposes.
''' </summary>
''' <returns></returns>
Public Overrides Function ToString() As String
Return String.Format("[{0},{1},{2}]", FirstName, LastName, Age)
End Function
End Structure
Note The example structure could just as easily be a class. Both work the same way for sorting
purposes.
238 Part II .NET Built-in Collections
Using a Lambda Expression for the Comparison(T) The following is an example that uses a
lambda expression to do the comparison. The lambda expression compares the ages of each
person and sorts the list accordingly.
C#
List<Person> list = new List<Person>();
list.Add(new Person("David","Alexander",44));
list.Add(new Person("Jeff","Hay",32));
list.Add(new Person("Kim","Akers",12));
list.Add(new Person("Michael","Allen",25));
list.Sort((a, b) =>
{
if (a.Age < b.Age)
{
return -1;
}
return 0;
}
);
list.Sort(Function(a, b)
If (a.Age < b.Age) Then
Return -1
End If
Using a Method for the Comparison(T) Here's an example that uses a method to do the
comparison.
Chapter 4 Generic Collections 239
C#
static int AgeComparison(Person a, Person b)
{
if (a.Age < b.Age)
{
return -1;
}
return 0;
}
Visual Basic
Function AgeComparison(ByVal a As Person, ByVal b As Person) As Integer
If (a.Age < b.Age) Then
Return -1
End If
Return 0
End Function
The AgeComparison method compares the ages of the two Person structures passed in. The
following code uses the AgeComparison method to sort the list.
C#
List<Person> list = new List<Person>();
list.Add(new Person("David","Alexander",44));
list.Add(new Person("Jeff","Hay",32));
list.Add(new Person("Kim","Akers",12));
list.Add(new Person("Michael","Allen",25));
list.Sort(AgeComparison);
Visual Basic
Dim list As List(Of Person) = New List(Of Person)()
list.Sort(AddressOf AgeComparison)
The method AgeComparison compares the age of each person and sorts the list accordingly.
The following example uses the Person structure defined in the Sort(Comparison(T)) method.
To use the Sort(IComparer(T)) method, you must first define a data type that implements the
IComparer(T) interface, as follows.
C#
// <summary>
/// Defines a class for doing age comparisons on the person structure.
/// </summary>
class PersonAgeComparer : IComparer<Person>
{
/// <summary>
/// Compares two people using their ages.
/// </summary>
/// <param name="a">The first person to compare.</param>
/// <param name="b">The second person to compare.</param>
/// <returns>Returns a -1 if the first person's age is less than the second person,
/// 0 if they are the same,
/// and 1 if the first person is greater than the second.</returns>
public int Compare(Person a, Person b)
{
if (a.Age < b.Age)
{
return -1;
}
return 0;
}
}
Visual Basic
''' <summary>
''' Defines a class for doing age comparisons on the person structure.
''' </summary>
Class PersonAgeComparer
Implements IComparer(Of Person)
Chapter 4 Generic Collections 241
''' <summary>
''' Compares two people using their ages.
''' </summary>
''' <param name="a">The first person to compare.</param>
''' <param name="b">The second person to compare.</param>
''' <returns>Returns a -1 if the first person's age is less than the second person,
''' 0 if they are the same,
''' and 1 if the first person is greater than the second.</returns>
Public Function Compare(ByVal a As Person, ByVal b As Person) As Integer _
Implements IComparer(Of Person).Compare
If (a.Age < b.Age) Then
Return -1
End If
Return 0
End Function
End Class
The following code uses the PersonAgeComparer class to do the comparison. The class imple-
ments the interface IComparer(T), which contains the method Compare. The IComparer( T)
.Compare method is called by the Sort method. The PersonAgeComparer class can be used
by the following driver.
C#
PersonAgeComparer comparer = new PersonAgeComparer();
Visual Basic
Dim comparer As PersonAgeComparer = New PersonAgeComparer()
To do this, you need to extend the Person structure defined earlier to include the sex of the
person by adding the following.
C#
/// <summary>
/// States if the person is male.
/// </summary>
public bool IsMale;
Visual Basic
''' <summary>
''' States if the person is male.
''' </summary>
public IsMale as Boolean
C#
/// <summary>
/// Initializes a new instance of the Person structure with the specified contents.
/// </summary>
/// <param name="firstName">The first name of the person.</param>
/// <param name="lastName">The last name of the person.</param>
/// <param name="age">The age of the person.</param>
public Person(string firstName, string lastName, int age)
{
FirstName = firstName;
LastName = lastName;
Age = age;
}
/// <summary>
/// Override the toString method for display purposes.
/// </summary>
/// <returns></returns>
public override string ToString()
{
return string.Format("[{0},{1},{2}]",FirstName,LastName,Age);
}
Chapter 4 Generic Collections 243
Visual Basic
''' <summary>
''' Initializes a new instance of the Person structure with the specified contents.
''' </summary>
''' <param name="firstName">The first name of the person.</param>
''' <param name="lastName">The last name of the person.</param>
''' <param name="age">The age of the person.</param>
Public Sub New(ByVal firstName As String, ByVal lastName As String, ByVal age As Integer)
Me.firstName = firstName
Me.lastName = lastName
Me.Age = age
End Sub
''' <summary>
''' Override the toString method for display purposes.
''' </summary>
''' <returns></returns>
Public Overrides Function ToString() As String
Return String.Format("[{0},{1},{2}]", FirstName, LastName, Age)
End Function
to this:
C#
/// <summary>
/// Initializes a new instance of the Person structure with the specified contents.
/// </summary>
/// <param name="firstName">The first name of the person.</param>
/// <param name="lastName">The last name of the person.</param>
/// <param name="age">The age of the person.</param>
/// <param name="isMale">States if the person is male.</param>
public Person(string firstName, string lastName, int age, bool isMale)
{
FirstName = firstName;
LastName = lastName;
Age = age;
IsMale = isMale;
}
/// <summary>
/// Override the toString method for display purposes.
/// </summary>
/// <returns></returns>
public override string ToString()
{
return string.Format("[{0},{1},{2},{3}]",FirstName,LastName,Age,IsMale ? "M" : "F");
}
Visual Basic
''' <summary>
''' Initializes a new instance of the Person structure with the specified contents.
''' </summary>
''' <param name="firstName">The first name of the person.</param>
''' <param name="lastName">The last name of the person.</param>
''' <param name="age">The age of the person.</param>
''' <param name="isMale">States if the person is male.</param>
244 Part II .NET Built-in Collections
Public Sub New(ByVal firstName As String, ByVal lastName As String, ByVal age As Integer, _
ByVal isMale As Boolean)
Me.FirstName = firstName
Me.LastName = lastName
Me.Age = age
Me.IsMale = isMale
End Sub
''' <summary>
''' Override the toString method for display purposes.
''' </summary>
''' <returns></returns>
Public Overrides Function ToString() As String
Return String.Format("[{0},{1},{2},{3}]", FirstName, LastName, Age, _
If(IsMale, "M", "F"))
End Function
You also need to create a comparison class. For this example, the comparison class must
compare names, so it's named PersonNameComparer.
C#
// <summary>
/// Defines a class for doing name comparisons on the person structure
/// using the last name first.
/// </summary>
class PersonNameComparer : IComparer<Person>
{
/// <summary>
/// Compares two people using their names.
/// </summary>
/// <param name="a">The first person to compare.</param>
/// <param name="b">The second person to compare.</param>
/// <returns>Returns a -1 if the first person's name is less than the second person,
/// 0 if they are the same,
/// and 1 if the first person is greater than the second.</returns>
public int Compare(Person a, Person b)
{
StringComparer comparer = StringComparer.CurrentCultureIgnoreCase;
// If the last names are not equal, there is no need to compare the first name.
if (lastNameComparison != 0)
{
return lastNameComparison;
}
// Return the result of the first name comparison since the last name is equal
return comparer.Compare(a.FirstName, b.FirstName);
}
}
Chapter 4 Generic Collections 245
Visual Basic
''' <summary>
''' Defines a class for doing name comparisons on the person structure
''' using the last name first.
''' </summary>
Class PersonNameComparer
Implements IComparer(Of Person)
''' <summary>
''' Compares two people using their names.
''' </summary>
''' <param name="a">The first person to compare.</param>
''' <param name="b">The second person to compare.</param>
''' <returns>Returns a -1 if the first person's name is less than the second person,
''' 0 if they are the same,
''' and 1 if the first person is greater than the second.</returns>
Public Function Compare(ByVal a As Person, ByVal b As Person) _
As Integer Implements IComparer(Of Person).Compare
Dim comparer As StringComparer = StringComparer.CurrentCultureIgnoreCase
' If the last names are not equal, there is no need to compare the first name.
If (lastNameComparison <> 0) Then
Return lastNameComparison
End If
' Return the result of the first name comparison since the last name is equal
Return comparer.Compare(a.FirstName, b.FirstName)
End Function
End Class
The Compare method returns a compare result by checking the last name and then the first
name.
The driver creates an unsorted list of people and then sorts them by sex. Next, it sorts each
sex group by name, using the Sort(int, int, IComparer(T)) method and PersonNameComparer.
C#
List<Person> list = new List<Person>();
list.Add(new Person("Annie", "Herriman", 12, false));
list.Add(new Person("David", "Alexander", 44, true));
list.Add(new Person("Michael", "Allen", 25, true));
list.Add(new Person("Lisa", "Andrews", 42, false));
list.Add(new Person("Jeff", "Hay", 32, true));
list.Add(new Person("Kim", "Akers", 28, false));
if (a.IsMale)
{
// returning a 1 will put the males after the females
return 1;
}
return -1;
});
// You know the first three are females because we only have three females,
// so you can sort the females by calling the method with a count of 0
list.Sort(0, 3, new PersonNameComparer());
// Next sort the remaining 3 males
list.Sort(3, 3, new PersonNameComparer());
list.Sort(Function(a, b)
If (a.IsMale = b.IsMale) Then
Return 0
End If
If (a.IsMale) Then
' returning a 1 will put the males after the females
Return 1
End If
Return -1
End Function
)
Console.WriteLine(ListToString(list))
' You know the first 3 are females because we only have 3 females,
' so you can sort the females by calling the method with a count of 0
list.Sort(0, 3, New PersonNameComparer())
' Next sort the remaining 3 males
list.Sort(3, 3, New PersonNameComparer())
Searching a List(T)
Use the Find, FindIndex, FindAll, and BinarySearch methods to find an item in the list.
C#
struct Person
{
/// <summary>
/// The age of the person.
/// </summary>
public int Age;
/// <summary>
/// The first name of the person.
/// </summary>
public string FirstName;
/// <summary>
/// The last name of the person.
/// </summary>
public string LastName;
/// <summary>
/// States if the person is male.
/// </summary>
public bool IsMale;
/// <summary>
/// States if the data is empty.
/// </summary>
public bool IsEmpty
{
get
{
return
(string.IsNullOrEmpty(LastName) ||
string.IsNullOrEmpty(FirstName) ||
Age == 0);
}
}
/// <summary>
/// Initializes a new instance of the Person structure with the specified contents.
/// </summary>
/// <param name="firstName">The first name of the person.</param>
/// <param name="lastName">The last name of the person.</param>
/// <param name="age">The age of the person.</param>
/// <param name="isMale">States if the person is male.</param>
248 Part II .NET Built-in Collections
Visual Basic
Structure Person
''' <summary>
''' The age of the person.
''' </summary>
Public Age As Integer
''' <summary>
''' The first name of the person.
''' </summary>
Public FirstName As String
''' <summary>
''' The last name of the person.
''' </summary>
Public LastName As String
''' <summary>
''' States if the person is male.
''' </summary>
Public IsMale As Boolean
''' <summary>
''' States if the data is empty.
''' </summary>
Public ReadOnly Property IsEmpty As Boolean
Get
Return (String.IsNullOrEmpty(LastName) Or String.IsNullOrEmpty(FirstName) _
Or Age = 0)
End Get
End Property
''' <summary>
''' Initializes a new instance of the Person structure with the specified contents.
''' </summary>
''' <param name="firstName">The first name of the person.</param>
''' <param name="lastName">The last name of the person.</param>
''' <param name="age">The age of the person.</param>
''' <param name="isMale">States if the person is male.</param>
Public Sub New(ByVal firstName As String, ByVal lastName As String, _
ByVal age As Integer, ByVal isMale As Boolean)
Chapter 4 Generic Collections 249
Me.FirstName = firstName
Me.LastName = lastName
Me.Age = age
Me.IsMale = isMale
End Sub
''' <summary>
''' Override the toString method for display purposes.
''' </summary>
''' <returns></returns>
Public Overrides Function ToString() As String
Return String.Format("[{0},{1},{2},{3}]", FirstName, LastName, Age, _
If(IsMale, "M", "F"))
End Function
End Structure
Using a Lambda Expression for the Search You can use lambda expressions for the Find
method. Say you want to find an object in a collection by using a complex criterion. Take a
look at the following example.
C#
List<Person> list = new List<Person>();
list.Add(new Person("Julia", "Llyina", 12, false));
list.Add(new Person("David", "Alexander", 44, true));
list.Add(new Person("Michael", "Allen", 25, true));
list.Add(new Person("Andrews", "Lisa", 42, false));
list.Add(new Person("Jeff", "Hay", 32, true));
list.Add(new Person("Kim", "Akers", 28, false));
Visual Basic
Dim comparer As PersonAgeComparer = New PersonAgeComparer()
This example finds a person in the list who is male and is over the age of 40. When executed,
[“David”,”Alexander”,44,M] is assigned to the found variable. When the item is not found, as
in the following search, the found variable gets assigned a default value.
250 Part II .NET Built-in Collections
C#
Person found = list.Find(person =>
{
return person.IsMale && person.Age > 100;
});
Visual Basic
Dim found As Person = list.Find(Function(person) person.IsMale And person.Age > 100)
For a reference type, the default value is null in C# or Nothing in Visual Basic, and for a value
type, it is 0. You could check for a default Person structure by adding a property such as the
following.
C#
/// <summary>
/// States if the data is empty.
/// </summary>
public bool IsEmpty
{
get
{
return
(string.IsNullOrEmpty(LastName) ||
string.IsNullOrEmpty(FirstName) ||
Age == 0);
}
}
Visual Basic
''' <summary>
''' States if the data is empty.
''' </summary>
Public ReadOnly Property IsEmpty As Boolean
Get
Return (String.IsNullOrEmpty(LastName) Or String.IsNullOrEmpty(FirstName) Or Age = 0)
End Get
End Property
The IsEmpty property checks to see whether the Age, LastName, or FirstName properties are
null, which would be the case in a default structure. You could just as easily check only the
Age property, but checking the LastName and FirstName properties makes sure operations
don't get performed on a person with no first or last name.
Using a Method for the Search You can also use a method to check criteria such as the
following.
Chapter 4 Generic Collections 251
C#
static bool FindOver40MalesPerson person)
{
return person.IsMale && person.Age >= 40;
}
Visual Basic
Function FindOver40Males(ByVal person As Person) As Boolean
Return person.IsMale And person.Age >= 40
End Function
This method returns true if the person is male and at or over the age of 40.
C#
List<Person> list = new List<Person>();
list.Add(new Person("Llyina", "Julia", 12, false));
list.Add(new Person("David", "Alexander", 44, true));
list.Add(new Person("Michael", "Allen", 25, true));
list.Add(new Person("Lisa", "Andrews", 42, false));
list.Add(new Person("Jeff", "Hay", 32, true));
list.Add(new Person("Kim", "Akers", 28, false));
Visual Basic
Dim list As New List(Of Person)()
list.Add(New Person("Llyina", "Julia", 12, False))
list.Add(New Person("David", "Alexander", 44, True))
list.Add(New Person("Michael", "Allen", 25, True))
list.Add(New Person("Lista", "Andrews", 42, False))
list.Add(New Person("Jeff", "Hay", 32, True))
list.Add(New Person("Kim", "Akers", 28, False))
This example finds a person in the list who meets the criteria in the method FindOver40
Males. When it executes, [“David”,”Alexander”,44,M] is assigned to the variable found. If
the item is not found, the code returns a default Person structure.
int FindIndex(Predicate(T) match) The FindIndex method searches the list for the first item
that matches the specified condition. If the method finds an item that matches the criteria, it
returns an index to that item; otherwise it returns a -1.
int FindLastIndex(Predicate(T) match) The FindLastIndex searches the list for the last item
that matches the specified condition. If the method finds an item that matches the criteria, it
returns an index to that item; otherwise it returns a -1.
Using a Method to Do the Matching First you need to define a method for checking the
criteria. The method does not need to be static.
C#
static bool FindOver40Males (Person person)
{
return person.IsMale && person.Age >= 40;
}
Visual Basic
Function FindOver40Males(ByVal person As Person) As Boolean
Return person.IsMale And person.Age >= 40
End Function
C#
List<Person> list = new List<Person>();
list.Add(new Person("Llyina", "Julia", 12, false));
list.Add(new Person("David", "Alexander", 44, true));
list.Add(new Person("Michael", "Allen", 25, true));
list.Add(new Person("Lisa", "Andrews", 42, false));
list.Add(new Person("Jeff", "Hay", 42, true));
list.Add(new Person("Kim", "Akers", 28, false));
found = list.FindLastIndex(FindOver40Males);
Visual Basic
Dim list As List(Of Person) = New List(Of Person)()
list.Add(New Person("Llyina", "Julia", 12, False))
list.Add(New Person("David", "Alexander", 44, True))
list.Add(New Person("Michael", "Allen", 25, True))
list.Add(New Person("Lisa", "Andrews", 42, False))
list.Add(New Person("Jeff", "Hay", 42, True))
list.Add(New Person("Kim", "Akers", 28, False))
Output
The first male over the age of 40 is at index 1.
The last male over the age of 40 is at index 4.
The code looks for the first and last males over the age of 40 by calling the FindOver40Males
method.
Using a Lambda Expression to Do the Matching You can use a lambda expression to find
the index.
C#
List<Person> list = new List<Person>();
list.Add(new Person("Llyina", "Julia", 12, false));
list.Add(new Person("David", "Alexander", 44, true));
list.Add(new Person("Michael", "Allen", 25, true));
list.Add(new Person("Lisa", "Andrews", 42, false));
list.Add(new Person("Jeff", "Hay", 42, true));
list.Add(new Person("Kim", "Akers", 28, false));
Visual Basic
Dim list As New List(Of Person)()
list.Add(New Person("Llyina", "Julia", 12, False))
list.Add(New Person("David", "Alexander", 44, True))
list.Add(New Person("Michael", "Allen", 25, True))
list.Add(New Person("List", "Andrews", 42, False))
list.Add(New Person("Jeff", "Hay", 42, True))
list.Add(New Person("Kim", "Akers", 28, False))
Output
The first male over the age of 40 is at index 1.
The last male over the age of 40 is at index 4.
The code looks for the first and last males over the age of 40.
int FindIndex(int startIndex, Predicate(T) match) The FindIndex method searches the list
for the first item that matches the specified condition, starting at the specified index. If the
method finds an item that matches the criteria, it returns an index to that item; otherwise it
returns a -1.
Using a Method to Do the Matching You can use a method to do the criteria check as
follows.
C#
List<int> list = new List<int>(new int[] { 33, 54, 23, 28, 8, 5, 14 });
Console.WriteLine("The first index that contains an even number from index 2-6 is {0}.",
found);
Visual Basic
Dim list As New List(Of Integer)(New Integer() {33, 54, 23, 28, 8, 5, 14})
Console.WriteLine("The first index that contains an even number from index 2-6 is {0}.", _
found)
Output
The first index that contains an even number from index 2-6 is 3.
The last index that contains an even number from index 0-5 is 4.
The code locates the first even number in the list [33,54,23,28,8,5,14], starting at index 2, and
then the last even number in the list ending at index 5.
Using a Lambda Expression to Do the Matching You can use a lambda expression to do
the criteria check as follows.
C#
List<int> list = new List<int>(new int[] { 33, 54, 23, 28, 8, 5, 14 });
Console.WriteLine("The first index that contains an even number from index 2-6 is {0}.",
found);
Console.WriteLine("The last index that contains an even number from index 0-5 is {0}.",
found);
Visual Basic
Dim list As New List(Of Integer)(New Integer() {33, 54, 23, 28, 8, 5, 14})
Console.WriteLine("The first index that contains an even number from index 2-6 is {0}.", _
found)
256 Part II .NET Built-in Collections
Output
The first index that contains an even number from index 2-6 is 3.
The last index that contains an even number from index 0-5 is 4.
The code locates the first even number in the list [33,54,23,28,8,5,14], starting at index 2, and
then the last even number in the list ending at index 5.
int FindIndex(int startIndex, int count, Predicate(T) match) The FindIndex method searches
the list for the first item that matches the specified condition, starting at the specified index
and ending at the number of items in count, which is startIndex + count - 1. If the method finds
an item that matches the criteria, it returns the index of that item; otherwise it returns a -1.
int FindLastIndex(int startIndex, int count, Predicate(T) match The FindLastIndex method
searches the list for the last item that matches the specified condition from (index – count +
1) to index. If the method finds an item that matches the criteria, it returns the index of that
item; otherwise it returns a -1.
Using a Method to Do the Matching You can use a method to do the criteria check as
follows.
C#
List<int> list = new List<int>(new int[] { 33, 54, 23, 28, 8, 5, 14 });
Console.WriteLine("The first index that contains an even number from index 2-4 is {0}.",
found);
Console.WriteLine("The last index that contains an even number from index 2-5 is {0}.",
found);
Output
The first index that contains an even number from index 2-4 is 3.
The last index that contains an even number from index 2-5 is 4.
The code tries to locate the first even number in the list [33,54,23,28,8,5,14], starting at index
2, and ending at index 4, which is (startindex + count – 1) and the last even number in the list
from (startindex – count + 1) to startindex, which is 2 to 5.
Chapter 4 Generic Collections 257
Using a Lambda to Do the Matching You can use a lambda expression to do the criteria
check as follows.
C#
List<int> list = new List<int>(new int[] { 33, 54, 23, 28, 8, 5, 14 });
Console.WriteLine("The first index that contains an even number from index 2-4 is {0}.",
found);
Console.WriteLine("The last index that contains an even number from index 2-5 is {0}.",
found);
Visual Basic
Dim list As New List(Of Integer)(New Integer() {33, 54, 23, 28, 8, 5, 14})
Console.WriteLine("The first index that contains an even number from index 2-4 is {0}.", _
found)
Console.WriteLine("The last index that contains an even number from index 2-5 is {0}.", _
found)
Output
The first index that contains an even number from index 2-4 is 3.
The last index that contains an even number from index 2-5 is 4.
The code tries to locate the first even number in the list [33,54,23,28,8,5,14], starting at index
2, and ending at index 4, which is (startindex + count – 1) and the last even number in the list
from (startindex – count + 1) to startindex, which is 2 to 5.
Using a Lambda Expression for the FindAll A lambda expression can be used for the
FindAll method as follows.
C#
List<int> list = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8 });
List<int> evens = list.FindAll(value => { return (value % 2) == 0; });
Visual Basic
Dim list As New List(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8})
Dim evens As List(Of Integer) = list.FindAll(Function(value) (value Mod 2) = 0)
The code searches the list for all elements that are even by checking to see whether the
element’s value mod 2 is equal to 0. After execution, the variable evens contains the values
[2,4,6,8].
Using a Method for the FindAll You can define a method to check whether an item
matches specific criteria, and pass it to the FindAll method as shown in the following example
method.
C#
static bool IsEven(int value)
{
return (value % 2) == 0;
}
Visual Basic
Function IsEven(ByVal value As Integer) As Boolean
Return ((value Mod 2) = 0)
End Function
The IsEven method checks to see whether the value is even, using the mod operation.
You can use the IsEven method with the FindAll method as follows.
C#
List<int> list = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8 });
List<int> evens = list.FindAll(IsEven);
Visual Basic
Dim list As New List(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8})
Dim evens As List(Of Integer) = list.FindAll(AddressOf IsEven)
The code searches the list for all elements that are even by checking to see whether the
element’s value mod 2 is equal to 0. After execution, the variable evens contains the values
[2,4,6,8].
Chapter 4 Generic Collections 259
Some of the following examples use the PersonComparer class defined in the following code.
C#
class PersonComparer : Comparer<Person>
{
public override int Compare(Person a, Person b)
{
int lastNameCompare = String.Compare(a.LastName, b.LastName, true);
if (lastNameCompare != 0)
{
return lastNameCompare;
}
if (a.IsMale != b.IsMale)
{
if (a.IsMale)
{
return 1;
}
else
{
return -1;
}
}
return 0;
}
}
260 Part II .NET Built-in Collections
Visual Basic
Class PersonComparer
Inherits Comparer(Of Person)
Public Overrides Function Compare(ByVal a As Person, ByVal b As Person) As Integer
End If
Return 0
End Function
End Class
int BinarySearch(T item) The BinarySearch method searches the list for the first item that
matches the specified item. If the method finds an item that matches the criteria, it returns
an index to that item; otherwise it returns a negative number that is the bitwise complement
of the next element that is larger than item or the bitwise complement of Count if no item is
larger than item.
C#
List<int> list = new List<int>(new int[] { 33, 54, 23, 28, 8, 5, 14} );
int found = list.BinarySearch(5);
Console.WriteLine("The unsorted list is {0}", ListToString(list));
Console.WriteLine("The number 5 is located at {0} in the unsorted list.", found);
list.Sort();
Console.WriteLine("The sorted list is now {0}", ListToString(list));
found = list.BinarySearch(5);
Console.WriteLine("The number 5 is located at {0} in the sorted list.", found);
Chapter 4 Generic Collections 261
Visual Basic
Dim list As New List(Of Integer)(New Integer() {33, 54, 23, 28, 8, 5, 14})
Dim found As Integer = list.BinarySearch(5)
Output
The unsorted list is [33,54,23,28,8,5,14]
The number 5 is located at -1 in the unsorted list.
The sorted list is now [5,8,14,23,28,33,54]
The number 5 is located at 0 in the sorted list.
Notice how the BinarySearch method returned a -1 for the search on the unsorted list and
a 0 for the sorted list. This is because the list needs to be sorted before the BinarySearch
method is called.
int BinarySearch(T item, IComparer(T) comparer) The BinarySearch method searches the
list for the first item that matches the specified item by using the specified comparer. If the
method finds an item that matches the criteria, it returns an index to that item; otherwise it
returns a negative number that is the bitwise complement of the next element that is larger
than item or the bitwise complement of Count if no item is larger.
C#
PersonComparer comparer = new PersonComparer();
List<Person> list = new List<Person>();
list.Add(new Person("Llyina", "Julia", 12, false));
list.Add(new Person("David", "Alexander", 44, true));
list.Add(new Person("Michael", "Allen", 25, true));
list.Add(new Person("Andrews", "Lisa", 42, false));
list.Add(new Person("Jeff", "Hay", 42, true));
list.Add(new Person("Kim", "Akers", 28, false));
Visual Basic
Dim comparer As New PersonComparer()
Output
The unsorted list is [[Llyina,Julia,12,F],[David,Alexander,44,M],[Michael,Allen,25,M],[Andre
ws,Lisa,42,F],[Jeff,Hay,42,M],[Kim,Akers,28,F]]
The sorted list is now [[Kim,Akers,28,F],[David,Alexander,44,M],[Michael,Allen,25,M],[Jeff,H
ay,42,M],[Llyina,Julia,12,F],[Andrews,Lisa,42,F]]
Found [Andrews,Lisa,42,F] at 5 in the sorted list.
The code sorts the list by using the comparer and then tries to find [“Andrews”,”Lisa”, 42, F]
by using the comparer. The item is located at index 5 in the sorted list.
int BinarySearch(int index, int count, T item, IComparer(T) comparer) This overload of the
BinarySearch method searches the list for the first item that matches the specified item by
using the specified comparer and range. If the method finds an item that matches the criteria,
it returns an index to that item; otherwise it returns a negative number that is the bitwise
complement of the next element that is larger than item or the bitwise complement of Count
if no item is larger.
C#
PersonComparer comparer = new PersonComparer();
List<Person> list = new List<Person>();
list.Add(new Person("Llyina", "Julia", 12, false));
list.Add(new Person("David", "Alexander", 44, true));
list.Add(new Person("Michael", "Allen", 25, true));
list.Add(new Person("Andrews", "Lisa", 42, false));
list.Add(new Person("Jeff", "Hay", 42, true));
list.Add(new Person("Kim", "Akers", 28, false));
Visual Basic
Dim comparer As New PersonComparer()
Output
The unsorted list is [[Llyina,Julia,12],[David,Alexander,44,M],[Michael,Allen,25,M],[Andrews
,Lisa,42,F],[Jeff,Hay,42,M],[Kim,Akers,28,F]]
The sorted list is now [[Kim,Akers,28,F],[David,Alexander,44,M],[Michael,Allen,25,M],[Jeff,H
ay,42,M],[Llyina,Julia,12,F],[Andrews,Lisa,42,F]]
Found [Andrews,Lisa,42,F] at -5 in the sorted list.
The code sorts the list by using the comparer and then tries to find [“Andrews”,”Lisa”, 42, F]
at elements 0 through 4 using the comparer. Because the item isn’t located, a -5 is returned.
You can find the index of the item that is greater than the item you searched for by using the
following.
C#
int higherIndex = ~found;
Visual Basic
Dim higherIndex As Integer = Not found
C#
List<int> list = new List<int>(new int[] { 1, 2, 4, 4, 4, 6, 7});
list[firstFour] = 3;
list[lastFour] = 5;
Visual Basic
Dim list As New List(Of Integer)(New Integer() {1, 2, 4, 4, 4, 6, 7})
list(firstFour) = 3
list(lastFour) = 5
The code changes the list from [1,2,4,4,4,6,7] to [1,2,3,4,5,6,7]. The IndexOf method locates
the first 4, and the LastIndexOf finds the last 4. The code then changes the element of the
first 4 to a 3 and the element with the last 4 to a 5.
int IndexOf(T item, int index) and int LastIndexOf(T item, int index)
The IndexOf and LastIndexOf methods return the index of the item that matches the speci-
fied item within the specified range. The methods search using a linear search algorithm,
and they use the default equality comparer to compare each item against the specified item.
The IndexOf method searches from index to the end of the list, and the LastIndexOf method
searches from the end of the list to index.
C#
List<int> list = new List<int>(new int[] { 1, 2, 7, 4, 1, 6, 7});
list[extra7] = 3;
list[extra1] = 5;
Visual Basic
Dim list As New List(Of Integer)(New Integer() {1, 2, 7, 4, 1, 6, 7})
list(extra7) = 3
list(extra1) = 5
Chapter 4 Generic Collections 265
The code changes the list from [1,2,7,4,1,6,7] to [1,2,3,4,5,6,7]. The IndexOf method locates
the first 1 after or at the item at index 3. The LastIndexOf finds the last 7 at or before index 3.
int IndexOf(T item, int index, int count) and int LastIndexOf(T item, int
index, int count)
The IndexOf and LastIndexOf methods return the index of the item that match the specified
item within the specified range. The item is searched for by using a linear search and using
the default equality comparer to compare the items in the list against the specified item. The
IndexOf method searches from the item at the position index for the number of items speci-
fied by count, and the LastIndexOf searches the list in reverse from the end to the number of
items specified by the count. Both methods return a -1 when matching item wasn’t found.
C#
List<int> list = new List<int>(new int[] { 1, 2, 6, 4, 2, 6, 7});
list[extra6] = 3;
list[extra2] = 5;
Visual Basic
Dim list As New List(Of Integer)(New Integer() {1, 2, 6, 4, 2, 6, 7})
list(extra6) = 3
list(extra2) = 5
The code changes the list from [1,2,6,4,2,6,7] to [1,2,3,4,5,6,7]. The IndexOf method locates
the first 6 between the elements 1 through 6, which is index through (index + count – 1). The
first 6 in that range is at index 2. The LastIndexOf finds the last 2 between the elements 5
through 0, which is index through (index - count + 1). The last 2 in that range is at index 5.
C#
List<int> list = new List<int>(new int[] { 1, 2, 3, 7, 6, 5, 4, 8 });
Visual Basic
Dim list As New List(Of Integer)(New Integer() {1, 2, 3, 7, 6, 5, 4, 8})
Output
Does the list contain a 3? Yes
Does the list contain a 21? No
The method checks to see whether 3 and 21 are in the list [1,2,3,7,6,5,4,8].
Using a Method to Check for Existence You can use the following method to do the
checks.
C#
static bool IsEven(int value)
{
return (value % 2) == 0;
}
Visual Basic
Function IsEven(ByVal value As Integer) As Boolean
Return ((value Mod 2) = 0)
End Function
C#
List<int> list = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8});
Visual Basic
Dim list As New List(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8})
Output
Does the list contain an even number? Yes
Chapter 4 Generic Collections 267
The code checks to see whether an even number exists in the list [1,2,3,4,5,6,7,8] by using
the IsEven method.
Using a Lambda Expression to Check for Existence You can use a lambda expression to
see whether there is an element present that matches the specified criteria as follows.
C#
List<int> list = new List<int>(new int[] { 1, 2, 3, 7, 6, 5, 4, 8 });
Visual Basic
Dim list As List(Of Integer) = New List(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8})
Output
Does the list contain an even number? Yes
The code checks to see whether an even number is present in the list [1,2,3,7,6,5,4,8] by
doing a mod operation on each element until it finds one that matches the criteria.
Using a Method to See Whether All Match You can use the TrueForAll method to check
whether all elements meet the specified criteria.
C#
static bool IsEven(int value)
{
return (value % 2) == 0;
}
268 Part II .NET Built-in Collections
Visual Basic
Function IsEven(ByVal value As Integer) As Boolean
Return ((value Mod 2) = 0)
End Function
Use the following code to use the method as the match parameter.
C#
List<int> list = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8});
List<int> evens = new List<int>(new int[] { 2, 4, 6, 8 });
Console.WriteLine("Does list contain all even numbers? {0}", allEvens ? "Yes" : "No");
allEvens = evens.TrueForAll(IsEven);
Console.WriteLine("Does evens contain all even numbers? {0}", allEvens ? "Yes" : "No");
Visual Basic
Dim list As New List(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8})
Dim evens As New List(Of Integer)(New Integer() {2, 4, 6, 8})
Console.WriteLine("Does list contain all even numbers? {0}", If(allEvens, "Yes", "No"))
Console.WriteLine("Does evens contain all even numbers? {0}", If(allEvens, "Yes", "No"))
Output
Does list contain all even numbers? No
Does evens contain all even numbers? Yes
The code checks to see whether the list [1,2,3,4,5,6,7,8] and [2,4,6,8] contains all even num-
bers by using the IsEven method.
Using a Lambda Expression to See Whether All Match You can use a lambda expression
to see whether all elements match specific criteria.
Chapter 4 Generic Collections 269
C#
List<int> list = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8});
List<int> evens = new List<int>(new int[] { 2, 4, 6, 8 });
Console.WriteLine("Does list contain all even numbers? {0}", allEvens ? "Yes" : "No");
Console.WriteLine("Does evens contain all even numbers? {0}", allEvens ? "Yes" : "No");
Visual Basic
Dim list As New List(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8})
Dim evens As New List(Of Integer)(New Integer() {2, 4, 6, 8})
Console.WriteLine("Does list contain all even numbers? {0}", If(allEvens, "Yes", "No"))
Console.WriteLine("Does evens contain all even numbers? {0}", If(allEvens, "Yes", "No"))
Output
Does list contain all even numbers? No
Does evens contain all even numbers? Yes
The code checks to see whether the list [1,2,3,4,5,6,7,8] and [2,4,6,8] contain all even num-
bers by using the mod operation.
C#
delegate TOutput Converter<TInput, TOutput>(TInput input);
Visual Basic
Public Delegate Function Converter(Of TInput,TOutput) (TInput) As TOutput
The method takes an argument as type T and returns an object of type TOutput.
Using a Method to Do the Conversion You can implement the Converter function as a
method. First, define a method, as shown in the following code.
C#
static string UnpackPhoneNumber(long phonenumber)
{
long areacode, exccode, subnum;
Visual Basic
Function UnpackPhoneNumber(ByVal phonenumber As Long) As String
Dim areacode As Long, exccode As Long, subnum As Long
C#
List<long> list = new List<long>(new long[] { 9015550100, 9015550188, 9015550113 });
Visual Basic
Dim list As New List(Of Long)(New Long() {9015550100, 9015550188, 9015550113})
The code turns the list [9015550100, 9015550188, 9015550113] into [(901) 555-0100,(901)
555-0188,(901) 555-0113], expanding each phone number from an 8-byte long to a
14-character string.
Using a Lambda Expression to Do the Conversion You can also use a lambda expression
to perform the conversion, as follows.
C#
List<long> list = new List<long>(new long[] { 9015550100, 9015550188, 9015550113 });
The preceding code turns the list [9015550100, 9015550188, 9015550113] into [(901) 555-
0100,(901) 555-0188,(901) 555-0113] by using some math to parse the phone numbers.
272 Part II .NET Built-in Collections
void Reverse()
The Reverse method reverses the order of the elements in the list, using the Array.Reverse
method.
C#
List<int> list = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8 });
list.Reverse();
Visual Basic
Dim list As New List(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8})
list.Reverse()
The list is changed from [1,2,3,4,5,6,7,8] to [8,7,6,5,4,3,2,1] after the Reverse method is called.
C#
List<int> list = new List<int>(new int[] { 1, 2, 3, 7, 6, 5, 4, 8 });
list.Reverse(3, 4);
Visual Basic
Dim list As List(Of Integer) = New List(Of Integer)(New Integer() {1, 2, 3, 7, 6, 5, 4, 8})
list.Reverse(3, 4)
The list is changed from [1,2,3,7,6,5,4,8]to [1,2,3,4,5,6,7,8] after calling the Reverse method.
void TrimExcess()
The TrimExcess method changes the capacity of the list when the list's Count is less than 90
percent of the current Capacity. You need to call both Clear and then TrimExcess to restore a
list to a condition similar to the original list created with the default constructor.
LinkedList(T) Overview
The LinkedList(T) class is a generic implementation of a doubly linked list. This class uses
LinkedListNode(T) for the nodes inside of the linked list. The LinkedList(T) class has no non-
generic equivalent.
Chapter 4 Generic Collections 273
More Info See Chapter 1 for more information about doubly linked lists.
LinkedList(T)()
This constructor creates a new empty linked list.
C#
LinkedList<String> llist = new LinkedList<String>();
Visual Basic
Dim llist As New LinkedList(Of String)()
LinkedList(T)(IEnumerable(T) collection)
This constructor creates a new linked list with the items in parameter collection as the items.
C#
LinkedList<string> llist = new LinkedList<string>(new string[] { "I", "like" });
Visual Basic
Dim llist As New LinkedList(Of String)(New String() {"I", "like"})
The code changes the list from ["I","like"] to ["I","like","dogs"]. The node containing “dogs” is
added after the Last node.
C#
LinkedList<string> llist = new LinkedList<string>(new string[] { "I", "like" });
llist.AddAfter(llist.Last, "dogs");
Visual Basic
Dim llist New LinkedList(Of String)(New String() {"I", "like"})
llist.AddAfter(llist.Last, "dogs")
The code changes the list from ["I","like"] to ["I","like","dogs"]. The node containing “dogs” is
added after the Last node.
C#
LinkedList<string> llist = new LinkedList<string>(new string[] { "like", "dogs" });
Visual Basic
Dim llist As New LinkedList(Of String)(New String() {"like", "dogs"})
The code changes the list from ["like","dogs"] to ["I","like","dogs"]. The node containing “I” is
added before the First node.
Chapter 4 Generic Collections 275
C#
LinkedList<string> llist = new LinkedList<string>(new string[] { "like", "dogs" });
llist.AddBefore(foundNode, "I");
Visual Basic
Dim llist As New LinkedList(Of String)(New String() {"like", "dogs"})
llist.AddBefore(foundNode, "I")
The code changes the list from ["like","dogs"] to ["I","like","dogs"]. The node containing “I” is
added before the found node. The Find method, which is discussed later in this chapter, is
used to find the node to add the newly created node in front of.
C#
LinkedList<string> llist = new LinkedList<string>(new string[] { "like", "dogs" });
llist.AddFirst("I");
Visual Basic
Dim llist As New LinkedList(Of String)(New String() {"like", "dogs"})
llist.AddFirst("I")
C#
LinkedList<string> llist = new LinkedList<string>(new string[] { "like", "dogs" });
llist.AddFirst(new LinkedListNode<string>("I"));
Visual Basic
Dim llist As New LinkedList(Of String)(New String() {"like", "dogs"})
The code changes the list from ["like","dogs"] to ["I","like","dogs"]. The LinkListNode(T) object is
created with the value “I” and added to the beginning of the list.
C#
LinkedList<string> llist = new LinkedList<string>(new string[] { "I", "like" });
llist.AddLast("dogs");
Visual Basic
Dim llist As New LinkedList(Of String)(New String() {"I", "like" })
llist.AddLast("dogs")
C#
LinkedList<string> llist = new LinkedList<string>(new string[] { "I", "like" });
llist.AddLast(new LinkedListNode<string>("dogs"));
Visual Basic
Dim llist As New LinkedList(Of String)(New String() {"I", "like"})
The code changes the list from ["I","like"] to ["I","like","dogs"]. The LinkListNode(T) object is
created with the value “dogs” and added to the end of the list.
Chapter 4 Generic Collections 277
void Clear()
The Clear method removes all nodes from the list. All references in the nodes are released.
C#
LinkedList<string> llist = new LinkedList<string>(new string[] { "I", "love", "dogs" });
llist.Clear();
Visual Basic
Dim llist As New LinkedList(Of String)(New String() {"I", "love", "dogs"})
llist.Clear()
The linked list does not contain any elements after the Clear method is called.
C#
LinkedList<string> llist = new LinkedList<string>(new string[] { "I", "don't", "like",
"dogs" });
llist.Remove("don't");
Visual Basic
Dim llist As New LinkedList(Of String)(New String() {"I", "don't", "like", "dogs"})
llist.Remove("don't")
The code changes the list from ["I","don't","like","dogs"] to ["I","like","dogs"] after the Remove
method is called.
C#
LinkedList<string> llist = new LinkedList<string>(new string[] { "I", "don't", "like",
"dogs" });
Visual Basic
Dim llist As New LinkedList(Of String)(New String() {"I", "don't", "like", "dogs"})
The code changes the list from ["I","don't","like","dogs"] to ["I","like","dogs"] after the Remove
method is called. You can use the Find method to find the node you want to remove if you
do not already have it.
void RemoveFirst()
The RemoveFirst method removes the first node of the list. An InvalidOperationException is
thrown if you call this method on an empty list.
C#
LinkedList<int> llist = new LinkedList<int>(new int[] { 1, 1, 2, 3, 4 });
llist.RemoveFirst();
Visual Basic
Dim llist As New LinkedList(Of Integer)(New Integer() {1, 1, 2, 3, 4})
llist.RemoveFirst()
The list is changed from [1,1,2,3,4] to [1,2,3,4] when the RemoveFirst method is called.
void RemoveLast()
The RemoveLast method removes the last node of the list. An InvalidOperationException is
thrown if you call this method on an empty list.
C#
LinkedList<int> llist = new LinkedList<int>(new int[] { 1, 2, 3, 4, 4 });
llist.RemoveLast();
Visual Basic
Dim llist As New LinkedList(Of Integer)(New Integer() {1, 2, 3, 4, 4})
llist.RemoveLast()
The list is changed from [1,2,3,4,4] to [1,2,3,4] when the RemoveLast method is called.
Chapter 4 Generic Collections 279
C#
LinkedList<int> llist = new LinkedList<int>(new int[] { 1, 2, 4, 4, 4, 6 });
Print(llist.ToArray());
llist.Find(4).Value = 3;
llist.FindLast(4).Value = 5;
Print(llist.ToArray());
Visual Basic
Dim llist As New LinkedList(Of Integer)(New Integer() {1, 2, 4, 4, 4, 6})
Print(llist.ToArray())
llist.Find(4).Value = 3
llist.FindLast(4).Value = 5
Print(llist.ToArray())
Output
[1,2,4,4,4,6]
[1,2,3,4,5,6]
The code changes the linked list from [1,2,4,4,4,6] to [1,2,3,4,5,6]. It does so by locating the
first 4 in the group and changing it to 3 and then finding the last 4 in the group and changing
it to 5.
280 Part II .NET Built-in Collections
C#
LinkedList<string> llist = new LinkedList<string>(new string[] { "I", "love", "dogs" });
Console.WriteLine(llist.First.Value);
Console.WriteLine(llist.Last.Value);
Visual Basic
Dim llist As New LinkedList(Of String)(New String() {"I", "love", "dogs"})
Console.WriteLine(llist.First.Value)
Console.WriteLine(llist.Last.Value)
Output
I
dogs
C#
LinkedList<string> llist = new LinkedList<string>(new string[] { "I", "like", "dogs" });
Visual Basic
Dim llist As New LinkedList(Of String)(New String() {"I", "love", "dogs"})
Output
The list contains 3 nodes.
The output shows the list as having 3 nodes because it contains [“I”,”like”,”dogs”].
Chapter 4 Generic Collections 281
C#
LinkedList<string> llist = new LinkedList<string>(new string[] { "I", "like", "dogs" });
Visual Basic
Dim llist As LinkedList(Of String) = New LinkedList(Of String)(New String() {"I", "love",
"dogs"})
Output
Does the list contain "dogs"? Yes
Does the list contain "cats"? No
The code checks to see whether “dogs” and “cats” are in the list.
Summary
In this chapter, you learned about the equality and ordering comparers. Understanding the
comparers helps you understand how the .NET Framework compares and orders objects. You
also learned about the built-in array class called List(T). You learned how to use it and how it
differs from the nongeneric implementation called ArrayList. And finally, you learned how to
create a generic linked list by using the LinkedList(T) class.
Chapter 5
Generic and Support Collections
After completing this chapter, you will be able to
■ Use the Queue(T) class.
■ Use the Stack(T) class.
■ Use the Dictionary(TKey, TValue) class.
■ Use the BitArray class.
■ Use the CollectionBase and DictionaryBase classes.
■ Use the HashSet(T) class.
■ Use SortedList(TKey,TValue) and SortedDictionary(TKey,TValue) classes.
Queue(T) Overview
The Queue(T) class is a generic implementation of a First In, First Out (FIFO) collection or
queue, as discussed in Chapter 3, “Understanding Collections: Queues, Stacks, and Circular
Buffers.”
The Queue(T) class uses an array for data storage in the same way that the QueuedArray(T)
class does in Chapter 3
Pushing items onto a queue that has full capacity takes O(n) operations; otherwise it takes
only O(1) operations. Popping an item from the queue always take O(1) operations.
The Queue(T) class is the generic version of the Queue class, so it does not have to box value
types like the nongeneric Queue class must do.
283
284 Part II .NET Built-in Collections
Creating a Queue
You can create a queue by using one of the three constructors described in the following
sections.
Queue(T)()
The default constructor can be used to create an empty queue as follows.
C#
Queue<int> queue = new Queue<int>();
Visual Basic
Dim queue As New Queue(Of Integer)()
Queue(T)(IEnumerable(T))
You can create a queue from another collection that contains the items you want to copy to
the queue. Each item is added to the queue in the order it is enumerated.
C#
int[] values = new int[] { 34, 2, 1, 88, 53 };
Queue<int> queue = new Queue<int>(values);
Visual Basic
Dim values As New Integer() {34, 2, 1, 88, 53}
Dim queue As New Queue(Of Integer)(values)
After the code is executed, queue contains the values [34, 2, 1, 88, 53].
Queue(T)(int size)
You can create a queue with an initial capacity.
C#
Queue<int> queue = new Queue<int>(100);
Visual Basic
Dim queue As New Queue(Of Integer)(100)
After the code is executed, queue contains an empty queue with a capacity of 100.
Chapter 5 Generic and Support Collections 285
C#
Queue<int> queue = new Queue<int>(new int [] { 22, 3, 6, 19 });
queue.Enqueue(33);
Visual Basic
Dim queue As New Queue(Of Integer)(New Integer() {22, 3, 6, 19})
queue.Enqueue(33)
The queue is changed from [22,3,6,19] to [22,3,6,19,33] after the Enqueue method is called
with the item 33.
void Clear()
All items are removed when the Clear method is called.
C#
Queue<int> queue = new Queue<int>(new int [] { 22, 3, 6, 19 });
queue.Clear();
Visual Basic
Dim queue As New Queue(Of Integer)(New Integer() {22, 3, 6, 19})
queue.Clear()
The queue is changed from [22,3,6,19] to [] after the Clear method is called.
286 Part II .NET Built-in Collections
T Dequeue()
An item can be removed from the queue by calling the Dequeue method. The Dequeue
method always removes the first item from the queue.
C#
Queue<int> queue = new Queue<int>(new int [] { 22, 3, 6, 19 });
Console.WriteLine(queue.Dequeue());
Visual Basic
Dim queue As New Queue(Of Integer)(New Integer() {22, 3, 6, 19})
Console.WriteLine(queue.Dequeue())
Output
22
The queue changes from [22,3,6,19] to [3,6,19] after the Dequeue method is called. That is
because item 22 is removed from the list and written to the console.
T Peek()
You can use the Peek method when you want to view the first element in the queue without
changing the contents of it.
C#
Queue<int> queue = new Queue<int>(new int [] { 22, 3, 6, 19 });
Console.WriteLine(queue.Peek());
Visual Basic
Dim queue As New Queue(Of Integer)(New Integer() {22, 3, 6, 19})
Console.WriteLine(queue.Peek())
Output
22
The queue remains [22,3,6,19] after the Peek method is called. The first element in the
queue, 22, is written to the console. This allows you to view the item you are going to remove
without removing it.
Chapter 5 Generic and Support Collections 287
C#
Queue<int> queue = new Queue<int>(new int [] { 22, 3, 6, 19 });
Visual Basic
Dim queue As New Queue(Of Integer)(New Integer() {22, 3, 6, 19})
Output
Does the queue contain a 6? Yes
Does the queue contain a 5? No
The code checks to see whether 6 and 5 are contained in the queue [22, 3, 6, 19].
int Count
The Count property returns the number of items currently in the queue.
C#
Queue<int> queue = new Queue<int>(new int[] { 22, 3, 6, 19});
Visual Basic
Dim queue As New Queue(Of Integer)(New Integer() {22, 3, 6, 19})
Output
The queue contains 4 items.
The Count is equal to 4 because the stack contains the elements [22, 3, 6, 19].
void TrimExcess()
The TrimExcess method reduces the capacity of the queue to the actual Count size. It is impor-
tant to note that the method only executes if the Count is less than 90 percent of the Capacity.
Stack(T) Overview
The Stack(T) class is a generic implementation of a Last In, First Out (LIFO) collection or a
stack as discussed in Chapter 3. This class uses an array for its internal data storage.
More Info For more information about how arrays work, see Chapter 1, “Understanding
Collections: Arrays and Linked Lists.”
Pushing onto a stack that has full capacity takes O(n) operations; otherwise it takes only O(1)
operations. Popping always takes O(1) operations.
The Stack(T) class is the generic version of the Stack class, so it does not have to box value
types like the Stack class has to do.
Creating a Stack
You can create a stack by using one of the three constructors described in the following
sections.
Stack(T)()
The default constructor can be used to create an empty stack as follows.
C#
Stack<int> stack = new Stack<int>();
Visual Basic
Dim stack As New Stack(Of Integer)()
Chapter 5 Generic and Support Collections 289
Stack (T)(IEnumerable(T))
You can also create a stack from another collection that contains the items you want to copy
to the stack.
C#
int[] values = new int[] { 34, 2, 1, 88, 53 };
Stack<int> stack = new Stack<int>(values);
Visual Basic
Dim values As New Integer() {34, 2, 1, 88, 53}
Dim stack As New Stack(Of Integer)(values)
C#
Stack<int> stack = new Stack<int>(100);
Visual Basic
Dim stack As New Stack(Of Integer)(100)
After the code is executed, stack contains an empty stack with a capacity of 100.
C#
Stack<int> stack = new Stack<int>(new int[] { 6, 87, 13, 29, 7 });
stack.Push(46);
Visual Basic
Dim stack As New Stack(Of Integer)(New Integer() {6, 87, 13, 29, 7})
stack.Push(46)
The stack is changed from [6,87,13,29,7] to [6,87,13,29,7,46] after the Push method is called.
This is because the Push method adds the item, 46, to the top of the stack.
290 Part II .NET Built-in Collections
void Clear()
All items can be removed from the stack by using the Clear method.
C#
Stack<int> stack = new Stack<int>(new int[] { 6, 87, 13, 29, 7 });
stack.Clear();
Visual Basic
Dim stack As New Stack(Of Integer)(New Integer() {6, 87, 13, 29, 7})
stack.Clear()
The stack is changed from [6,87,13,29,7] to [] after the Clear method is called. This is because
the Clear method removes all items from the stack.
T Pop()
The Pop method can be used to remove the item from the top of the stack. The top of the
stack is always the last item added.
C#
Stack<int> stack = new Stack<int>(new int[] { 6, 87, 13, 29, 7 });
Console.WriteLine(stack.Pop());
Visual Basic
Dim stack As New Stack(Of Integer)(New Integer() {6, 87, 13, 29, 7})
Console.WriteLine(stack.Pop())
Output
7
The stack is changed from [6,87,13,29,7] to [6,87,13,29] after the Pop method is called. This is
because the Pop method removes the last item in the stack.
Chapter 5 Generic and Support Collections 291
T Peek()
The Peek method can be used to view the item at the top of the stack. The item at the top of
the stack is returned but not removed.
C#
Stack<int> stack = new Stack<int>(new int[] { 6, 87, 13, 29, 7 });
Console.WriteLine(stack.Peek());
Visual Basic
Dim stack As New Stack(Of Integer)(New Integer() {6, 87, 13, 29, 7})
Console.WriteLine(stack.Peek())
Output
7
The stack remains [6,87,13,29,7] after the Peek method is called because it doesn’t alter the
stack. In this case, a 7 is returned from the Peek method.
C#
Stack<int> stack = new Stack<int>(new int[] { 6, 87, 13, 29, 7 });
Visual Basic
Dim stack As New Stack(Of Integer)(New Integer() {6, 87, 13, 29, 7})
Output
Does the stack contain a 13? Yes
Does the stack contain a 12? No
The code checks to see whether 13 and 12 exist in the stack [6,87,13,29,7].
292 Part II .NET Built-in Collections
int Count
The Count property returns the number of items currently in the stack.
C#
Stack<int> stack = new Stack<int>(new int[] { 6, 87, 13, 29, 7 });
Visual Basic
Dim stack As New Stack(Of Integer)(New Integer() {6, 87, 13, 29, 7})
Output
The stack contains 5 items.
The Count is equal to 5 because the stack contains the elements [7,29,13,87,6].
TrimExcess
The TrimExcess method reduces the capacity of the stack to the actual Count size. It is im-
portant to note that the method executes only if the Count is less than 90 percent of the
Capacity.
Dictionary(TKey,TValue) Overview
The Dictionary(TKey,TValue) class is a generic associative array that uses a hash table for its
internal implementation. You learned about associative arrays in Chapter 2, “Understanding
Collections: Associative Arrays.” The Hashtable class is not used to implement the hash table
used by the Dictionary(TKey, TValue) class.
Adding to a dictionary that is full takes O(n) operations; otherwise it takes only O(1) opera-
tions. Removing approaches an O(1) operation.
The Dictionary(T) class is the generic version of the Hashtable class, so it does not have to
box value types like the Hashtable class has to do.
Chapter 5 Generic and Support Collections 293
With a lookup, the bucket is determined by using the same procedure that is used with add-
ing an item. After the bucket is determined, the linked list associated with that bucket is ac-
cessed and then traversed to find the item you are looking for. Each node contains the hash
code of the item that is stored in it. The hash code is first compared against the hash code of
the item you are looking at for performance reasons. It is faster to do an integer comparison
than the majority of other data comparisons. If that hash code comparison passes, the key is
then compared and the procedure continues to the next node if the key comparison fails.
With a remove, the item is determined using the same procedure that is used with looking
up an item. After the item is found, the node is then removed from the linked list and added
to a freed linked list so that the node can be used with future calls on the add procedure.
C#
static void Print<T>(ArrayEx<T> list)
{
Console.Write("[");
bool first = true;
for (int i = 0; i < list.Count; ++i)
294 Part II .NET Built-in Collections
{
if (!first)
{
Console.Write(",");
}
first = false;
Console.Write(list[i]);
}
Console.WriteLine("]");
}
Visual Basic
Sub Print(Of T)(ByVal array As T())
Console.Write("[")
Dim first As Boolean = True
For Each item As T In Array
If (Not first) Then
Console.Write(",")
End If
first = False
Console.Write(item)
Next
Console.WriteLine("]")
End Sub
Creating a Dictionary
You can create a dictionary by using one of the six constructors described in the following
sections.
Dictionary(TKey, TValue)()
This constructor creates an empty dictionary.
C#
Dictionary<string, int> dictionary = new Dictionary<string, int>();
Visual Basic
Dim dictionary As New Dictionary(Of String, Integer)()
C#
Dictionary<string, int> dictionary = new Dictionary<string, int>();
dictionary["one"] = 1;
dictionary["two"] = 2;
dictionary["three"] = 3;
Visual Basic
Dim dictionary As Dictionary(Of String, Integer) = New Dictionary(Of String, Integer)()
dictionary("one") = 1
dictionary("two") = 2
dictionary("three") = 3
The str2num variable is created with the items [[one, 1],[two, 2],[three, 3]].
C#
Dictionary<string, int> dictionary = new Dictionary<string, int>
(StringComparer.CurrentCultureIgnoreCase);
dictionary["one"] = 1;
dictionary["two"] = 2;
dictionary["three"] = 3;
Visual Basic
Dim dictionary As New Dictionary(Of String, Integer) _
(StringComparer.CurrentCultureIgnoreCase)
dictionary("one") = 1
dictionary("two") = 2
dictionary("three") = 3
The preceding code creates a new dictionary object with a string comparer that ignores case.
If the default comparer is used, it does not ignore case and throws an exception on int one =
dictionary[“One”]; for C# or Dim one as Integer = dictionary(“One”) for Visual Basic.
296 Part II .NET Built-in Collections
C#
Dictionary<string, int> dictionary = new Dictionary<string, int>(200);
Visual Basic
Dim dictionary As New Dictionary(Of String, Integer)(200)
C#
Dictionary<string, int> dictionary = new Dictionary<string, int>();
dictionary["one"] = 1;
dictionary["two"] = 2;
dictionary["three"] = 3;
Visual Basic
Dim dictionary As New Dictionary(Of String, Integer)()
dictionary("one") = 1
dictionary("two") = 2
dictionary("three") = 3
The str2num variable is created with the items [[one, 1],[two, 2],[three, 3]] and the
StringComparer.CurrentCultureIgnoreCase comparer.
C#
Dictionary<string, int> dictionary = new Dictionary<string, int>(25,
StringComparer.CurrentCultureIgnoreCase);
Chapter 5 Generic and Support Collections 297
Visual Basic
Dim dictionary As New Dictionary(Of String, Integer) _
(25, StringComparer.CurrentCultureIgnoreCase)
C#
Dictionary<int, String> dictionary = new Dictionary<int, String>();
Visual Basic
Dim dictionary As New Dictionary(Of Integer, String)()
The dictionary contains [[9688, Barbara Zighetti],[9689, Eric Gruber]] after the code is execut-
ed. “Barbara Zighetti” is associated with the employee ID of 9688, and “Eric Gruber” is associ-
ated with the employee ID of 9689.
C#
Dictionary<int, String> dictionary = new Dictionary<int, String>();
Visual Basic
Dim dictionary As New Dictionary(Of Integer, String)()
The dictionary contains [[9688, Barbara Zighetti],[9689, Eric Gruber]] after the code is execut-
ed. “Barbara Zighetti” is associated with the employee ID of 9688, and “Eric Gruber” is associ-
ated with the employee ID of 9689.
void Clear()
The Clear method removes all keys and values from the dictionary. The capacity of the dic-
tionary does not change.
C#
Dictionary<int, String> dictionary = new Dictionary<int, String>();
dictionary.Clear();
Visual Basic
Dim dictionary As New Dictionary(Of Integer, String)()
dictionary.Clear()
The dictionary is changed from [[9688, Barbara Zighetti],[9689, Eric Gruber]] to [] after the
code is executed. The Clear method removes all keys and values from the dictionary.
C#
Dictionary<int, String> dictionary = new Dictionary<int, String>();
dictionary.Remove(9689);
Visual Basic
Dim dictionary As New Dictionary(Of Integer, String)()
dictionary.Remove(9689)
The dictionary is changed from [[9688, Barbara Zighetti],[9689, Eric Gruber]] to [[9688,
Barbara Zighetti]] after the code is executed. The Remove method removes the entry [9689,
Eric Gruber] because it is associated with the specified key 9689.
C#
Dictionary<int, String> dictionary = new Dictionary<int, String>();
try
{
string name;
else
{
Console.WriteLine("9999 wasn't found in the dictionary");
}
}
catch (Exception)
{
Console.WriteLine("An exception was thrown.");
}
Visual Basic
Dim dictionary As New Dictionary(Of Integer, String)()
Output
9999 wasn't found in the dictionary
As you can see, the TryGetValue does not throw an exception if the key doesn’t exist in the
dictionary. A false is returned if the key doesn’t exist; otherwise true is returned.
C#
Dictionary<int, String> dictionary = new Dictionary<int, String>();
try
{
Console.WriteLine("\"{0}\" is associated with 9690", dictionary[9690]);
}
catch (KeyNotFoundException ex)
{
Console.WriteLine("9890 wasn't found in the dictionary");
}
Chapter 5 Generic and Support Collections 301
Visual Basic
Dim dictionary As New Dictionary(Of Integer, String)()
Try
Console.WriteLine("""{0}"" is associated with 9690", dictionary(9690))
Catch ex As KeyNotFoundException
Console.WriteLine("9890 wasn't found in the dictionary")
End Try
Output
"Eric Gruber" is associated with 9689
9890 wasn't found in the dictionary
As you can see, the Item property returns the value if the key exists, or it throws a
KeyNotFoundException if it doesn’t.
C#
Dictionary<int, String> dictionary = new Dictionary<int, String>();
if (dictionary.ContainsKey(9689))
{
Console.WriteLine("9689 was found in the dictionary");
}
else
{
Console.WriteLine("9689 was not found in the dictionary");
}
if (dictionary.ContainsKey(9690))
302 Part II .NET Built-in Collections
{
Console.WriteLine("9690 was found in the dictionary");
}
else
{
Console.WriteLine("9690 was not found in the dictionary");
}
Visual Basic
Dim dictionary As New Dictionary(Of Integer, String)()
If (dictionary.ContainsKey(9689)) Then
Console.WriteLine("9689 was found in the dictionary")
Else
Console.WriteLine("9689 was not found in the dictionary")
End If
If (dictionary.ContainsKey(9690)) Then
Console.WriteLine("9690 was found in the dictionary")
Else
Console.WriteLine("9690 was not found in the dictionary")
End If
Output
9689 was found in the dictionary
9690 was not found in the dictionary
The code outputs the results of using the ContainsKey method with 9689 and 9690 on a dic-
tionary that contains [[9688, Barbara Zighetti],[9689, Eric Gruber]].
C#
Dictionary<int, String> dictionary = new Dictionary<int, String>();
if (dictionary.ContainsValue("Barbara Zighetti"))
{
Console.WriteLine("\"Barbara Zighetti\" was found in the dictionary");
}
else
Chapter 5 Generic and Support Collections 303
{
Console.WriteLine("\"Barbara Zighetti\" was not found in the dictionary");
}
if (dictionary.ContainsValue("David Wright"))
{
Console.WriteLine("\"David Wright\" was found in the dictionary");
}
else
{
Console.WriteLine("\"David Wright\" was not found in the dictionary");
}
Visual Basic
Dim dictionary As New Dictionary(Of Integer, String)()
Output
"Barbara Zighetti" was found in the dictionary
"David Wright" was not found in the dictionary
The code outputs the results of using the ContainsValue method with “Barbara Zighetti” and
“David Wright” on a dictionary that contains [[9688, Barbara Zighetti],[9689, Eric Gruber]].
int Count
The Count property returns the number of key value pairs in the dictionary.
C#
Dictionary<int, String> dictionary = new Dictionary<int, String>();
Visual Basic
Dim dictionary As New Dictionary(Of Integer, String)()
Output
The dictionary contains 2 items.
Dictionary<TKey,TValue>.KeyCollection Keys
The Keys property can be used to retrieve all the keys in the dictionary. The keys are not in a
specific order but are returned in the same order as their associated values are returned from
the Values property. It is important to note that the Keys collection is dynamic. If subsequent
additions/deletions are made to the dictionary, they are reflected in the Keys collection.
C#
Dictionary<int, String> dictionary = new Dictionary<int, String>();
Dictionary<int, String>.KeyCollection keys = dictionary.Keys;
Visual Basic
Dim dictionary as New Dictionary(Of Integer, String)()
Dim keys As Dictionary(Of Integer, String).KeyCollection = dictionary.Keys
Output
The keys in the dictionary are:
[9688,9689]
Adding [9687,"David Wright"]
The keys in the dictionary are:
[9688,9689,9687]
The dictionary contains [[9688, Barbara Zighetti],[9689, Eric Gruber]] at the first Print but
then is changed to [[9688, Barbara Zighetti],[9689, Eric Gruber],[9687, David Wright]]. Notice
how the collection changes to reflect the update.
Dictionary<TKey,TValue>.ValueCollection Values
The Values property can be used to retrieve all the values in the dictionary. The values are not
in a specific order but are in the same order as the associated keys in the Keys property. As
with the Keys collection, the Values collection is dynamic. If subsequent additions/deletions
are made to the dictionary, they are reflected in the Values collection.
C#
Dictionary<int, String> dictionary = new Dictionary<int, String>();
Dictionary<int, String>.ValueCollection values = dictionary.Values;
Visual Basic
Dim dictionary As New Dictionary(Of Integer, String)()
Dim values As Dictionary(Of Integer, String).ValueCollection = dictionary.Values
Output
The values in the collection are:
[Barbara Zighetti,Eric Gruber]
Adding [9687,"David Wright"]
The values in the collection are:
[Barbara Zighetti,Eric Gruber,David Wright]
The dictionary contains [[9688, Barbara Zighetti],[9689, Eric Gruber]] at the first Print but
then is changed to [[9688, Barbara Zighetti],[9689, Eric Gruber],[9687, David Wright]]. Notice
how the collection changes to reflect the update.
BitArray Overview
You can use the BitArray class to manipulate an array of Booleans that represent bits. Each bit
value is represented as a Boolean.
The BitArray class stores each bit in a compact form. Bit 0 could represent the most signifi-
cant bit (MSB) or the least significant bit (LSB). This book displays the 0 bit as the LSB and
displays the bits in groups of four. The value 0000 0010 would represent only bit 1 being set
in a group of eight bits, 0001 0010 would represent only bits 1 and 4 being set in a group of
eight bits. Displaying bits this way helps you if you need to put the bits in a calculator to get
the integer form.
The BitArray class is defined in the System.Collections namespace and has no generic
equivalent.
Creating a BitArray
You can create a BitArray object using the six constructors. The BitArray class does not have a
default constructor.
BitArray(BitArray bits)
Use this constructor when you want to create a copy of another BitArray instance.
Chapter 5 Generic and Support Collections 307
C#
BitArray x = new BitArray(8, false);
x.Set(2, true);
After the code executes, x and y both contain eight bits, and bit 2 will be the only bit set true.
BitArray(Boolean[] values)
This constructor creates a BitArray instance with the bits set according to the values passed
into it. The first element is bit 0, the second is bit 1, and so on.
C#
BitArray bits = new BitArray(new bool[] { false, true, false, false });
Visual Basic
Dim bits As New BitArray(New Boolean() {False, True, False, False})
BitArray(Byte[] bytes)
This constructor creates a BitArray by using the specified bytes. Each byte represents a set of
eight bits. The first byte represents the first set, the second byte the second set, and so on.
The first set would be bits 0 through 7, the second set would be the next eight (bits 8 through
15), the third set would be the next eight bits (16 through 23), and so on. The LSB bit of each
byte is copied first; bytes[0] & 1 would be bit 0, bytes[0] & 2 would be bit 1, bytes[1] & 0 would
be bit 8, and so on.
C#
BitArray bits = new BitArray(new byte[] { 1, 2 });
Visual Basic
Dim bits As New BitArray(New Byte() {1, 2})
The executed code creates a BitArray that is set to 0000 0010 0000 0001. Byte 0 would set
bits 0 through 7 to 0000 0001, and byte 1 would set bits 8 through 15 to 0000 0010. This
book then shows the bits as 15 through 0 by flipping the bits to 0000 0010 0000 0001.
C#
BitArray bits = new BitArray(4, true);
Visual Basic
Dim bits As New BitArray(4, True)
BitArray(Int32 length)
Calling this constructor is the same as calling the previous constructor with defaultValue set
to false. See the BitArray(int32 length, Boolean defaultValue) constructor discussed in the pre-
vious section for more details.
BitArray(Int32[] values)
This constructor works the same way as the BitArray(Byte[]) constructor except each set now
represents 32 bits instead of 8. So the first set would be 0 through 31, the second set would
be 32 through 62, and so on. See the BitArray(Bytes[] bytes) constructor described earlier in
this chapter for more details.
C#
BitArray bits = new BitArray(4);
bits.Set(0, true);
bits.Set(1, true);
Visual Basic
Dim bits As New BitArray(4)
bits.Set(0, True)
bits.Set(1, True)
The preceding code creates a four-bit array and changes the bits from the initial value of
0000 to 0011.
Chapter 5 Generic and Support Collections 309
C#
BitArray bits = new BitArray(4);
bits.SetAll(true);
Visual Basic
Dim bits As New BitArray(4)
bits.SetAll(True)
The preceding code creates a four-bit array and changes the bits from an initial value of 0000
to 1111.
C#
BitArray bits = new BitArray(new byte[] { 3 });
Visual Basic
Dim bits As New BitArray(New Byte() {3})
Output
3 is stored as 00000011
The executed code displays the 3 as 0000 0011. You also get the number 3 if you enter 0000
0011 into a calculator in binary form and convert it to decimal.
C#
BitArray bits = new BitArray(4);
Visual Basic
Dim bits As New BitArray(4)
Output
bit 0 is False
Negating bit 0
bit 0 is now True
C#
[Flags()]
public enum FileFlags
{
IsArchived = 0,
IsReadOnly = 1,
IsCompressed = 2,
IsHidden = 3,
NUM_FLAGS
}
Visual Basic
<Flags()>_
Public Enum FileFlags
IsArchived = 0
IsReadOnly = 1
IsCompressed = 2
IsHidden = 3
NUM_FLAGS
End Enum
Chapter 5 Generic and Support Collections 311
But what if you want to store more than one flag in the same variable? This is when the
OR operation comes in. Using OR, you can combine more than one flag because flags
are represented as bits and the OR operation sets the resulting bit if either one of the
operands have the bit set. So, if you wanted to store Fi and Fj, you would say int value =
2 ^ i | 2 ^ j (using C#).
Now how could you check to see whether the bit FX is set? This is where the AND op-
eration comes in. If you AND the value with FX , the result would be 0 if the bit FX is not
set; the result would be FX if it is set. The AND operation sets the bit in the result if and
only if both operands have the same bit set. So to check and see whether FX is con-
tained in value, you could do (value & 2^x) == 2^x) (using C#).
Now what if you wanted to remove a flag from the value? This is where the NOT opera-
tion comes in. The NOT operation turns off all bits that are on and turns on all bits that
are off. So, ~( FX ) would mean all flags except FX are on. If you perform the operation
(~(FX ) & value), you will be ANDing every flag except FX with 1. This results in any flag
that was on staying on and Fx being turned off.
Each value represents the file being archived, read-only, compressed, or hidden. The first
value is for bit 0, the second is for bit 1, and so on. You could have a BitArray represent those
flags. To display the flags, you could define the following method.
C#
static string FlagsToString(BitArray bits)
{
StringBuilder sb = new StringBuilder();
sb.Append(((FileFlags)i).ToString());
}
return sb.ToString();
}
Visual Basic
Function FlagsToString(ByVal bits As BitArray) As String
Dim sb As New System.Text.StringBuilder()
Return sb.ToString()
End Function
This method returns a string that contains all the flags that are true in the BitArray by walking
each flag and checking to see whether it is set in the BitArray.
C#
static BitArray FlagsToBitArray(params FileFlags[] flags)
{
BitArray retval = new BitArray((int)FileFlags.NUM_FLAGS);
return retval;
}
Visual Basic
Function FlagsToBitArray(ByVal ParamArray flags As FileFlags()) As BitArray
Dim retval As New BitArray(Int(FileFlags.NUM_FLAGS))
Return retval
End Function
The next sections use these methods to demonstrate the following BitArray methods.
Chapter 5 Generic and Support Collections 313
C#
BitArray bits = FlagsToBitArray(FileFlags.IsCompressed, FileFlags.IsArchived,
FileFlags.IsHidden);
Console.WriteLine("bits = {0}", FlagsToString(bits));
Console.WriteLine("Keep only IsArchived on if it is on");
bits = bits.And(FlagsToBitArray(FileFlags.IsArchived));
Console.WriteLine("bits = {0}", FlagsToString(bits));
Visual Basic
Dim bits As BitArray = FlagsToBitArray(FileFlags.IsCompressed, FileFlags.IsArchived, _
FileFlags.IsHidden)
Console.WriteLine("bits = {0}", FlagsToString(bits))
Console.WriteLine("Keep only IsArchived on if it is on")
bits = bits.And(FlagsToBitArray(FileFlags.IsArchived))
Console.WriteLine("bits = {0}", FlagsToString(bits))
Output
bits = IsArchived | IsCompressed | IsHidden
Keep only IsArchived on if it is on
bits = IsArchived
BitArray Not()
The Not method performs a bitwise NOT on all the bits of the current BitArray. The method
creates another BitArray object that contains the result of the NOT on each bit. The meth-
od essentially flips each bit of the current BitArray.
C#
BitArray bits = FlagsToBitArray(FileFlags.IsCompressed, FileFlags.IsArchived,
FileFlags.IsHidden);
Console.WriteLine("bits = {0}", FlagsToString(bits));
Console.WriteLine("Flip all flags");
bits = bits.Not();
Console.WriteLine("bits = {0}", FlagsToString(bits));
Visual Basic
Dim bits As BitArray = FlagsToBitArray(FileFlags.IsCompressed, _
FileFlags.IsArchived, FileFlags.IsHidden)
Console.WriteLine("bits = {0}", FlagsToString(bits))
Console.WriteLine("Flip all flags")
bits = bits.Not()
Console.WriteLine("bits = {0}", FlagsToString(bits))
314 Part II .NET Built-in Collections
Output
bits = IsArchived | IsCompressed | IsHidden
Flip all flags
bits = IsReadOnly
C#
BitArray bits = FlagsToBitArray(FileFlags.IsCompressed, FileFlags.IsArchived,
FileFlags.IsHidden);
Console.WriteLine("bits = {0}", FlagsToString(bits));
Console.WriteLine("Turning IsReadOnly flag on");
bits = bits.Or(FlagsToBitArray(FileFlags.IsReadOnly));
Console.WriteLine("bits = {0}", FlagsToString(bits));
Visual Basic
Dim bits As BitArray = FlagsToBitArray(FileFlags.IsCompressed, _
FileFlags.IsArchived, FileFlags.IsHidden)
Console.WriteLine("bits = {0}", FlagsToString(bits))
Console.WriteLine("Turning IsReadOnly flag on")
bits = bits.Or(FlagsToBitArray(FileFlags.IsReadOnly))
Console.WriteLine("bits = {0}", FlagsToString(bits))
Output
bits = IsArchived | IsCompressed | IsHidden
Turning IsReadOnly flag on
bits = IsArchived | IsReadOnly | IsCompressed | IsHidden
You may have noticed that this code is like executing bits[(int)FileFlags.IsReadOnly] = true.
C#
static string PrintString(BitArray bits)
{
string str = "";
byte value = 0;
if (i % 8 == 7)
{
str = (char)value + str;
value = 0;
}
}
return str;
}
Visual Basic
Function PrintString(ByVal bits As BitArray) As String
Dim str As String = ""
Return str
End Function
A long time ago developers used an XOR operation to encrypt text. The following code does
just that. I would never suggest using it or showing it to your boss; your boss may wonder
why he has spent so much money on security when he could have just used XOR. However,
you and I realize it can easily be broken.
C#
BitArray bits = new BitArray(new byte[]
{ (byte)'h', (byte)'e', (byte)'l', (byte)'l', (byte)'o' });
BitArray key = new BitArray(new byte[] { 99, 88, 55, 22, 11 });
Console.WriteLine("string = {0}", PrintString(bits));
Console.WriteLine("Encrypting");
bits = bits.Xor(key);
Console.WriteLine("string = {0}", PrintString(bits));
Console.WriteLine("Decrypting");
bits = bits.Xor(key);
Console.WriteLine("string = {0}", PrintString(bits));
Visual Basic
Dim bits As New BitArray(New Byte() _
{Asc("h"c), Asc("e"c), Asc("l"c), Asc("l"c), Asc("o"c)})
Dim key As BitArray = New BitArray(New Byte() {99, 88, 55, 22, 11})
Console.WriteLine("string = {0}", PrintString(bits))
Console.WriteLine("Encrypting")
bits = bits.Xor(key)
Console.WriteLine("string = {0}", PrintString(bits))
Console.WriteLine("Decrypting")
bits = bits.Xor(key)
Console.WriteLine("string = {0}", PrintString(bits))
Output
string = hello
Encrypting
string = ♂=[zd
Decrypting
string = hello
Using CollectionBase
CollectionBase is a dynamic array that uses an ArrayList as its internal data storage. The
CollectionBase class exposes methods that allow you to do additional processing before and
after CollectionBase operates on the internal ArrayList. The methods that allow you to do
preprocessing begin with On, and the ones that allow you to do additional post-processing
begin with On and end with Complete.
Sometimes you may need to bypass the additional processing you created. Two internal
protected properties allow you to do so: InnerList and List. You need to know the difference
between these before you move on to the next section.
The List property interacts with the internal ArrayList by calling the appropriate On* method
discussed later in this section. The InnerList property returns the internal ArrayList, which al-
lows you to do operations without having the On* methods called.
The following code snippets can be used within OnValidate to check whether the item is a
Timer.
C#
if (value != null && !(value is System.Timers.Timer))
{
throw new ArgumentException("value must be of type System.Timers.Timer.", "value");
}
Visual Basic
If (Not value Is Nothing And Not (TypeOf value Is System.Timers.Timer)) Then
Throw New ArgumentException("value must be of type System.Timers.Timer.", "value")
End If
The following code snippets can be used within OnValidate to check whether the object is
the value type int.
C#
if (value == null || !(value is int))
{
throw new ArgumentException("value must be of type int.", "value");
}
318 Part II .NET Built-in Collections
Visual Basic
If (value Is Nothing Or Not (TypeOf value Is Integer)) Then
Throw New ArgumentException("value must be of type int.", "value")
End If
C#
OnClear();
InnerList.Clear();
OnClearComplete();
Visual Basic
OnClear()
InnerList.Clear()
OnClearComplete()
In the past, I have found it sometimes useful to store the objects being cleared during the
OnClearComplete called. Maybe you fire an event after the Clear has been done and the
object that received the event needs to know what objects have been cleared so that it may
remove those items as well. A simple way to accomplish this is to have a property that stores
the removed items, such as the following.
C#
object[] m_clearItems;
public object[] ClearedItems { get { return m_clearItems; } }
Visual Basic
Dim m_clearItems() As Object
Public ReadOnly Property ClearedItems() As Object
Get
Return m_clearItems
End Get
End Property
You could then set the cleared items in the array by doing the following in your OnClear
method.
C#
m_clearItems = InnerList.ToArray();
Visual Basic
m_clearItems = InnerList.ToArray()
Chapter 5 Generic and Support Collections 319
Next, you will want to release the memory for the objects after you fire the event in your
OnClearComplete call, as follows.
C#
m_clearItems = null;
Visual Basic
m_clearItems = Nothing
This implementation has several problems. First, the receiver of the event has to cast the
CollectionBase to your implementation to interact with the ClearedItems property. Second, if
the InnerList.Clear fails, you have no way of removing the items in m_clearItems. Last but not
least, it isn’t very thread safe because multiple threads could make simultaneous calls to the
OnClearComplete on the same object instance, which could cause m_clearItems to be over-
written before you handle it in the initial event. You learn more about threads and how to
handle this issue in Chapter 8, “Using Threads with Collections.”
C#
OnValidate(value);
OnInsert(index, value);
InnerList.Insert(index, value);
try
{
OnInsertComplete(index, value);
}
catch
{
InnerList.RemoveAt(index);
throw;
}
Visual Basic
OnValidate(value)
OnInsert(index, value)
InnerList.Insert(index, value)
Try
OnInsertComplete(index, value)
Catch
InnerList.RemoveAt(index)
Throw
End Try
320 Part II .NET Built-in Collections
C#
OnValidate(value);
OnInsert(InnerList.Count, value);
int index = InnerList.Add(value);
try
{
OnInsertComplete(index, value);
}
catch
{
InnerList.RemoveAt(index);
throw;
}
return index;
Visual Basic
OnValidate(value)
OnInsert(InnerList.Count, value)
Dim index As Integer =InnerList.Add(value)
Try
OnInsertComplete(index, value)
Catch
InnerList.RemoveAt(index)
Throw
End Try
Return index
Both methods do the same thing, except the Add method must first calculate the index of the
item being added before calling the Add method. For an Add, the index is always Count be-
cause the item is added to the end of the list. As seen from the preceding code, the OnInsert
method is called before the Insert or Add method is called on the internal ArrayList. After the
item has been successfully added or inserted into the list, the OnInsertComplete is called. If
you throw an exception in OnInsertComplete, the framework automatically removes the added
object from the internal ArrayList by using the position it was added at. You are responsible
for undoing anything you have done in OnInsert and OnInsertComplete before the exception
is thrown. You could do something like the following.
C#
protected override void OnInsertComplete(int index, object value)
{
try
{
base.OnInsertComplete(index, value);
throw;
}
}
Visual Basic
Protected Overrides Sub OnInsertComplete(ByVal index As Integer, ByVal value As Object)
Try
MyBase.OnInsertComplete(index, value)
' Code to do after the insert has completed
Catch
' Undo OnInsert
Throw
End Try
End
C#
OnValidate(value);
int index = InnerList.IndexOf(value);
if (index < 0)
{
throw new ArgumentException("Index out of range");
}
OnRemove(index, value);
InnerList.RemoveAt(index);
try
{
OnRemoveComplete(index, value);
}
catch
{
InnerList.Insert(index, value);
throw;
}
Visual Basic
OnValidate(value)
Dim index As Integer = InnerList.IndexOf(value)
If (index < 0) Then
Throw New ArgumentException("Index out of range")
322 Part II .NET Built-in Collections
End If
OnRemove(index, value)
InnerList.RemoveAt(index)
Try
OnRemoveComplete(index, value)
Catch
InnerList.Insert(index, value)
Throw
End Try
As seen in the preceding code, the OnRemove method is called before the RemoveAt on
the internal ArrayList. A RemoveAt is performed instead of a Remove to insure the index
and value passed to you are the ones being removed and not another instance of the
value that is equal to the one passed in. After the item has been successfully removed, the
OnRemoveComplete is called. If you throw an exception in OnRemoveComplete, the frame-
work automatically adds the removed object back to the internal ArrayList at the position it
was removed from. You are responsible for undoing anything you have done in OnRemove
in the OnRemoveComplete before the exception is thrown. You could do something like the
following.
C#
protected override void OnRemoveComplete(int index, object value)
{
try
{
base.OnRemoveComplete(index, value);
throw;
}
}
Visual Basic
Protected Overrides Sub OnRemoveComplete(ByVal index As Integer, ByVal value As Object)
Try
MyBase.OnRemoveComplete(index, value)
' Code to do after the remove has completed
Catch
' Undo OnRemove
Throw
End Try
End Sub
Chapter 5 Generic and Support Collections 323
C#
OnValidate(value);
object oldValue = InnerList[index];
OnSet(index, oldValue, value);
InnerList[index] = value;
try
{
OnSetComplete(index, oldValue, value);
}
catch
{
InnerList[index] = oldValue;
throw;
}
Visual Basic
OnValidate(value)
Dim oldValue As Object = InnerList.Item(index)
OnSet(index, oldValue, value)
InnerList.Item(index) = value
Try
OnSetComplete(index, oldValue, value)
Catch
InnerList.Item(index) = oldValue
Throw
End Try
As seen in the preceding code, the OnSet method is called before the Item { set; } is called
on the internal ArrayList. After the element is successfully set, OnSetComplete is called. If you
throw an exception in OnSetComplete, the framework automatically sets the element at the
specified index back to the original value. You are responsible for undoing anything you have
done in OnSet in the OnSetComplete method before the exception is thrown. You could do
something like the following.
C#
protected override void OnSetComplete(int index, object oldValue, object newValue)
{
try
{
base.OnSetComplete(index, oldValue, newValue);
{
// Undo OnSet
throw;
}
}
Visual Basic
Protected Overrides Sub OnSetComplete(ByVal index As Integer, ByVal oldValue As Object, _
ByVal newValue As Object)
Try
MyBase.OnSetComplete(index, oldValue, newValue)
Throw
End Try
End Sub
Using DictionaryBase
DictionaryBase is an associative array that uses a Hashtable as its internal data storage. The
methods in DictionaryBase are designed so that you can do additional processing before and
after DictionaryBase operates on the internal Hashtable. To do this, DictionaryBase defines
methods that allow you to do additional processing before and after operating on the inter-
nal Hashtable. The methods that allow you to preprocess begin with On, and the ones that
allow you to do additional post-processing begin with On and end with Complete.
Sometimes you may need to bypass the additional processing you created. Two internal pro-
tected properties allow you to do so: InnerHashtable and Dictionary. You need to know the
difference between these before you move on to the next section.
The Dictionary property interacts with the internal Hashtable by calling the appropriate On*
method discussed later in this section. The InnerHashtable property returns the internal
Hashtable, which allows you to do operations without having the On* methods called.
C#
OnClear();
InnerHashtable.Clear();
OnClearComplete();
Visual Basic
OnClear()
InnerHashtable.Clear()
OnClearComplete()
In the past, I have found it sometimes useful to store the objects being cleared during
the OnClearComplete call. You can look at the OnClear method in the section titled, “void
OnClear() and void OnClearComplete()” to get an idea about how to accomplish this.
void OnGet()
The OnGet method is called in the Item { get; }. The method is implemented as follows.
C#
object currentValue =InnerHashtable[key];
OnGet(key, currentValue);
return currentValue;
The OnGet method allows you to interact with the object before it is returned to you.
C#
OnValidate(key, value);
OnInsert(key, value);
InnerHashtable.Add(key, value);
try
326 Part II .NET Built-in Collections
{
OnInsertComplete(key, value);
}
catch
{
InnerHashtable.Remove(key);
throw;
}
Visual Basic
OnValidate(key, value)
OnInsert(key, value)
InnerHashtable.Add(key, value)
Try
OnInsertComplete(key, value)
Catch
InnerHashtable.Remove(key)
Throw
End Try
As seen in the preceding code, the OnInsert method is called before the Add method is
called on the internal Hashtable. After the item has been successfully added to the internal
Hashtable, the OnInsertComplete is called. If you throw an exception in OnInsertComplete, the
framework automatically removes the added object from the internal Hashtable. You are re-
sponsible for undoing anything you have done in the OnInsert and OnInsertComplete meth-
ods before the exception is thrown. See the section titled “void OnInsert(int index, Object
value) and void OnInsertComplete(int index, Object value)” if you need information about how
to handle an exception being thrown in OnInsertComplete.
C#
if (InnerHashtable.Contains(key))
{
object obj2 = InnerHashtable[key];
OnValidate(key, obj2);
OnRemove(key, obj2);
InnerHashtable.Remove(key);
try
{
OnRemoveComplete(key, obj2);
}
catch
Chapter 5 Generic and Support Collections 327
{
InnerHashtable.Add(key, obj2);
throw;
}
}
Visual Basic
If (InnerHashtable.Contains(key)) Then
Dim obj2 As Object = InnerHashtable(key)
OnValidate(key, obj2)
OnRemove(key, obj2)
InnerHashtable.Remove(key)
Try
OnRemoveComplete(key, obj2)
Catch
InnerHashtable.Add(key, obj2)
Throw
End Try
End If
As seen in the preceding code, the Contains method of the InnerHashtable is first called to
verify that the key exists in the dictionary. Then the key’s value is retrieved from the internal
Hashtable, and the OnValidate and OnRemove methods are called before the Remove method
is invoked on the internal Hashtable. Next, the object is retrieved from the InnerHashtable
and OnValidate is called. The item is added back to the internal Hashtable if an exception is
encountered in OnRemoveComplete. See the section titled “void OnRemove(int index, object
value) and void OnRemoveComplete(int index, object value) if you need information about how
to handle an exception being thrown in OnRemoveComplete.
C#
OnValidate(key, value);
bool flag = true;
object oldValue = InnerHashtable[key];
if (oldValue == null)
{
flag = InnerHashtable.Contains(key);
}
OnSet(key, oldValue, value);
InnerHashtable[key] = value;
try
328 Part II .NET Built-in Collections
{
OnSetComplete(key, oldValue, value);
}
catch
{
if (flag)
{
InnerHashtable[key] = oldValue;
}
else
{
InnerHashtable.Remove(key);
}
throw;
}
Visual Basic
OnValidate(key, value)
Dim flag As Boolean = True
Dim oldValue As Object = InnerHashtable.Item(key)
If (oldValue Is Nothing) Then
flag = InnerHashtable.Contains(key)
End If
OnSet(key, oldValue, value)
InnerHashtable.Item(key) = value
Try
OnSetComplete(key, oldValue, value)
Catch
If flag Then
InnerHashtable.Item(key) = oldValue
Else
InnerHashtable.Remove(key)
End If
Throw
End Try
As seen in the preceding code, the OnSet method is called before the Item { set; } is called
on the internal Hashtable. Before the call is made, the code attempts to get the original
value from the internal Hashtable. If it retrieves a null, it checks to see whether the internal
Hashtable actually contains the key so that it may reset it if the OnSetComplete throws an
exception. After the element has been successfully set, the OnSetComplete is called. If an er-
ror is encountered in OnSetComplete, the code sets the key back to its original value. Please
refer to the section titled, “void OnSet(int index, object oldValue, object newValue) and void
OnSetComplete(int index, object oldValue, object newValue)” if you need more information
about how to handle an exception being thrown in OnSetComplete.
Chapter 5 Generic and Support Collections 329
HashSet(T) Overview
A set is a type of collection that cannot contain duplicates. For example, [1, 3, 1, 4] is not a set
because 1 is in element 0 and 2.
Because the HashSet(T) has a hash-based implementation, the Add, Remove, and Contains
methods are O(1) operations instead of O(n) like List(T).Remove and List(T).Contains.
Creating a HashSet(T)
You can create a HashSet(T) in four ways.
HashSet(T) ()
You can create an empty HashSet(T) instance by writing the following.
C#
HashSet<int> a = new HashSet<int>();
Visual Basic
Dim a As New HashSet(Of Integer)
HashSet(T) (IEnumerable(T))
You can create a set with default values by passing an object that implements the
IEnumerable(T) interface.
C#
HashSet<int> a = new HashSet<int>( new int [] { 2 , 4, 5 } );
Visual Basic
Dim a As New HashSet(Of Integer)(New Integer() {2, 4, 5})
HashSet(T) ( IEqualityComparer(T))
The constructor creates an empty HashSet(T) object with the specified comparer.
C#
HashSet<string> a = new HashSet<string>(StringComparer.CurrentCultureIgnoreCase);
Visual Basic
Dim a As New HashSet(Of String)(StringComparer.CurrentCultureIgnoreCase)
C#
HashSet<string> a = new HashSet<string>(new string[] {"hi", "hola", "Hi"},
StringComparer.CurrentCultureIgnoreCase);
Visual Basic
Dim a As New HashSet(Of String)(New String() {"hi", "hola", "Hi"}, _
StringComparer.CurrentCultureIgnoreCase)
A HashSet(string) object is created containing the set [“hi”,”hola”]. The last “hi” is not added
to the set because the StringComparer.CurrentCultureIgnoreCase was used for the comparer.
C#
HashSet<string> hs = new HashSet<string>(StringComparer.CurrentCultureIgnoreCase);
hs.Add("hi");
hs.Add("Hi");
hs.Add("hola");
Chapter 5 Generic and Support Collections 331
Visual Basic
Dim hs As New HashSet(Of String)(StringComparer.CurrentCultureIgnoreCase)
hs.Add("hi")
hs.Add("Hi")
hs.Add("hola")
The set now contains [“hi”,”hola”]. “Hi” is not added to the set because String
Comparer.CurrentCultureIgnoreCase states that “hi” and “Hi” are the same. Also
notice how the Add method didn’t throw an exception when the “Hi” was added.
void Clear()
The Clear method removes all items from the list.
C#
HashSet<string> hs = new HashSet<string>(StringComparer.CurrentCultureIgnoreCase);
hs.Add("hi");
hs.Add("hola");
hs.Clear();
Visual Basic
Dim hs As New HashSet(Of String)(StringComparer.CurrentCultureIgnoreCase)
hs.Add("hi")
hs.Add("hola")
hs.Clear()
The set is empty, [], after the Clear method is called. (But just as a reminder, the set still has
the same capacity.)
C#
HashSet<int> hs = new HashSet<int>(new int[] { 1, 6, 2, 3 });
hs.Remove(6);
Visual Basic
Dim hs As New HashSet(Of Integer)(New Integer() {1, 6, 2, 3})
hs.Remove(6)
The preceding code changes the set from [ 1, 6, 2, 3 ] to [ 1, 2, 3 ] because a 6 was passed
into the Remove method.
C#
delegate bool Predicate<T>(T obj);
Visual Basic
Dim instance As New Predicate(Of T)(AddressOf HandlerMethod) as Boolean
Predicate(T) returns true when an object matches the criteria, meaning that it should be re-
moved. The RemoveWhere method returns the number of items removed.
Using a Method with RemoveWhere The following code removes all even values by using
a method for the Predicate(T).
C#
static bool IsEven(int value)
{
return (value % 2) == 0;
}
Visual Basic
Function IsEven(ByVal value As Integer) As Boolean
Return ((value Mod 2) = 0)
End Function
Using a Lambda Expression with RemoveWhere The following code removes all even values
by using a lambda expression for the Predicate(T).
Chapter 5 Generic and Support Collections 333
C#
HashSet<int> hs = new HashSet<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
hs.RemoveWhere(item => { return (item % 2) == 0; });
Visual Basic
Dim hs As New HashSet(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9})
hs.RemoveWhere(Function(x) x Mod 2 = 0)
C#
HashSet<int> odds = new HashSet<int>(new int[] { 1, 3, 5, 7, 9 });
HashSet<int> primes = new HashSet<int>(new int[] { 2, 5, 7 });
HashSet<int> oddPrimes = new HashSet<int>(odds);
oddPrimes.IntersectWith(primes);
Visual Basic
Dim odds As New HashSet(Of Integer)(New Integer() {1, 3, 5, 7, 9})
Dim primes As New HashSet(Of Integer)(New Integer() {2, 5, 7})
Dim oddPrimes New HashSet(Of Integer)(odds)
oddPrimes.IntersectWith(primes)
The set oddPrimes contains {5,7}, because 5 and 7 are present in primes, {2,5,7}, and odds,
{1,3,5,7,9}.
C#
HashSet<int> numbers = new HashSet<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
HashSet<int> evens = new HashSet<int>(new int[] { 2, 4, 6, 8 });
HashSet<int> reversedNumbers = new HashSet<int>(new int[] { 9, 8, 7, 6, 5, 4, 3, 2, 1 });
Visual Basic
Dim numbers As New HashSet(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9})
Dim evens As New HashSet(Of Integer)(New Integer() {2, 4, 6, 8})
Dim reversedNumbers As New HashSet(Of Integer)(New Integer() {9, 8, 7, 6, 5, 4, 3, 2, 1})
Output
Evens is a proper subset of numbers.
Reversednumbers is not a proper subset of numbers.
Not all of the items in numbers are in evens, which makes evens a proper subset of numbers.
The reversedNumbers variable contains the same items as numbers, which makes it not a
proper subset.
C#
HashSet<int> numbers = new HashSet<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
HashSet<int> evens = new HashSet<int>(new int[] { 2, 4, 6, 8 });
HashSet<int> reversedNumbers = new HashSet<int>(new int[] { 9, 8, 7, 6, 5, 4, 3, 2, 1 });
Visual Basic
Dim numbers As New HashSet(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9})
Dim evens As New HashSet(Of Integer)(New Integer() {2, 4, 6, 8})
Dim reversedNumbers As New HashSet(Of Integer)(New Integer() {9, 8, 7, 6, 5, 4, 3, 2, 1})
Chapter 5 Generic and Support Collections 335
Console.WriteLine("Numbers {0} a proper superset of evens.", _
If(numbers.IsProperSupersetOf(evens), "is", "is not"))
Console.WriteLine("Numbers {0} a proper superset of reversedNumbers.", _
If(numbers.IsProperSupersetOf(reversedNumbers), "is", "is not"))
Output
Numbers is a proper superset of evens.
Numbers is not a proper superset of reversedNumbers.
Not all of the items in numbers are present in evens, which makes numbers a proper superset
of evens. The reversedNumbers variable contains the same items as numbers, which makes it
not a proper superset.
C#
HashSet<int> evens = new HashSet<int>(new int[] { 2, 4, 6, 8 });
HashSet<int> odds = new HashSet<int>(new int[] { 1, 3, 5, 7, 9 });
HashSet<int> numbers = new HashSet<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
Visual Basic
Dim evens As New HashSet(Of Integer)(New Integer() {2, 4, 6, 8})
Dim odds As New HashSet(Of Integer)(New Integer() {1, 3, 5, 7, 9})
Dim numbers New HashSet(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9})
Output
Evens are a subset of numbers.
Evens are not a subset of odds.
Every item in evens is present in numbers, which makes evens a subset of numbers. None of
the items in odd is present in evens and vice versa, making neither a subset of the other.
336 Part II .NET Built-in Collections
C#
HashSet<int> evens = new HashSet<int>(new int[] { 2, 4, 6, 8 });
HashSet<int> odds = new HashSet<int>(new int[] { 1, 3, 5, 7, 9 });
HashSet<int> numbers = new HashSet<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
Visual Basic
Dim evens As New HashSet(Of Integer)(New Integer() {2, 4, 6, 8})
Dim odds As New HashSet(Of Integer)(New Integer() {1, 3, 5, 7, 9})
Dim numbers As New HashSet(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9})
Output
Evens are not a superset of numbers.
Numbers are a superset of odds.
Every item in numbers is not present in evens, which makes evens not a superset of numbers.
All of the items in odd are present in numbers, which makes numbers a superset of odds.
C#
HashSet<int> evens = new HashSet<int>(new int[] { 2, 4, 6, 8 });
HashSet<int> odds = new HashSet<int>(new int[] { 1, 3, 5, 7, 9 });
HashSet<int> numbers = new HashSet<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
Visual Basic
Dim evens As New HashSet(Of Integer)(New Integer() {2, 4, 6, 8})
Dim odds As New HashSet(Of Integer)(New Integer() {1, 3, 5, 7, 9})
Dim numbers As New HashSet(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9})
Output
Evens overlaps with numbers.
Evens does not overlap with odds.
At least one item in evens is present in numbers. No items are present in both evens and odds.
C#
HashSet<int> evens = new HashSet<int>(new int[] { 2, 4, 6, 8 });
HashSet<int> odds = new HashSet<int>(new int[] { 1, 3, 5, 7, 9 });
HashSet<int> numbers = new HashSet<int>(odds);
numbers.UnionWith(evens);
Visual Basic
Dim evens As New HashSet(Of Integer)(New Integer() {2, 4, 6, 8})
Dim odds As New HashSet(Of Integer)(New Integer() {1, 3, 5, 7, 9})
Dim numbers As New HashSet(Of Integer)(odds)
numbers.UnionWith(evens)
The set numbers contains {1,3,5,7,9,2,4,6,8} after the set odds, {1,3,5,7,9}, is unioned with the
set evens, {2,4,6,8}.
C#
HashSet<int> odds = new HashSet<int>(new int[] { 1, 3, 5, 7, 9 });
HashSet<int> numbers = new HashSet<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
HashSet<int> evens = new HashSet<int>(numbers);
evens.ExceptWith(odds);
338 Part II .NET Built-in Collections
C#
Dim odds As New HashSet(Of Integer)(New Integer() {1, 3, 5, 7, 9})
Dim numbers As New HashSet(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9})
Dim evens As New HashSet(Of Integer)(numbers)
evens.ExceptWith(odds)
The ExceptWith method removes all items that are in odds, making evens contain only even
numbers, which in this case is {2,4,6,8}.
C#
HashSet<int> evens = new HashSet<int>(new int[] { 2, 4, 6, 8 });
Visual Basic
Dim evens As New HashSet(Of Integer)(New Integer() {2, 4, 6, 8})
Output
Evens doesn't contain 1.
Evens contains 2.
void TrimExcess()
The TrimExcess method changes the capacity of the set. You need to call both Clear and then
TrimExcess to restore a set to a condition similar to the original set created with the default
constructor.
Chapter 5 Generic and Support Collections 339
SortedList(TKey, TValue)
The SortedList(TKey, TValue) class is basically the same as the SortedDictionary(TKey, TValue)
class discussed in the next section. You should use the SortedList(TKey, TValue) class instead if
memory is a concern or you are primary, dealing with presorted data. The SortedList class is
the nongeneric version of this class.
SortedDictionary(TKey, TValue)
The difference between SortedDictionary(TKey, TValue) and Dictionary(TKey,TValue) is that
the keys are sorted using the comparison operator in the SortedDictionary(TKey, TValue) but
not in Dictionary(TKey, TValue), as seen in the following code.
340 Part II .NET Built-in Collections
C#
Dictionary<string, int> unsorted = new Dictionary<string, int>();
unsorted.Add("one", 1);
unsorted.Add("two", 2);
unsorted.Add("three", 3);
unsorted.Add("four", 4);
unsorted.Add("five", 5);
unsorted.Add("six", 6);
Console.WriteLine();
sorted.Add("one", 1);
sorted.Add("two", 2);
sorted.Add("three", 3);
sorted.Add("four", 4);
sorted.Add("five", 5);
sorted.Add("six", 6);
Console.WriteLine();
Visual Basic
Dim unsorted As New Dictionary(Of String, Integer)()
unsorted.Add("one", 1)
unsorted.Add("two", 2)
unsorted.Add("three", 3)
unsorted.Add("four", 4)
unsorted.Add("five", 5)
unsorted.Add("six", 6)
Console.WriteLine()
sorted.Add("one", 1)
sorted.Add("two", 2)
sorted.Add("three", 3)
Chapter 5 Generic and Support Collections 341
sorted.Add("four", 4)
sorted.Add("five", 5)
sorted.Add("six", 6)
Console.WriteLine()
Output
The unsorted list of keys are
one
two
three
four
five
six
As you can see, the keys are sorted in the SortedDictionary(TKey, TValue) but not in the
Dictionary(TKey, TValue). Other than that, you use the classes in the same way.
Summary
In this chapter, you saw how to use the built-in collections to create a queue, stack, hash
set, and dictionary. You also saw how to create an array of bits or Booleans by using the
BitArray class. The DictionaryBase and CollectionBase classes were briefly covered in case
you run into some old code that uses them. You also saw how to use the sorted collections
SortedList(TKey, TValue) and SortedDictionary(TKey,TValue).
Developer’s Guide to Collections in Microsoft .NET
Part III
Using Collections
343
Chapter 6
.NET Collection Interfaces
After completing this chapter, you will be able to
■ Add enumeration interfaces to classes.
■ Add collection interfaces to classes.
■ Add list interfaces to classes.
■ Add dictionary interfaces to classes.
Note The IEnumerable(T) and IEnumerator(T) interfaces are the generic interfaces to the
IEnumerable and IEnumerator. All they do is add type-safe support. Both of them derive from
their non-type–safe counterparts.
You can use the enumerator interfaces to iterate over a collection without needing to know
the internal workings of the collection. The IEnumerable and IEnumerable(T) interfaces are
used to define an object as being enumerable and are defined as follows.
C#
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
345
346 Part III Using Collections
Visual Basic
Public Interface IEnumerable(Of T)
Inherits IEnumerable
End Interface
End Interface
You can retrieve the enumerator of an object that exposes IEnumerable and/or
IEnumerable(T) by calling the GetEnumerator method. All enumerator objects define
the IEnumerator and/or IEnumerator(T) interface and are defined as follows.
C#
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
Visual Basic
Public Interface IEnumerator
Function MoveNext() As Boolean
ReadOnly Property Current As Object
Sub Reset()
End Interface
The IEnumerator and IEnumerator(T) interfaces let you traverse over a collection without
knowing anything about it. The Reset method is only for COM interoperability.
Consider how the foreach statement uses the enumerator interfaces. The Range class, shown
in the following example, defines a range of numbers to iterate over. The enumerator for the
Range class writes to the console the name of the enumerator method or property that is
Chapter 6 .NET Collection Interfaces 347
called. This lets you see how foreach interacts with the two interfaces. You can find the imple-
mentation of the class in the Chapter 6\CS\Criver\Range.cs file for C# or the Chapter 6\VB
\Driver\Range.vb file for Microsoft Visual Basic.
C#
Range range = new Range(10);
Console.WriteLine("starting");
Console.WriteLine("finished");
Visual Basic
DevGuideToCollections.UnitTests.RunTests()
Console.WriteLine("starting")
Console.WriteLine("finished")
Output
starting
IEnumerable.GetEnumerator
IEnumerator.MoveNext
IEnumerator.Current
i=1
IEnumerator.MoveNext
IEnumerator.Current
i=2
IEnumerator.MoveNext
IEnumerator.Current
i=3
IEnumerator.MoveNext
IEnumerator.Current
i=4
IEnumerator.MoveNext
IEnumerator.Current
i=5
IEnumerator.MoveNext
IEnumerator.Current
i=6
IEnumerator.MoveNext
IEnumerator.Current
348 Part III Using Collections
i=7
IEnumerator.MoveNext
IEnumerator.Current
i=8
IEnumerator.MoveNext
IEnumerator.Current
i=9
IEnumerator.MoveNext
IEnumerator.Current
i=10
IEnumerator.MoveNext=false
finished
At the beginning of the loop, the foreach statement queries the range variable for the
IEnumerable(T) interface. After it gets the interface, it calls the GetEnumerator method to
obtain the enumerator for the range variable. The enumerator then calls the MoveNext
method and gets the Current property until all items have been traversed. So from this, you
can conclude that the foreach statement in the preceding code is the same as doing the
following.
C#
if (range is IEnumerable<int>)
{
IEnumerator<int> enumerator = ((IEnumerable<int>)range).GetEnumerator();
while (enumerator.MoveNext())
{
int i = enumerator.Current;
Console.WriteLine("\t\ti={0}", i);
}
}
Visual Basic
If (TypeOf range Is IEnumerable(Of Integer)) Then
Dim enumerator As DirectCast(range, IEnumerable(Of Integer)).GetEnumerator()
While (enumerator.MoveNext())
Dim i As Integer = enumerator.Current
Console.WriteLine(vbTab & vbTab & "i={0}", i)
End While
End If
Note You should use the foreach statement instead of the preceding code. One advantage
of using the foreach statement is that it lets your code use anything that may come out in the
unforeseen future without you having to modify your code. Also, the foreach statement is a lot
easier to read and understand.
Chapter 6 .NET Collection Interfaces 349
Enumeration support lets your class be used with some of the predefined functionality in the
Microsoft .NET Framework, such as AddRange and constructors defined in some of the .NET
collection classes as well as other classes. The following examples are implemented in partial
classes to make it easier to follow along. The implemented interface name is contained in
the file name of the file that contains the partial class. Add the keyword partial to each of the
classes you created in Chapters 1 through 3.
Note You can follow along with the code in these sections by looking at the code in the Chapter
6\CS\DevGuideToCollections folder for C# and the Chapter 6\VB\DevGuideToCollections folder
for Visual Basic.
So the following:
C#
public class ArrayEx<T>
Visual Basic
Public Class ArrayEx(Of T)
becomes:
C#
public partial class ArrayEx<T>
Visual Basic
Partial Public Class ArrayEx(Of T)
This lets you create a file that contains only the additional implementation. You can combine
the code into one file, but it’s easier to follow along in the book by keeping each implemen-
tation in a separate file.
ArrayEx(T)
First you need to add the IEnumerable(T) interface to the ArrayEx(T) class. Add an Array
Ex.Enumerable.cs class file or an ArrayEx.Enumerable.vb class file to the C# project or the
Visual Basic project you created in Chapters 1 through 3.
350 Part III Using Collections
Note Throughout this chapter, you’re asked to add class files to the C# or Visual Basic project
you worked with in Chapters 1 through 3. You create partial class files the same way you create
class files. To create a class file, right-click the project in the Solution Explorer, click Add, and then
click Class. From there, type the name of the class or the class file name. If you type the name of
the class, Microsoft Visual Studio will create a file by using the name of the class and add a .cs or
.vb extension to the file name, depending on whether you are in C# or Visual Basic. If you type a
file name, Visual Studio will create a class named the same as the portion of your file name pre-
ceding the first period. For example, if you add a class and typed the file name ArrayEx.List.cs or
ArrayEx.List.vb, Visual Studio would create a file with that name but the class within the file would
be named ArrayEx (the portion of the name up to the first period).
Note You can find the completed source code in Samples\Chapter 6\##\DevGuideToCollections,
where ## is either CS or VB.
C#
public class ArrayEx
Visual Basic
Public Class ArrayEx(Of T)
to this:
C#
public partial class ArrayEx<T> : IEnumerable<T>
Visual Basic
Partial Public Class ArrayEx(Of T)
Implements IEnumerable(Of T)
The IEnumerable(T) interface contains a generic and nongeneric GetEnumerator method. You
need to implement both methods. Both methods return the enumerator for this class, which
you implement next as the Enumerator structure. The Enumerator structure takes a reference
to the instance of the class being iterated in the constructor, so it has access to the data it
needs to iterate through. The methods are defined as follows.
C#
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
public IEnumerator<T> GetEnumerator()
{
return new Enumerator(this);
}
/// <summary>
Chapter 6 .NET Collection Interfaces 351
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return new Enumerator(this);
}
Visual Basic
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Public Function GetEnumerator() As IEnumerator(Of T) _
Implements IEnumerable(Of T).GetEnumerator
Return New Enumerator(Me)
End Function
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Private Function GetEnumerator1() As IEnumerator Implements IEnumerable.GetEnumerator
Return New Enumerator(Me)
End Function
Next, the Enumerator structure needs to be implemented. The Enumerator structure derives
from the IEnumerator(T) interface so that it can properly be used as an enumerator. The
structure is defined as follows.
C#
public struct Enumerator : IEnumerator<T>
{
private ArrayEx<T> m_array;
private int m_index;
private int m_updateCode;
Visual Basic
Public Structure Enumerator
Implements IEnumerator(Of T)
The m_array field contains a reference to the ArrayEx(T) instance being iterated through. The
m_index field contains the index that you are currently on. The m_updateCode is used to de-
termine whether the collection has changed since the enumeration started.
The implementation of the constructor simply assigns m_array field to the reference being
passed in and sets the m_index to -1, which is explained in the MoveNext method later in this
section.
C#
internal Enumerator(ArrayEx<T> array)
{
m_array = array;
m_index = -1;
m_updateCode = array.m_updateCode;
}
Visual Basic
Friend Sub New(ByVal array As ArrayEx(Of T))
m_array = array
m_index = -1
m_updateCode = array.m_updateCode
End Sub
C#
/// <summary>
/// Called when the resources should be released.
/// </summary>
public void Dispose()
{
}
Visual Basic
''' <summary>
''' Called when the resources should be released.
''' </summary>
Public Sub Dispose() Implements IDisposable.Dispose
End Sub
Chapter 6 .NET Collection Interfaces 353
The Enumerator structure implements the IEnumerator and IEnumerator(T) Current proper-
ties. Both implementations return the value at the current index by getting the element in
m_array at index m_index.
C#
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
object System.Collections.IEnumerator.Current
{
get { return m_array[m_index]; }
}
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
public T Current
{
get { return m_array[m_index]; }
}
Visual Basic
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Public ReadOnly Property Current() As T Implements IEnumerator(Of T).Current
Get
Return m_array(m_index)
End Get
End Property
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Private ReadOnly Property Current1() As Object Implements IEnumerator.Current
Get
Return m_array(m_index)
End Get
End Property
C#
/// <summary>
/// Advances the enumerator to the next element.
/// </summary>
/// <returns>
/// true if the enumerator was successfully advanced to the next element;
/// false if the enumerator has passed the end of the collection.
/// </returns>
354 Part III Using Collections
++m_index;
return true;
}
Visual Basic
''' <summary>
''' Advances the enumerator to the next element.
''' </summary>
''' <returns>
''' true if the enumerator was successfully advanced to the next element;
''' false if the enumerator has passed the end of the collection.
''' </returns>
Public Function MoveNext() As Boolean Implements IEnumerator(Of T).MoveNext
If (m_updateCode <> m_array.m_updateCode) Then
Throw New InvalidOperationException("The array was updated while traversing")
End If
m_index += 1
Return True
End Function
At the beginning of the “Adding Enumeration Support to Classes” section, you were shown
how the .NET Framework calls the IEnumerator(T).MoveNext method immediately after the
IEnumerable(T).GetEnumerator method. The MoveNext method always increments the index.
Because ArrayEx(T) has a zero-based index, you need to make sure that m_index is set to 0
after the first call. To do this, the Reset method and constructor sets m_index to -1 so that the
first call of MoveNext sets m_index to 0. This eliminates having an extra variable to state that
the enumerator has been reset or to start from the beginning.
The Reset method sets the m_index field to -1 as described in the preceding MoveNext code
section and resets the m_updatecode.
Chapter 6 .NET Collection Interfaces 355
C#
/// <summary>
/// Sets the enumerator to its initial position,
/// which is before the first element in the collection.
/// </summary>
public void Reset()
{
m_updateCode = m_array.m_updateCode;
m_index = -1;
}
Visual Basic
''' <summary>
''' Sets the enumerator to its initial position,
''' which is before the first element in the collection.
''' </summary>
Public Sub Reset() Implements IEnumerator(Of T).Reset
m_updateCode = m_array.m_updateCode
m_index = -1
End Sub
CircularBuffer(T)
First you need to add the IEnumerable(T) interface to the CircularBuffer(T) class. Add a
CircularBuffer.Enumerable.cs class file or a CircularBuffer.Enumerable.vb class file to the C# or
Visual Basic project you worked with in Chapters 1 through 3. Change the class declaration
from the following:
C#
public class CircularBuffer
Visual Basic
Public Class CircularBuffer(Of T)
to this:
C#
public partial class CircularBuffer<T> : IEnumerable<T>
Visual Basic
Partial Public Class CircularBuffer(Of T)
Implements IEnumerable(Of T)
The IEnumerable(T) interface contains a generic and nongeneric GetEnumerator method. You
need to implement both methods. Both methods return the enumerator for this class, which
you implement next as the Enumerator structure. The Enumerator structure takes a reference
to the instance of the class being iterated in the constructor, so it has access to the data it
needs to iterate through. The methods are defined as follows.
356 Part III Using Collections
C#
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
public IEnumerator<T> GetEnumerator()
{
return new Enumerator(this);
}
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return new Enumerator(this);
}
Visual Basic
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Public Function GetEnumerator() As IEnumerator(Of T) _
Implements IEnumerable(Of T).GetEnumerator
Return New Enumerator(Me)
End Function
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Private Function GetEnumerator1() As IEnumerator Implements IEnumerable.GetEnumerator
Return New Enumerator(Me)
End Function
Next, the Enumerator structure needs to be implemented. The Enumerator structure derives
from the IEnumerator(T) interface so that it can be used properly as an enumerator. The
structure is defined as follows.
C#
public struct Enumerator : IEnumerator<T>, IEnumerator
{
private CircularBuffer<T> m_buffer;
private int m_index;
private int m_start;
private int m_updateCode;
internal Enumerator(CircularBuffer<T> buffer);
public T Current { get; }
public void Dispose();
object IEnumerator.Current { get; }
Chapter 6 .NET Collection Interfaces 357
public bool MoveNext();
public void Reset();
}
Visual Basic
Public Structure Enumerator
Implements IEnumerator(Of T)
Private m_buffer As CircularBuffer(Of T)
Private m_index As Integer
Private m_start As Integer
Private m_updateCode as Integer
Friend Sub New(ByVal buffer As CircularBuffer(Of T))
Public ReadOnly Property Current As T
Public Sub Dispose() Implements IDisposable.Dispose
Private ReadOnly Property Current1 As Object
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
Public Sub Reset() Implements IEnumerator.Reset
Private ReadOnly Property System.Collections.IEnumerator.Current As Object
End Structure
The m_buffer field contains a reference to the CircularBuffer(T) instance being iterated
through. The m_index field contains the index that you are currently on. The m_start stores
the index of the first valid element in the buffer. The m_updateCode is used to determine
whether the collection has changed since the enumeration started.
The implementation of the constructor simply assigns the m_buffer field to the reference be-
ing passed in and sets the m_index to -1, which is explained in the MoveNext method later
in this section. The constructor also changes m_start to the start of the buffer, which is ex-
plained in the Current property later in this section.
C#
internal Enumerator(CircularBuffer<T> buffer)
{
m_buffer = buffer;
m_index = -1;
m_start = m_buffer.m_start;
m_updateCode = m_buffer.m_updateCode;
}
Visual Basic
Friend Sub New(ByVal buffer As CircularBuffer(Of T))
m_buffer = buffer
m_index = -1
m_start = m_buffer.m_start
m_updateCode = m_buffer.m_updateCode
End Sub
C#
/// <summary>
/// Called when the resources should be released.
/// </summary>
public void Dispose()
{
}
Visual Basic
''' <summary>
''' Called when the resources should be released.
''' </summary>
Public Sub Dispose() Implements IDisposable.Dispose
End Sub
The Enumerator structure implements the IEnumerator and IEnumerator(T) Current proper-
ties. Both implementations return the value at the current index. Because a circular buffer
is circular and its head can be anywhere in the array, you need a way of indexing the buf-
fer from the head or m_start as implemented in this structure. To do this, you can maintain
an index called m_index that you increment in the same way you did in the ArrayEx(T).
Enumerator structure. You can then add m_index to m_start to access the current element
the enumerator has moved to. You can then use the modulus (mod) operation to handle
indexing past the end of the array. So m_data[(m_index + m_start) % m_buffer.m_capacity],
using C#, allows you to use m_index as an index and start the index from the head of the
buffer or m_start. Doing a mod operation ensures that the value stays from 0 to (m_capacity
– 1) and properly moves m_index to the beginning of the array when m_index passes the end
of the internal array.
C#
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
public T Current
{
get { return m_buffer.m_data[(m_index + m_start) % m_buffer.m_capacity]; }
}
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
object System.Collections.IEnumerator.Current
{
get { return m_buffer.m_data[(m_index + m_start) % m_buffer.m_capacity]; }
}
Visual Basic
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Public ReadOnly Property Current() As T Implements IEnumerator(Of T).Current
Get
Return m_buffer.m_data((m_index + m_start) Mod m_buffer.m_capacity)
Chapter 6 .NET Collection Interfaces 359
End Get
End Property
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Private ReadOnly Property Current1() As Object Implements IEnumerator.Current
Get
Return m_buffer.m_data((m_index + m_start) Mod m_buffer.m_capacity)
End Get
End Property
The MoveNext method works the same way as the ArrayEx(T).Enumerator.MoveNext method.
C#
/// <summary>
/// Advances the enumerator to the next element.
/// </summary>
/// <returns>
/// true if the enumerator was successfully advanced to the next element;
/// false if the enumerator has passed the end of the collection.
/// </returns>
public bool MoveNext()
{
if (m_updateCode != m_buffer.m_updateCode)
{
throw new InvalidOperationException("The array was updated while traversing");
}
++m_index;
return true;
}
Visual Basic
''' <summary>
''' Advances the enumerator to the next element.
''' </summary>
''' <returns>
''' true if the enumerator was successfully advanced to the next element;
''' false if the enumerator has passed the end of the collection.
''' </returns>
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
If (m_updateCode <> m_buffer.m_updateCode) Then
Throw New InvalidOperationException("The array was updated while traversing")
End If
m_index += 1
Return False
End If
Return True
End Function
The Reset method sets the m_index field to -1 as described in the preceding MoveNext code
section. The m_start field is also set to the head of the buffer as explained in the Current
property description earlier in this section.
C#
/// <summary>
/// Sets the enumerator to its initial position,
/// which is before the first element in the collection.
/// </summary>
public void Reset()
{
m_index = -1;
m_start = m_buffer.m_start;
m_updateCode = m_buffer.m_updateCode;
}
Visual Basic
''' <summary>
''' Sets the enumerator to its initial position,
''' which is before the first element in the collection.
''' </summary>
Public Sub Reset() Implements IEnumerator.Reset
m_index = -1
m_start = m_buffer.m_start
m_updateCode = m_buffer.m_updateCode
End Sub
C#
public class SingleLinkedList
Visual Basic
Public Class SingleLinkedList(Of T)
Chapter 6 .NET Collection Interfaces 361
to this:
C#
public partial class SingleLinkedList<T> : IEnumerable<T>
Visual Basic
Partial Public Class SingleLinkedList(Of T)
Implements IEnumerable(Of T)
C#
public class DoubleLinkedList
Visual Basic
Public Class DoubleLinkedList(Of T)
to this:
C#
public partial class DoubleLinkedList<T> : IEnumerable<T>
Visual Basic
Partial Public Class DoubleLinkedList(Of T)
Implements IEnumerable(Of T)
The IEnumerable(T) interface contains a generic and nongeneric GetEnumerator method. You
need to implement both methods. Both methods return the enumerator for this class, which
you implement next as the Enumerator structure. The Enumerator structure takes a reference
to the instance of the class being iterated in the constructor, so it has access to the data it
needs to iterate through. The methods are defined as follows.
C#
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
public IEnumerator<T> GetEnumerator()
{
return new Enumerator(this);
}
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return new Enumerator(this);
}
362 Part III Using Collections
Visual Basic
' <summary>
' Gets the enumerator for this collection.
' </summary>
' <returns>The enumerator for this collection.</returns>
Public Function GetEnumerator() As IEnumerator(Of T) _
Implements IEnumerable(Of T).GetEnumerator
Return New Enumerator(Me)
End Function
' <summary>
' Gets the enumerator for this collection.
' </summary>
' <returns>The enumerator for this collection.</returns>
Private Function GetEnumerator1() As IEnumerator Implements IEnumerable.GetEnumerator
Return New Enumerator(Me)
End Function
Next, the Enumerator structure needs to be implemented. The Enumerator structure derives
from the IEnumerator(T) interface so that it can be used properly as an enumerator.
C#
public struct Enumerator : IEnumerator<T>
{
private SingleLinkedList<T> m_list;
private SingleLinkedListNode<T> m_current;
private bool m_end;
internal Enumerator(SingleLinkedList<T> list);
public T Current { get; }
public void Dispose();
object IEnumerator.Current { get; }
public bool MoveNext();
public void Reset();
}
Visual Basic
Public Structure Enumerator
Implements IEnumerator(Of T)
Private m_list As SingleLinkedList(Of T)
Private m_current As SingleLinkedListNode(Of T)
Private m_end As Boolean
Private m_updateCode as Integer
Friend Sub New(ByVal list As SingleLinkedList(Of T))
Public ReadOnly Property Current As T
Public Sub Dispose() Implements IDisposable.Dispose
Private ReadOnly Property Current1 As Object
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
Public Sub Reset() Implements IEnumerator.Reset
Private ReadOnly Property System.Collections.IEnumerator.Current As Object
End Structure
Chapter 6 .NET Collection Interfaces 363
C#
public struct Enumerator : IEnumerator<T>
{
private DoubleLinkedList<T> m_list;
private DoubleLinkedListNode<T> m_current;
private bool m_end;
private int m_updateCode;
internal Enumerator(DoubleLinkedList<T> list);
public T Current { get; }
public void Dispose();
object IEnumerator.Current { get; }
public bool MoveNext();
public void Reset();
}
Visual Basic
Public Structure Enumerator
Implements IEnumerator(Of T)
Private m_list As DoubleLinkedList(Of T)
Private m_current As DoubleLinkedListNode(Of T)
Private m_end As Boolean
Private m_updateCode as Integer
Friend Sub New(ByVal list As DoubleLinkedList(Of T))
Public ReadOnly Property Current As T
Public Sub Dispose() Implements IDisposable.Dispose
Private ReadOnly Property Current1 As Object
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
Public Sub Reset() Implements IEnumerator.Reset
Private ReadOnly Property System.Collections.IEnumerator.Current As Object
End Structure
The implementation of the constructor simply assigns m_list field to the reference being passed
in and sets the m_current to null and m_end to false, which is explained in the MoveNext
method later in this section.
C#
internal Enumerator(SingleLinkedList<T> list)
{
m_list = list;
m_current = null;
m_end = false;
m_updateCode = list.m_updateCode;
}
364 Part III Using Collections
Visual Basic
Friend Sub New(ByVal list As SingleLinkedList(Of T))
m_list = list
m_current = Nothing
m_end = False
m_updateCode = list.m_updateCode
End Sub
C#
internal Enumerator(DoubleLinkedList<T> list)
{
m_list = list;
m_current = null;
m_end = false;
m_updateCode = list.m_updateCode;
}
Visual Basic
Friend Sub New(ByVal list As DoubleLinkedList(Of T))
m_list = list
m_current = Nothing
m_end = False
m_updateCode = list.m_updateCode
End Sub
C#
/// <summary>
/// Called when the resources should be released.
/// </summary>
public void Dispose()
{
}
Visual Basic
' <summary>
' Called when the resources should be released.
' </summary>
Public Sub Dispose() Implements IDisposable.Dispose
End Sub
The Enumerator structure implements the IEnumerator and IEnumerator(T) Current properties.
Both implementations return the value at the current node by getting the value in m_current.
C#
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
Chapter 6 .NET Collection Interfaces 365
public T Current
{
get { return m_current.Data; }
}
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
object System.Collections.IEnumerator.Current
{
get { return m_current.Data; }
}
Visual Basic
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Public ReadOnly Property Current() As T Implements IEnumerator(Of T).Current
Get
Return m_current.Data
End Get
End Property
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Private ReadOnly Property Current1() As Object Implements IEnumerator.Current
Get
Return m_current.Data
End Get
End Property
C#
/// <summary>
/// Advances the enumerator to the next element.
/// </summary>
/// <returns>
/// true if the enumerator was successfully advanced to the next element;
/// false if the enumerator has passed the end of the collection.
/// </returns>
public bool MoveNext()
{
if (m_updateCode != m_list.m_updateCode)
{
throw new InvalidOperationException("The list was updated while traversing");
}
if (m_end || m_list.IsEmpty)
{
return false;
}
if (m_current == null)
366 Part III Using Collections
{
m_current = m_list.Head;
}
else if (m_current == m_list.Tail)
{
m_end = true;
m_current = null;
return false;
}
else
{
m_current = m_current.Next;
}
return true;
}
Visual Basic
''' <summary>
''' Advances the enumerator to the next element.
''' </summary>
''' <returns>
''' true if the enumerator was successfully advanced to the next element;
''' false if the enumerator has passed the end of the collection.
''' </returns>
Public Function MoveNext() As Boolean Implements IEnumerator(Of T).MoveNext
If (m_updateCode <> m_list.m_updateCode) Then
Throw New InvalidOperationException("The list was updated while traversing")
End If
Return True
End Function
At the beginning of the “Adding Enumeration Support to Classes” section earlier in this
chapter, you saw how the .NET Framework calls the IEnumerator(T).MoveNext method im-
mediately after the IEnumerable(T).GetEnumerator method. You need to ensure that the very
first MoveNext call moves m_current to the beginning of the linked list. You will know that
you should start at the beginning of the list when m_current is equal to null. If on a call to
Chapter 6 .NET Collection Interfaces 367
MoveNext you discover you are on the last node, you can set m_end to true and return false
to state that you have moved past the end of the list.
The Reset method sets the m_current to null and m_end to false as described in the
MoveNext code earlier in this section.
C#
/// <summary>
/// Sets the enumerator to its initial position,
/// which is before the first element in the collection.
/// </summary>
public void Reset()
{
m_current = null;
m_end = false;
m_updateCode = m_list.m_updateCode;
}
Visual Basic
''' <summary>
''' Sets the enumerator to its initial position,
''' which is before the first element in the collection.
''' </summary>
Public Sub Reset() Implements IEnumerator(Of T).Reset
m_current = Nothing
m_end = False
m_updateCode = m_list.m_updateCode
End Sub
QueuedArray(T)
First, you need to add the IEnumerable(T) interface to the QueuedArray(T) class. Add a
QueuedArray.Enumerable.cs class file or a QueuedArray.Enumerable.vb class file to the C# or
Visual Basic project you worked with in Chapters 1 through 3. Change the class declaration
from the following:
C#
public class QueuedArray
Visual Basic
Public Class QueuedArray(Of T)
to this:
C#
public partial class QueuedArray<T> : IEnumerable<T>
368 Part III Using Collections
Visual Basic
Partial Public Class QueuedArray(Of T)
Implements IEnumerable(Of T)
The IEnumerable(T) interface contains a generic and nongeneric GetEnumerator method. You
need to implement both methods. Both methods return the enumerator for this class, which
you implement next as the Enumerator structure. The Enumerator structure takes a reference
to the instance of the class being iterated in the constructor, so it has access to the data it
needs to iterate through. The methods are defined as follows.
C#
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
public IEnumerator<T> GetEnumerator()
{
return new Enumerator(this);
}
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return new Enumerator(this);
}
Visual Basic
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Public Function GetEnumerator() As IEnumerator(Of T) _
Implements IEnumerable(Of T).GetEnumerator
Return New Enumerator(Me)
End Function
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Private Function GetEnumerator1() As IEnumerator Implements IEnumerable.GetEnumerator
Return New Enumerator(Me)
End Function
Next, the Enumerator structure needs to be implemented. The Enumerator structure derives
from the IEnumerator(T) interface so that it can properly be used as an enumerator. The
structure is defined as follows.
Chapter 6 .NET Collection Interfaces 369
C#
public struct Enumerator : IEnumerator<T>, IEnumerator
{
private QueuedArray<T> m_buffer;
private int m_index;
private int m_head;
private int m_updateCode;
internal Enumerator(QueuedArray<T> buffer);
public T Current { get; }
public void Dispose();
object IEnumerator.Current { get; }
public bool MoveNext();
public void Reset();
}
Visual Basic
Public Structure Enumerator
Implements IEnumerator(Of T)
Private m_buffer As QueuedArray(Of T)
Private m_index As Integer
Private m_head As Integer
Private m_updateCode As Integer
Friend Sub New(ByVal buffer As QueuedArray(Of T))
Public ReadOnly Property Current As T
Public Sub Dispose() Implements IDisposable.Dispose
Private ReadOnly Property Current1 As Object
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
Public Sub Reset() Implements IEnumerator.Reset
Private ReadOnly Property System.Collections.IEnumerator.Current As Object
End Structure
The m_buffer field contains a reference to the QueuedArray(T) instance being iterated
through. The m_index field contains the index that you are currently on. The m_head field
stores the index of the first valid element in the queue. The m_updateCode is used to deter-
mine whether the collection has changed since the enumeration started.
The implementation of the constructor simply assigns the m_buffer field to the reference
being passed in and sets the m_index to -1, which is explained in the MoveNext property
later in this section. The constructor also sets m_head to the head of the buffer, which is
explained in the Current property later in this section.
C#
internal Enumerator(QueuedArray<T> buffer)
{
m_buffer = buffer;
m_index = -1;
m_head = m_buffer.m_head;
m_updateCode = m_buffer.m_updateCode;
}
370 Part III Using Collections
Visual Basic
Friend Sub New(ByVal buffer As QueuedArray(Of T))
m_buffer = buffer
m_index = -1
m_head = m_buffer.m_head
m_updateCode = m_buffer.m_updateCode
End Sub
C#
/// <summary>
/// Called when the resources should be released.
/// </summary>
public void Dispose()
{
}
Visual Basic
''' <summary>
''' Called when the resources should be released.
''' </summary>
Public Sub Dispose() Implements IDisposable.Dispose
End Sub
The Enumerator structure implements the IEnumerator and IEnumerator(T) Current properties.
Both implementations return the value at the current index. QueuedArray(T) uses the internal
array like the circular buffer uses its internal array, so the head can be anywhere in the array.
You need a way of indexing the buffer from the head that is stored in this structure. To do
this, you can maintain an index called m_index that you increment in the same way you did in
the ArrayEx(T).Enumerator structure. You can then add m_index to m_head to access the cur-
rent element the enumerator has moved to. You can then use the mod operation to handle
indexing past the end of the array. So m_data[(m_index + m_head) % m_buffer.m_data.Length]
(using C#) allows you to use m_index as an index and starts the index from the head of the
array. Doing a mod ensures that the value stays from 0 to (m_data.Length – 1) and properly
moves m_index to the beginning of the array when it reaches the end.
C#
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
public T Current
{
get { return m_buffer.m_data[(m_index + m_head) % m_buffer.m_data.Length]; }
}
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
Chapter 6 .NET Collection Interfaces 371
object System.Collections.IEnumerator.Current
{
get { return m_buffer.m_data[(m_index + m_head) % m_buffer.m_data.Length]; }
}
Visual Basic
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Public ReadOnly Property Current() As T Implements IEnumerator(Of T).Current
Get
Return m_buffer.m_data((m_index + m_head) Mod m_buffer.m_data.Length)
End Get
End Property
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Private ReadOnly Property Current1() As Object Implements IEnumerator(Of T).Current
Get
Return m_buffer.m_data((m_index + m_head) Mod m_buffer.m_data.Length)
End Get
End Property
The MoveNext method works the same way as the ArrayEx(T).Enumerator.MoveNext method.
C#
/// <summary>
/// Advances the enumerator to the next element.
/// </summary>
/// <returns>
/// true if the enumerator was successfully advanced to the next element;
/// false if the enumerator has passed the end of the collection.
/// </returns>
public bool MoveNext()
{
if (m_updateCode != m_buffer.m_updateCode)
{
throw new InvalidOperationException("The queue was updated while traversing");
}
++m_index;
return true;
}
372 Part III Using Collections
Visual Basic
''' <summary>
''' Advances the enumerator to the next element.
''' </summary>
''' <returns>
''' true if the enumerator was successfully advanced to the next element;
''' false if the enumerator has passed the end of the collection.
''' </returns>
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
If (m_updateCode <> m_buffer.m_updateCode) Then
Throw New InvalidOperationException("The queue was updated while traversing")
End If
m_index += 1
Return True
End Function
The Reset method sets the m_index field to -1, as described in the preceding MoveNext
method. The m_head field is also set to the head of the buffer, as described in the Current
property earlier in this section.
C#
/// <summary>
/// Sets the enumerator to its initial position,
/// which is before the first element in the collection.
/// </summary>
public void Reset()
{
m_index = -1;
m_head = m_buffer.m_head;
m_updateCode = m_buffer.m_updateCode;
}
Visual Basic
''' <summary>
''' Sets the enumerator to its initial position,
''' which is before the first element in the collection.
''' </summary>
Public Sub Reset() Implements IEnumerator.Reset
m_index = -1
m_head = m_buffer.m_head
m_updateCode = m_buffer.m_updateCode
End Sub
Chapter 6 .NET Collection Interfaces 373
QueuedLinkedList(T)
First you need to add the IEnumerable(T) interface to the QueuedLinkedList(T) class. Add a
QueuedLinkedList.Enumerable.cs class file or a QueuedLinkedList.Enumerable.vb class file
to the C# or Visual Basic project you worked with in Chapters 1 through 3. Change the class
declaration from the following:
C#
public class QueuedLinkedList
Visual Basic
Public Class QueuedLinkedList(Of T)
to this:
C#
public partial class QueuedLinkedList<T> : IEnumerable<T>
Visual Basic
Partial Public Class QueuedLinkedList(Of T)
Implements IEnumerable(Of T)
C#
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
public IEnumerator<T> GetEnumerator()
{
return m_data.GetEnumerator();
}
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return m_data.GetEnumerator();
}
374 Part III Using Collections
Visual Basic
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Public Function GetEnumerator() As IEnumerator(Of T) _
Implements IEnumerable(Of T).GetEnumerator
Return m_data.GetEnumerator()
End Function
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Private Function GetEnumerator1() As IEnumerator Implements IEnumerable.GetEnumerator
Return m_data.GetEnumerator()
End Function
Because the GetEnumerator method returns the enumerator of the internal data storage, you
don’t need to implement an Enumerator structure.
StackedArray(T)
First you need to add the IEnumerable(T) interface to the StackedArray(T) class. Add a
StackedArray.Enumerable.cs class file or a StackedArray.Enumerable.vb class file to the C# or
Visual Basic project you worked with in Chapters 1 through 3. Change the class declaration
from the following:
C#
public class StackedArray
Visual Basic
Public Class StackedArray(Of T)
to this:
C#
public partial class StackedArray<T> : IEnumerable<T>
Visual Basic
Partial Public Class StackedArray(Of T)
Implements IEnumerable(Of T)
Chapter 6 .NET Collection Interfaces 375
The IEnumerable(T) interface contains a generic and nongeneric GetEnumerator method. You
need to implement both methods. Both methods return the enumerator for this class, which
you implement as the Enumerator structure next. The Enumerator structure takes a reference
to the instance of the class being iterated in the constructor, so it has access to the data it
needs to iterate through. The methods are defined as follows.
C#
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
public IEnumerator<T> GetEnumerator()
{
return new Enumerator(this);
}
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return new Enumerator(this);
}
Visual Basic
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Public Function GetEnumerator() As IEnumerator(Of T) _
Implements IEnumerable(Of T).GetEnumerator
Return New Enumerator(Me)
End Function
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Private Function GetEnumerator1() As IEnumerator Implements IEnumerable.GetEnumerator
Return New Enumerator(Me)
End Function
Next the Enumerator structure needs to be implemented. The Enumerator structure derives
from the IEnumerator(T) interface so that it can be used properly as an enumerator. The
structure is defined as follows.
376 Part III Using Collections
C#
public struct Enumerator : IEnumerator<T>
{
private StackedArray<T> m_array;
private int m_index;
private int m_updateCode;
Visual Basic
Public Structure Enumerator
Implements IEnumerator(Of T)
Private m_array As StackedArray(Of T)
Private m_index As Integer
Private m_updateCode As Integer
Friend Sub New(ByVal array As StackedArray(Of T))
Public ReadOnly Property Current As T
Public Sub Dispose() Implements IDisposable.Dispose
Public ReadOnly Property Current1 As Object
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
Public Sub Reset() Implements IEnumerator.Reset
Public ReadOnly Property System.Collections.IEnumerator.Current As Object
End Structure
The m_array field contains a reference to the StackedArray(T) instance being iterated through.
The m_index field contains the index that you are currently on. The m_updateCode is used to
determine whether the collection has changed since the enumeration started.
The implementation of the constructor simply assigns the m_array field to the reference
being passed in and sets the m_index to m_array.Count, which is explained in the MoveNext
method later in this section.
C#
internal Enumerator(ArrayEx<T> array)
{
m_array = array;
m_index = m_array.Count;
m_updateCode = m_array.m_data.UpdateCode;
}
Visual Basic
Friend Sub New(ByVal array As StackedArray(Of T))
m_array = array
m_index = m_array.Count
m_updateCode = m_array.m_data.UpdateCode
End Sub
Chapter 6 .NET Collection Interfaces 377
C#
/// <summary>
/// Called when the resources should be released.
/// </summary>
public void Dispose()
{
}
Visual Basic
''' <summary>
''' Called when the resources should be released.
''' </summary>
Public Sub Dispose() Implements IDisposable.Dispose
End Sub
The Enumerator structure implements the IEnumerator and IEnumerator(T) Current proper-
ties. Both implementations return the value at the current index by getting the element in
m_array at index m_index.
C#
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
object System.Collections.IEnumerator.Current
{
get { return m_array[m_index]; }
}
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
public T Current
{
get { return m_array[m_index]; }
}
Visual Basic
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Public ReadOnly Property Current() As T Implements IEnumerator(Of T).Current
Get
Return m_array.m_data(m_index)
End Get
End Property
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
378 Part III Using Collections
C#
/// <summary>
/// Advances the enumerator to the next element.
/// </summary>
/// <returns>
/// true if the enumerator was successfully advanced to the next element;
/// false if the enumerator has passed the end of the collection.
/// </returns>
public bool MoveNext()
{
if (m_updateCode != m_array.m_data.UpdateCode)
{
throw new InvalidOperationException("The stack was updated while traversing");
}
--m_index;
if (m_index <= 0)
{
return false;
}
return true;
}
Visual Basic
''' <summary>
''' Advances the enumerator to the next element.
''' </summary>
''' <returns>
''' true if the enumerator was successfully advanced to the next element;
''' false if the enumerator has passed the end of the collection.
''' </returns>
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
If (m_updateCode <> m_array.m_data.UpdateCode) Then
Throw New InvalidOperationException("The stack was updated while traversing")
End If
m_index -= 1
Return True
End Function
Chapter 6 .NET Collection Interfaces 379
At the beginning of the “Adding Enumeration Support to Classes” section, you saw how
the .NET Framework calls the IEnumerator(T).MoveNext method immediately after the
IEnumerable(T).GetEnumerator method. The MoveNext method always decrements the index.
Stacks are accessed in Last In, First Out (LIFO) order. So the iterator needs to go in reverse
order if you want to iterate over the collection in the order items are removed. Because
ArrayEx(T) has a zero-based index, you need to ensure that m_index is set to (m_array.Count
– 1) after the first call. To do this, the Reset method and constructor set m_index to m_array
.Count so that the first call to MoveNext sets it to (m_array.Count – 1). This eliminates having
an extra variable to state that the enumerator has been reset or to start from the beginning.
The Reset method sets the m_index field to m_array.Count as described in the MoveNext
code earlier in this section.
C#
/// <summary>
/// Sets the enumerator to its initial position,
/// which is before the first element in the collection.
/// </summary>
public void Reset()
{
m_index = m_array.Count;
m_updateCode = m_array.m_data.UpdateCode;
}
Visual Basic
''' <summary>
''' Sets the enumerator to its initial position,
''' which is before the first element in the collection.
''' </summary>
Public Sub Reset() Implements IEnumerator.Reset
m_index = m_array.Count
m_updateCode = m_array.m_data.UpdateCode
End Sub
StackedLinkedList(T)
First you need to add the IEnumerable(T) interface to the StackedLinkedList(T) class. Add a
StackedLinkedList.Enumerable.cs class file or a StackedLinkedList.Enumerable.vb class file to
the C# or Visual Basic project you worked with in Chapters 1 through 3. Change the class
declaration from the following:
C#
public class StackedLinkedList
Visual Basic
Public Class StackedLinkedList(Of T)
380 Part III Using Collections
to this:
C#
public partial class StackedLinkedList<T> : IEnumerable<T>
Visual Basic
Partial Public Class StackedLinkedList(Of T)
Implements IEnumerable(Of T)
The IEnumerable(T) interface contains a generic and nongeneric GetEnumerator method. You
need to implement both methods. Both methods return the enumerator for this class, which
you implement next as the Enumerator structure. The Enumerator structure takes a reference
to the instance of the class being iterated in the constructor, so it has access to the data it
needs to iterate through. The methods are defined as follows.
C#
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
public IEnumerator<T> GetEnumerator()
{
return new Enumerator(this);
}
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return new Enumerator(this);
}
Visual Basic
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Public Function GetEnumerator() As IEnumerator(Of T) _
Implements IEnumerable(Of T).GetEnumerator
Return New Enumerator(Me)
End Function
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Private Function GetEnumerator1() As IEnumerator Implements IEnumerable.GetEnumerator
Return New Enumerator(Me)
End Function
Chapter 6 .NET Collection Interfaces 381
Next the Enumerator structure needs to be implemented. The Enumerator structure derives
from the IEnumerator(T) interface so that it can be used properly as an enumerator. The
structure is defined as follows.
C#
public struct Enumerator : IEnumerator<T>
{
private StackedLinkedList<T> m_list;
private DoubleLinkedListNode<T> m_current;
private bool m_end;
private int m_updateCode;
internal Enumerator(StackedLinkedList<T> list);
public T Current { get; }
public void Dispose();
object IEnumerator.Current { get; }
public bool MoveNext();
public void Reset();
}
Visual Basic
Public Structure Enumerator
Implements IEnumerator(Of T)
Private m_list As StackedLinkedList(Of T)
Private m_current As DoubleLinkedListNode(Of T)
Private m_end As Boolean
Private m_updateCode As Integer
Friend Sub New(ByVal list As StackedLinkedList(Of T))
Public ReadOnly Property Current As T
Public Sub Dispose() Implements IDisposable.Dispose
Public ReadOnly Property Current1 As Object
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
Public Sub Reset() Implements IEnumerator.Reset
Public ReadOnly Property System.Collections.IEnumerator.Current As Object
End Structure
The m_list field contains a reference to the StackedLinkedList(T) instance being iterated
through. The m_current field contains the current node the enumerator is on. The m_end
field states whether the enumerator has reached the end of the list. The m_updateCode is
used to determine whether the collection has changed since the enumeration started.
The implementation of the constructor simply assigns m_list field to the reference be-
ing passed in and sets the m_current to null and m_end to false, which is explained in the
MoveNext method later in this section.
C#
internal Enumerator(StackedLinkedList<T> list)
{
m_list = list;
m_current = null;
m_end = false;
m_updateCode = list.m_data.UpdateCode;
}
382 Part III Using Collections
Visual Basic
Friend Sub New(ByVal list As StackedLinkedList(Of T))
m_list = list
m_current = Nothing
m_end = False
m_updateCode = list.m_data.UpdateCode
End Sub
C#
/// <summary>
/// Called when the resources should be released.
/// </summary>
public void Dispose()
{
}
Visual Basic
''' <summary>
''' Called when the resources should be released.
''' </summary>
Public Sub Dispose() Implements IDisposable.Dispose
End Sub
The Enumerator structure implements the IEnumerator and IEnumerator(T) Current properties.
Both implementations return the value at the current node by getting the value in m_current.
C#
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
public T Current
{
get { return m_current.Data; }
}
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
object System.Collections.IEnumerator.Current
{
get { return m_current.Data; }
}
Visual Basic
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Chapter 6 .NET Collection Interfaces 383
Public ReadOnly Property Current() As T Implements IEnumerator(Of T).Current
Get
Return m_current.Data
End Get
End Property
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Public ReadOnly Property Current1() As Object Implements IEnumerator.Current
Get
Return m_current.Data
End Get
End Property
C#
/// <summary>
/// Advances the enumerator to the next element.
/// </summary>
/// <returns>
/// true if the enumerator was successfully advanced to the next element;
/// false if the enumerator has passed the end of the collection.
/// </returns>
public bool MoveNext()
{
if (m_updateCode != m_list.m_data.UpdateCode)
{
throw new InvalidOperationException("The stack was updated while traversing");
}
if (m_end || m_list.IsEmpty)
{
return false;
}
if (m_current == null)
{
m_current = m_list.m_data.Tail;
}
else if (m_current == m_list.m_data.Head)
{
m_end = true;
m_current = null;
return false;
}
else
{
m_current = m_current.Previous;
}
return true;
}
384 Part III Using Collections
Visual Basic
''' <summary>
''' Advances the enumerator to the next element.
''' </summary>
''' <returns>
''' true if the enumerator was successfully advanced to the next element;
''' false if the enumerator has passed the end of the collection.
''' </returns>
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
If (m_updateCode <> m_list.m_data.UpdateCode) Then
Throw New InvalidOperationException("The stack was updated while traversing")
End If
Return True
End Function
At the beginning of the “Adding Enumeration Support to Classes” section, you saw how
the .NET Framework calls the IEnumerator(T).MoveNext method immediately after the
IEnumerable(T).GetEnumerator method. Also, stacks are LIFO collection types, so you need
to iterate from the end of the list to the beginning. To do that, you need to ensure that the
very first MoveNext call moves m_current to the end of the linked list. You will know that you
should start at the end of the list when m_current is equal to null. If on a call to MoveNext
you discover that you are on the first node, you can set m_end to true and return false to
state that you have moved past the beginning of the list.
The Reset method sets the m_current to null and m_end to false as described in the
MoveNext method earlier in this section.
C#
/// <summary>
/// Sets the enumerator to its initial position,
/// which is before the first element in the collection.
/// </summary>
public void Reset()
{
m_updateCode = m_list.m_data.UpdateCode;
m_current = null;
m_end = false;
}
Chapter 6 .NET Collection Interfaces 385
Visual Basic
''' <summary>
''' Sets the enumerator to its initial position,
''' which is before the first element in the collection.
''' </summary>
Public Sub Reset() Implements IEnumerator.Reset
m_updateCode = m_list.m_data.UpdateCode
m_current = Nothing
m_end = False
End Sub
AssociativeArrayAL(TKey,TValue)
First you need to add the IEnumerable(T) interface to the AssociativeArrayAL(TKey,TValue) class.
Add an AssociativeArrayAL.Enumerable.cs class file or an AssociativeArrayAL.Enumerable.vb
class file to the C# or Visual Basic project you worked with in Chapters 1 through 3. Change
the class declaration from the following:
C#
public class AssociativeArrayAL
Visual Basic
Public Class AssociativeArrayAL(Of TKey, TValue)
to this:
C#
public partial class AssociativeArrayAL<TKey,TValue> : IEnumerable<
KeyValuePair<TKey,TValue> >
Visual Basic
Partial Public Class AssociativeArrayAL(Of TKey, TValue)
Implements IEnumerable(Of KeyValuePair(Of TKey, TValue))
The IEnumerable(T) interface contains a generic and nongeneric GetEnumerator method. You
need to implement both methods. Both methods return the enumerator for this class, which
you implement next as the Enumerator structure. The Enumerator structure takes a reference
to the instance of the class being iterated in the constructor, so it has access to the data it
needs to iterate through. The methods are defined as follows.
C#
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
public IEnumerator<T> GetEnumerator()
{
return new Enumerator(this);
}
386 Part III Using Collections
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return new Enumerator(this);
}
Visual Basic
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Public Function GetEnumerator() As IEnumerator(Of KeyValuePair(Of TKey, TValue)) _
Implements IEnumerable(Of KeyValuePair(Of TKey, TValue)).GetEnumerator
Return New Enumerator(Me)
End Function
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Private Function GetEnumerator1() As IEnumerator Implements IEnumerable.GetEnumerator
Return New Enumerator(Me)
End Function
Next the Enumerator structure needs to be implemented. The Enumerator structure derives
from the IEnumerator(T) interface so that it can be used properly as an enumerator. The
structure will be defined as follows.
C#
public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>
{
private AssociativeArrayAL<TKey, TValue> m_aa;
private DoubleLinkedListNode<AssociativeArrayAL<TKey, TValue>.KVPair> m_currentNode;
private KeyValuePair<TKey, TValue> m_current;
private int m_updateCode;
private bool m_end;
internal Enumerator(AssociativeArrayAL<TKey, TValue> aa);
public KeyValuePair<TKey, TValue> Current { get; }
public void Dispose();
object IEnumerator.Current { get; }
public bool MoveNext();
public void Reset();
}
Chapter 6 .NET Collection Interfaces 387
Visual Basic
Public Structure Enumerator
Implements IEnumerator(Of KeyValuePair(Of TKey, TValue))
Private m_aa As AssociativeArrayAL(Of TKey, TValue)
Private m_currentNode As DoubleLinkedListNode(Of KVPair(Of TKey, TValue))
Private m_current As KeyValuePair(Of TKey, TValue)
Private m_updateCode As Integer
Private m_end As Boolean
Friend Sub New(ByVal aa As AssociativeArrayAL(Of TKey, TValue))
Public ReadOnly Property Current As KeyValuePair(Of TKey, TValue)
Public Sub Dispose() Implements IDisposable.Dispose
Public ReadOnly Property Current1 As Object
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
Public Sub Reset() Implements IEnumerator.Reset
Public ReadOnly Property System.Collections.IEnumerator.Current As Object
End Structure
The m_aa field contains a reference to the AssociativeArrayAL(TKey,TValue) instance being it-
erated through. The m_currentNode field contains the current node the enumerator is on. The
m_end field states whether the enumerator has reached the end of the list. The m_updateCode
stores the current update code. The m_current field stores the current key/value pair.
The implementation of the constructor simply assigns the m_aa field to the reference being
passed in and sets the m_currentNode to null and m_end to false, which is explained in the
MoveNext code later in this section.
C#
internal Enumerator(AssociativeArrayAL<TKey, TValue> aa)
{
m_aa = aa;
Visual Basic
Friend Sub New(ByVal aa As AssociativeArrayAL(Of TKey, TValue))
m_aa = aa
C#
/// <summary>
/// Called when the resources should be released.
/// </summary>
public void Dispose()
{
}
Visual Basic
''' <summary>
''' Called when the resources should be released.
''' </summary>
Public Sub Dispose() _
Implements IEnumerator( _
Of System.Collections.Generic.KeyValuePair(Of TKey, TValue) _
).Dispose
End Sub
The Enumerator structure implements the IEnumerator and IEnumerator(T) Current properties.
Both implementations return the value at the current index by returning m_current.
C#
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
public T Current
{
get { return m_current; }
}
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
object System.Collections.IEnumerator.Current
{
get { return m_current; }
}
Visual Basic
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Public ReadOnly Property Current() As KeyValuePair(Of TKey, TValue) Implements _
IEnumerator( _
Of System.Collections.Generic.KeyValuePair(Of TKey, TValue) _
).Current
Get
Return m_current
End Get
End Property
Chapter 6 .NET Collection Interfaces 389
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Public ReadOnly Property Current1() As Object Implements IEnumerator.Current
Get
Return m_current
End Get
End Property
C#
/// <summary>
/// Advances the enumerator to the next element.
/// </summary>
/// <returns>
/// true if the enumerator was successfully advanced to the next element;
/// false if the enumerator has passed the end of the collection.
/// </returns>
public bool MoveNext()
{
if (m_updateCode != m_aa.m_updateCode)
{
throw new InvalidOperationException("The hash table was updated while traversing");
}
if (m_end)
{
return false;
}
if (m_currentNode == null)
{
if (m_aa.IsEmpty)
{
m_end = true;
return false;
}
m_currentNode = m_aa.m_list.Head;
return true;
}
m_currentNode = m_currentNode.Next;
if (m_currentNode == null)
{
m_end = true;
return false;
}
390 Part III Using Collections
return true;
}
Visual Basic
''' <summary>
''' Advances the enumerator to the next element.
''' </summary>
''' <returns>
''' true if the enumerator was successfully advanced to the next element;
''' false if the enumerator has passed the end of the collection.
''' </returns>
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
If (m_updateCode <> m_aa.m_updateCode) Then
Throw New InvalidOperationException("The hash table was updated while traversing")
End If
If (m_end) Then
Return False
End If
m_currentNode = m_aa.m_list.Head
Return True
End If
m_currentNode = m_currentNode.Next
Return True
End Function
The MoveNext method operates similarly to the MoveNext method of the CircularBuffer( T)
.Enumerator structure. (See the section titled “SingleLinkedList(T) and DoubleLinkedList(T)”
earlier in this chapter for more information.) The update code is used to see whether the
Chapter 6 .NET Collection Interfaces 391
collection has changed since the traversing started. This eliminates an unknown state the
collection could be in if it is rehashed or items are removed or inserted.
The Reset method sets the m_currentNode to null and m_end to false as described in the
MoveNext method earlier in this section.
C#
/// <summary>
/// Sets the enumerator to its initial position,
/// which is before the first element in the collection.
/// </summary>
public void Reset()
{
m_currentNode = null;
m_end = false;
m_updateCode = m_aa.m_updateCode;
m_current = default(KeyValuePair<TKey, TValue>);
}
Visual Basic
''' <summary>
''' Sets the enumerator to its initial position,
''' which is before the first element in the collection.
''' </summary>
Public Sub Reset() Implements IEnumerator.Reset
m_currentNode = Nothing
m_end = False
m_updateCode = m_aa.m_updateCode
m_current = CType(Nothing, KeyValuePair(Of TKey, TValue))
End Sub
AssociativeArrayHT(TKey,TValue)
First you need to add the IEnumerable(T) interface to the AssociativeArrayHT(TKey,TValue) class.
Add an AssociativeArrayHT.Enumerable.cs class file or an AssociativeArrayHT.Enumerable.vb
class file to the C# or Visual Basic project you worked with in Chapters 1 through 3. Change the
class declaration from the following:
C#
public class AssociativeArrayHT
Visual Basic
Public Class AssociativeArrayHT(Of TKey, TValue)
to this:
C#
public partial class AssociativeArray<TKey,TValue> : IEnumerable< KeyValuePair<TKey,TValue>>
392 Part III Using Collections
Visual Basic
Partial Public Class AssociativeArrayHT(Of TKey, TValue)
Implements IEnumerable(Of KeyValuePair(Of TKey, TValue))
The IEnumerable(T) interface contains a generic and nongeneric GetEnumerator method. You
need to implement both methods. Both methods return the enumerator for this class, which
you implement next as the Enumerator structure. The Enumerator structure takes a reference
to the instance of the class being iterated in the constructor, so it has access to the data it
needs to iterate through. The methods are defined as follows.
C#
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
public IEnumerator<T> GetEnumerator()
{
return new Enumerator(this);
}
/// <summary>
/// Gets the enumerator for this collection.
/// </summary>
/// <returns>The enumerator for this collection.</returns>
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return new Enumerator(this);
}
Visual Basic
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Public Function GetEnumerator() As IEnumerator(Of KeyValuePair(Of TKey, TValue)) _
Implements IEnumerable(Of KeyValuePair(Of TKey, TValue)).GetEnumerator
Return New Enumerator(Me)
End Function
''' <summary>
''' Gets the enumerator for this collection.
''' </summary>
''' <returns>The enumerator for this collection.</returns>
Private Function GetEnumerator1() As IEnumerator Implements IEnumerable.GetEnumerator
Return New Enumerator(Me)
End Function
Next the Enumerator structure needs to be implemented. The Enumerator structure derives
from the IEnumerator(T) interface so that it can be used properly as an enumerator. The
structure is defined as follows.
Chapter 6 .NET Collection Interfaces 393
C#
public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>
{
private AssociativeArrayHT<TKey, TValue> m_aa;
private int m_entryIndex;
private int m_bucketIndex;
private int m_updateCode;
private KeyValuePair<TKey, TValue> m_current;
internal Enumerator(AssociativeArrayHT<TKey, TValue> aa);
public KeyValuePair<TKey, TValue> Current { get; }
public void Dispose();
object IEnumerator.Current { get; }
public bool MoveNext();
public void Reset();
}
Visual Basic
Public Structure Enumerator
Implements IEnumerator(Of KeyValuePair(Of TKey, TValue))
Private m_aa As AssociativeArrayHT(Of TKey, TValue)
Private m_entryIndex As Integer
Private m_bucketIndex As Integer
Private m_updateCode As Integer
Private m_current As KeyValuePair(Of TKey, TValue)
Friend Sub New(ByVal aa As AssociativeArrayHT(Of TKey, TValue))
Public ReadOnly Property Current As KeyValuePair(Of TKey, TValue)
Public Sub Dispose() Implements IDisposable.Dispose
Public ReadOnly Property Current1 As Object
Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
Public Sub Reset() Implements IEnumerator.Reset
Public ReadOnly Property System.Collections.IEnumerator.Current As Object
End Structure
The m_aa field contains a reference to the AssociativeArrayHT(T) instance being iter-
ated through. The m_current field contains the current key/value pair the enumerator is
on. The m_entryIndex and m_bucketIndex contain the entry and bucket the index is on. The
m_updateCode stores a snapshot of the update code when the traversing beings. The imple-
mentation of the constructor simply assigns the m_aa field to the reference being passed in
and sets the m_bucketIndex and m_entryIndex to NULL_REFERENCE, which is explained in the
MoveNext property later in this section.
C#
internal Enumerator(AssociativeArrayHT<TKey, TValue> aa)
{
m_current = default(KeyValuePair<TKey, TValue>);
m_aa = aa;
m_updateCode = m_aa.m_updateCode;
m_bucketIndex = NULL_REFERENCE;
m_entryIndex = NULL_REFERENCE;
}
394 Part III Using Collections
Visual Basic
Friend Sub New(ByVal aa As AssociativeArrayHT(Of TKey, TValue))
m_current = CType(Nothing, KeyValuePair(Of TKey, TValue))
m_aa = aa
m_updateCode = m_aa.m_updateCode
m_bucketIndex = NULL_REFERENCE
m_entryIndex = NULL_REFERENCE
End Sub
C#
/// <summary>
/// Called when the resources should be released.
/// </summary>
public void Dispose()
{
}
Visual Basic
''' <summary>
''' Called when the resources should be released.
''' </summary>
Public Sub Dispose() Implements IEnumerator(Of KeyValuePair(Of TKey, TValue)).Dispose
End Sub
The Enumerator structure implements the IEnumerator and IEnumerator(T) Current properties.
Both implementations return the value at the current index by returning m_current.
C#
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
public T Current
{
get { return m_current; }
}
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
object System.Collections.IEnumerator.Current
{
get { return m_current; }
}
Visual Basic
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Chapter 6 .NET Collection Interfaces 395
Public ReadOnly Property Current() As KeyValuePair(Of TKey, TValue) Implements _
IEnumerator( _
Of System.Collections.Generic.KeyValuePair(Of TKey, TValue) _
).Current
Get
Return m_current
End Get
End Property
''' <summary>
''' Gets the element at the current position of the enumerator.
''' </summary>
Public ReadOnly Property Current1() As Object Implements IEnumerator.Current
Get
Return m_current
End Get
End Property
C#
/// <summary>
/// Advances the enumerator to the next element.
/// </summary>
/// <returns>
/// true if the enumerator was successfully advanced to the next element;
/// false if the enumerator has passed the end of the collection.
/// </returns>
public bool MoveNext()
{
if (m_updateCode != m_aa.m_updateCode)
{
throw new InvalidOperationException("The hash table was updated while traversing");
}
return false;
}
Visual Basic
''' <summary>
''' Advances the enumerator to the next element.
''' </summary>
''' <returns>
''' true if the enumerator was successfully advanced to the next element;
''' false if the enumerator has passed the end of the collection.
''' </returns>
396 Part III Using Collections
Return False
End Function
At the beginning of the “Adding Enumeration Support to Classes” section, you saw how
the .NET Framework calls the IEnumerator( T).MoveNext method immediately after the
IEnumerable( T).GetEnumerator method. The MoveNext method calls the AssociativeArrayHT
(TKey,TValue).MoveNext method. This method is described in the “Adding Helper Methods
and Properties” sections in Chapter 2, “Understanding Collections: Associative Arrays.”
The Reset method sets the m_bucketIndex and m_entryIndex to NULL_REFERENCE as described
in the AssociativeArrayHT(TKey,TValue).MoveNext method in Chapter 2.
C#
/// <summary>
/// Sets the enumerator to its initial position, which is before the
/// first element in the collection.
/// </summary>
public void Reset()
{
m_bucketIndex = NULL_REFERENCE;
m_updateCode = m_aa.m_updateCode;
m_entryIndex = NULL_REFERENCE;
m_current = new KeyValuePair<TKey, TValue>();
}
Visual Basic
''' <summary>
''' Sets the enumerator to its initial position, which is before the
''' first element in the collection.
''' </summary>
Public Sub Reset() Implements IEnumerator.Reset
m_bucketIndex = NULL_REFERENCE
m_updateCode = m_aa.m_updateCode
m_entryIndex = NULL_REFERENCE
m_current = CType(Nothing, KeyValuePair(Of TKey, TValue))
End Sub
Chapter 6 .NET Collection Interfaces 397
The ICollection interface adds size, enumerator, and synchronization support to a collection
type. All of the collection classes are derived from this interface. The following is the interface
definition.
C#
public interface ICollection : System.Collections.IEnumerable
{
void CopyTo(System.Array array, int index);
int Count { get; }
bool IsSynchronized { get; }
object SyncRoot { get; }
}
Visual Basic
Public Interface ICollection
Inherits IEnumerable
Sub CopyTo(ByVal array As Array, ByVal index As Integer)
ReadOnly Property Count As Integer
ReadOnly Property IsSynchronized As Boolean
ReadOnly Property SyncRoot As Object
End Interface
The ICollection(T) interface defines generic collection manipulation support for a collection.
The interface has support for adding, removing, and checking the contents of a collection, as
you can see from the following interface definition.
C#
public interface ICollection<T> : System.Collections.Generic.IEnumerable<T>
{
void Add(T item);
void Clear();
bool Contains(T item);
void CopyTo(T[] array, int arrayIndex);
int Count { get; }
bool IsReadOnly { get; }
bool Remove(T item);
}
398 Part III Using Collections
Visual Basic
Public Interface ICollection(Of T)
Inherits IEnumerable(Of T)
Sub Add(ByVal item As T)
Sub Clear()
Function Contains(ByVal item As T) As Boolean
Sub CopyTo(ByVal array As T(), ByVal arrayIndex As Integer)
Function Remove(ByVal item As T) As Boolean
ReadOnly Property Count As Integer
ReadOnly Property IsReadOnly As Boolean
End Interface
Some collection types, such as stack and queue, have problems implementing the
ICollection(T) interface because the user cannot specifically state where they want to add
an item or what item they want to remove. However, all collections do have a size and can
be copied to an array. Read-only collections are a special type of collection that cannot
have items removed from them or added to them. The IsReadOnly property is used to state
whether a collection is read-only. The CopyTo method lets you copy the contents of the col-
lection to an array starting at the specified index of the array.
So, you can use the ICollection interface to do basic collection operations such as getting the
size of the collection, copying it to an array, and threading support with the IsSynchronized
and SyncRoot properties that are discussed in Chapter 8, “Using Threads with Collections.”
With ICollection(T), you can do generic collection manipulations such as removals and addi-
tions as well as get the size of the collection, check the contents of it, and copy the contents.
Note You can follow along with the code in these sections by using the code in the
DevGuideToCollections project in the Chapter 6\CS\DevGuideToCollections folder for
C# or the Chapter 6\VB\DevGuideToCollections folder for Visual Basic.
ArrayEx(T)
First you need to add the ICollection(T) and ICollection interfaces to the ArrayEx(T) class.
Add an ArrayEx.Collection.cs class file or an ArrayEx.Collection.vb class file to the C# or Visual
Basic project you worked with in Chapters 1 through 3. Change the class declaration from the
following:
Chapter 6 .NET Collection Interfaces 399
C#
public class ArrayEx
Visual Basic
Public Class ArrayEx(Of T)
to this:
C#
public partial class ArrayEx<T> : ICollection<T>, System.Collection.ICollection
Visual Basic
Partial Public Class ArrayEx(Of T)
Implements ICollection(Of T), ICollection
All of the methods and properties except CopyTo, IsReadyOnly, IsSynchronized, and SyncRoot
are already implemented.
The IsSynchronized and SyncRoot properties are discussed in detail in Chapter 8, so for now
you’ll implement them as follows.
C#
bool System.Collections.ICollection.IsSynchronized
{
get { return false; }
}
object System.Collections.ICollection.SyncRoot
{
get { throw new NotImplementedException(); }
}
Visual Basic
Private ReadOnly Property IsSynchronized() As Boolean Implements ICollection.IsSynchronized
Get
Return False
End Get
End Property
Because the ArrayEx(T) can be modified, the IsReadOnly property needs to return false.
400 Part III Using Collections
C#
/// <summary>
/// Gets a value indicating whether the collection is read only.
/// </summary>
public bool IsReadOnly
{
get { return false; }
}
Visual Basic
''' <summary>
''' Gets a value indicating whether the collection is read only.
''' </summary>
Public ReadOnly Property IsReadOnly() As Boolean Implements ICollection(Of T).IsReadOnly
Get
Return False
End Get
End Property
There isn’t anything special about the array in ArrayEx(T), so the CopyTo methods can use
the Array.Copy to method. You need to copy only the elements that are actually used, so the
Count property instead of the Capacity property has to be passed to the Array.Copy method
as well.
C#
public void CopyTo(T[] array, int arrayIndex)
{
Array.Copy(m_data, 0, array, arrayIndex, m_count);
}
Visual Basic
''' <summary>
''' Copies the elements of the array to the specified array.
''' </summary>
''' <param name="array">The array to copy the data to.</param>
''' <param name="arrayIndex">The index in array where copying begins.</param>
Public Sub CopyTo(ByVal array As T(), ByVal arrayIndex As Integer) _
Implements ICollection(Of T).CopyTo
System.Array.Copy(m_data, 0, array, arrayIndex, m_count)
End Sub
The Implements statement is shown in bold in the following code. It needs to be added to
the following methods and properties in the ArrayEx.vb file you created in Chapter 1 for
Visual Basic users.
Visual Basic
Public Function Contains(ByVal item As T) As Boolean Implements ICollection(Of T).Contains
CircularBuffer(T)
Users cannot specifically state where they want to add an item or remove it, so only
ICollection is implemented, and not ICollection(T). First you need to add the ICollection
interface to the CircularBuffer(T) class. Add a CircularBuffer.Collection.cs class file or a
CircularBuffer.Collection.vb class file to the C# or Visual Basic project you worked with in
Chapters 1 through 3. Change the class declaration from the following:
C#
public class CircularBuffer
Visual Basic
Public Class CircularBuffer(Of T)
to this:
C#
public partial class CircularBuffer<T> : System.Collection.ICollection
Visual Basic
Partial Public Class CircularBuffer(Of T)
Implements ICollection
All of the methods and properties except CopyTo, IsSynchronized, and SyncRoot are already
implemented.
402 Part III Using Collections
The IsSynchronized and SyncRoot properties are discussed in detail in Chapter 8, so for now
they are implemented as follows.
C#
bool System.Collections.ICollection.IsSynchronized
{
get { return false; }
}
object System.Collections.ICollection.SyncRoot
{
get { throw new NotImplementedException(); }
}
Visual Basic
Private ReadOnly Property IsSynchronized() As Boolean Implements ICollection.IsSynchronized
Get
Return False
End Get
End Property
Items have to be copied to the array from the start of the buffer to the end of the buffer. So
you need to define a method as follows for copying.
C#
void System.Collections.ICollection.CopyTo(Array array, int index)
{
if (array == null)
{
throw new ArgumentNullException("array");
}
if (index < 0)
{
throw new ArgumentOutOfRangeException("arrayIndex");
}
if (array.Rank > 1)
{
throw new RankException("Cannot handle multidimensional arrays");
}
Visual Basic
Private Sub CopyTo(ByVal array As Array, ByVal index As Integer) _
Implements ICollection.CopyTo
If (array Is Nothing) Then
Throw New ArgumentNullException("array")
End If
The method starts at m_start and counts up until it reaches the number of items in the buf-
fer. This is accomplished by adding an index to m_start and using the modulus operator by
m_capacity.
The Implements statement is shown in bold in the following code. It needs to be added to
the following property in the CircularBuffer.vb file you created in Chapter 3 for Visual Basic
users.
Visual Basic
Public ReadOnly Property Count() As Integer Implements ICollection.Count
Change the class declaration for the SingleLinkedList(T) from the following:
C#
public class SingleLinkedList
Visual Basic
Public Class SingleLinkedList(Of T)
to this:
C#
public partial class SingleLinkedList<T> : ICollection<T>, System.Collection.ICollection
Visual Basic
Partial Public Class SingleLinkedList(Of T)
Implements ICollection(Of T), ICollection
Change the class declaration for the DoubleLinkedList(T) from the following:
C#
public class DoubleLinkedList
Visual Basic
Public Class DoubleLinkedList(Of T)
to this:
C#
public partial class DoubleLinkedList <T> : ICollection<T>, System.Collection.ICollection
Visual Basic
Partial Public Class DoubleLinkedList(Of T)
Implements ICollection(Of T), ICollection
All of the methods and properties except Add, CopyTo, IsReadyOnly, IsSynchronized, and
SyncRoot are already implemented.
The IsSynchronized and SyncRoot properties are discussed in detail in Chapter 8, so for now
they are implemented as follows.
C#
bool System.Collections.ICollection.IsSynchronized
{
get { return false; }
}
object System.Collections.ICollection.SyncRoot
{
get { throw new NotImplementedException(); }
}
Chapter 6 .NET Collection Interfaces 405
Visual Basic
Private ReadOnly Property IsSynchronized() As Boolean Implements ICollection.IsSynchronized
Get
Return False
End Get
End Property
The Add method simply adds the specified item to the end of the linked list.
C#
void System.Collections.Generic.ICollection<T>.Add(T item)
{
AddToEnd(item);
}
Visual Basic
Private Sub Add(ByVal item As T) Implements ICollection(Of T).Add
AddToEnd(item)
End Sub
Both classes can be modified, so the IsReadOnly property needs to return false.
C#
/// <summary>
/// Gets a value indicating whether the collection is read only.
/// </summary>
public bool IsReadOnly
{
get { return false; }
}
Visual Basic
''' <summary>
''' Gets a value indicating whether the collection is read only.
''' </summary>
Public ReadOnly Property IsReadOnly() As Boolean Implements ICollection(Of T).IsReadOnly
Get
Return False
End Get
End Property
406 Part III Using Collections
For the CopyTo method, you need to copy all nodes from the beginning of the list to the end
of the list, as follows.
C#
/// <summary>
/// Copies the elements of the array to the specified array.
/// </summary>
/// <param name="array">The array to copy the data to.</param>
/// <param name="arrayIndex">The index in array where copying begins.</param>
public void CopyTo(T[] array, int arrayIndex)
{
if (array == null)
{
throw new ArgumentNullException("array");
}
if (arrayIndex < 0)
{
throw new ArgumentOutOfRangeException("arrayIndex");
}
if (index < 0)
{
throw new ArgumentOutOfRangeException("arrayIndex");
}
if (array.Rank > 1)
{
throw new RankException("Cannot handle multidimensional arrays");
}
Chapter 6 .NET Collection Interfaces 407
if (array.Length - index < m_count)
{
throw new ArgumentException
("The specified array is not large enough to hold the data.", "array");
}
Visual Basic
''' <summary>
''' Copies the elements of the array to the specified array.
''' </summary>
''' <param name="array">The array to copy the data to.</param>
''' <param name="arrayIndex">The index in array where copying begins.</param>
Public Sub CopyTo(ByVal array As T(), ByVal arrayIndex As Integer) _
Implements ICollection(Of T).CopyTo
If (array Is Nothing) Then
Throw New ArgumentNullException("array")
End If
The CopyTo method starts at Head and traverses the list until it gets to Tail, while keep-
ing an index in variable i. The index is used to copy the current node’s data to the index of
(i + index) into the specified array. Don’t forget to change the SingleLinkedListNode(T) to
DoubleLinkedListNode(T) for the DoubleLinkedList(T) implementation.
The Implements statement is shown in bold in the following code. It needs to be added to
the following methods and properties in the SingleLinkedList.vb and DoubleLinkedList.vb files
you created in Chapter 1 for Visual Basic users.
Visual Basic
Public ReadOnly Property Count() As Integer _
Implements ICollection.Count , ICollection(Of T).Count
QueuedArray(T)
Users cannot specifically state where they want to add an item or remove it, so only
ICollection is implemented, and not ICollection(T). First you need to add the ICollection
interface to the QueuedArray(T) class. Add a QueuedArray.Collection.cs class file or a
QueuedArray.Collection.vb class file to the C# or Visual Basic project you worked with in
Chapters 1 through 3. Change the class declaration from the following:
C#
public class QueuedArray
Visual Basic
Public Class QueuedArray(Of T)
to this:
C#
public partial class QueuedArray<T> : System.Collection.ICollection
Chapter 6 .NET Collection Interfaces 409
Visual Basic
Partial Public Class QueuedArray(Of T)
Implements ICollection
All of the methods and properties except CopyTo, IsSynchronized, and SyncRoot are already
implemented.
The IsSynchronized and SyncRoot properties are discussed in detail in Chapter 8, so for now
they are implemented as follows.
C#
bool System.Collections.ICollection.IsSynchronized
{
get { return false; }
}
object System.Collections.ICollection.SyncRoot
{
get { throw new NotImplementedException(); }
}
Visual Basic
Private ReadOnly Property IsSynchronized() As Boolean Implements ICollection.IsSynchronized
Get
Return False
End Get
End Property
Items need to be copied to the array from the start of the array to the end of the array. So
you need to define a method as follows for copying.
C#
void System.Collections.ICollection.CopyTo(Array array, int index)
{
if (array == null)
{
throw new ArgumentNullException("array");
}
if (index < 0)
{
throw new ArgumentOutOfRangeException("arrayIndex");
}
410 Part III Using Collections
if (array.Rank > 1)
{
throw new RankException("Cannot handle multidimensional arrays");
}
Visual Basic
Private Sub CopyTo(ByVal array As Array, ByVal index As Integer) _
Implements ICollection.CopyTo
If (array Is Nothing) Then
Throw New ArgumentNullException("array")
End If
The method starts at m_head and counts up until it reaches the number of items in the
buffer. This is accomplished by adding an index to m_head and then applying the modulus
operator by m_data.Length.
The Implements statement is shown in bold in the following code. It needs to be added to
the following property in the QueuedArray.vb file you created in Chapter 3 for Visual Basic
users.
Visual Basic
Public ReadOnly Property Count() As Integer Implements ICollection.Count
Chapter 6 .NET Collection Interfaces 411
QueuedLinkedList(T)
Users cannot specifically state where they want to add an item or remove it, so only
ICollection is implemented, and not ICollection(T). First you need to add the ICollection in-
terface to the QueudLinkedList(T) class. Add a QueuedLinkedList.Collection.cs class file or a
QueuedLinkedList.Collection.vb class file to the C# or Visual Basic project you worked with in
Chapters 1 through 3. Change the class declaration from the following:
C#
public class QueuedLinkedList
Visual Basic
Partial Public Class QueuedLinkedList(Of T)
to this:
C#
public partial class QueuedLinkedList<T> : System.Collection.ICollection
Visual Basic
Partial Public Class QueuedLinkedList(Of T)
Implements ICollection
All of the methods and properties except CopyTo, IsSynchronized, and SyncRoot are already
implemented.
The IsSynchronized and SyncRoot properties are discussed in detail in Chapter 8, so for now
they are implemented as follows.
C#
bool System.Collections.ICollection.IsSynchronized
{
get { return false; }
}
object System.Collections.ICollection.SyncRoot
{
get { throw new NotImplementedException(); }
}
Visual Basic
Private ReadOnly Property IsSynchronized() As Boolean Implements ICollection.IsSynchronized
Get
Return False
End Get
End Property
412 Part III Using Collections
Items are iterated in a queue in the same order they are iterated in the internal linked list
data storage. So you can call the CopyTo method of the internal data storage instead.
C#
void System.Collections.ICollection.CopyTo(Array array, int index)
{
((System.Collections.ICollection)m_data).CopyTo(array, index);
}
Visual Basic
Private Sub CopyTo(ByVal array As Array, ByVal index As Integer) _
Implements ICollection.CopyTo
DirectCast(m_data, ICollection).CopyTo(array, index)
End Sub
The Implements statement is shown in bold in the following code. It needs to be added to
the following property in the QueuedLinkedList.vb file you created in Chapter 3 for Visual
Basic users.
Visual Basic
Public ReadOnly Property Count() As Integer Implements ICollection.Count
StackedArray(T)
Users cannot specifically state where they want to add an item or remove it, so only ICollection
is implemented and not ICollection(T). First you need to add the ICollection interface to the
StackedArray(T) class. Add a StackedArray.Collection.cs file or a StackedArray.Collection.vb
class file to the C# or Visual Basic project you worked with in Chapters 1 through 3. Change
the class declaration from the following:
C#
public class StackedArray
Visual Basic
Public Class StackedArray(Of T)
to this:
C#
public partial class StackedArray<T> : System.Collection.ICollection
Chapter 6 .NET Collection Interfaces 413
Visual Basic
Partial Public Class StackedArray(Of T)
Implements ICollection
All of the methods and properties except CopyTo, IsSynchronized, and SyncRoot are already
implemented.
The IsSynchronized and SyncRoot properties are discussed in detail in Chapter 8, so for now
they are implemented as follows.
C#
bool System.Collections.ICollection.IsSynchronized
{
get { return false; }
}
object System.Collections.ICollection.SyncRoot
{
get { throw new NotImplementedException(); }
}
Visual Basic
Private ReadOnly Property IsSynchronized() As Boolean Implements ICollection.IsSynchronized
Get
Return False
End Get
End Property
Stacks are implemented as LIFO, so items need to be copied to the specified array from the
end of the internal storage to the beginning of the internal storage. So you need to define a
method as follows for copying.
C#
void System.Collections.ICollection.CopyTo(Array array, int index)
{
if (array == null)
{
throw new ArgumentNullException("array");
}
if (index < 0)
{
throw new ArgumentOutOfRangeException("arrayIndex");
}
414 Part III Using Collections
if (array.Rank > 1)
{
throw new RankException("Cannot handle multidimensional arrays");
}
Visual Basic
Private Sub CopyTo(ByVal array As Array, ByVal index As Integer) _
Implements ICollection.CopyTo
If (array Is Nothing) Then
Throw New ArgumentNullException("array")
End If
The method counts from 0 to m_data.Count using variable i. It then takes the element at
(m_data.Count – i – 1) and assigns it to the specified array at (index + i).
The Implements statement is shown in bold in the following code. It needs to be added to
the following property in the StackedArray.vb file you created in Chapter 3 for Visual Basic
users.
Visual Basic
Public ReadOnly Property Count() As Integer Implements ICollection.Count
Chapter 6 .NET Collection Interfaces 415
StackedLinkedList(T)
Users cannot specifically state where they want to add an item or remove it, so only
ICollection is implemented and not ICollection(T). First you need to add the ICollection inter
face to the StackedLinkedList(T) class. Add a StackedLinkedList.Collection.cs class file or a
StackedLinkedList.Collection.vb class file to the C# or Visual Basic project you worked with in
Chapters 1 through 3. Change the class declaration from the following:
C#
public class StackedLinkedList
Visual Basic
Public Class StackedLinkedList(Of T)
to this:
C#
public partial class StackedLinkedList<T> : System.Collection.ICollection
Visual Basic
Partial Public Class StackedLinkedList(Of T)
Implements ICollection
All of the methods and properties except CopyTo, IsSynchronized, and SyncRoot are already
implemented.
The IsSynchronized and SyncRoot properties are discussed in detail in Chapter 8, so for now
they are implemented as follows.
C#
bool System.Collections.ICollection.IsSynchronized
{
get { return false; }
}
object System.Collections.ICollection.SyncRoot
{
get { throw new NotImplementedException(); }
}
Visual Basic
Private ReadOnly Property IsSynchronized() As Boolean Implements ICollection.IsSynchronized
Get
Return False
End Get
End Property
416 Part III Using Collections
Stacks are implemented as LIFO, so items need to be copied to the specified array from the
end of the internal storage to the beginning of the internal storage. So you need to define a
method as follows for copying.
C#
void System.Collections.ICollection.CopyTo(Array array, int index)
{
if (array == null)
{
throw new ArgumentNullException("array");
}
if (index < 0)
{
throw new ArgumentOutOfRangeException("arrayIndex");
}
if (array.Rank > 1)
{
throw new RankException("Cannot handle multidimensional arrays");
}
Visual Basic
Private Sub CopyTo(ByVal array As Array, ByVal index As Integer) _
Implements ICollection.CopyTo
If (array Is Nothing) Then
Throw New ArgumentNullException("array")
End If
The method traverses the list from the tail to the head while keeping track of the current
index in i. The element at the current node then is copied to the index at (i + index) in the
specified array.
The Implements statement is shown in bold in the following code. It needs to be added to
the following property in the StackedLinkedList.vb file you created in Chapter 3 for Visual
Basic users.
Visual Basic
Public ReadOnly Property Count() As Integer Implements ICollection.Count
AssociativeArrayAL(TKey,TValue)
First you need to add the ICollection and ICollection(T) interfaces to the AssociativeArrayAL
(TKey,TValue) class. Add an AssociativeArrayAL.Collection.cs class file or an AssociativeArray
AL.Collection.vb class file to the C# or Visual Basic project you worked with in Chapters 1
through 3. Change the class declaration from the following:
C#
public class AssociativeArrayAL
Visual Basic
Public Class AssociativeArrayAL(Of TKey, TValue)
to this:
C#
public partial class AssociativeArrayAL<TKey,TValue> :
System.Collection.ICollection,
ICollection< KeyValuePair<TKey,TValue> >
418 Part III Using Collections
Visual Basic
Partial Public Class AssociativeArrayAL(Of TKey, TValue)
Implements ICollection, ICollection(Of KeyValuePair(Of TKey, TValue))
All of the methods and properties except CopyTo, IsSynchronized, and SyncRoot are already
implemented.
The IsSynchronized and SyncRoot properties are discussed in detail in Chapter 8, so for now
they are implemented as follows.
C#
bool System.Collections.ICollection.IsSynchronized
{
get { return false; }
}
object System.Collections.ICollection.SyncRoot
{
get { throw new NotImplementedException(); }
}
Visual Basic
Private ReadOnly Property IsSynchronized() As Boolean Implements ICollection.IsSynchronized
Get
Return False
End Get
End Property
C#
/// <summary>
/// Gets a value indicating whether the collection is read only.
/// </summary>
public bool IsReadOnly
{
get { return false; }
}
Visual Basic
Public ReadOnly Property IsReadOnly() As Boolean _
Implements ICollection(Of KeyValuePair(Of TKey, TValue)).IsReadOnly
Get
Return False
End Get
End Property
Chapter 6 .NET Collection Interfaces 419
The Add method takes a KeyValuePair(TKey,TValue) as an argument. You need to pass the
properties of the Add method parameter to the Add(TKey key, TValue value) method created
in Chapter 2, as follows.
C#
void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
{
Add(item.Key, item.Value);
}
Visual Basic
Private Sub Add(ByVal item As KeyValuePair(Of TKey, TValue)) _
Implements ICollection(Of KeyValuePair(Of TKey, TValue)).Add
Add(item.Key, item.Value)
End Sub
C#
bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
{
EntryData data = FindKey(item.Key);
if (data.IsEmpty)
{
return false;
}
Visual Basic
Private Function Contains(ByVal item As KeyValuePair(Of TKey, TValue)) As Boolean _
Implements ICollection(Of KeyValuePair(Of TKey, TValue)).Contains
Dim node As DoubleLinkedListNode(Of KVPair) = FindKey(item.Key)
If (node Is Nothing) Then
Return False
End If
The Contains method uses the FindKey method to find the key. If the key is located, the
Contains method then compares the value to verify that it’s an exact match.
C#
bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
{
EntryData data = FindKey(item.Key);
if (data.IsEmpty)
{
return false;
}
if (!EqualityComparer<TValue>.Default.Equals(item.Value, data.Value))
{
return false;
}
return Remove(data);
}
Visual Basic
Private Function Remove(ByVal item As KeyValuePair(Of TKey, TValue)) As Boolean _
Implements ICollection(Of KeyValuePair(Of TKey, TValue)).Remove
Dim node As DoubleLinkedListNode(Of KVPair) = FindKey(item.Key)
If (node Is Nothing) Then
Return False
End If
Return Remove(node)
End Function
The Remove method uses the FindKey method to find the key. If the key is located, the
Remove method compares the value to verify that it’s an exact match. If it is, the method
then removes the item.
Both CopyTo methods traverse the collection and copy the elements to the specified array.
C#
void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array,
int arrayIndex)
{
if (array == null)
{
throw new ArgumentNullException("array");
}
if (arrayIndex < 0)
{
throw new ArgumentOutOfRangeException("arrayIndex");
}
int i = arrayIndex;
foreach (KeyValuePair<TKey, TValue> kvp in this)
{
array[i] = kvp;
++i;
}
}
void System.Collections.ICollection.CopyTo(Array array, int index)
{
if (array == null)
{
throw new ArgumentNullException("array");
}
if (index < 0)
{
throw new ArgumentOutOfRangeException("arrayIndex");
}
if (array.Rank > 1)
{
throw new RankException("Cannot handle multidimensional arrays");
}
int i = index;
foreach (KeyValuePair<TKey, TValue> kvp in this)
{
array.SetValue(kvp, i);
++i;
}
}
Visual Basic
Private Sub CopyTo(ByVal array As KeyValuePair(Of TKey, TValue)(), _
ByVal arrayIndex As Integer) _
Implements ICollection(Of KeyValuePair(Of TKey, TValue)).CopyTo
If (array Is Nothing) Then
Throw New ArgumentNullException("array")
End If
("array", "The specified array is not large enough to hold the data.")
End If
The Implements statement is shown in bold in the following code. It needs to be added
to the following methods and properties in the AssociativeArrayAL.vb file you created in
Chapter 2 for Visual Basic users.
Visual Basic
Public ReadOnly Property Count() As Integer _
Implements ICollection(Of KeyValuePair(Of TKey, TValue)).Count, ICollection.Count
AssociativeArrayHT(TKey,TValue)
First you need to add the ICollection and ICollection(T) interfaces to the AssociativeArray
HT(TKey,TValue) class. Add an AssociativeArrayHT.Collection.cs class file or an Associative
ArrayHT.Collection.vb class file to the C# or Visual Basic project you worked with in Chapters
1 through 3. Change the class declaration from the following:
Chapter 6 .NET Collection Interfaces 423
C#
public class AssociativeArrayHT
Visual Basic
Public Class AssociativeArrayHT(Of TKey, TValue)
to this:
C#
public partial class AssociativeArrayHT<TKey,TValue> :
System.Collection.ICollection,
ICollection< KeyValuePair<TKey,TValue> >
Visual Basic
Partial Public Class AssociativeArrayHT(Of TKey, TValue)
Implements ICollection, ICollection(Of KeyValuePair(Of TKey, TValue))
All of the methods and properties except CopyTo, IsSynchronized, and SyncRoot are already
implemented.
The IsSynchronized and SyncRoot properties are discussed in detail in Chapter 8, so for now
they are implemented as follows.
C#
bool System.Collections.ICollection.IsSynchronized
{
get { return false; }
}
object System.Collections.ICollection.SyncRoot
{
get { throw new NotImplementedException(); }
}
Visual Basic
Private ReadOnly Property IsSynchronized() As Boolean Implements ICollection.IsSynchronized
Get
Return False
End Get
End Property
The Add method takes a KeyValuePair(TKey,TValue) as an argument. You need to pass the
properties of the Add method parameter to the Add(TKey key, TValue value) method created
in Chapter 2, as follows.
C#
void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
{
Add(item.Key, item.Value);
}
Visual Basic
Private Sub Add(ByVal item As KeyValuePair(Of TKey, TValue)) _
Implements ICollection(Of KeyValuePair(Of TKey, TValue)).Add
Add(item.Key, item.Value)
End Sub
C#
bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
{
EntryData data = FindKey(item.Key);
if (data.IsEmpty)
{
return false;
}
Visual Basic
Private Function Contains(ByVal item As KeyValuePair(Of TKey, TValue)) As Boolean _
Implements ICollection(Of KeyValuePair(Of TKey, TValue)).Contains
Dim data As EntryData = FindKey(item.Key)
If (data.IsEmpty) Then
Return False
End If
The Contains method uses the FindKey method to find the key. If the key is located, the
FindKey method then compares the value to verify that it’s an exact match.
C#
bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
{
EntryData data = FindKey(item.Key);
if (data.IsEmpty)
{
return false;
}
if (!EqualityComparer<TValue>.Default.Equals(item.Value, data.Value))
{
return false;
}
return Remove(data);
}
Visual Basic
Private Function Remove(ByVal item As KeyValuePair(Of TKey, TValue)) As Boolean _
Implements ICollection(Of KeyValuePair(Of TKey, TValue)).Remove
Dim data As EntryData = FindKey(item.Key)
If (data.IsEmpty) Then
Return False
End If
Return Remove(data)
End Function
The Remove method uses the FindKey method to find the key. If the key is located, the FindKey
method then compares the value to verify that it’s an exact match. If it is, the method then
removes the item.
Both CopyTo methods traverse the collection and copy the elements to the specified array.
C#
void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array,
int arrayIndex)
{
if (array == null)
{
throw new ArgumentNullException("array");
}
if (arrayIndex < 0)
{
throw new ArgumentOutOfRangeException("arrayIndex");
}
426 Part III Using Collections
int i = arrayIndex;
foreach (KeyValuePair<TKey, TValue> kvp in this)
{
array[i] = kvp;
++i;
}
}
void System.Collections.ICollection.CopyTo(Array array, int index)
{
if (array == null)
{
throw new ArgumentNullException("array");
}
if (index < 0)
{
throw new ArgumentOutOfRangeException("arrayIndex");
}
if (array.Rank > 1)
{
throw new RankException("Cannot handle multidimensional arrays");
}
int i = index;
foreach (KeyValuePair<TKey, TValue> kvp in this)
{
array.SetValue(kvp, i);
++i;
}
}
Visual Basic
Private Sub CopyTo(ByVal array As KeyValuePair(Of TKey, TValue)(), _
ByVal arrayIndex As Integer) _
Implements ICollection(Of KeyValuePair(Of TKey, TValue)).CopyTo
If (array Is Nothing) Then
Throw New ArgumentNullException("array")
End If
Chapter 6 .NET Collection Interfaces 427
If (arrayIndex < 0) Then
Throw New ArgumentOutOfRangeException("arrayIndex")
End If
The Implements statement is shown in bold in the following code. It needs to be added to
the following methods and properties in the AssociativeArrayHT.vb file created in Chapter 2
for Visual Basic users.
Visual Basic
Public ReadOnly Property Count() As Integer _
Implements ICollection(Of KeyValuePair(Of TKey, TValue)).Count, ICollection.Count
C#
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
T this[int index] { get; set; }
int IndexOf(T item);
void Insert(int index, T item);
void RemoveAt(int index);
}
Visual Basic
Public Interface IList(Of T)
Inherits ICollection(Of T), IEnumerable(Of T), IEnumerable
Function IndexOf(ByVal item As T) As Integer
Sub Insert(ByVal index As Integer, ByVal item As T)
Sub RemoveAt(ByVal index As Integer)
Default Property Item(ByVal index As Integer) As T
End Interface
C#
public interface IList : ICollection, IEnumerable
{
bool IsFixedSize { get; }
bool IsReadOnly { get; }
object this[int index] { get; set; }
int Add(object value);
void Clear();
bool Contains(object value);
int IndexOf(object value);
void Insert(int index, object value);
void Remove(object value);
void RemoveAt(int index);
}
Chapter 6 .NET Collection Interfaces 429
Visual Basic
Public Interface IList
Inherits ICollection, IEnumerable
Function Add(ByVal value As Object) As Integer
Sub Clear()
Function Contains(ByVal value As Object) As Boolean
Function IndexOf(ByVal value As Object) As Integer
Sub Insert(ByVal index As Integer, ByVal value As Object)
Sub Remove(ByVal value As Object)
Sub RemoveAt(ByVal index As Integer)
ReadOnly Property IsFixedSize As Boolean
ReadOnly Property IsReadOnly As Boolean
Default Property Item(ByVal index As Integer) As Object
End Interface
At first glance, it looks as if the IList interface has more functionality than the IList(T). The
IList(T) interface derives from the ICollection(T) interface, which contains all of the methods
and properties in IList except IsFixedSize, but in type-safe form. IList contains the nongeneric
form of Add, Item, Contains, IndexOf, Insert, and Remove, and the IList(T) interface contains
the generic form of these methods and properties. Through these two interfaces, you are
able to add, insert, remove, and access objects in an indexed type collection.
The IsFixedSize property in IList is used to state whether the collection has a fixed size. A
collection that is fixed size can not grow or shrink, so any method’s calls, such as Clear, Add,
Remove, RemoveAt, or Insert, are an invalid operation. However, you can set the elements in
the collection to a different value.
In addition to the Add method being a nongeneric type in IList and a generic type in IList(T),
the IList.Add method returns an integer. The return value is the index of where the item was
added.
ArrayEx(T)
First you need to add the IList(T) and IList interfaces to the ArrayEx(T) class. Add an
ArrayEx.List.cs class file or an ArrayEx.List.vb class file to the C# or Visual Basic project you
worked with in Chapters 1 through 3. Change the class declaration from the following:
430 Part III Using Collections
C#
public class ArrayEx
Visual Basic
Public Class ArrayEx(Of T)
to this:
C#
public partial class ArrayEx<T> : IList<T>, System.Collections.IList
Visual Basic
Partial Public Class ArrayEx(Of T)
Implements IList(Of T), IList
All of the methods and properties except IsFixedSize and the non-generic versions of Add,
Contains, IndexOf, Insert, Remove, and Item are already implemented.
Because ArrayEx(T) can be resized, you need to return a false for the IsFixedSize property, as
follows.
C#
bool System.Collections.IList.IsFixedSize
{
get { return false; }
}
Visual Basic
Private ReadOnly Property IsFixedSize() As Boolean Implements IList.IsFixedSize
Get
Return False
End Get
End Property
You need to return a true if you want the collection to be of a fixed size after it is created.
You also need to implement the nongeneric versions of Add, Contains, IndexOf, Insert,
Remove, and Item. For these methods and properties, you can cast the specified object in the
parameter to type T and then call the generic equivalent of the method or property you are
in so that you do not have to duplicate any code. To do this, you need to check and see if the
specified object can be cast to type T in each one of the methods and properties. The easiest
way to do this is to create a method that does the check for you and call it at the beginning
of each of the methods and properties. You can use the following method to do so.
Chapter 6 .NET Collection Interfaces 431
C#
bool CanCastToT(object value)
{
if (!(value is T) && ((value != null) || typeof(T).IsValueType))
{
return false;
}
return true;
}
Visual Basic
Private Function CanCastToT(ByVal value As Object)
If (Not (TypeOf value Is T) And ((value IsNot Nothing) Or GetType(T).IsValueType)) Then
Return False
End If
Return True
End Function
The CanCastToT method verifies that the specified object can be cast to type T. The method
first checks to see whether the value is of type T by doing !(value is T) (using C#). This check
fails if value is not of type T or it is null. If it fails, you need to ensure that it is not because
value is null. If value is null, you need to make sure T is not a value type, because null values
cannot be assigned to a value type. You can check for that by doing an AND operation with
((value != null) || typeof(T).IsValueType) (using C#). The typeof(T).IsValueType property returns
true if type T is a value type.
Now that you have created a method to check whether the specified object can be cast to T,
you can implement the remaining methods and properties.
C#
bool System.Collections.IList.Contains(object value)
{
if (CanCastToT(value))
{
throw new ArgumentException("Value is not of type T", "value");
}
return Contains((T)value);
}
Add((T)value);
return Count - 1;
}
Visual Basic
Private Function Add(ByVal value As Object) As Integer Implements IList.Add
If (CanCastToT(value)) Then
Throw New ArgumentException("Value is not of type T", "value")
End If
Chapter 6 .NET Collection Interfaces 433
Add(DirectCast(value, T))
Return Count - 1
End Function
Private Sub Insert(ByVal index As Integer, ByVal value As Object) Implements IList.Insert
If (CanCastToT(value)) Then
Throw New ArgumentException("Value is not of type T", "value")
End If
Insert(index, DirectCast(value, T))
End Sub
Each method and property calls the CanCastToT method to verify that value can be cast to
type T. If the specified object cannot be cast to T, an ArgumentException is thrown to notify
the caller that he or she specified an invalid argument type. Also, do you remember that ear-
lier you learned that the return type of the Add method of IList is different than the IList(T)?
The preceding implementation for the Add method returns a (Count – 1), which is the loca-
tion of the item after the ArrayEx(T).Add method executes.
434 Part III Using Collections
The Implements statement is shown in bold in the following code. It needs to be added to
the following methods and properties in the ArrayEx.vb file created in Chapter 1 for Visual
Basic users.
Visual Basic
Default Public Property Item(ByVal index As Integer) As T Implements IList(Of T).Item
Public Sub Insert(ByVal index As Integer, ByVal item As T) Implements IList(Of T).Insert
IDictionary(TKey,TValue) Overview
The IDictionary and IDictionary(TKey,TValue) interfaces define an object as a collection of
key/value pairs.
C#
public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>,
IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable
{
// Methods
void Add(TKey key, TValue value);
bool ContainsKey(TKey key);
bool Remove(TKey key);
bool TryGetValue(TKey key, out TValue value);
// Properties
TValue this[TKey key] { get; set; }
ICollection<TKey> Keys { get; }
ICollection<TValue> Values { get; }
}
public interface IDictionary : ICollection, IEnumerable
{
// Methods
void Add(object key, object value);
void Clear();
bool Contains(object key);
IDictionaryEnumerator GetEnumerator();
void Remove(object key);
// Properties
bool IsFixedSize { get; }
bool IsReadOnly { get; }
object this[object key] { get; set; }
ICollection Keys { get; }
ICollection Values { get; }
}
Chapter 6 .NET Collection Interfaces 435
Visual Basic
Public Interface IDictionary(Of TKey, TValue)
Inherits ICollection(Of KeyValuePair(Of TKey, TValue)), _
IEnumerable(Of KeyValuePair(Of TKey, TValue)), IEnumerable
' Methods
Sub Add(ByVal key As TKey, ByVal value As TValue)
Function ContainsKey(ByVal key As TKey) As Boolean
Function Remove(ByVal key As TKey) As Boolean
Function TryGetValue(ByVal key As TKey, <Out> ByRef value As TValue) As Boolean
' Properties
Default Property Item(ByVal key As TKey) As TValue
ReadOnly Property Keys As ICollection(Of TKey)
ReadOnly Property Values As ICollection(Of TValue)
End Interface
' Properties
ReadOnly Property IsFixedSize As Boolean
ReadOnly Property IsReadOnly As Boolean
Default Property Item(ByVal key As Object) As Object
ReadOnly Property Keys As ICollection
ReadOnly Property Values As ICollection
End Interface
The IDictionary and IDictionary(TKey, TValue) interfaces both allow you to enumerate the
object’s key and value as a pair. They do this by returning a key/value pair object called
DictionaryEntry and KeyValuePair(TKey, TValue) respectively. Both objects contain the value
and the key that goes with the value. In Chapter 2, you learned that a dictionary is another
name for an associative array, so you can also use the interfaces to access associative arrays.
AssociativeArrayAL(TKey,TValue)
First you need to add the IDictionary(TKey,TValue) interface to the AssociativeArrayAL
(TKey,TValue) class. Add an AssociativeArrayAL.Dictionary.cs class file or an Associative
ArrayAL.Dictionary.vb class file to the C# or Visual Basic project you worked with in
Chapters 1 through 3. Change the class declaration from the following:
C#
public class AssociativeArrayAL
Visual Basic
Public Class AssociativeArrayAL(Of TKey, TValue)
to this:
C#
public partial class AssociativeArrayAL<TKey,TValue> : IDictionary<TKey,TValue>
Visual Basic
Partial Public Class AssociativeArrayAL(Of TKey, TValue)
Implements IDictionary(Of TKey, TValue)
The IDictionary(TKey,TValue) interface defines the Keys and Values properties, so you need to
remove the old implementation you created in Chapter 2. The new implementation returns
an ICollection(TKey) and ICollection(TValue) respectively.
C#
public ICollection<TKey> Keys
{
get { return new KeyCollection(this); }
}
Visual Basic
Public ReadOnly Property Keys() As ICollection(Of TKey) _
Implements IDictionary(Of TKey, TValue).Keys
Get
Return New KeyCollection(Me)
End Get
End Property
You need to create a KeyCollection and ValueCollection class for the Keys and Values prop-
erties. The full implementation for the KeyCollection and ValueCollection properties can be
found in the AssociativeArrayAL(TKey, TValue) partial class located in the Chapter 6\CS
\DevGuideToCollections\AssociativeArrayAL.Dictionary.cs file for C# or the Chapter 6\VB
\DevGuideToCollections\AssociativeArrayAL.Dictionary.vb file for Visual Basic.
More Info See the section titled “Adding Enumeration Support to Classes” if you have ques-
tions about the implementation.
The Implements statement is shown in bold in the following code. It needs to be added
to the following methods and properties in the AssociativeArrayAL.vb file you created in
Chapter 2 for Visual Basic users.
Visual Basic
Public Function ContainsKey(ByVal key As TKey) As Boolean _
Implements IDictionary(Of TKey, TValue).ContainsKey
AssociativeArrayHT(TKey,TValue)
First you need to add the IDictionary(TKey,TValue) interface to the AssociativeArrayHT
(TKey,TValue) class. Add an AssociativeArrayHT.Dictionary.cs class file or an AssociativeArray
HT.Dictionary.vb class file to the C# or Visual Basic project you worked with in Chapters 1
through 3. Change the class declaration from the following:
C#
public class AssociativeArrayHT
to this:
C#
public partial class AssociativeArrayHT<TKey,TValue> : IDictionary<TKey,TValue>
438 Part III Using Collections
The IDictionary(TKey,TValue) interface defines the Keys and Values, so you need to remove
the old implementation you created in Chapter 2. The new implementation returns an
ICollection(TKey) and ICollection(TValue) respectively.
C#
public ICollection<TKey> Keys
{
get { return new KeyCollection(this); }
}
Visual Basic
Public ReadOnly Property Keys() As ICollection(Of TKey) _
Implements IDictionary(Of TKey, TValue).Keys
Get
Return New KeyCollection(Me)
End Get
End Property
You need to create a KeyCollection and ValueCollection class for the Keys and Values prop-
erties. The full implementation for the KeyCollection and ValueCollection classes can be
found in the AssociativeArrayHT(TKey, TValue) partial class located in the Chapter 6\CS
\DevGuideToCollections\AssociativeArrayHT.Dictionary.cs file for C# or the Chapter 6
\VB\DevGuideToCollections\AssociativeArrayHT.Dictionary.vb file for Visual Basic.
More Info See the section titled “Adding Enumeration Support to Classes” if you have ques-
tions about the implementation.
The Implements statement is shown in bold in the following code. It needs to be added
to the following methods and properties in the AssociativeArrayHT.vb file you created in
Chapter 2 for Visual Basic users.
Chapter 6 .NET Collection Interfaces 439
Visual Basic
Public Function ContainsKey(ByVal key As TKey) As Boolean _
Implements IDictionary(Of TKey, TValue).ContainsKey
Summary
In this chapter, you saw how to implement the common .NET collection interfaces for
your classes. You saw that the IEnumerator(T) and IEnumerable(T) interfaces allow your
classes to be enumerated. You also saw that the ICollection interface allows your collection
to return the length of the collection and to support synchronization. Implementing the
ICollection(T) interface gives your collection common collection support. You also saw how
the ICollection(T) and ICollection interfaces are not derived from each other. You then saw
how to add indexing support to your collections through the IList(T) interface and how to
add dictionary support through the IDictionary(TKey, TValue) interface. All of the interfaces
discussed in this chapter allow your collections to be used like any other .NET collections, as
Chapter 7 partially demonstrates.
Chapter 7
Introduction to LINQ
After completing this chapter, you will be able to
■ Understand what LINQ is.
■ Write LINQ queries against collection classes.
What Is LINQ?
In the previous chapters, you saw how to create and interact with collections using the
foreach statement and Microsoft .NET interfaces. In this chapter, you will discover a different
approach: how to query your collections with the Language Integrated Query (LINQ). With
LINQ, you can query data by using native language syntax.
C#
Random rnd = new Random();
List<int> randomNumbers = new List<int>();
for (int i = 0; i < 10; i++)
{
randomNumbers.Add(rnd.Next(100));
}
sortedNumbers.Sort();
sortedNumbers.Reverse();
Visual Basic
Dim rnd As Random = New Random()
Dim randomNumbers As New List(Of Integer)()
For i As Integer = 0 To 9
randomNumbers.Add(rnd.Next(100))
Next
sortedNumbers.Sort()
sortedNumbers.Reverse()
441
442 Part III Using Collections
C#
Random rnd = new Random();
var randomNumbers = from n in Enumerable.Range(1, 10) select rnd.Next(100);
var sortedNumbers = from random in randomNumbers orderby random descending select random;
Visual Basic
Dim rnd As Random = New Random()
Dim randomNumbers = From n In Enumerable.Range(1, 10) Select rnd.Next(100)
Dim sortedNumbers = From random In randomNumbers Order By random Descending Select random
Both examples create a random list of numbers sorted in descending order. Notice how the
LINQ code did not require the Sort and Reverse methods. Using LINQ, you can also nest the
statements just shown as follows.
C#
Random rnd = new Random();
Visual Basic
Dim rnd As Random = New Random()
LINQ Basics
To get started writing LINQ queries, it helps to know the three distinct actions of a LINQ
query as defined in MSDN.
You can use .NET collections—the only data source that this chapter focuses on—as
data sources because they all implement the IEnumerable or IEnumerable(T) interfaces.
Because the classes you created in Chapter 6, “.NET Collection Interfaces,” also implement
IEnumerable(T), you can use them in a LINQ query such as the following.
C#
ArrayEx<int> array = new ArrayEx<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
Visual Basic
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Dim oddInts = From element In array Where (element Mod 2) = 1 Select element
Output
1
3
5
7
9
The preceding code executes a query that returns all odd numbers from the variable array
and then writes them to the console.
C#
ArrayEx<int> array = new ArrayEx<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
Visual Basic
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Output
Building the query
Adding 11, 12, and 13 to the data source
Iterating over the query
1
3
5
7
9
11
13
The preceding example shows how you can easily introduce a bug into your application.
Queries execute when they are iterated over—not when they are created. The query itself
creates an “execution plan.” That plan was stored in oddInts but not executed until the code
iterated over oddInts. Note that during the time between the creation and execution of the
query, the numbers 11, 12, and 13 were added to the data source array and showed up in
the results.
Also note that the query executes every time you iterate over the query variable, not only the
first time, as the next example shows you.
C#
ArrayEx<int> array = new ArrayEx<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
Visual Basic
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Output
Building the query
Adding 11, 12, and 13 to the data source
Iterating over the query
1
3
5
7
9
11
13
Adding 14, 15, 16, and 17 to the data source
Iterating over the query
1
3
5
7
9
11
13
15
17
The two iterations over the oddInts variable provided two different results because items
were added to the original data source before each iteration. This is because the query vari-
able holds the query command (the execution plan) and not the query result.
The ToList and ToArray methods force immediate execution of the query, as shown in the
following example.
C#
ArrayEx<int> array = new ArrayEx<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
Visual Basic
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Output
Building the query
Adding 11, 12, and 13 to the data source
Iterating over the query
1
3
5
7
9
448 Part III Using Collections
The oddInts variable now stores the results of the query rather than the query commands.
The ToArray(T) method returns a T[] and the ToList(T) method returns a List(T).
Methods that perform aggregation functions over the query, such as Count, Max, Average,
and First, also execute the query because they must perform a foreach to obtain their result.
These methods return a single value and not an enumeration.
C#
ArrayEx<int> array = new ArrayEx<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
Visual Basic
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Output
Building the query
Adding 11, 12, and 13 to the data source
Count of the original query is still 5
Adding 14, 15, 16, and 17 to the data source
Count of the original query is still 5
The count variable never changes because the query was executed when it was created.
C#
public enum Sex
{
Unknown,
Male,
Female,
}
Visual Basic
Public Enum Sex
Unknown
Male
Female
End Enum
Public Sub New(ByVal first As String, ByVal last As String, ByVal age As Integer, _
ByVal sex As Sex)
Me.FirstName = first
Me.LastName = last
Me.age = age
Me.sex = sex
End Sub
End Structure
Using this structure in the following examples will help with some of the complex examples.
Chapter 7 Introduction to LINQ 451
C#
var v1 = 0 == 1;
Visual Basic
Dim v1 = 0 = 1
There are several things you should know about this new keyword.
First, the variable must be initialized. If it is not, you will get a compiler error during compile
time.
Second, the variable cannot change data types. The following examples produce a compiler
error during compile time.
C#
var v1 = 0 == 1;
v1 = "test";
Visual Basic
Dim v1 = 0 = 1
v1 = "test"
In the preceding example, you cannot change the variable v1 from a bool to string, because
the compiler first inferred the data type as a bool.
Third, the variable must be a local variable, not a parameter or return type to a method, or a
field or property of a class or struct.
452 Part III Using Collections
Anonymous Types
You can use anonymous types to create a type that exposes only read-only properties with-
out explicitly declaring the type. The type will be created as an object but will not have a
name (thus, why it’s called anonymous). Anonymous types also cannot have methods, fields,
events, or writable properties. The following is an example of an anonymous type.
C#
var person = new { LastName = "Alexander", FirstName = "Sean", MiddleInitial = "P" };
Console.WriteLine(person.LastName);
Visual Basic
Dim person = New With {.LastName = "Alexander", .FirstName = "Sean", .MiddleInitial = "P"}
Console.WriteLine(person.LastName)
Output
Alexander
Right now, you may be wondering how anonymous types can be useful if you cannot change
their properties and cannot return them to the calling method. Later in this chapter, you will
see how you can use LINQ to return a subset of data, or even a new data type created from
parts of the data source.
Object Initialization
You can initialize objects during their creation, as illustrated in the following examples.
C#
Person person = new Person()
{
LastName = “Alexander”,
FirstName = “Sean P.”,
Age = 46,
Sex = Sex.Male
};
Visual Basic
Dim person As New Person() With _
{ _
.LastName = “Alexander”, _
.FirstName = “Sean P.”, _
.Age = 46, _
.Sex = Sex.Male _
}
C#
Person person = new Person();
person.LastName = "Alexander";
person.FirstName = "Sean P.";
person.Age = 46;
person.Sex = Sex.Male;
Visual Basic
Dim person As New Person()
person.LastName = "Alexander"
person.FirstName = "Sean P."
person.Age = 46
person.Sex = Sex.Male
The usefulness of this shorter form of object initialization will become apparent in some of
the following LINQ examples.
Collection Initialization
Previous chapters used the following syntax to create a collection with default values.
C#
ArrayEx<int> array = new ArrayEx<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
Visual Basic
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
In Chapter 6, you went through the process of adding the interface ICollection(T) to your
custom collection classes. With this interface, you can now create a collection instance with
default items by using the following syntax instead.
C#
ArrayEx<int> array = new ArrayEx<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
You can create and initialize any class that implements ICollection(T) in this same manner.
The data source must implement the IEnumerable, IEnumerable(T), or IQueryable(T) interfaces.
For collections, you should only be concerned with the IEnumerable and IEnumerable(T) inter
faces. You can think of the from clause as a foreach statement written like the following.
C#
foreach (range variable) in (datasource)
Visual Basic
For each (range variable) in (datasource)
The difference between the foreach statement and the from clause is that the range variable
in a from clause is used as a syntactic convenience and never actually stores the current item.
However, you can use this variable in other clauses to interact with the retrieved information
about the current item.
C#
ArrayEx<int> array = new ArrayEx<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Visual Basic
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Output
1
2
3
4
5
6
7
8
9
10
The code traverses the array variable and returns a collection that contains the elements in
array. This is accomplished by specifying the range variable element in the select clause.
The following example demonstrates how to use the select clause to perform an operation
on the range variable—in this case, a multiplication.
C#
ArrayEx<int> array = new ArrayEx<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Visual Basic
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Output
2
4
6
8
10
12
14
16
18
20
The preceding example returns a collection that contains each element in the array, multi-
plied by 2 because of the element * 2 operation included in the select clause.
You can also use the select clause to return a different type than the original data source con-
tained, as shown in the following code.
456 Part III Using Collections
C#
ArrayEx<int> array = new ArrayEx<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Visual Basic
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Output
1
2
3
4
5
6
7
8
9
10
This example returns a collection of strings instead of ints. This was accomplished by using
element.ToString() in the select clause.
You can also use the select clause to return the fields or properties of an object instead of the
object.
C#
Person []people = new Person[]
{
new Person("Cristina","Potra"),
new Person("Jeff","Phillips"),
new Person("Mark","Alexieff"),
new Person("Holly","Holt"),
new Person("Gordon L.","Hee"),
new Person("Annie","Herriman"),
};
var names = from person in people select person.LastName + ", " + person.FirstName;
foreach (string name in names)
{
Console.WriteLine(name);
}
Chapter 7 Introduction to LINQ 457
Visual Basic
Dim people As Person() = New Person() _
{ _
New Person("Cristina", "Potra"), _
New Person("Jeff", "Phillips"), _
New Person("Mark", "Alexieff"), _
New Person("Holly", "Holt"), _
New Person("Gordon L.", "Hee"), _
New Person("Annie", "Herriman") _
}
Dim names = From person In people Select person.LastName + ", " + person.FirstName
For Each name As String In names
Console.WriteLine(name)
Next
Output
Potra, Cristina
Phillips, Jeff
Alexieff, Mark
Holt, Holly
Hee, Gordon L.
Herriman, Annie
The preceding code combines the person’s last name with his or her first name and then
returns a collection containing only that information. This was accomplished by specifying
person.LastName + “, “ + person.FirstName in the select clause.
You can create an anonymous type from another value type or object, as shown in the fol-
lowing code.
C#
ArrayEx<int> array = new ArrayEx<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Visual Basic
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Output
{ Value = 1, IsEven = False, Doubled = 2 }
{ Value = 2, IsEven = True, Doubled = 4 }
{ Value = 3, IsEven = False, Doubled = 6 }
{ Value = 4, IsEven = True, Doubled = 8 }
{ Value = 5, IsEven = False, Doubled = 10 }
{ Value = 6, IsEven = True, Doubled = 12 }
{ Value = 7, IsEven = False, Doubled = 14 }
{ Value = 8, IsEven = True, Doubled = 16 }
{ Value = 9, IsEven = False, Doubled = 18 }
{ Value = 10, IsEven = True, Doubled = 20 }
This example creates an anonymous type containing the properties Value, IsEven, and Doubled
by specifying new { Value = element, IsEven = (element % 2 == 0), Doubled=element*2 } in
the select clause. Now you should be able to see how anonymous types can be useful.
The following example filters the range element by checking to see if it is even.
C#
ArrayEx<int> array = new ArrayEx<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Visual Basic
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Dim even = From element In array Where element Mod 2 = 0 Select element
For Each element As Integer In even
Console.WriteLine(element)
Next
Output
2
4
6
8
10
Chapter 7 Introduction to LINQ 459
The preceding code returns only the even numbers by performing a modulo operation in
the where clause.
You can filter a list of objects by using the object’s fields, properties, or methods.
C#
Person []people = new Person[]
{
new Person("Cristina","Potra", 22, Sex.Female),
new Person("Jeff","Phillips", 17, Sex.Male),
new Person("Mark","Alexieff", 55, Sex.Male),
new Person("Holly","Holt", 58, Sex.Female),
new Person("Gordon L.","Hee", 87, Sex.Male),
new Person("Annie","Herriman", 34, Sex.Female),
};
Visual Basic
Dim people As Person() = New Person() _
{ _
New Person("Cristina", "Potra", 22, Sex.Female), _
New Person("Jeff", "Phillips", 17, Sex.Male), _
New Person("Mark", "Alexieff", 55, Sex.Male), _
New Person("Holly", "Holt", 58, Sex.Female), _
New Person("Gordon L.", "Hee", 87, Sex.Male), _
New Person("Annie", "Herriman", 34, Sex.Female) _
}
Output
Phillips, Jeff
The preceding code returns only minors by specifying person.Age < 18 in the where clause.
460 Part III Using Collections
The following example shows how you can sort a list of integers in descending order.
C#
ArrayEx<int> array = new ArrayEx<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var numbers = from element in array orderby element descending select element;
foreach (int element in numbers)
{
Console.WriteLine(element);
}
Visual Basic
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Dim numbers = From element In array Order By element Descending Select element
For Each element As Integer In numbers
Console.WriteLine(element)
Next
Output
10
9
8
7
6
5
4
3
2
1
The preceding code sorts the list in descending order by specifying element descending in
the orderby clause.
The following example shows how you can sort on multiple fields.
C#
Person []people = new Person[]
{
new Person("Cristina","Potra", 22, Sex.Female),
new Person("Jeff","Phillips", 17, Sex.Male),
new Person("Mark","Alexieff", 55, Sex.Male),
new Person("Holly","Holt", 58, Sex.Female),
new Person("Gordon L.","Hee", 87, Sex.Male),
new Person("Annie","Herriman", 34, Sex.Female),
};
Chapter 7 Introduction to LINQ 461
var names = from person in people
orderby person.LastName, person.FirstName
select person.LastName + ", " + person.FirstName;
foreach (string name in names)
{
Console.WriteLine(name);
}
Visual Basic
Dim people As Person() = New Person() _
{ _
New Person("Cristina", "Potra", 22, Sex.Female), _
New Person("Jeff", "Phillips", 17, Sex.Male), _
New Person("Mark", "Alexieff", 55, Sex.Male), _
New Person("Holly", "Holt", 58, Sex.Female), _
New Person("Gordon L.", "Hee", 87, Sex.Male), _
New Person("Annie", "Herriman", 34, Sex.Female) _
}
Output
Alexieff, Mark
Hee, Gordon L.
Herriman, Annie
Holt, Holly
Phillips, Jeff
Potra, Cristina
The preceding code sorts the returned values by specifying person.LastName, person
.FirstName in the orderby clause. The orderby clause sorts by the person.LastName field,
and then by person.FirstName. Also note how the comparison is performed on the specified
fields, and not on the object itself. You cannot specify person because there is no default
comparer for the Person struct.
The following example shows how you can group on the range variable.
C#
ArrayEx<int> array = new ArrayEx<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
{
Console.WriteLine("{0}", group.Key ? "Even" : "Odd");
foreach (int element in group)
{
Console.WriteLine("\t" + element);
}
}
Visual Basic
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Dim numbers = From element In array Group element By Key = ((element Mod 2) = 0) Into Group
For Each group In numbers
Console.WriteLine("{0}", IIf(group.Key, "Even", "Odd"))
For Each element As Integer In group.Group
Console.WriteLine(vbTab & element)
Next
Next
Output
Odd
1
3
5
7
9
Even
2
4
6
8
10
The preceding code groups the numbers in an even and odd group by specifying
element%2==0 for the group clause.
You can also group on individual fields, as shown in the following example.
C#
Person []people = new Person[]
{
new Person("Cristina","Potra", 22, Sex.Female),
new Person("Jeff","Phillips", 17, Sex.Male),
new Person("Mark","Alexieff", 55, Sex.Male),
new Person("Holly","Holt", 58, Sex.Female),
new Person("Gordon L.","Hee", 87, Sex.Male),
new Person("Annie","Herriman", 34, Sex.Female),
};
Visual Basic
Dim people As Person() = New Person() _
{ _
New Person("Cristina", "Potra", 22, Sex.Female), _
New Person("Jeff", "Phillips", 17, Sex.Male), _
New Person("Mark", "Alexieff", 55, Sex.Male), _
New Person("Holly", "Holt", 58, Sex.Female), _
New Person("Gordon L.", "Hee", 87, Sex.Male), _
New Person("Annie", "Herriman", 34, Sex.Female) _
}
Dim names = From person In people Group person By Key = person.Sex Into Group
For Each group In names
Console.WriteLine("{0}", group.Key)
For Each person As Person In group.Group
Console.WriteLine(vbTab & "{0}, {1}", person.LastName, person.FirstName)
Next
Next
Output
Female
Potra, Cristina
Holt, Holly
Herriman, Annie
Male
Phillips, Jeff
Alexieff, Mark
Hee, Gordon L.
The preceding code groups the people according to their sex by specifying person.Sex for
the group clause.
Inner Join
Inner joins produce a result by combining multiple collections into one. The result of the
query is the expression in the select clause. Each item in the data source is traversed and
combined with the item in the join collection that matches the join criteria. Items that do
not match are not returned.
464 Part III Using Collections
C#
ArrayEx<int> array = new ArrayEx<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var joined = from element in array join e in integerTypes on element % 2 equals e.Id
select new { Value = element, Type=e.Name };
foreach (var tmp in joined)
{
Console.WriteLine(tmp);
}
Visual Basic
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Dim joined = From element In array Join e In integerTypes On element Mod 2 Equals e.Id _
Select New With {.Value = element, .Type = e.Name}
For Each tmp In joined
Console.WriteLine(tmp)
Next
Output
{ Value = 1, Type = Odd }
{ Value = 2, Type = Even }
{ Value = 3, Type = Odd }
{ Value = 4, Type = Even }
{ Value = 5, Type = Odd }
{ Value = 6, Type = Even }
{ Value = 7, Type = Odd }
{ Value = 8, Type = Even }
{ Value = 9, Type = Odd }
{ Value = 10, Type = Even }
For demonstration purposes, integerTypes stores a collection of anonymous types that repre-
sent the following IntegerType enumeration.
C#
public enum IntegerType
{
Even = 0,
Odd = 1
}
Chapter 7 Introduction to LINQ 465
Visual Basic
Public Enum IntegerType
Even = 0
Odd = 1
End Enum
This is accomplished by storing the integer form of IntegerType as an Id and the string form
of IntegerType as the Name. The two collections are inner joined by comparing the element%2
to the Id field in integerTypes. The enumeration was written so that element%2 contains the
same value as IntegerType.Even when the number is even and IntegerType.Odd when the
number is odd. You could instead create a data type and collection for holding the informa-
tion in integerTypes.
Outer Join
An outer join operates the same way as an inner join except it also returns results for items in
the data source that do not have a matching item in the join collection.
C#
var primes = from value in new ArrayEx<int>() { 2, 3, 5, 7 }
select new { Id = value, Name = "Prime" };
Visual Basic
Dim primes = From value In New ArrayEx(Of Integer)(New Integer() {2, 3, 5, 7}) _
Select New With {.Id = value, .Name = "Prime"}
Dim array As New ArrayEx(Of Integer)(New Integer() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
Output
{ Value = 1, Type = Not Prime }
{ Value = 2, Type = Prime }
{ Value = 3, Type = Prime }
{ Value = 4, Type = Not Prime }
{ Value = 5, Type = Prime }
{ Value = 6, Type = Not Prime }
{ Value = 7, Type = Prime }
{ Value = 8, Type = Not Prime }
{ Value = 9, Type = Not Prime }
{ Value = 10, Type = Not Prime }
The preceding code executes a prime query to demonstrate the left join. An inner join is then
executed at the line from element in array join prime in primes on element equals prime.Id.
The into primeGroup stores the results of the call into a temporary identifier named prime
Group. The primeGroup.DefaultIfEmpty () is then used as the data source. The DefaultIfEmpty
method will return a default value, null in this case, if a matching item in the join table cannot
be located. The select clause checks to see if the item is null and if so, returns “Not Prime”;
otherwise, it returns subprime.Name.
C#
Person[] people = new Person[]
{
new Person("Cristina","Potra", 22, Sex.Female),
new Person("Jeff","Phillips", 17, Sex.Male),
new Person("Mark","Alexieff", 55, Sex.Male),
Chapter 7 Introduction to LINQ 467
new Person("Holly","Holt", 58, Sex.Female),
new Person("Gordon L.","Hee", 87, Sex.Male),
new Person("Annie","Herriman", 34, Sex.Female),
};
Visual Basic
Dim people As Person() = New Person() _
{ _
New Person("Cristina", "Potra", 22, Sex.Female), _
New Person("Jeff", "Phillips", 17, Sex.Male), _
New Person("Mark", "Alexieff", 55, Sex.Male), _
New Person("Holly", "Holt", 58, Sex.Female), _
New Person("Gordon L.", "Hee", 87, Sex.Male), _
New Person("Annie", "Herriman", 34, Sex.Female) _
}
Output
The name Potra, Cristina contains an 'i'
The name Phillips, Jeff contains an 'i'
The name Alexieff, Mark contains an 'i'
The name Herriman, Annie contains an 'i'
The preceding code checks each first name for the letter i and then selects the range variable
if it contains i. You could also accomplish this without using the let clause and simply doing
the following.
C#
var peopleWithIName = (from person in people
where (person.FirstName +
person.LastName).ToLower().Contains(“i”)
select person);
468 Part III Using Collections
Visual Basic
Dim peopleWithIName = (From person In people _
Where (person.FirstName + _
person.LastName).ToLower().Contains("i") _
Select person)
There are several things to note about the example that uses the let clause. First, notice that
the Distinct method is called on the results. The let clause splits the full name into an array
of chars. The array is then used as a data source, which causes the select clause to execute
each time it finds an i. The Distinct method removes all duplicates from the results. Therefore,
when a name contains more than one i, the Distinct method will remove the duplicates
caused by repeated execution of the select clause. You could write the LINQ statement as the
following.
C#
var peopleWithIName = from person in people
let chars = (person.FirstName + person.LastName).ToCharArray()
where
((from ch in chars where ch == 'i' ||
ch == 'I' select ch).Count() > 0)
select person;
Visual Basic
Dim peopleWithIName = From person In people _
Let chars = (person.FirstName + person.LastName).ToCharArray() _
Where _
((From ch In chars Where ch = "i"c Or _
ch = "I"c Select ch).Count() > 0) _
Select person
The where clause is now executed using a nested query on the subexpression created by the
let clause. The Count method is used to count the occurrences of i.
Summary
In this chapter, you learned about LINQ and how to use it. As described, LINQ gives you an
exciting new way to write queries against collections and other data sources. LINQ statements
take less time to write than natural language statements and are very powerful.
Chapter 8
Using Threads with Collections
After completing this chapter, you will be able to
■ Understand what a thread is.
■ Understand why you should synchronize access to collections.
■ Identify what tools are available to help you synchronize access to collections and
when to use each one.
■ Add synchronization support to your custom collection classes.
What Is a Thread?
Windows is a multitasking operating system. From the user’s point of view, this means that
multiple processes seem to run at the same time. From the system viewpoint, multitasking is
far more complex than that. You can find many books that go into the inner workings of pro-
cesses and multitasking, but doing so is beyond the scope of this book. Instead, this section
provides only the most basic explanation.
A computer running Windows runs far more processes than it has processors, so it is impossi-
ble for the operating system to run all the processes at the same time. Instead, the operating
system switches processes quickly among the available processors in a way that makes it ap
pear to run all of the processes at the same time. In general, the faster the operating system
can switch processes, the more responsive the overall system is. To switch the processes in
and out, the operating system must maintain data (context) on each process, such as where it
was when it was switched out code that was executing at that point. To understand threads,
think of your process as being an operating system and your threads as processes running in-
side of your operating system. Each process can consist of multiple threads that can execute
code within that process. (Each thread is still controlled by the real operating system, but
each process is allowed to suggest to the real operating system when the thread should run.)
You may have realized that each thread requires memory for managing the thread and
process cycles to switch the thread in and out. This means that the more threads you add,
the more resources you use up. So, for performance reasons, you should create threads only
when needed. Thread pools and synchronization objects, which can help you with perfor-
mance and deadlock, are discussed later in this chapter.
469
470 Part III Using Collections
C#
static void ReverseArray(object state)
{
for (; ; )
{
ArrayEx<int> array = state as ArrayEx<int>;
for (int i = 0; i < array.Count / 2; ++i)
{
int tmp = array[i];
array[i] = array[array.Count - 1 - i];
array[array.Count - 1 - i] = tmp;
}
Thread.Sleep(100);
}
}
int sum = 0;
Console.WriteLine("Summing array");
for (int i = 0; i < array.Count; ++i)
{
sum += array[i];
}
if (s_sum != sum)
{
Console.WriteLine("Calculated sum was {0} instead of {1}", sum, s_sum);
}
else
Chapter 8 Using Threads with Collections 471
{
Console.WriteLine("Sum is {0}", sum);
}
Thread.Sleep(100);
}
}
Visual Basic
Sub ReverseArray(ByVal state As Object)
While (True)
Dim array As ArrayEx(Of Integer) = CType(state, ArrayEx(Of Integer))
The ReverseArray method simply reverses the specified array and the SumArray method
sums the values of the array. Each method sleeps for approximately 100 milliseconds (ms)
before repeating the operation. The SumArray method also checks the calculated sum
against the true sum to verify that it calculated the sum correctly.
The code in the rest of this section uses the ReverseArray method to simulate a writer thread
and the SumArray method to simulate a reader thread.
The following code creates a thread to execute the SumArray and ReverseArray methods on
the variable array.
472 Part III Using Collections
C#
ArrayEx<int> array = new ArrayEx<int>();
s_sum = 0;
for (int i = 0; i < 10; ++i)
{
array.Add(i);
s_sum += i;
}
Thread.Sleep(3000);
Visual Basic
Dim array As ArrayEx(Of Integer) = New ArrayEx(Of Integer)
s_sum = 0
For i As Integer = 0 To 9
array.Add(i)
s_sum += i
Next
Thread.Sleep(3000)
Output
Summing array
Sum is 45
Summing array
Sum is 45
Summing array
Sum is 45
Summing array
...
Sum is 45
Summing array
Sum is 45
Summing array
Sum is 45
Summing array
Sum is 45
Summing array
Chapter 8 Using Threads with Collections 473
The variable array is assigned ten numbers from 0 through 9 and a true sum is calculated and
stored in the static field s_sum so that the SumArray method can verify its results. On my ma-
chine, both threads are able to execute without any noticeable problems. On the other hand,
changing the 10 to 10000 or the 9 to 9999 causes the following output on my machine.
Output
Summing array
Sum is 49995000
Summing array
Calculated sum was 59115551 instead of 49995000
Summing array
Calculated sum was 38547424 instead of 49995000
Summing array
Calculated sum was 61658817 instead of 49995000
Summing array
Calculated sum was 37244712 instead of 49995000
Summing array
Calculated sum was 57821468 instead of 49995000
Summing array
Calculated sum was 40425072 instead of 49995000
Summing array
Calculated sum was 60709149 instead of 49995000
Summing array
Calculated sum was 39022363 instead of 49995000
Summing array
Calculated sum was 62030664 instead of 49995000
Summing array
Calculated sum was 36669912 instead of 49995000
Summing array
Calculated sum was 62146863 instead of 49995000
Summing array
Calculated sum was 39314880 instead of 49995000
Summing array
Calculated sum was 64503512 instead of 49995000
Summing array
Calculated sum was 46560888 instead of 49995000
Summing array
Calculated sum was 59602675 instead of 49995000
Summing array
...
Your machine results are likely to differ because they depend upon your processor(s), any-
thing else running in the background of your machine, and so on. Please note that the point
of this exercise is to show that you cannot run a simple case to see if you should synchronize
data to a thread. Instead, you should just do it! I have seen many companies fight countless
bugs in their production systems because their developers misunderstood how and when
threads read and write data. Many developers will tell you that threading problems are the
most difficult bugs to track down.
474 Part III Using Collections
Pseudocode
While waiting for Collection Update Event
Lock
Process collection
Unlock
Reset Collection Update Event
Loop
The preceding code resets the collection update event after the collection is unlocked. It is
possible that a thread could update the collection and fire the event between the unlock and
reset event. If this happens, the current thread will have to wait for another event to happen
because the thread accidently reset the current event.
Pseudocode
Thread A
Lock C
Process some data (Takes 5 seconds)
Lock D
Process Some Data
Unlock D
Unlock C
Thread B
Lock D
Process some data (Takes 5 seconds)
Lock C
Process Some Data
Unlock C
Unlock D
The preceding example causes Thread A to deadlock on Lock D because Thread B has locked
Lock D and will not unlock it until it obtains Lock C—which is locked by Thread A.
With some locking mechanisms, you have to be aware of recursive locking. Take a look at the
following example.
Chapter 8 Using Threads with Collections 475
Pseudocode
Class Collection
Method Add(item)
Lock C
Process Collection
Unlock C
Thread A
Lock C
collection.Add(item)
Unlock C
The preceding example locks Lock C and then adds an item to the collection, which in return
locks Lock C again. In such cases, some locking mechanisms will throw an exception on a re-
cursive lock, whereas others may ignore the double lock, because both lock the same thread.
You will need to read the documentation on MSDN to learn about recursive locking.
Note A deadlock occurs when a thread is waiting on a continue condition that never happens,
possibly because the thread that is supposed to set the continue condition has also deadlocked,
has neglected to set the continue condition, or has crashed. The continue condition might be
anything from a lock (Mutex, Semaphore, and so on) to a variable (such as boolean, or int).
Interlocked Operations
You can perform atomic operations on variables of types such as Int32 and Int64 by using
the static methods in the Interlocked class. The Interlocked class helps guarantee that vari-
able modifications are performed as one transactional operation. You would not normally
use interlocked operations for collections, because when updating a collection, you typically
update more than just the count, and you also have to ensure that no other operations can
be performed on the collection while you are updating its internal data storage. Therefore,
this book does not discuss the Interlocked class in detail, but it’s mentioned here so you’ll be
aware of it for other cases where it may come in handy, such as reference counting or cre-
ating custom locks. You will see a brief description of the CompareExchange method later
in this chapter in the “Getting Started” section in “Adding Synchronization Support to Your
Collections Classes,” which shows how to use the CompareExchange method to add synchro-
nization support to custom collection classes.
476 Part III Using Collections
Signaling
Signaling is a way of allowing a thread to wait for a condition. The condition is defined using
the WaitHandle class. You can use signaling to improve the performance of your application.
Suppose you have a thread that processes data only when items are added to a collection.
An inefficient way of doing this would be as follows. Please note that locking is left out of the
following example.
C#
while (bRunning )
{
if (collections.Count > 0)
{
// Process all data
collections.Clear();
}
Thread.Sleep(100);
}
Visual Basic
While (bRunning)
collections.Clear()
End If
Thread.Sleep(100)
End While
The code spins in an infinite loop, waiting for an item to be added to the collection. The
Thread.Sleep method is called in the code to help with performance. Although the code
doesn’t contain a lot of statements, the operating system can swap the thread in and out at
any moment. In contrast, look at the next example (which also leaves out locking).
C#
while (bRunning )
{
itemsAvailable.WaitOne();
collections.Clear();
itemsAvailable.Reset();
}
Chapter 8 Using Threads with Collections 477
Visual Basic
While (bRunning)
itemsAvailable.WaitOne()
collections.Clear()
itemsAvailable.Reset()
End While
C#
ManualResetEvent itemsAvailable = new ManualResetEvent(false);
Visual Basic
Dim itemsAvailable As ManualResetEvent = New ManualResetEvent(False)
The code no longer needs to call Thread.Sleep because it executes only when other code calls
itemsAvailable.Set(). At that point, it clears the collection, and then calls the Reset method on
the ManualResetEvent class so that the next call to WaitOne will not result in a false true. The
downside to this example is that it doesn’t provide a way to tell the thread to wake up for
other cases, such as when the thread should shut down gracefully, or if you wanted to have
the thread deal with multiple lists. Luckily the WaitHandle class has the WaitAll and WaitAny
methods. The WaitAll method waits for all the specified elements to be signaled, whereas
the WaitAny method waits for any of the elements to be signaled before resuming execu-
tion. The WaitAny method is perfect if you also want to be able to wake the thread up for a
shutdown.
C#
for (; ; )
{
int wakeup = WaitHandle.WaitAny(conditions);
if (wakeup == 0)
{
break;
}
if (wakeup == 1)
{
// Process all data
collections.Clear();
}
}
478 Part III Using Collections
Visual Basic
Dim wakeup As Integer = WaitHandle.WaitAny(conditions)
While (True)
If (wakeup = 0) Then
Exit While
End If
If (wakeup = 1) Then
' Process all data
collections.Clear()
End If
End While
The wait conditions are defined in the conditions variable as the following.
C#
WaitHandle []conditions = new WaitHandle[]
{
new ManualResetEvent(false), // Shutdown
new AutoResetEvent(false), // Items Available
};
Visual Basic
Dim conditions() As WaitHandle = New WaitHandle() _
{ _
New ManualResetEvent(False), _
New AutoResetEvent(False) _
}
The WaitAny method returns the index of the item that was signaled. You could also use
an enum or a const instead of integer values to make it easier to read. In this case, the items
Available.Reset event is changed to an AutoResetEvent. The AutoResetEvent automatically
resets the event.
Locking
Locks give control to a specified number of threads at a time. When a lock allows access to
only one thread at a time, it is an exclusive lock.
Exclusive Locks
With the Mutex and Monitor classes, you can define a lock with exclusive access. The Mutex
class is a heavier lock that also allows locking across processes, whereas a Monitor class can
only be used inside of the AppDomain. The Mutex class is also derived from WaitHandle,
which allows it to be used with the WaitHandle methods. Microsoft Visual Basic and C# allow
easy access to the Monitor class with the SyncLock and lock statements respectively. The fol-
lowing code examples use the Monitor class instead of the Mutex class because:
Chapter 8 Using Threads with Collections 479
■ Process synchronization is not needed.
■ WaitHandle support is not needed.
■ The Monitor class has built-in syntax.
■ The additional overhead of a Mutex is not needed.
Monitors can be used with native syntax as follows.
C#
lock (locking object)
{
// Critical code section
}
Visual Basic
SyncLock [locking object]
‘ Critical code section
End SyncLock
C#
System.Threading.Monitor.Enter(locking object);
try
{
// Critical code section
}
finally
{
System.Threading.Monitor.Exit(locking object);
}
Visual Basic
System.Threading.Monitor.Enter(locking object)
Try
' Critical code section
Finally
System.Threading.Monitor.Exit(locking object)
End Try
The Enter, Wait, and TryEnter methods are used to acquire the lock, whereas the Exit and Wait
methods are used to release the lock. All methods take as an argument the object to acquire
or release the lock on.
Note You should not use Me in Visual Basic or this in C# as an argument to the Monitor methods.
Instead, you should create an object and use that instead. Everyone has access to Me and this and
can use them as a lock however they like. For example, you can guarantee that the object you
create as a lock for reading can only be used for reading. Doing it this way can eliminate some
potential deadlock problems later on.
480 Part III Using Collections
You will learn how to use the Monitor class with collections in the section “Using the Monitor
Class,” later in this chapter.
Non-Exclusive Locks
The Semaphore, ReaderWriterLock, and ReaderWriterLockSlim classes allow access to multiple
threads.
You can use the Semaphore class to define how many threads can have access before other
calling threads are blocked. After a thread releases the semaphore, another thread can take
its place.
Collection classes allow two types of access: reading and writing. An infinite number of
threads can read the collection simultaneously, but only one thread can write to it safely at
one time—and only then if no threads are reading the collection at that point. You must also
consider that you don’t want to “starve” your writing threads. Starvation can occur because
multiple threads can read but only one thread can write at a time. Rather than always granting
a thread read access when another thread is currently reading, you must balance the system
if another thread is waiting to write. Luckily, the .NET Framework defined a ReaderWriterLock
and ReaderWriterLockSlim class for you to use instead of creating your own.
Later in this chapter, in the section “Using the ReaderWriterLockSlim Class,” you will learn how
to use the ReaderWriterLockSlim class with collections.
Note ReaderWriterLockSlim has all of the functionality of ReaderWriterLock but provides simpli-
fied rules for recursions and for upgrading and downgrading locks. These new changes should
increase the performance of the locks, and also avoid some deadlocking cases. For this reason,
MSDN recommends that you use ReaderWriterLockSlim for all new development. So, this book
uses only ReaderWriterLockSlim.
ICollection Revisited
In Chapter 6, “.NET Collection Interfaces,” you learned about the ICollection interface and
how to add it to your custom collection classes. The implementation of the IsSynchronized
and SyncRoot properties were left to this chapter. These two properties are used for
synchronization.
The IsSynchronized property states whether the properties and methods for the collection
are thread safe. In the “Implementing a Synchronized Wrapper Class” section later in this
chapter, you discover how to create a class that wraps your custom class to make it synchro-
nized. In that case, you need to return a true to state that the class is synchronized and guar-
antee that each method and property that interacts with the collection is thread safe. In the
“SyncRoot vs. the Synchronized Wrapper Class (IsSynchronized)” section later in this chapter,
you see the advantages and disadvantages of both.
The SyncRoot property returns an object that can be used to synchronize access to the col-
lection. The return object is used with the lock statement and Monitor class.
Getting Started
In this section, you will modify the classes you modified in Chapter 6 to support synchroniza-
tion, or you can look at the examples in the folder for Chapter 7, “Introduction to LINQ.” For
each collection type, you need to modify the file with the name <TypeName>.Collection.cs for
C# or <TypeName>.Collection.vb for Visual Basic. Fortunately, the SyncRoot and IsSynchronized
properties in all the classes are implemented in the same way. In the file you will find the
IsSynchronized and SyncRoot properties. You will not be adding synchronization support
directly to the methods and properties, so you will not need to modify the IsSynchronized
property. Instead, you will allow users to control the synchronization through the SyncRoot
property. To do this, each class needs to change the following code.
C#
object System.Collections.ICollection.SyncRoot
{
get { throw new NotImplementedException(); }
}
Visual Basic
Private ReadOnly Property SyncRoot() As Object Implements ICollection.SyncRoot
Get
Throw New NotImplementedException()
End Get
End Property
482 Part III Using Collections
C#
object m_syncRoot;
object System.Collections.ICollection.SyncRoot
{
get
{
if (m_syncRoot == null)
{
System.Threading.Interlocked.CompareExchange(ref m_syncRoot, new object(),
null);
}
return m_syncRoot;
}
}
Visual Basic
Private m_syncRoot As Object
Private ReadOnly Property SyncRoot() As Object Implements ICollection.SyncRoot
Get
If (m_syncRoot Is Nothing) Then
System.Threading.Interlocked.CompareExchange(m_syncRoot, New Object(), Nothing)
End If
Return m_syncRoot
End Get
End Property
The preceding code uses the Interlocked.CompareExchange method to create the SyncRoot
object. The CompareExchange method sets m_syncRoot to new object() if m_syncRoot is null.
Doing this synchronizes the reading and writing of m_syncRoot across threads. Remember,
you cannot use m_syncRoot because it may be set to null. You could instead create m_syncRoot
in the constructor; however, that carries the disadvantage that every collection would contain
a SyncRoot object even if it were never used.
Now you can synchronize your collection classes using the following code.
C#
ArrayEx<int> list = new ArrayEx<int>();
lock (((ICollection)list).SyncRoot)
{
list.Add(12);
}
Visual Basic
Dim list As ArrayEx(Of Integer) = New ArrayEx(Of Integer)
C#
ArrayEx<int> list = new ArrayEx<int>();
if (System.Threading.Monitor.TryEnter(((ICollection)list).SyncRoot))
{
try
{
list.Add(12);
}
finally
{
System.Threading.Monitor.Exit(((ICollection)list).SyncRoot);
}
}
Visual Basic
If (System.Threading.Monitor.TryEnter(CType(list, ICollection).SyncRoot)) Then
Try
list.Add(12)
Finally
System.Threading.Monitor.Exit(CType(list, ICollection).SyncRoot)
End Try
End If
Note The information in this section for a Synchronized Wrapper class also holds true for other
collection classes that return true for the IsSynchronized property. Refer to the “Implementing a
Synchronized Wrapper Class” section later in this chapter for more information about Synchronized
Wrapper classes.
C#
test.Add(item);
Visual Basic
test.Add(item)
C#
lock (((ICollection)test).SyncRoot)
{
test.Add(item);
}
Visual Basic
SyncLock CType(test, ICollection).SyncRoot
test.Add(item)
End SyncLock
With SyncRoot, all developers must wrap their calls with a lock statement. The Synchronized
Wrapper handles this for the user. One drawback to using the Synchronized Wrapper is that
synchronization occurs within the synchronized methods and properties only, and not in be-
tween. Take a look at the following example of a Synchronized Wrapper.
C#
object item = null;
if (test.Count > 0)
{
// The collection may no longer contain values
item = test[0];
}
// Process item
Visual Basic
Dim item As Object = Nothing
If (test.Count > 0) Then
‘ The collection may no longer contain values
item = test(0)
End If
‘ Process item
C#
object item = null;
lock (((ICollection)test).SyncRoot)
{
if (test.Count > 0)
{
item = test[0];
}
}
// Process item
Visual Basic
Dim item As Object = Nothing
If (test.Count > 0) Then
' The collection may no longer contain values
item = test(0)
End If
' Process item
Chapter 8 Using Threads with Collections 485
Using synchronized wrappers does not guarantee that Count will not change before you
get the first item. One way to handle this problem would be to create another method that
combines the operations. This is possible only if you created the class, because adding an
extensible function doesn’t safeguard you against the original developer changing the way
she creates locks. This may also substantially increase the number of methods, and may cause
developers to add multiple methods with different names that do the same thing.
The chances of 3 increase when you use SyncRoot, as shown in the following examples. The
first example uses a Synchronized Wrapper.
C#
b.Add(a[0]);
a.RemoveAt(0);
Visual Basic
b.Add(a(0))
a.RemoveAt(0)
C#
List<object> a = new List<object>();
List<object> b = new List<object>();
lock (((ICollection)a).SyncRoot)
{
lock (((ICollection)b).SyncRoot)
{
b.Add(a[0]);
}
a.RemoveAt(0);
}
Visual Basic
Dim a As List(Of Integer) = New List(Of Integer)
Dim b As List(Of Integer) = New List(Of Integer)
The user locked the b collection while in a collection lock. Any thread that locks the a collec-
tion while in the b collection lock while this code is running will deadlock. It is easy to see this
in the preceding code but not when the nested lock happens later in a function call stack.
You could fix the nested problem by doing a TryEnter instead, but now you get into what to
do when the nested lock fails. Again, in the preceding example, you can easily figure it out,
486 Part III Using Collections
but not in the production code where the nested lock happens after numerous events, func-
tion calls, and property changes have occurred—at which point it might be time to finally
convince management that designing by compiling isn’t the way to go.
Because SyncRoot provides more control to users, users are able to handle performance
problems.
C#
a.AddRange(items);
Visual Basic
a.AddRange(items)
C#
List<object> a = new List<object>();
lock (((ICollection)a).SyncRoot)
{
foreach (object item in items)
{
a.Add(item);
}
}
Visual Basic
Dim a As List(Of Integer) = New List(Of Integer)
SyncLock CType(a, ICollection).SyncRoot
For Each item In items
a.Add(item)
Next
End SyncLock
Using the SyncRoot property also means that users have to know what they are doing.
Holding a lock should be as short as possible. Adding an AddRange method to your Synchro
nized Wrapper class would enable you to add a number of items, release the lock, acquire
the lock again, add some more items, release the lock, and so on. This would be handy if the
user specified a very large number of items to add. With some education, users could do it
with the SyncRoot call instead.
In general, Synchronized Wrappers are good when you want to have complete control over
how synchronization is done, want limited access to the wrapped collection, or want synchro-
nization to be transparent to the user of the collection. But with a little education, you could
teach users how to use the SyncRoot property and how to be aware of threading issues.
Chances are that you will use the SyncRoot property the majority of the time, but think about
Chapter 8 Using Threads with Collections 487
the following scenario: You have an internal collection of objects called InternalObjects that
is stored in the MySingleton object and is dynamically updated. For some reason, you decide
to provide the users with a collection of publicly accessible objects, called PublicObject. Users
can access the collection of PublicObjects, which could be updated because of changes to the
collection of InternalObjects. You may find a need to lock both collections if either collection
is locked, or you may need a more complex locking mechanism that gives higher priority to
the collection of InternalObjects. Implementing synchronization in the InternalObjects and
PublicObject collection classes would eliminate the need for users to learn your complex syn-
chronization procedures.
C#
lock (locking object)
{
// Critical code section
}
Visual Basic
SyncLock [locking object]
‘ Critical code section
End SyncLock
As discussed before, the preceding statement is the same as writing the following.
C#
System.Threading.Monitor.Enter(locking object);
try
{
// Critical code section
}
finally
{
System.Threading.Monitor.Exit(locking object);
}
Visual Basic
System.Threading.Monitor.Enter(locking object)
Try
' Critical code section
Finally
System.Threading.Monitor.Exit(locking object)
End Try
488 Part III Using Collections
The advantage of using the built-in language syntax is that developers can easily read
and understand what you did. The code also handles the possibility of an exception being
thrown. However, the code doesn’t allow you to time out on obtaining a lock because it uses
the Enter method instead of the TryEnter method. The TryEnter method takes the object you
want to acquire a lock on as an argument. If the lock is obtained, it returns true, and if not,
it returns false. A false return means that another thread has acquired the lock. Here’s an
example.
C#
if (System.Threading.Monitor.TryEnter(locking object))
{
try
{
// Critical Code Section
}
finally
{
System.Threading.Monitor.Exit(locking object);
}
}
else
{
// Failed to obtain lock
}
Visual Basic
If (System.Threading.Monitor.TryEnter(locking object)) Then
Try
' Critical code section
Finally
System.Threading.Monitor.Exit(conditions)
End Try
Else
' Failed to obtain lock
End If
Optionally, you can also specify a time to wait for the lock. The method will wait the specified
amount of time to acquire the lock. If a lock is acquired before the time expires, the method
returns true; otherwise it returns false. The following call waits for one second before giving
up on acquiring the lock.
C#
System.Threading.Monitor.TryEnter(locking object, TimeSpan.FromSeconds(1)
Visual Basic
System.Threading.Monitor.TryEnter(locking object, TimeSpan.FromSeconds(1)
Chapter 8 Using Threads with Collections 489
One advantage of using TryEnter versus Enter is that you can potentially recover from dead-
lock scenarios. Your code won’t deadlock if all your lock statements waited only 100 ms to
acquire a lock. However, you now have to figure out what to do when you do not acquire a
lock and also make sure that 100 ms is long enough to wait on a lock. For example, if it takes
one second to process a list and a thread waits one second to acquire the list, you may find
that acquiring the lock for the list randomly times out. This could be a result of the processor
being overwhelmed, the operating system not getting back to your thread in time, and so
on. For this reason, you should base your timeout on how long is too long to wait—not on
how long it should take for an operation to complete.
Tip To help with timeouts, you can reduce the wait duration and make multiple access attempts.
For example, instead of waiting 10 seconds, you could wait 500 ms for up to 20 times. This ap-
proach allows you to do other things while you wait, such as check to see if you should abort,
notify users of application progress, and notify the thread that is using the collection that you are
waiting.
For the collection classes, the object you pass as the locking variable is ICollection.SyncRoot
and not the collection instance itself. You use the ICollection.SyncRoot property for synchro-
nizing the collection portion of an object. However, that’s not the case with the actual collec-
tion instance itself.
C#
ArrayEx<int> list = new ArrayEx<int>();
lock (list)
{
// Critical code section
}
Visual Basic
Dim list As ArrayEx(Of Integer) = New ArrayEx(Of Integer)
SyncLock list
' Critical code section
End SyncLock
C#
ArrayEx<int> list = new ArrayEx<int>();
lock (((ICollection)list).SyncRoot)
{
// Critical code section
}
490 Part III Using Collections
Visual Basic
Dim list As ArrayEx(Of Integer) = New ArrayEx(Of Integer)
SyncLock CType(list, ICollection).SyncRoot
' Critical code section
End SyncLock
C#
static void AverageArray(object data)
{
for (; ; )
{
State state = (State)data;
int sum = 0;
float average = 0;
Console.WriteLine("Averaging array");
for (int i = 0; i < state.Items.Count; ++i)
{
sum += state.Items[i];
}
average = (float)sum / (float)state.Items.Count;
if (state.Average != average)
{
Console.WriteLine("Calculated average was {0} instead of {1}",
average, state.Average);
}
else
{
Console.WriteLine("Average is {0}", average);
}
Thread.Sleep(100);
}
}
Chapter 8 Using Threads with Collections 491
Visual Basic
Sub AverageArray(ByVal data As Object)
While (True)
Dim state As State = CType(data, State)
The AverageArray method simply calculates the mean average of the values in the array and
displays the resulting value on the screen, whether the average is calculated correctly or not.
You’ll also need to modify the SumArray and ReverseArray methods to handle a structure for
the argument instead of an ArrayEx(T). (The code in bold represents the changed code.)
C#
static void ReverseArray(object data)
{
for (; ; )
{
State state = (State)data;
Console.WriteLine("Reversing array");
for (int i = 0; i < state.Items.Count / 2; ++i)
{
int tmp = state.Items[i];
state.Items[i] = state.Items[state.Items.Count - 1 - i];
state.Items[state.Items.Count - 1 - i] = tmp;
}
Console.WriteLine("Reversed array");
Thread.Sleep(100);
}
}
{
for (; ; )
{
State state = (State)data;
int sum = 0;
Console.WriteLine("Summing array");
for (int i = 0; i < state.Items.Count; ++i)
{
sum += state.Items[i];
}
if (state.Sum != sum)
{
Console.WriteLine("Calculated sum was {0} instead of {1}", sum, state.Sum);
}
else
{
Console.WriteLine("Sum is {0}", sum);
}
Thread.Sleep(100);
}
}
Visual Basic
Sub ReverseArray(ByVal data As Object)
While (True)
Dim state As State = CType(data, State)
Console.WriteLine("Reversing array")
For i As Integer = 0 To (state.Items.Count / 2) - 1
Dim tmp As Integer = state.Items(i)
state.Items(i) = state.Items(state.Items.Count - 1 - i)
state.Items(state.Items.Count - 1 - i) = tmp
Next
Console.WriteLine("Reversed array")
Thread.Sleep(100)
End While
End Sub
C#
public class State
{
public ArrayEx<int> Items;
public ReaderWriterLockSlim Lock;
public int Sum;
public float Average;
}
Visual Basic
Public Class State
Public Items As ArrayEx(Of Integer)
Public Lock As ReaderWriterLockSlim
Public Sum As Integer
Public Average As Single
End Class
The State class holds the items used in the calculation as well as a Sum and Average of the
items. The Lock field will not be used yet.
C#
State data = new State();
data.Lock = new ReaderWriterLockSlim();
data.Items = new ArrayEx<int>();
data.Sum = 0;
for (int i = 0; i < 10000; ++i)
{
data.Items.Add(i);
data.Sum += i;
}
data.Average = (float)data.Sum / (float)data.Items.Count;
Thread.Sleep(3000);
Visual Basic
Dim data As State = New State()
data.Lock = New ReaderWriterLockSlim()
data.Items = New ArrayEx(Of Integer)
data.Sum = 0
For i As Integer = 0 To 9
data.Items.Add(i)
data.Sum += i
Next
data.Average = CSng(data.Sum) / CSng(data.Items.Count)
494 Part III Using Collections
Thread.Sleep(3000)
Output
Reversing array
Averaging array
Summing array
Calculated average was 3557 instead of 4999
Calculated sum was 32600504 instead of 49995000
Reversed array
Averaging array
Summing array
Reversing array
Calculated average was 5438 instead of 4999
Calculated sum was 61774504 instead of 49995000
Reversed array
Summing array
Averaging array
Reversing array
Calculated sum was 48314241 instead of 49995000
Calculated average was 4211 instead of 4999
Reversed array
Summing array
Reversing array
Averaging array
Calculated sum was 63563839 instead of 49995000
Calculated average was 5400 instead of 4999
Reversed array
As you can see, without synchronization support, this code still has synchronization prob-
lems. You can use the ReaderWriterLockSlim class to allow the two reader threads access to
the collection when the writer thread isn’t writing to the collection, and vice versa.
The reader threads will need to use the EnterReadLock and ExitReadLock methods. The
EnterReadLock method attempts to acquire the lock as a reader. When it succeeds, writer
threads will not be able to acquire the lock until no reader thread still has the lock. Reader
threads must call ExitReadLock to release the lock when they no longer need it. The fol-
lowing code shows the AverageArray method modified to support synchronization using
ReaderWriterLockSlim.
C#
static void AverageArray(object data)
{
for (; ; )
{
State state = (State)data;
int sum = 0;
float average = 0;
Chapter 8 Using Threads with Collections 495
Console.WriteLine("Averaging array");
state.Lock.EnterReadLock();
try
{
for (int i = 0; i < state.Items.Count; ++i)
{
sum += state.Items[i];
}
average = (float)sum / (float)state.Items.Count;
}
finally
{
state.Lock.ExitReadLock();
}
if (state.Average != average)
{
Console.WriteLine("Calculated average was {0} instead of {1}",
average, state.Average);
}
else
{
Console.WriteLine("Average is {0}", average);
}
Thread.Sleep(100);
}
}
Visual Basic
Sub AverageArray(ByVal data As Object)
While (True)
Dim state As State = CType(data, State)
Try
For i As Integer = 0 To state.Items.Count - 1
sum += state.Items(i)
Next
average = CSng(sum) / CSng(state.Items.Count)
Finally
state.Lock.ExitReadLock()
End Try
If (state.Average <> average) Then
Console.WriteLine("Calculated average was {0} instead of {1}", _
average, state.Average)
Else
Console.WriteLine("Average is {0}", average)
End If
Thread.Sleep(100)
End While
End Sub
496 Part III Using Collections
Note how the ExitReadLock method gets called within the finally statement. This ensures that
the lock gets released even if the code throws an exception.
The EnterReadLock method does not return until it acquires the lock. You will need to use
the TryEnterReadLock method instead if you want to return when the lock has already been
acquired by a writer thread. The following code is the modified SumArray method that uses
TryEnterReadLock instead of EnterReadLock.
C#
static void SumArray(object data)
{
for (; ; )
{
State state = (State)data;
int sum = 0;
Console.WriteLine("Summing array");
if (state.Lock.TryEnterReadLock(TimeSpan.FromMilliseconds(100)))
{
if (state.Lock.CurrentReadCount > 1)
{
Console.WriteLine("Sharing the lock with AverageArray");
}
try
{
for (int i = 0; i < state.Items.Count; ++i)
{
sum += state.Items[i];
}
}
finally
{
state.Lock.ExitReadLock();
}
if (state.Sum != sum)
{
Console.WriteLine("Calculated sum was {0} instead of {1}", sum, state.Sum);
}
else
{
Console.WriteLine("Sum is {0}", sum);
}
}
else
{
Console.WriteLine("Couldn't acquire lock");
}
Thread.Sleep(100);
}
}
Chapter 8 Using Threads with Collections 497
Visual Basic
Sub SumArray(ByVal data As Object)
While (True)
Dim state As State = CType(data, State)
The code will print “Couldn’t acquire lock” if TryEnterReadLock fails to acquire the lock. If
it succeeds, the code is the same as EnterReadLock. The SumArray method also uses the
CurrentReadCount property to display whether it is sharing the lock with the AverageArray
method. The CurrentReadCount property returns the number of read threads that have
currently acquired the lock.
The writer thread needs to use the EnterWriteLock and ExitWriteLock methods. The Enter
WriteLock method attempts to acquire the lock as a writer. When it succeeds, other reader or
writer threads will not be able to acquire the lock until it is released. The writer thread must
call ExitWriterLock to release the lock when it is no longer needed. The following shows the
ReverseArray method modified to support synchronization using ReaderWriterLockSlim.
498 Part III Using Collections
C#
static void ReverseArray(object data)
{
for (; ; )
{
State state = (State)data;
Console.WriteLine("Reversing array");
state.Lock.EnterWriteLock();
try
{
for (int i = 0; i < state.Items.Count / 2; ++i)
{
int tmp = state.Items[i];
state.Items[i] = state.Items[state.Items.Count - 1 - i];
state.Items[state.Items.Count - 1 - i] = tmp;
}
}
finally
{
state.Lock.ExitWriteLock();
}
Console.WriteLine("Reversed array");
Thread.Sleep(100);
}
}
Visual Basic
Sub ReverseArray(ByVal data As Object)
While (True)
Dim state As State = CType(data, State)
Console.WriteLine("Reversing array")
state.Lock.EnterWriteLock()
Try
Again, the ExitWriteLock occurs within the finally statement to ensure that the lock is released
even if the code throws an exception. You could use the TryEnterWriteLock method to avoid
blocking while waiting for the writer lock.
Chapter 8 Using Threads with Collections 499
Output
Reversing array
Averaging array
Summing array
Reversed array
Sharing the lock with AverageArray
Average is 4999.5
Sum is 49995000
Reversing array
Reversed array
Averaging array
Summing array
Sharing the lock with AverageArray
Average is 4999.5
Sum is 49995000
Reversing array
Reversed array
Summing array
Averaging array
Sum is 49995000
Average is 4999.5
Reversing array
Reversed array
Averaging array
Summing array
Sharing the lock with AverageArray
Average is 4999.5
Sum is 49995000
Reversing array
Reversed array
Summing array
Averaging array
Sum is 49995000
Average is 4999.5
Reversing array
Reversed array
Averaging array
Summing array
Sum is 49995000
Average is 4999.5
Reversing array
Reversed array
Averaging array
Summing array
Average is 4999.5
Sum is 49995000
Reversing array
Reversed array
Notice that the synchronization problems are no longer present. You can also see the ran-
dom nature of threads and sleeps by looking at the unpredictable pattern of the output.
Sometimes the lock is shared with the AverageArray thread; sometimes it isn’t.
500 Part III Using Collections
Handling Recursion
In the previous example, the threads execute only within one method, but in real-world
programming, you will also call methods from threads that interact with a collection instead
of simply having the thread interact directly with the collection. These methods could, in
turn, acquire the lock and then potentially call other methods or events that do the same, in
a process referred to as recursive locking.
There are times when you will want to allow recursive locking and times when you will
not. With the ReaderWriterLockSlim class, you can define the recursive locking policy you
want when you first create the lock. The two options are LockRecursionPolicy.NoRecursion
and LockRecursionPolicy.SupportRecursion. You are not able to call EnterReadLock,
EnterWriteLock, or EnterUpgradeableReadLock within another EnterReadLock, EnterWriteLock,
or EnterUpgradeableReadLock if LockRecursionPolicy.NoRecursion is set.
When LockRecursionPolicy.SupportRecursion is set, you can enter a lock from within another
lock. Each call to EnterXLock or TryEnterXLock must call the corresponding ExitXLock; however,
you do not have to call ExitXLock in the exact reverse order that you called the EnterXLock
calls. You can use the IsReadLockHeld, IsUpgradeableReadLockHeld, and IsWriteLockHeld
properties to determine whether the current thread has already called EnterReadLock,
EnterWriteLock, or EnterUpgradeableReadLock.
C#
public class ScheduledEvent
{
public DateTime ExecutionTime;
public string Name;
public Action Action;
}
Visual Basic
Public Class ScheduledEvent
Public ExecutionTime As DateTime
Public Name As String
Public Action As Action
End Class
Chapter 8 Using Threads with Collections 501
The ExecutionTime is the time at which the event should occur, and Action is the action that
should take place at the specified time. The list and lock are defined as the following.
C#
ReaderWriterLockSlim lckEvents = new ReaderWriterLockSlim();
ArrayEx<ScheduledEvent> events = new ArrayEx<ScheduledEvent>();
Visual Basic
Dim lckEvents As ReaderWriterLockSlim = New ReaderWriterLockSlim()
Dim events As ArrayEx(Of ScheduledEvent) = New ArrayEx(Of ScheduledEvent)()
More than one thread may be receiving the events. Perhaps one thread adds to the events,
one removes events from the list and executes them, and yet another thread displays the list
to users when requested—and maybe to other threads that read from the list. The code for
the execution and removal thread might look like the following.
C#
for (; ; )
{
lckEvents.EnterWriteLock();
try
{
for (int i = 0; i < events.Count; )
{
if (events[i].ExecutionTime <= DateTime.Now)
{
events[i].Action();
Visual Basic
While (True)
lckEvents.EnterWriteLock()
Try
For i As Integer = 0 To events.Count - 1
If (events(i).ExecutionTime <= DateTime.Now) Then
events(i).Action()
The downside of doing it this way is that no other threads may access the list even if the col-
lection isn’t modified. You can use the upgradeable locks to lock a collection for read-only
access and change the lock to write access later. Take a look at the following example.
C#
for (; ; )
{
bool enterWriteLock = false;
lckEvents.EnterUpgradeableReadLock();
try
{
for (int i = 0; i < events.Count; )
{
if (events[i].ExecutionTime <= DateTime.Now)
{
if (!enterWriteLock)
{
lckEvents.EnterWriteLock();
enterWriteLock = true;
}
events[i].Action();
Visual Basic
While (True)
Dim enterWriteLock As Boolean = False
lckEvents.EnterUpgradeableReadLock()
Try
For i As Integer = 0 To events.Count - 1
If (events(i).ExecutionTime <= DateTime.Now) Then
events(i).Action()
The thread now acquires an UpgradeableReadLock. If this finds an event that should be exe-
cuted, a write lock is then acquired. When an upgradeable lock has been acquired, any num-
ber of threads can acquire a read lock as long as no threads are waiting to acquire a write
lock. Only one thread may acquire an upgradeable lock at a time.
504 Part III Using Collections
The following shows a simple way to provide a Synchronized Wrapper around a class that
implements IList(T).
C#
public class SynchronizedListWrapper<T> : IList<T>, ICollection
{
object m_syncRoot;
IList<T> m_items;
#endregion
#endregion
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return m_items.GetEnumerator();
}
#endregion
#endregion
}
Visual Basic
Public Class SynchronizedListWrapper(Of T)
Implements IList(Of T)
Implements ICollection
Public Sub Insert(ByVal index As Integer, ByVal item As T) Implements IList(Of T).Insert
SyncLock m_syncRoot
m_items.Insert(index, item)
End SyncLock
End Sub
#End Region
#End Region
#End Region
510 Part III Using Collections
#End Region
#End Region
End Class
The constructor accepts the IList(T) you are providing synchronization support to and an
optional SyncRoot that you want to use for synchronization. You can provide synchronization
support to the ArrayEx(T) class by doing the following.
C#
ArrayEx<int> list = new ArrayEx<int>() { 55, 8 };
Visual Basic
Dim list As New ArrayEx(Of Integer)(New Integer() {55, 8})
Dim slist As New SynchronizedReadOnlyCollection(Of Integer)(list)
Now all the calls you make to list using the slist variable are synchronized. You could also
later do the following.
Chapter 8 Using Threads with Collections 511
C#
lock(((System.Collections.ICollection)slist).SyncRoot)
{
list.Add(12);
}
Visual Basic
SyncLock (CType(list, System.Collections.ICollection).SyncRoot)
list.Add(12)
End SyncLock
SynchronizedCollection(T)
The SynchronizedCollection(T) class provides synchronization support for an array. All of the
items are stored in a List(T) class that may be accessed through the Items property. You use
the SynchronizedCollection(T) class in the same way that you use the List(T) class except the
methods and properties are thread safe. You can pass the SyncRoot object in as a parameter
to the constructor. This is useful if you want several collections or objects to be synchronized
using the same object.
512 Part III Using Collections
SynchronizedKeyedCollection(T)
The SynchronizedKeyedCollection(T) class provides synchronization support for an associative
array. All items are stored in a Dictionary(T) class, but the class doesn’t provide a way to ac-
cess the Dictionary(T) object. You use SynchronizedKeyedCollection(T) in the same way that
you use the Dictionary(T) class except that the methods and properties are thread safe. You
can pass the SyncRoot object in as a parameter to the constructor. This is useful if you want
several collections or objects to be synchronized using the same object.
SynchronizedReadOnlyCollection(T)
The SynchronizedReadOnlyCollection(T) class provides synchronization support to a read-only
collection. This class allows you to perform synchronized read-only methods and properties
on the collection. You can pass the SyncRoot object in as a parameter to the constructor. This
is useful if you want several collections or objects to be synchronized using the same object.
Summary
In this chapter, you learned about threads and how to use them with collections. With threads,
you can do simultaneous operations on collections that require you to synchronize access to
your collections. You also learned about objects that may be used to synchronize your collec-
tions and about some built-in synchronized collection classes for working with collections that
are not synchronized.
Chapter 9
Serializing Collections
After completing this chapter, you will be able to
■ Understand what serialization is.
■ Use the serialization formatters.
■ Add serialization support to your custom collection classes.
Serialization
Serialization is the process of converting an object in a defined way to a format that can be
stored and retrieved. The formatted data can be stored in memory, in a file, in a database,
through a data stream sent over the network, and in other ways. The storage format could be
a plain text format, such as XML, or a pure binary format.
Note The Serializable and NonSerializable attributes are actually named as the
SerializableAttribute and NonSerializableAttribute classes. The compiler lets you remove the
postfix Attribute from the names when using them as attributes in your code.
513
514 Part III Using Collections
C#
[Serializable()]
class MyClass
{
public string PublicField;
private string PrivateField;
protected string ProtectedField;
[NonSerialized()]
public string NonSerializedField;
Visual Basic
<Serializable()> _
Public Class [MyClass]
Public PublicField As String
Private PrivateField As String
Protected ProtectedField As String
<NonSerialized()> _
Public NonSerializedField As String
The preceding class is marked as serializable by using the Serializable attribute. Four fields
are defined along with a public property. The PublicField is declared public, the PrivateField
as private, and the ProtectedField as protected. The last field, NonSerializableField, is defined
as public and has the NonSerializable attribute set. A public property is also defined so that
you can see what happens with properties. The following code is used to serialize the data to
memory and then deserialize it from memory.
C#
MemoryStream stream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, data);
stream.Position = 0;
Console.WriteLine("PublicField={0}", deserialized.PublicField);
Console.WriteLine("Property={0}", deserialized.Property);
Console.WriteLine("NonSerializedField={0}", deserialized.NonSerializedField);
Console.WriteLine("PrivateField={0}", deserialized.PrivateFieldValue);
Console.WriteLine("ProtectedField={0}", deserialized.ProtectedFieldValue);
516 Part III Using Collections
Visual Basic
Dim stream As MemoryStream = New MemoryStream()
Dim formatter As BinaryFormatter = New BinaryFormatter()
formatter.Serialize(stream, data)
stream.Position = 0
Console.WriteLine("PublicField={0}", deserialized.PublicField)
Console.WriteLine("Property={0}", deserialized.Property)
Console.WriteLine("NonSerializedField={0}", deserialized.NonSerializedField)
Console.WriteLine("PrivateField={0}", deserialized.PrivateFieldValue)
Console.WriteLine("ProtectedField={0}", deserialized.ProtectedFieldValue)
Serializing requires a stream and a formatter. You can use any class that derives from
the Stream class to stream the contents, and you can use any class that implements the
IFormatter interface as a formatter. The base Formatter class is intended to help simplify
the creation of custom formatter classes when you need to create your own. The IFormatter
interface contains two methods: Serialize and Deserialize. The Serialize method serializes
the specified data into the specified stream; and the Deserialize method, as you may have
guessed, deserializes data from a specified stream, recreating the original objects. The pre-
ceding example uses a MemoryStream for serialization. The code moves the MemoryStream
position back to the beginning after the data has been serialized so that the Deserialize
method will start from the beginning of the stream. The example then displays the results of
the deserialization on the screen as follows.
Output
PublicField=A
Property=
NonSerializedField=
PrivateField=B
ProtectedField=C
Notice how all fields are serialized except the NonSerializedField field. This is because that
field had the NonSerializable attribute set on it. Properties are not serialized either. So, the
NonSerializedField field was not serialized using the Property property either. Also, calling
serialize on an object that has not been decorated with the Serializable attribute will result in
an exception, as shown in the following code.
C#
struct NonSerializableStruct
{
public double X;
public double Y;
}
Chapter 9 Serializing Collections 517
Visual Basic
Structure NonSerializableStruct
Public X As Double
Public Y As Double
End Structure
C#
NonSerializableStruct data = new NonSerializableStruct();
try
{
formatter.Serialize(stream, data);
Console.WriteLine("Data was serialized");
}
catch (SerializationException ex)
{
Console.WriteLine("Class isn't marked as serializable");
}
Visual Basic
Dim data As NonSerializableStruct = New NonSerializableStruct()
Try
formatter.Serialize(stream, data)
Console.WriteLine("Data was serialized")
Catch ex As SerializationException
Console.WriteLine("Class isn't marked as serializable")
End Try
Output
Class isn't marked as serializable
The SerializationException exception is also thrown if the return type of one of the fields does
not have the Serializable attribute set.
To use the ISerializable interface, you need to mark the class as Serializable, implement the
interface, and add a protected deserialize constructor. Take a look at the following example.
C#
[Serializable()]
class MyClass : ISerializable
{
public string PublicField;
private string PrivateField;
protected string ProtectedField;
[NonSerialized()]
public string NonSerializedField;
#endregion
}
Visual Basic
<Serializable()> _
Public Class [MyClass]
Implements ISerializable
<NonSerialized()> _
Public NonSerializedField As String
#End Region
End Class
The following code can be used to serialize and deserialize the data.
C#
MemoryStream stream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, data);
stream.Position = 0;
Console.WriteLine("PublicField={0}", deserialized.PublicField);
Console.WriteLine("Property={0}", deserialized.Property);
Console.WriteLine("NonSerializedField={0}", deserialized.NonSerializedField);
Console.WriteLine("PrivateField={0}", deserialized.PrivateFieldValue);
Console.WriteLine("ProtectedField={0}", deserialized.ProtectedFieldValue);
Visual Basic
Dim stream As MemoryStream = New MemoryStream()
Dim formatter As BinaryFormatter = New BinaryFormatter()
formatter.Serialize(stream, data)
stream.Position = 0
Output
PublicField=A
Property=D
NonSerializedField=D
PrivateField=B
ProtectedField=C
As you can see, the NonSerializable attribute no longer applies because you stated what you
want to serialize and deserialize in the ISerializable.GetObjectData method and deserialize
constructor.
Note One problem with the Serializable attribute is that it doesn’t handle type changes. For
example, if you define a class and serialize the data, the default formatters cannot handle class
renames or property renames after the class has been serialized. Although it is possible to in-
form them of your changes, you might want to look at writing a version number by using the
ISerializable interface. The deserializing can look at the version number and then handle field
name changes.
For example, change the ArrayEx(T) class definition from the following:
C#
[DebuggerDisplay("Count={Count}")]
[DebuggerTypeProxy(typeof(ArrayDebugView))]
public partial class ArrayEx<T>
Visual Basic
<DebuggerDisplay("Count={Count}")> _
<DebuggerTypeProxy(GetType(ArrayDebugView))> _
Partial Public Class ArrayEx(Of T)
522 Part III Using Collections
to this:
C#
[DebuggerDisplay("Count={Count}")]
[DebuggerTypeProxy(typeof(ArrayDebugView))]
[Serializable()]
public partial class ArrayEx<T>
Visual Basic
<DebuggerDisplay("Count={Count}")> _
<DebuggerTypeProxy(GetType(ArrayDebugView))> _
<Serializable()> _
Partial Public Class ArrayEx(Of T)
C#
[NonSerialized()]
object m_syncRoot;
Visual Basic
<NonSerialized()> _
Private m_syncRoot As Object
Your ArrayEx(T) class can now be serialized. There is no need to serialize the synchronization
object because it is only an object created with new object(). The deserialize method just calls
new object() when it determines that the m_syncRoot needs to be deserialized. So serializing
m_syncRoot makes the serialized data larger.
C#
class ArrayEx
{
}
Visual Basic
Public Class ArrayEx
End Class
Chapter 9 Serializing Collections 523
to the following:
C#
[Serializable()]
public partial class ArrayEx<T> : ISerializable
{
}
Visual Basic
<Serializable()> _
Partial Public Class ArrayEx
Implements ISerializable
End Class
C#
private ArrayEx(SerializationInfo info, StreamingContext context)
{
m_count = info.GetInt32("count");
m_data = (T[])info.GetValue("data", typeof(T[]));
}
Visual Basic
Private Sub New(ByVal info As SerializationInfo, ByVal context As StreamingContext)
m_count = info.GetInt32("count")
m_data = CType(info.GetValue("data", GetType(T())), T())
End Sub
C#
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("count", m_count);
#if ALWAYS_SERIALIZE_INTERNAL_ARRAY
info.AddValue("data", m_data);
#else
// Always serialize multiples of GROW_BY
int sizeToSerialize = (m_count % GROW_BY == 0) ? m_count : m_count +
(GROW_BY - m_count % GROW_BY);
{
T[] tmp = new T[sizeToSerialize];
Array.Copy(m_data, tmp, sizeToSerialize);
info.AddValue("data", tmp);
}
#endif
}
Visual Basic
Sub GetObjectData(ByVal info As SerializationInfo, ByVal context As StreamingContext)
Implements ISerializable.GetObjectData
info.AddValue("count", m_count)
C#
class SingleLinkedList
{
}
Visual Basic
Public Class SingleLinkedList
End Class
to this:
C#
[Serializable()]
public partial class SingleLinkedList<T> : ISerializable
{
private SingleLinkedList(SerializationInfo info, StreamingContext context)
{
int count = info.GetInt32("count");
if (count > 0)
{
T[] tmp = (T[])info.GetValue("nodes", typeof(T[]));
for (int i = 0; i < count; ++i)
{
AddToEnd(tmp[i]);
}
}
}
if (Count > 0)
{
T[] tmp = ToArray();
info.AddValue("nodes", tmp);
}
}
#endregion
}
526 Part III Using Collections
Visual Basic
<Serializable()> _
Partial Public Class SingleLinkedList(Of T)
Implements ISerializable
info.AddValue("nodes", tmp)
End If
End Sub
End Class
C#
class DoubleLinkedList
{
}
Visual Basic
Public Class DoubleLinkedList
End Class
to the following:
C#
[Serializable()]
public partial class DoubleLinkedList<T>: ISerializable
{
private DoubleLinkedList(SerializationInfo info, StreamingContext context)
{
int count = info.GetInt32("count");
if (count > 0)
Chapter 9 Serializing Collections 527
{
T[] tmp = (T[])info.GetValue("nodes", typeof(T[]));
for (int i = 0; i < count; ++i)
{
AddToEnd(tmp[i]);
}
}
}
if (Count > 0)
{
T[] tmp = ToArray();
info.AddValue("nodes", tmp);
}
}
#endregion
}
Visual Basic
<Serializable()> _
Partial Public Class DoubleLinkedList(Of T)
Implements ISerializable
info.AddValue("nodes", tmp)
End If
End Sub
End Class
528 Part III Using Collections
C#
[NonSerialized()]
object m_syncRoot;
Visual Basic
<NonSerialized()> _
Private m_syncRoot As Object
C#
class AssociativeArrayAL
{
}
Visual Basic
Public Class AssociativeArrayAL
End Class
to this:
C#
[Serializable()]
public partial class AssociativeArrayAL<TKey, TValue>
{
}
Visual Basic
<Serializable()> _
Partial Public Class AssociativeArrayAL(Of TKey, TValue)
End Class
Chapter 9 Serializing Collections 529
C#
/// <summary>
/// Used for storing a key value pair.
/// </summary>
private struct KVPair
Visual Basic
''' <summary>
''' Used for storing a key value pair.
''' </summary>
Private Structure KVPair
to the following:
C#
/// <summary>
/// Used for storing a key value pair.
/// </summary>
[Serializable()]
private struct KVPair
Visual Basic
''' <summary>
''' Used for storing a key value pair.
''' </summary>
<Serializable()> _
Private Structure KVPair
C#
[NonSerialized()]
object m_syncRoot;
Visual Basic
<NonSerialized()> _
Private m_syncRoot As Object
C#
class AssociativeArrayHT
{
}
Visual Basic
Public Class AssociativeArrayHT
End Class
to this:
C#
[Serializable()]
public partial class AssociativeArrayHT<TKey, TValue> : ISerializable
{
private AssociativeArrayHT(SerializationInfo info, StreamingContext context)
{
int count = info.GetInt32("count");
if (count > 0)
{
List<KeyValuePair<TKey, TValue>> tmp =
(List<KeyValuePair<TKey, TValue>>)info.GetValue("kvps",
typeof(List<KeyValuePair<TKey, TValue>>));
foreach (KeyValuePair<TKey, TValue> kvp in tmp)
{
Add(kvp.Key, kvp.Value);
}
}
}
if (Count > 0)
{
List<KeyValuePair<TKey, TValue>> kvps = new List<KeyValuePair<TKey, TValue>>();
info.AddValue("kvps", kvps);
}
}
#endregion
}
Chapter 9 Serializing Collections 531
Visual Basic
<Serializable()> _
Partial Public Class AssociativeArrayHT(Of TKey, TValue)
Implements ISerializable
info.AddValue("kvps", kvps)
End If
End Sub
End Class
C#
[NonSerialized()]
object m_syncRoot;
Visual Basic
<NonSerialized()> _
Private m_syncRoot As Object
C#
class QueuedArray
{
}
Visual Basic
Class QueuedArray
End Class
to this:
C#
[Serializable()]
public partial class QueuedArray<T>
{
}
Visual Basic
<Serializable()> _
Partial Public Class QueuedArray(Of T)
End Class
C#
class QueuedLinkedList
{
}
Visual Basic
Class QueuedLinkedList
End Class
to the following:
Chapter 9 Serializing Collections 533
C#
[Serializable()]
public partial class QueuedLinkedList<T>
{
}
Visual Basic
<Serializable()> _
Partial Public Class QueuedLinkedList(Of T)
End Class
C#
[NonSerialized()]
object m_syncRoot;
Visual Basic
<NonSerialized()> _
Private m_syncRoot As Object
C#
class StackedArray
{
}
Visual Basic
Class StackedArray
End Class
to this:
C#
[Serializable()]
public partial class StackedArray<T>
{
}
534 Part III Using Collections
Visual Basic
<Serializable()> _
Partial Public Class StackedArray(Of T)
End Class
C#
class StackedLinkedList
{
}
Visual Basic
Class StackedLinkedList
End Class
to this:
C#
[Serializable()]
public partial class StackedLinkedList<T>
{
}
Visual Basic
<Serializable()> _
Partial Public Class StackedLinkedList(Of T)
End Class
C#
[NonSerialized()]
object m_syncRoot;
Visual Basic
<NonSerialized()> _
Private m_syncRoot As Object
Chapter 9 Serializing Collections 535
Summary
In this chapter, you saw how to add serialization support to the collection classes you created
in Chapter 1 through Chapter 3, “Understanding Collections: Queues, Stacks, and Circular
Buffers.” You also saw how to serialize the collection classes you worked with in Chapter 1
through Chapter 5, “Generic and Support Collections.” Using the information you learned in
this chapter, you can serialize collections and also control the data being serialized.
Developer’s Guide to Collections in Microsoft .NET
Part IV
Using Collections with UI
Controls
537
Chapter 10
Using Collections with Windows
Form Controls
After completing this chapter, you will be able to
■ Perform simple bindings with the controls.
■ Create two-way bindings with the controls.
■ Understand the sample code.
Simple Binding
Some Windows Form controls can bind to collections that implement IList and IEnumerable
by setting the DataSource property of the ListBox, ComboBox, or DataGridView classes to the
collection; the following shows an example.
C#
ListBox lb = new ListBox();
Visual Basic
Dim lb As New ListBox()
To control which property of the items in the collection these controls should display, you set
the DisplayMember property of the ComboBox or ListBox control, as follows.
C#
ComboBox cb = new ComboBox();
539
540 Part IV Using Collections with UI Controls
{
Id = 2,
Name = "Tailspin Toys",
Website = "http://www.tailspintoys.com/"
}
};
cb.DisplayMember = "Name";
Visual Basic
Dim cb As New ComboBox()
You can also control which property the controls use for the value of one or more selected
items for ComboBox or ListBox controls by using the ValueMember property, as follows.
C#
cb.ValueMember = "Id";
Visual Basic
cb.ValueMember = "Id"
When a user selects an item or items, the control’s SelectedValue property then contains the
Id property of the selected item or items.
Note You must perform all Remove, Clear, and Add method calls through BindingSource if you
specify a data source that doesn’t implement IBindingList. Otherwise, the bound item will not see
your changes.
Note The sample code in the DevGuideToCollections project, in either the Chapter 10\CS\
DevGuideToCollections folder or Chapter 10\VB\DevGuideToCollections folder, contains a full
implementation of the IBindingList interface called WinFormsBindingList(T). The implementation
of each interface implemented by WinFormsBindingList(T) is broken out into separate files to
make it easier to follow. You’ll find the implementation of IBindingList in the language-specific
files WinFormsBindingList.BindingList.cs and WinFormsBindingList.BindingList.vb. You can review
those files for the full implementation of the properties and methods discussed later in this sec-
tion. Chapter 6, “.NET Collection Interfaces,” and Chapter 8, “Using Threads with Collections,”
provide information about how to implement the other interfaces. WinFormsBindingList(T), which
you wrote in Chapter 1, “Understanding Collections: Arrays and Linked Lists,” is the ArrayEx(T)
class modified to support the IBindingList interface.
AllowEdit This property returns a value that states whether items in the list can be updated.
The WinFormsBindingList(T) returns true if the items contained in the collection support
INotifyPropertyChanged. The DataGridView doesn’t allow editing of the items if the AllowEdit
property is false. So the WinFormsBindingList will not allow users to modify the items if the
items do not provide property change notifications.
AllowNew This property returns a value that states whether items can be added through
the AddNew method. The property should return false if the IList.IsFixedSize or IList.IsReadOnly
property is set to true.
AllowRemove This property returns a value that states whether items can be removed
from the collection. The property should return false if the IList.IsFixedSize or IList.IsReadOnly
property is set to true.
542 Part IV Using Collections with UI Controls
AddNew This method adds a new item to the list. It throws an exception if the Allow
New property is false. The following code shows the implementation contained in
WinFormsBindingList(T).
C#
public object AddNew()
{
if (!AllowNew)
{
throw new InvalidOperationException();
}
T retval = Activator.CreateInstance<T>();
Add(retval);
m_newIndex = Count – 1;
return retval;
}
Visual Basic
Public Function AddNew() As Object Implements IBindingList.AddNew
If (Not AllowNew) Then
Throw New InvalidOperationException()
End If
Add(retval)
m_newIndex = Count - 1
Return retval
End Function
The field m_newIndex holds the index of the item being added. This is needed so that the
bound item, such as the DataGridView, can cancel a newly added item. This is accomplished
through the ICancelAddNew interface. The ICancelAddNew interface implementation used in
WinFormsBinding(T) is as follows.
C#
public void CancelNew(int itemIndex)
{
if (m_newIndex.HasValue && m_newIndex.Value == itemIndex)
{
RemoveAt(itemIndex);
}
}
Chapter 10 Using Collections with Windows Form Controls 543
public void EndNew(int itemIndex)
{
if (m_newIndex.HasValue && m_newIndex.Value == itemIndex)
{
m_newIndex = null;
}
}
Visual Basic
Public Sub CancelNew(ByVal itemIndex As Integer) Implements ICancelAddNew.CancelNew
If (m_newIndex.HasValue And m_newIndex.Value = itemIndex) Then
RemoveAt(itemIndex)
End If
End Sub
If a user starts a new row in the DataGridView and then clicks elsewhere, the DataGridView
calls ICancelAddNew.CancelNew; otherwise it calls ICancelAddNew.EndNew after the user
enters data in the new row. The CancelNew implementation removes the newly added item
from the collection. The EndNew implementation sets m_newIndex to null to denote that the
item has been committed.
The collection must raise the ListChanged event whenever the list is modified. This oc-
curs whenever the Add, Clear, Insert, Remove, or RemoveAt methods are called as well as
when the Item set property is called. The easiest way to do this is by creating OnXXX and
OnXXXComplete methods like those discussed in the “Using CollectionBase” section in
Chapter 5, ”Generic and Support Collections.” Each method except the Add method has
OnXXX and OnXXXComplete methods. The Add and Insert methods use the OnInsert and
OnInsertComplete methods; the RemoveAt and Remove methods use the OnRemove and
OnRemoveComplete methods. The OnXXX method is called at the beginning of the opera-
tion, and the OnXXXComplete is called at the end of the method. The following code shows
how the Add, Clear, Remove, RemoveAt, and Insert methods as well as the Item property are
copied from ArrayEx(T) and modified to support the OnXXX and OnXXXComplete methods.
544 Part IV Using Collections with UI Controls
C#
public void Add(T item)
{
OnInsert(item, Count);
InnerList.Add(item);
OnInsertComplete(item, Count - 1);
}
public void Clear()
{
var removed = ToArray();
OnClear(removed);
InnerList.Clear();
OnClearComplete(removed);
}
public bool Remove(T item)
{
if (!AllowRemove)
{
throw new NotSupportedException();
}
if (index >= 0)
{
OnRemove(item, index);
InnerList.Remove(item);
OnRemoveComplete(item, index);
}
if (!AllowRemove)
{
throw new NotSupportedException();
}
if (IsSorted)
Chapter 10 Using Collections with Windows Form Controls 545
{
throw new NotSupportedException("You cannot remove by index on a sorted list.");
}
T item = m_data[index];
OnRemove(item, index);
InnerList.RemoveAt(index);
OnRemoveComplete(item, index);
}
public void Insert(int index, T item)
{
OnInsert(item, index);
InnerList.Insert(index, item);
OnInsertComplete(item, index);
}
public T this[int index]
{
set
{
T oldValue = InnerList[index];
InnerList[index] = value;
Visual Basic
Public Sub Add(ByVal item As T) Implements ICollection(Of T).Add
OnInsert(item, Count)
InnerList.Add(item)
OnInsertComplete(item, Count - 1)
End Sub
InnerList.Remove(item)
OnRemoveComplete(item, index)
End If
If (IsSorted) Then
Throw New NotSupportedException("You cannot remove by index on a sorted list.")
End If
OnRemove(item, index)
InnerList.RemoveAt(index)
OnRemoveComplete(item, index)
End Sub
Public Sub Insert(ByVal index As Integer, ByVal item As T) Implements IList(Of T).Insert
OnInsert(item, index)
InnerList.Insert(index, item)
OnInsertComplete(item, index)
End Sub
InnerList(index) = value
The OnXXX methods check whether the corresponding method can be called, such as when
a user calls Insert instead of Add on a sorted list.
C#
void OnSet(int index, T oldValue, T newValue)
{
if (IsSorted)
{
throw new NotSupportedException("You cannot set items in a sorted list");
}
}
void OnClear(T[] itemsRemoved)
{
}
void OnInsert(T item, int index)
{
// You can only add to the end of the list when sorting is on
if (IsSorted && index != Count)
{
throw new NotSupportedException();
}
}
void OnRemove(T item, int index)
{
}
Visual Basic
Sub OnSet(ByVal index As Integer, ByVal oldValue As T, ByVal newValue As T)
If (IsSorted) Then
Throw New NotSupportedException("You cannot set items in a sorted list")
End If
End Sub
To keep it simple, the WinFormsBindingList(T) class throws an exception when a user tries to
insert an item in a sorted list instead of trying to figure out whether the user should insert
the item in the sorted or unsorted list. If you decide to implement insertion into a sorted
list in your version, you should determine whether the user wants to insert into the sorted
list she currently sees or the stored, unsorted list she sees when she makes a call such as
Insert(2,value). For example, if the unsorted list is [3,1,2,7,6,5] and the sorted list the user sees
548 Part IV Using Collections with UI Controls
is [1,2,3,5,6,7], where should 4 be inserted in the unsorted list if the user calls Insert(3,4)?
Does the user want you to insert 4 into the unsorted list at index 3 because the unsorted list
is completely different?
The OnXXXComplete methods are responsible for notifying the bound item of list changes,
registering for property changes, and handling special cases after an add or removal, such as
re-indexing or re-sorting the list.
C#
void OnSetComplete(int index, T oldValue, T newValue)
{
UnRegisterForPropertyChanges(oldValue);
RegisterForPropertyChanges(newValue);
if (SupportsSearching)
{
ReIndex();
}
if (SupportsSearching)
{
ReIndex();
}
if (m_originalList != null)
{
m_originalList.Clear();
}
if (m_originalList != null)
{
m_originalList.Insert(index, item);
}
}
void OnRemoveComplete(T item, int index)
{
UnRegisterForPropertyChanges(item);
if (SupportsSearching)
{
ReIndex();
}
OnListChanged(new ListChangedEventArgs(ListChangedType.ItemDeleted, index));
if (m_originalList != null)
{
m_originalList.Remove(item);
}
}
Visual Basic
Sub OnSetComplete(ByVal index As Integer, ByVal oldValue As T, ByVal newValue As T)
UnRegisterForPropertyChanges(oldValue)
RegisterForPropertyChanges(newValue)
If (SupportsSearching) Then
ReIndex()
End If
If (SupportsSearching) Then
ReIndex()
End If
550 Part IV Using Collections with UI Controls
C#
void UnRegisterForPropertyChanges(T item)
{
// No need to register for property changes if none of the
// IBindingList features are supported
if ((!m_supportsPropertyChanged && !m_supportsPropertyChanging) ||
(!SupportsSorting && !SupportsChangeNotification && !SupportsSearching))
{
return;
}
Visual Basic
Sub UnRegisterForPropertyChanges(ByVal item As T)
' No need to register for property changes if none of the
' IBindingList features are supported
If ((Not m_supportsPropertyChanged And Not m_supportsPropertyChanging) Or _
(Not SupportsSorting And Not SupportsChangeNotification And Not SupportsSearching)) _
Then
Return
End If
Property change notification is required internally for re-indexing and re-sorting the list if
the property that changed is being indexed or sorted. Property change notifications are also
needed so that bound controls can receive the ListChangeType.ItemChange notification.
The collection raises the ListChanged event through the OnListChanged method.
C#
void OnListChanged(ListChangedEventArgs e)
{
if (!SupportsChangeNotification)
{
return;
}
if (ListChanged != null)
{
ListChanged(this, e);
}
}
Visual Basic
Sub OnListChanged(ByVal e As ListChangedEventArgs)
If (Not SupportsChangeNotification) Then
Return
End If
RaiseEvent ListChanged(Me, e)
End Sub
Notification can be turned off, so the code checks the SupportsChangeNotification flag
before raising the ListChanged event.
Chapter 10 Using Collections with Windows Form Controls 553
SupportsSorting This property tells the bound item whether sorting is implemented.
SortDirection This property holds the current sort direction of the sort on the SortProperty.
The property is not valid if IsSorted is false.
SortProperty This property states the currently sorted property. The property is not valid if
IsSorted is false.
IsSorted This property states whether the contents are currently sorted. This will be true if
ApplySort has been called and RemoveSort has not been called after the last ApplySort. The
contents of SortDirection and SortProperty are valid only when IsSorted is true.
ApplySort This method applies the specified sort to the list and is implemented as follows.
C#
public void ApplySort(PropertyDescriptor property, ListSortDirection direction)
{
if (!SupportsSorting)
{
throw new NotSupportedException();
}
if (!IsSorted)
{
if (m_originalList == null)
{
m_originalList = m_data;
m_data = new List<T>(m_originalList);
}
}
m_sortDirection = direction;
m_sortDescriptor = property;
IsSorted = true;
Sort();
}
Visual Basic
Public Sub ApplySort( _
ByVal [property] As PropertyDescriptor, _
ByVal direction As ListSortDirection _
) Implements IBindingList.ApplySort
If (Not SupportsSorting) Then
Throw New NotSupportedException()
End If
554 Part IV Using Collections with UI Controls
Before a sort operation, the original list is saved and copied over to m_data. The field m_data
is then sorted using the Sort method.
C#
void Sort()
{
if (m_sortDescriptor == null)
{
return;
}
m_data.Sort(
new LambdaComparer<T>
(
(x, y) =>
{
object xValue = m_sortDescriptor.GetValue(x);
object yValue = m_sortDescriptor.GetValue(y);
if (m_sortDirection == ListSortDirection.Descending)
{
return System.Collections.Comparer.Default.Compare(
xValue, yValue) * -1;
}
if (SupportsSearching)
{
ReIndex();
}
Visual Basic
Function Compare(ByVal x As T, ByVal y As T) As Integer
Dim xValue As Object = m_sortDescriptor.GetValue(x)
Dim yValue = m_sortDescriptor.GetValue(y)
If (m_sortDirection = ListSortDirection.Descending) Then
Return System.Collections.Comparer.Default.Compare(xValue, yValue) * -1
End If
Return System.Collections.Comparer.Default.Compare(xValue, yValue)
End Function
Sub Sort()
If (m_sortDescriptor Is Nothing) Then
Return
End If
m_data.Sort(AddressOf Compare)
If (SupportsSearching) Then
ReIndex()
End If
In the preceding code, the Sort method uses a lambda expression in the Microsoft Visual
C# code and a Compare function in the Microsoft Visual Basic code to do the sorting. The
lambda expression sort creates the following custom class that implements IComparer(T)
and passes the Compare method call to the lambda expression.
C#
class LambdaComparer<T> : IComparer<T>
{
Func<T, T, int> m_compare;
m_compare = compareFunction;
}
The Sort method then notifies the bound item of the collection change by raising the
ListChanged event.
C#
public void RemoveSort()
{
if (!SupportsSorting)
{
throw new NotSupportedException();
}
m_sortDescriptor = null;
IsSorted = false;
if (m_originalList != null)
{
m_data = m_originalList;
m_originalList = null;
}
if (SupportsSearching)
{
ReIndex();
}
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
}
Visual Basic
Public Sub RemoveSort() Implements IBindingList.RemoveSort
If (Not SupportsSorting) Then
Throw New NotSupportedException()
End If
m_sortDescriptor = Nothing
m_isSorted = False
If (m_originalList IsNot Nothing) Then
m_data = m_originalList
m_originalList = Nothing
End If
If (SupportsSearching) Then
ReIndex()
End If
OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1))
End Sub
The original list is restored and then indexed. The bound item is then notified of the change.
Chapter 10 Using Collections with Windows Form Controls 557
SupportsSearching Bound items can call this property to determine whether a collection
supports searching. The SupportsSearching property lets you know whether the collection
implements the Find method—but not necessarily whether the collection supports indexing.
Most collections do not support indexing, but the sample code for this book provides an
example indexing implementation to get you started.
Find The Find method searches for the item that contains the specified property that
matches the specified key.
C#
public int Find(PropertyDescriptor property, object key)
{
if (!SupportsSearching)
{
throw new NotSupportedException();
}
return -1;
}
try
558 Part IV Using Collections with UI Controls
{
object value = property.GetValue(item);
if (System.Collections.Comparer.Default.Compare(value, key) == 0)
{
return i;
}
}
catch
{
}
}
return -1;
}
Visual Basic
Public Function Find( _
ByVal [property] As PropertyDescriptor, _
ByVal key As Object _
) As Integer Implements IBindingList.Find
If (Not SupportsSearching) Then
Throw New NotSupportedException()
End If
Return -1
End If
Try
Dim value = [property].GetValue(item)
Return -1
End Function
Chapter 10 Using Collections with Windows Form Controls 559
The Find method first checks to see whether the property has been indexed. If it has, it then
uses the index table to find the item; otherwise, it performs a linear search for the item.
AddIndex This method adds a PropertyDescriptor to the list to be indexed. The implemen-
tation in WinFormsBindingList(T) is as follows.
C#
public void AddIndex(PropertyDescriptor property)
{
IndexData ?data = FindIndexData(property);
if (!data.HasValue)
{
if (m_indexes == null)
{
m_indexes = new List<WinFormsBindingList<T>.IndexData>();
}
m_indexes.Add
(
new IndexData()
{
Indexes = new Dictionary<object,List<int>>(),
PropertyDescriptor = property
}
);
}
ReIndex();
}
return null;
}
560 Part IV Using Collections with UI Controls
Visual Basic
Public Sub AddIndex(ByVal [property] As PropertyDescriptor) Implements IBindingList.AddIndex
Dim data As Nullable(Of IndexData) = FindIndexData([property])
Return Nothing
End Function
The FindIndexData method tries to locate the index of the collection item that belongs to the
specified property. If the property isn’t already indexed, it creates a new index, and then calls
the ReIndex method to force a re-indexing of all of the data, as follows.
C#
void ReIndex()
{
if (m_indexes == null)
{
return;
}
Visual Basic
Sub ReIndex()
If (m_indexes Is Nothing) Then
Return
End If
' Iterate over each item and add the index to the collection
For i As Integer = 0 To Count - 1
Dim item As T = InnerList(i)
AddIndexData(Data, value, i)
Catch
End Try
Next
Next
End Sub
562 Part IV Using Collections with UI Controls
ReIndex works by erasing all of the old indexed data and then traversing each item in the
collection and indexing that item. As you can tell, this is not an efficient way of indexing the
system, but it shows the basic idea behind the AddIndex and RemoveIndex methods.
RemoveIndex This method removes a PropertyDescriptor from the list of indexed proper-
ties. The implementation in WinFormsBindingList(T) is as follows.
C#
public void RemoveIndex(PropertyDescriptor property)
{
if (m_indexes == null)
{
return;
}
if (data.HasValue)
{
m_indexes.Remove(data.Value);
}
ReIndex();
}
Visual Basic
Public Sub RemoveIndex(ByVal [property] As PropertyDescriptor) _
Implements IBindingList.RemoveIndex
If (m_indexes Is Nothing) Then
Return
End If
If (data.HasValue) Then
m_indexes.Remove(data.Value)
End If
ReIndex()
End Sub
Note The sample code in the DevGuideToCollections project, in either the Chapter 10\CS\
DevGuideToCollections folder or Chapter 10\VB\DevGuideToCollections folder, contains a
full implementation of the IBindingListView interface called WinFormsBindingListView(T). The
implementation of each interface implemented by WinFormsBindingListView(T) is broken
out into separate files to make the logic easier to follow. You’ll find the implementation of
IBindingListView in the language-specific files WinFormsBindingListView.BindingListView.cs and
WinFormsBindingListView.BindingListView.vb. You can review those files for the full implementa-
tion of the properties and methods discussed later in this section. Chapters 6 and 8 describe how
to implement the other interfaces. WinFormsBindingListView(T) is the WinFormsBindingList(T)
class modified to support the IBindingListView interface. See the “Implementing the IBindingList
Interface” section earlier in this chapter for information about implementing IBindingList.
Filter Set this property to filter a collection. The filter format for WinFormsBindingList
View(T) is a small subset of the expression that you can pass to DataColumn.Expression.
The allowed syntax is as follows.
Using that syntax, you can write code such as the following
[Name] = ‘Value’
NOT IsDeleted
C#
public string Filter
{
get
{
return m_filter;
}
set
{
if (m_filter == value)
{
return;
}
m_filterRoot = FilterParser.Parse(value);
m_filter = value;
if (!string.IsNullOrEmpty(value))
{
// Something needs to be filtered
if (!m_isFiltering)
{
ApplyFilter();
}
else
{
ReapplyFilter();
}
}
else
{
RemoveFilterInternal();
}
}
}
Visual Basic
Public Property Filter() As String Implements IBindingListView.Filter
Get
Return m_filter
End Get
Set(ByVal value As String)
If (m_filter = value) Then
Return
End If
m_filterRoot = FilterParser.Parse(value)
m_filter = value
The Filter property parses the specified string and then determines whether it should apply
the filter for the first time, reapply it, or remove the current filter. If the filter is null or empty,
the code calls the RemoveFilterInternal method to remove the filter.
C#
void RemoveFilterInternal()
{
if (!m_isFiltering)
{
return;
}
if (IsSorted)
{
m_data = new List<T>(m_originalList);
Sort();
}
else
{
m_data = m_originalList;
m_originalList = null;
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
}
m_isFiltering = false;
}
Visual Basic
Sub RemoveFilterInternal()
If (Not m_isFiltering) Then
Return
End If
If (IsSorted) Then
m_data = New List(Of T)(m_originalList)
Sort()
Else
m_data = m_originalList
m_originalList = Nothing
OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1))
End If
m_isFiltering = False
End Sub
566 Part IV Using Collections with UI Controls
The RemoveFilterInternal function restores the original list if the list is currently not being
sorted, or it copies the original list and then calls Sort if the list is currently being sorted.
The Filter property calls ApplyFilter if no filter has been applied yet, as follows.
C#
void ApplyFilter()
{
if (m_isFiltering)
{
return;
}
if (m_originalList == null)
{
m_originalList = m_data;
m_data = new List<T>();
}
m_isFiltering = true;
ReapplyFilter();
}
Visual Basic
Sub ApplyFilter()
If (m_isFiltering) Then
Return
End If
m_isFiltering = True
ReapplyFilter()
End Sub
ApplyFilter saves the original list and creates an empty list if the list is currently not being
sorted. The empty list is filled in by the ReapplyFilter method.
The ReapplyFilter method traverses the original list and checks the IsFiltered method to see
whether the item has been filtered.
Chapter 10 Using Collections with Windows Form Controls 567
C#
bool IsFiltered(T item)
{
if (m_filterRoot == null)
{
return false;
}
return m_filterRoot.Eval(item);
}
void ReapplyFilter()
{
if (!IsFiltering)
{
return;
}
m_data.Clear();
foreach (T item in m_originalList)
{
if (IsFiltered(item))
{
m_data.Add(item);
}
}
if (IsSorted)
{
Sort();
}
Visual Basic
Function IsFiltered(ByVal item As T) As Boolean
If (m_filterRoot Is Nothing) Then
Return False
End If
Return m_filterRoot.Eval(item)
End Function
Sub ReapplyFilter()
If (Not m_isFiltering) Then
Return
End If
568 Part IV Using Collections with UI Controls
m_data.Clear()
For Each item As T In m_originalList
If (IsFiltered(item)) Then
m_data.Add(item)
End If
Next
If (IsSorted) Then
Sort()
End If
RemoveFilter This method removes the current filter. This is the equivalent of writing
Filter = String.Empty.
C#
public void RemoveFilter()
{
if (!SupportsAdvancedSorting)
{
throw new NotSupportedException();
}
Filter = String.Empty;
}
Visual Basic
Public Sub RemoveFilter() Implements IBindingListView.RemoveFilter
If (Not SupportsAdvancedSorting) Then
Throw New NotSupportedException()
End If
Filter = String.Empty
End Sub
SupportsAdvancedSorting This property states whether the list supports advanced sorting.
Modifications to IBindingList ApplySort must be modified to add the specified sort prop-
erties to m_sortDescriptors.
Chapter 10 Using Collections with Windows Form Controls 569
C#
public void ApplySort(PropertyDescriptor property, ListSortDirection direction)
{
if (!SupportsSorting)
{
throw new NotSupportedException();
}
SaveUnsortedList(false);
m_sortDescriptors.Clear();
m_sortDescriptors.Add(new ListSortDescription(property,direction));
IsSorted = true;
Sort();
}
Visual Basic
Public Sub ApplySort(ByVal sorts As ListSortDescriptionCollection) _
Implements IBindingListView.ApplySort
If (Not SupportsAdvancedSorting) Then
Throw New NotSupportedException()
End If
SaveUnsortedList(True)
m_sortDescriptors.Clear()
For Each sort As ListSortDescription In sorts
m_sortDescriptors.Add(sort)
Next
m_isSorted = True
Sort()
End Sub
The RemoveSort method restores the original list if filtering is not enabled and notifies the
bound item of the list change.
C#
public void RemoveSort()
{
if (!SupportsSorting)
{
throw new NotSupportedException();
}
m_sortDescriptors.Clear();
IsSorted = false;
if (m_originalList != null)
{
m_data = m_originalList;
m_originalList = null;
}
if (IsFiltering)
570 Part IV Using Collections with UI Controls
{
ReapplyFilter();
}
if (SupportsSearching)
{
ReIndex();
}
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
}
Visual Basic
Public Sub RemoveSort() Implements IBindingList.RemoveSort
If (Not SupportsSorting) Then
Throw New NotSupportedException()
End If
m_sortDescriptors.Clear()
m_isSorted = False
If (m_originalList IsNot Nothing) Then
m_data = m_originalList
m_originalList = Nothing
End If
If (IsFiltering) Then
ReapplyFilter()
End If
If (SupportsSearching) Then
ReIndex()
End If
OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1))
End Sub
The SortDirection and SortProperty properties return the first item in m_sortDescriptors if
m_sortDescriptors contains only one item.
C#
public ListSortDirection SortDirection
{
get
{
if (SupportsSorting)
{
if (m_sortDescriptors.Count == 1)
{
return m_sortDescriptors[0].SortDirection;
}
return ListSortDirection.Ascending;
}
Visual Basic
Public ReadOnly Property SortDirection() As ListSortDirection _
Implements IBindingList.SortDirection
Get
If (SupportsSorting) Then
If (m_sortDescriptors.Count = 1) Then
Return m_sortDescriptors(0).SortDirection
End If
Return ListSortDirection.Ascending
End If
The Sort method must also be modified to traverse through each item in m_sortDescriptors.
C#
void Sort()
{
if (m_sortDescriptors.Count <= 0)
{
return;
}
572 Part IV Using Collections with UI Controls
m_data.Sort(
new LambdaComparer<T>
(
(x, y) =>
{
int result = 0;
if (sd.SortDirection == ListSortDirection.Descending)
{
result = System.Collections.Comparer.Default.Compare
(xValue, yValue) * -1;
}
else
{
result = System.Collections.Comparer.Default.Compare
(xValue, yValue);
}
if (result != 0 || i == m_sortDescriptors.Count - 1)
{
return result;
}
}
System.Diagnostics.Debug.Assert(false);
return 0;
}
));
if (SupportsSearching)
{
ReIndex();
}
Visual Basic
Function Compare(ByVal x As T, ByVal y As T) As Integer
System.Diagnostics.Debug.Assert(False)
Return 0
End Function
Sub Sort()
If (m_sortDescriptors.Count <= 0) Then
Return
End If
m_data.Sort(AddressOf Compare)
If (SupportsSearching) Then
ReIndex()
End If
C#
public void ApplySort(ListSortDescriptionCollection sorts)
{
if (!SupportsAdvancedSorting)
{
throw new NotSupportedException();
}
SaveUnsortedList(true);
m_sortDescriptors.Clear();
foreach (ListSortDescription sort in sorts)
{
m_sortDescriptors.Add(sort);
}
IsSorted = true;
Sort();
}
574 Part IV Using Collections with UI Controls
Visual Basic
Public Sub ApplySort( _
ByVal [property] As PropertyDescriptor, _
ByVal direction As ListSortDirection _
) Implements IBindingList.ApplySort
If (Not SupportsSorting) Then
Throw New NotSupportedException()
End If
SaveUnsortedList(False)
m_sortDescriptors.Clear()
m_sortDescriptors.Add(New ListSortDescription([property], direction))
m_isSorted = True
Sort()
End Sub
The SaveUnsortedList method saves the original list if the list is not currently being sorted.
C#
void SaveUnsortedList(bool advance)
{
if (!IsSorted)
{
if (m_originalList == null)
{
m_originalList = m_data;
m_data = new List<T>(m_originalList);
}
}
}
Visual Basic
Sub SaveUnsortedList(ByVal advance As Boolean)
If (Not IsSorted) Then
If (m_originalList Is Nothing) Then
m_originalList = m_data
m_data = New List(Of T)(m_originalList)
End If
End If
End Sub
To use BindingSource, create an instance of the BindingSource class and assign the DataSource
property to your collection.
C#
List<int> items = new List<int>();
// Populate items
Visual Basic
Dim items As New List(Of Integer)()
C#
dataGridView1.DataSource = source;
Visual Basic
dataGridView1.DataSource = source
You need to call the Add, Clear, Insert, Remove, and RemoveAt methods in BindingSource
instead of the data source if your data source doesn’t implement IBindingList, as follows.
C#
source.Add(1);
source.RemoveAt(0);
Visual Basic
source.Add(1)
source.RemoveAt(0)
576 Part IV Using Collections with UI Controls
C#
comboBox1.DataSource = m_datasource;
comboBox1.DisplayMember = "Name";
comboBox1.ValueMember = "Id";
Visual Basic
comboBox1.DataSource = m_datasource
comboBox1.DisplayMember = "Name"
comboBox1.ValueMember = "Id"
The Update panel shows the currently selected item in the combo box. You can update the
selected item by clicking on the Update Item button. The combo box is automatically up-
dated to reflect the updated item. Because each item implements INotifyPropertyChanged,
bound controls receive notifications of property changes when items are updated with the
following code.
C#
m_showing.Name = NameTextBox.Text;
m_showing.Website = WebsiteTextBox.Text;
Visual Basic
m_showing.Name = NameTextBox.Text
m_showing.Website = WebsiteTextBox.Text
You can use the Add panel to add new items to the combo box. You add items to the combo
box by clicking the Add Item button. The combo box is automatically updated to reflect the
newly added item. The code for adding an item is as follows.
C#
Company company = new Company();
company.Id = int.Parse(AddIdTextBox.Text);
company.Name = AddNameTextBox.Text;
company.Website = AddWebsiteTextBox.Text;
m_datasource.Add(company);
Chapter 10 Using Collections with Windows Form Controls 577
Visual Basic
Dim company As New Company()
company.Id = Integer.Parse(AddIdTextBox.Text)
company.Name = AddNameTextBox.Text
company.Website = AddWebsiteTextBox.Text
m_datasource.Add(company)
You can remove the selected item in the combo box by clicking the Remove Item button. The
following shows the code for removing an item.
C#
if (comboBox1.SelectedIndex >= 0)
{
m_datasource.RemoveAt(comboBox1.SelectedIndex);
}
Visual Basic
If (comboBox1.SelectedIndex >= 0) Then
m_datasource.RemoveAt(comboBox1.SelectedIndex)
End If
C#
listBox1.DataSource = m_datasource;
listBox1.DisplayMember = "Name";
Visual Basic
listBox1.DataSource = m_datasource
listBox1.DisplayMember = "Name"
The Update panel shows the currently selected item in the list box. You update the selected
item by clicking the Update Item button. The list box is automatically updated to reflect the
updated item. Because each item implements INotifyPropertyChanged, bound controls receive
notifications of property changes when items are updated with the following code.
C#
m_showing.Name = NameTextBox.Text;
m_showing.Website = WebsiteTextBox.Text;
Visual Basic
m_showing.Name = NameTextBox.Text
m_showing.Website = WebsiteTextBox.Text
578 Part IV Using Collections with UI Controls
You can use the Add panel to add new items to the list box. You add items to the list box
by clicking the Add Item button. The list box is automatically updated to reflect the newly
added item. The code for adding an item is as follows.
C#
Company company = new Company();
company.Id = int.Parse(AddIdTextBox.Text);
company.Name = AddNameTextBox.Text;
company.Website = AddWebsiteTextBox.Text;
m_datasource.Add(company);
Visual Basic
Dim company As New Company()
company.Id = Integer.Parse(AddIdTextBox.Text)
company.Name = AddNameTextBox.Text
company.Website = AddWebsiteTextBox.Text
m_datasource.Add(company)
You can remove the selected item in the list box by clicking the Remove Item button. The fol-
lowing shows the code for removing an item.
C#
if (comboBox1.SelectedIndex >= 0)
{
m_datasource.RemoveAt(listBox1.SelectedIndex);
}
Visual Basic
If (comboBox1.SelectedIndex >= 0) Then
m_datasource.RemoveAt(listBox1.SelectedIndex)
End If
C#
m_datasource = DL.GetDataSource();
dataGridView1.DataSource = m_datasource;
Visual Basic
m_datasource = DL.GetDataSource()
dataGridView1.DataSource = m_datasource
Chapter 10 Using Collections with Windows Form Controls 579
Items can be added and removed from the data grid by using the same code that is in
the ComboBoxBinding and ListBoxBinding forms. In fact, both of the forms use the same
data source instance in the sample code as the DataGridViewBinding form, so updat-
ing the data source in the ComboBoxBinding or ListBoxBinding form also updates the
DataGridViewBinding form.
Items can be searched by entering a search string in the search box, selecting a property to
search on, and clicking the Search button. A message box will appear stating the row the
item was found in. You can also test indexing by selecting the properties you want to index
on and then searching on the indexed property. The code for searching is as follows.
C#
if (string.IsNullOrEmpty(SearchTextBox.Text) || SearchPropertyComboBox.SelectedIndex < 0)
{
return;
}
try
{
found = m_datasource.Find(pd, pd.Converter.ConvertFromString(SearchTextBox.Text));
if (found >= 0)
{
MessageBox.Show(string.Format("Found '{0}' at index {1}",
SearchTextBox.Text, found));
}
else
{
MessageBox.Show(string.Format("Didn't find '{0}'", SearchTextBox.Text));
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
Visual Basic
If (String.IsNullOrEmpty(SearchTextBox.Text) Or SearchPropertyComboBox.SelectedIndex < 0)
Then
Return
End If
Try
found = m_datasource.Find(pd, pd.Converter.ConvertFromString(SearchTextBox.Text))
C#
m_datasource = DL.GetDataSourceView();
m_binding = New BindingSource();
m_binding.DataSource = m_datasource;
dataGridView1.DataSource = m_binding;
Visual Basic
m_datasource = DL.GetDataSourceView()
m_binding = New BindingSource()
m_binding.DataSource = m_datasource
dataGridView1.DataSource = m_binding
Users can filter items by entering the filter string into the Filter text box and clicking the Filter
button. The following code shows how to filter the IBindingListView object.
C#
m_datasource.Filter = FilterTextBox.Text;
Visual Basic
m_datasource.Filter = FilterTextBox.Text
The IBindingListView collection can be sorted on multiple properties by using the Sort prop-
erty on the BindingSource. To do this in the UI, enter the sort text into the Sort text box, and
click the Sort button. The code for doing this is as follows.
C#
m_binding.Sort = SortTextBox.Text;
Visual Basic
m_binding.Sort = SortTextBox.Text
Chapter 10 Using Collections with Windows Form Controls 581
C#
m_source = new BindingSource();
m_source.DataSource = new List<Company>(DL.GetData());
listBox1.DataSource = m_source;
listBox1.DisplayMember = "Name";
Visual Basic
m_source = New BindingSource()
m_source.DataSource = New List(Of Company)(DL.GetData())
listBox1.DataSource = m_source
listBox1.DisplayMember = "Name"
The Update panel shows the currently selected item in the list box. You can update the se-
lected item by clicking the Update Item button. The list box is automatically updated to
reflect the updated item. Because each item implements INotifyPropertyChanged, items are
updated with the following code.
C#
m_showing.Name = NameTextBox.Text;
m_showing.Website = WebsiteTextBox.Text;
Visual Basic
m_showing.Name = NameTextBox.Text
m_showing.Website = WebsiteTextBox.Text
You can use the Add panel to add new items to the list box. You add items to the list box by
clicking the Add Item button. The list box updates automatically to reflect the newly added
item. The item needs to be added using the BindingSource instead of the List(T) for the list
box to see the changes, because List(T) doesn’t implement IBindingList.
C#
Company company = new Company();
company.Id = int.Parse(AddIdTextBox.Text);
company.Name = AddNameTextBox.Text;
company.Website = AddWebsiteTextBox.Text;
m_source.Add(company);
582 Part IV Using Collections with UI Controls
Visual Basic
Dim company As New Company()
company.Id = Integer.Parse(AddIdTextBox.Text)
company.Name = AddNameTextBox.Text
company.Website = AddWebsiteTextBox.Text
m_source.Add(company)
You can remove the selected item in the list box by clicking the Remove Item button. The
following shows the code for removing an item. The item needs to be removed using the
BindingSource instead of the List(T) for the list box to see the changes, because List(T)
doesn’t implement IBindingList.
C#
if (comboBox1.SelectedIndex >= 0)
{
m_source.RemoveAt(listBox1.SelectedIndex);
}
Visual Basic
If (comboBox1.SelectedIndex >= 0) Then
m_source.RemoveAt(listBox1.SelectedIndex)
End If
Summary
In this chapter, you saw how to bind collections to controls used in Windows Forms. You saw
how to have two-way bound controls by using the IBindingList interface. You also saw that
you can use the IBindingList interface to sort and search, and use the IBindingListView inter-
face to do advanced filtering and sorting.
Chapter 11
Using Collections with WPF and
Silverlight Controls
After completing this chapter, you will be able to
■ Use and implement the INotifyCollectionChanged interface.
■ Use the ObservableCollection(T) class.
■ Use the ICollectionView and CollectionView derived classes.
■ Bind collections to WPF and Silverlight controls.
INotifyCollectionChanged Overview
The INotifyCollectionChanged interface is an interface that exposes a CollectionChanged
event. The CollectionChanged event is raised whenever the collection is changed, such as
when it is cleared, removed, or added to. The INotifyCollectionChanged interface is imple-
mented by the ObservableCollection(T) class, which you learn about later in this chapter.
Note To see the final code, open the DevGuideToCollections project, in either the Chapter 11\
CS\DevGuideToCollections folder or the Chapter 11\VB\DevGuideToCollections folder. The imple-
mentation of each interface implemented by NotificationList(T) is broken out into separate files
to make it easier to follow. You’ll find the implementation of NotifyCollectionChanged in the lan-
guage-specific files NotificationList.CollectionChanged.cs and NotificationList.CollectionChanged
.vb. You can review those files for the full implementation of the properties and methods discussed
later in this section. Chapter 6, “.NET Collection Interfaces,” and Chapter 8, “Using Threads with
Collections,” provide information about how to implement the other interfaces. NotificationList(T) is
the WinFormsBindingList(T) class you created in Chapter 10, “Using Collections with Windows Form
Controls,” modified to support the INotifyCollectionChanged interface.
583
584 Part IV Using Collections with UI Controls
The ListChanged event must be raised whenever the list is modified. The list is modified
whenever the Add, Clear, Insert, Remove, or RemoveAt method is called, as well as the Item
set property. The easiest way to raise the ListChanged event when the list is modified is by
creating OnXXX and OnXXXComplete methods like those discussed in the CollectionBase sec-
tion in Chapter 5, “Generic and Support Collections.” Each method except the Add method
has OnXXX and OnXXXComplete methods. The Add and Insert methods use the OnInsert
and OnInsertComplete methods; the RemoveAt and Remove methods use the OnRemove
and OnRemoveComplete methods. The OnXXX method is called at the beginning of the
operation and the OnXXXComplete method is called at the end of the method. The follow-
ing code shows how the code in ArrayEx(T) is modified to support handling the OnXXX and
OnXXXComplete methods.
C#
public void Add(T item)
{
OnInsert(item, Count);
InnerList.Add(item);
OnInsertComplete(item, Count - 1);
}
if (index >= 0)
{
OnRemove(item, index);
InnerList.Remove(item);
OnRemoveComplete(item, index);
}
T item = InnerList[index];
OnRemove(item, index);
InnerList.RemoveAt(index);
OnRemoveComplete(item, index);
}
InnerList.Insert(index, item);
OnInsertComplete(item, index);
}
InnerList[index] = value;
Visual Basic
Public Sub Add(ByVal item As T) Implements ICollection(Of T).Add
OnInsert(item, Count)
InnerList.Add(item)
OnInsertComplete(item, Count - 1)
End Sub
InnerList.Remove(item)
OnRemoveComplete(item, index)
End If
OnRemove(item, index)
InnerList.RemoveAt(index)
OnRemoveComplete(item, index)
End Sub
Public Sub Insert(ByVal index As Integer, ByVal item As T) Implements IList(Of T).Insert
OnInsert(item, index)
InnerList.Insert(index, item)
OnInsertComplete(item, index)
End Sub
InnerList(index) = value
The OnXXX methods are not needed to implement the INotifyCollectionChange interface,
but you might find some uses for them later. For now, they remain empty, as in the following
example.
Chapter 11 Using Collections with WPF and Silverlight Controls 587
C#
void OnSet(int index, T oldValue, T newValue)
{
}
Visual Basic
Sub OnSet(ByVal index As Integer, ByVal oldValue As T, ByVal newValue As T)
End Sub
End Sub
End Sub
End Sub
However, each of the OnXXXComplete methods needs to raise the appropriate event for the
XXX operation. To simplify some of the coding, you can create two helper methods for rais-
ing the PropertyChanged and CollectionChanged events, as follows.
C#
void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (CollectionChanged != null)
{
CollectionChanged(this, e);
}
}
588 Part IV Using Collections with UI Controls
Visual Basic
Sub OnPropertyChanged(ByVal propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
C#
void OnSetComplete(int index, T oldValue, T newValue)
{
OnPropertyChanged("Item[]");
OnCollectionChanged(new NotifyCollectionChangedEventArgs
(NotifyCollectionChangedAction.Replace, oldValue, newValue, index));
}
if (itemsRemoved != null)
{
items = new object[itemsRemoved.Length];
OnCollectionChanged(new NotifyCollectionClearedEventArgs(items));
}
Visual Basic
Sub OnSetComplete(ByVal index As Integer, ByVal oldValue As T, ByVal newValue As T)
OnPropertyChanged("Item[]")
OnCollectionChanged(New NotifyCollectionChangedEventArgs _
(NotifyCollectionChangedAction.Replace, oldValue, newValue, index))
End Sub
items = tmp.ToArray()
End If
OnCollectionChanged(New NotifyCollectionClearedEventArgs(items))
End Sub
Because most collection operations result in the size and contents of the collection chang-
ing, collection change operations also notify any bound objects of the Count and Item[]
changing.
590 Part IV Using Collections with UI Controls
C#
public void Clear()
{
var removed = ToArray();
OnClear(removed);
InnerList.Clear();
OnClearComplete(removed);
}
Visual Basic
Public Sub Clear() Implements ICollection(Of T).Clear, IList.Clear
Dim removed As T() = ToArray()
OnClear(removed)
InnerList.Clear()
OnClearComplete(removed)
End Sub
The OnClearComplete method then raises the CollectionChange event with the items that are
removed, as follows.
C#
OnCollectionChanged(new NotifyCollectionClearedEventArgs(items));
Visual Basic
OnCollectionChanged(new NotifyCollectionClearedEventArgs(items))
The advantage of doing this is that it allows any attached objects to unregister from the
objects being cleared. Because the event is raised after the collection is cleared, the receiver
wouldn’t have access to the cleared objects otherwise.
C#
public class NotifyCollectionClearedEventArgs : NotifyCollectionChangedEventArgs
{
object[] m_clearedItems;
Visual Basic
Public Class NotifyCollectionClearedEventArgs
Inherits NotifyCollectionChangedEventArgs
End Class
C#
ObservableCollection<int> numbersToProcess = new ObservableCollection<int>();
ObservableCollection<int> processedNumbers = new ObservableCollection<int>();
numbersToProcess.CollectionChanged +=
(sender, e) =>
{
if (e.Action ==
System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
processedNumbers.Add((int)e.NewItems[0]);
numbersToProcess.RemoveAt(e.NewStartingIndex);
}
};
processedNumbers.CollectionChanged +=
(sender, e) =>
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
int value = (int)e.NewItems[0];
if (value % 2 != 0)
{
// Not able to process odd numbers yet
numbersToProcess.Add(value);
}
}
};
numbersToProcess.Add(1);
Visual Basic
Private numbersToProcess As New ObservableCollection(Of Int32)()
Private processedNumbers As New ObservableCollection(Of Int32)()
numbersToProcess.Add(1)
The code simulates processing numbers as they are added to the collection by hooking to
the CollectionChanged event of numbersToProcess. As numbers are added to the list, they
are then added to processedNumbers to simulate the numbers being processed. The process
Numbers collection can process only numbers that are even; any odd numbers are added
back to the numbersToProcess collection so that they can be processed later. This causes the
CollectionChanged event to be raised on numbersToProcess again, and the operation hap-
pens over and over until a StackOverflowException exception is thrown. This example shows
you how changing collections in events can cause some very hard-to-find bugs when the
CollectionChanged event of processedNumbers or numbersToProcess triggers several other
events that eventually do the same as the preceding code.
The solution for the preceding code is to not change the collection within its change event.
For this simple scenario, you could solve it by checking to see whether numbersToProcess
contains the item you are adding before you add it. This solution, however, probably wouldn’t
work in a real-world scenario. See the “Handling Recursive Collection Change Events” section
later in this chapter to see what the ObservableCollection(T) class does in this case.
ObservableCollection(T) Overview
The ObservableCollection(T) class is a generic collection that implements INotifyCollection
Changed. The class is perfect for data binding in Windows Presentation Foundation (WPF) and
Microsoft Silverlight because it implements the INotifyCollectionChanged interface, unlike the
List(T) class you learn about in Chapter 4, “Generic Collections.” The INotifyCollectionChanged
interfaces notify the bound control, such as the ListBox, ListView, and TreeView, of changes
whenever the collection is modified.
More Info For information on how to use the List(T) class, see Chapter 4. For a brief overview
on lists or arrays, see Chapter 1, “Understanding Collections: Arrays and Linked Lists.”
594 Part IV Using Collections with UI Controls
C#
ObservableCollection<int> list = new ObservableCollection<int>();
list.Add(1);
list.Add(4);
list.Remove(1);
list.Clear();
Visual Basic
Sub OnCollectionChanged(ByVal sender As Object, ByVal e As NotifyCollectionChangedEventArgs)
Select Case (e.Action)
Case System.Collections.Specialized.NotifyCollectionChangedAction.Add
Console.WriteLine("{0} was added to the list", e.NewItems(0))
Case System.Collections.Specialized.NotifyCollectionChangedAction.Remove
Console.WriteLine("{0} was removed from the list", e.OldItems(0))
Case System.Collections.Specialized.NotifyCollectionChangedAction.Reset
Console.WriteLine("The list was cleared")
End Select
End Sub
list.Add(1)
list.Add(4)
list.Remove(1)
list.Clear()
Output
1 was added to the list
4 was added to the list
1 was removed from the list
The list was cleared
Chapter 11 Using Collections with WPF and Silverlight Controls 595
Note Unlike the NotificationList(T) that you created earlier in this chapter, the Observable
Collection(T) does not pass to you the items that were cleared in the CollectionChanged event
when you call the Clear method. That can present a problem if you need to unregister from
items that are being cleared.
C#
int m_recursive;
void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (CollectionChanged != null)
{
try
{
++m_recursive;
CollectionChanged(this, e);
}
finally
{
--m_recursive;
}
}
}
Visual Basic
Dim m_recursive As Int32
A check is then done before each list change operation such as Clear, Remove, RemoveAt,
and Add, to see if m_recursive is greater than 0. A value greater than 0 means that someone is
currently in the OnCollectionChanged event. The invocation list of CollectionChanged is also
checked to see if more than one delegate is registered to receive CollectionChanged events.
This presents an issue with the preceding coding example because only one delegate is reg-
istered to receive CollectionChanged events.
C#
void CheckForReentry()
{
if (m_recursive > 0 && CollectionChanged.GetInvocationList().Length > 1)
{
throw new InvalidOperationException();
}
}
Visual Basic
Sub CheckForReentry()
If (m_recursive > 0 And CollectionChanged.GetInvocationList().Length > 1) Then
Throw New InvalidOperationException()
End If
End Sub
Each collection change method checks for reentrance before doing any operations, as
follows.
Pseudocode
... Method(...)
{
CheckForReentry();
...
OnPropertyChanged("Count");
OnPropertyChanged("Item[]");
OnCollectionChanged(...);
}
C#
public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
{
if (m_properties == null)
{
// Only get the public properties marked with Browsable = true.
PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties
(typeof(T), new Attribute[] { new BrowsableAttribute(true) });
return m_properties;
}
Visual Basic
Public Function GetItemProperties(ByVal listAccessors As PropertyDescriptor()) As
PropertyDescriptorCollection Implements ITypedList.GetItemProperties
If (m_properties Is Nothing) Then
' Only get the public properties marked with Browsable = true.
Dim pdc As TypeDescriptor.GetProperties _
(GetType(T), New Attribute() {New BrowsableAttribute(True)})
598 Part IV Using Collections with UI Controls
Return m_properties
End Function
The ITypedList interface defines the properties to bind to when they differ from the public
properties of the bound object. For the collection class, you want the properties of the object
defined by T instead of WinFormsBindingList(T).
Later in the chapter, you will see an example using the ListCollectionView.
C#
ExampleComboBox.ItemsSouce = m_datasource;
ExampleComboBox.DisplayMemberPath = "Name";
ExampleComboBox.SelectedValuePath = "Id";
Chapter 11 Using Collections with WPF and Silverlight Controls 599
Visual Basic
ExampleComboBox.ItemsSouce = m_datasource
ExampleComboBox.DisplayMemberPath = "Name"
ExampleComboBox.SelectedValuePath = "Id"
However, the combo box does not receive dynamic updates unless the collection imple-
ments INotifyCollectionChanged. To have the combo box receive notifications, you use the
ObservableCollection(T) class, which implements INotifyCollectionChanged as well as the
NotificationList(T) you created earlier in this chapter.
The Update panel shows the currently selected item in the combo box. You update the select-
ed item by clicking the Update Item button. The combo box is automatically updated to reflect
the updated item. Because each item implements INotifyPropertyChanged, bound controls
receive notifications of property changes when items are updated with the following code.
C#
m_showing.Name = NameTextBox.Text;
m_showing.Website = WebsiteTextBox.Text;
Visual Basic
m_showing.Name = NameTextBox.Text
m_showing.Website = WebsiteTextBox.Text
You can use the Add panel to add new items to the combo box. You add items to the combo
box by clicking the Add Item button. The combo box is automatically updated to reflect the
newly added item. The code for adding an item is as follows.
C#
Company company = new Company();
company.Id = int.Parse(AddIdTextBox.Text);
company.Name = AddNameTextBox.Text;
company.Website = AddWebsiteTextBox.Text;
m_datasource.Add(company);
Visual Basic
Dim company As Company = New Company()
company.Id = Integer.Parse(AddIdTextBox.Text)
company.Name = AddNameTextBox.Text
company.Website = AddWebsiteTextBox.Text
m_datasource.Add(company)
600 Part IV Using Collections with UI Controls
You remove the selected item in the combo box by clicking the Remove Item button. The
following shows the code for removing an item.
C#
if (comboBox1.SelectedIndex >= 0)
{
m_datasource.RemoveAt(comboBox1.SelectedIndex);
}
Visual Basic
If (comboBox1.SelectedIndex >= 0) Then
m_datasource.RemoveAt(comboBox1.SelectedIndex)
End If
C#
ExampleListBox.ItemsSouce = m_datasource;
ExampleListBox.DisplayMemberPath = "Name";
ExampleListBox.SelectedValuePath = "Id";
Visual Basic
ExampleListBox.ItemsSouce = m_datasource
ExampleListBox.DisplayMemberPath = "Name"
ExampleListBox.SelectedValuePath = "Id"
However, the list box does not receive dynamic updates unless the collection imple-
ments INotifyCollectionChanged. To have the list box receive notifications, you use the
ObservableCollection(T) class, which implements INotifyCollectionChanged as well as the
NotificationList(T) you created earlier in this chapter.
The Update panel shows the currently selected item in the list box. You update the selected
item by clicking the Update Item button. The list box is automatically updated to reflect the
updated item. Because each item implements INotifyPropertyChanged, bound controls receive
notifications of property changes when items are updated with the following code.
C#
m_showing.Name = NameTextBox.Text;
m_showing.Website = WebsiteTextBox.Text;
Visual Basic
m_showing.Name = NameTextBox.Text
m_showing.Website = WebsiteTextBox.Text
Chapter 11 Using Collections with WPF and Silverlight Controls 601
You can use the Add panel to add new items to the list box. You add items to the list box
by clicking the Add Item button. The list box is automatically updated to reflect the newly
added item. The code for adding an item is as follows.
C#
Company company = new Company();
company.Id = int.Parse(AddIdTextBox.Text);
company.Name = AddNameTextBox.Text;
company.Website = AddWebsiteTextBox.Text;
m_datasource.Add(company);
Visual Basic
Dim company As Company = New Company()
company.Id = Integer.Parse(AddIdTextBox.Text)
company.Name = AddNameTextBox.Text
company.Website = AddWebsiteTextBox.Text
m_datasource.Add(company)
You remove the selected item in the list box by clicking the Remove Item button. The follow-
ing shows the code for removing an item.
C#
if (comboBox1.SelectedIndex >= 0)
{
m_datasource.RemoveAt(listBox1.SelectedIndex);
}
Visual Basic
If (comboBox1.SelectedIndex >= 0) Then
m_datasource.RemoveAt(listBox1.SelectedIndex)
End If
C#
ExampleListView.ItemsSouce = m_datasource;
ExampleListView.DisplayMemberPath = "Name";
ExampleListView.SelectedValuePath = "Id";
602 Part IV Using Collections with UI Controls
Visual Basic
ExampleListView.ItemsSouce = m_datasource
ExampleListView.DisplayMemberPath = "Name"
ExampleListView.SelectedValuePath = "Id"
However, the list view does not receive dynamic updates unless the collection imple-
ments INotifyCollectionChanged. To have the list view receive notifications, you use the
ObservableCollection(T) class, which implements INotifyCollectionChanged as well as the
NotificationList(T) you created earlier in this chapter.
The Update panel shows the currently selected item in the list view. You update the selected
item by clicking the Update Item button. The list view is automatically updated to reflect the
updated item. Because each item implements INotifyPropertyChanged, bound controls receive
notifications of property changes when items are updated with the following code.
C#
m_showing.Name = NameTextBox.Text;
m_showing.Website = WebsiteTextBox.Text;
Visual Basic
m_showing.Name = NameTextBox.Text
m_showing.Website = WebsiteTextBox.Text
You can use the Add panel to add new items to the list view. You add items to the list view
by clicking the Add Item button. The list view is automatically updated to reflect the newly
added item. The code for adding an item is as follows.
C#
Company company = new Company();
company.Id = int.Parse(AddIdTextBox.Text);
company.Name = AddNameTextBox.Text;
company.Website = AddWebsiteTextBox.Text;
m_datasource.Add(company);
Visual Basic
Dim company As Company = New Company()
company.Id = Integer.Parse(AddIdTextBox.Text)
company.Name = AddNameTextBox.Text
company.Website = AddWebsiteTextBox.Text
m_datasource.Add(company)
Chapter 11 Using Collections with WPF and Silverlight Controls 603
You remove the selected item in the list view by clicking the Remove Item button. The fol-
lowing shows the code for removing an item.
C#
if (comboBox1.SelectedIndex >= 0)
{
m_datasource.RemoveAt(listBox1.SelectedIndex);
}
Visual Basic
If (comboBox1.SelectedIndex >= 0) Then
m_datasource.RemoveAt(listBox1.SelectedIndex)
End If
C#
ExampleTreeBox.ItemsSouce = m_datasource;
ExampleTreeBox.DisplayMemberPath = "Name";
ExampleTreeBox.SelectedValuePath = "Id";
Visual Basic
ExampleTreeBox.ItemsSouce = m_datasource
ExampleTreeBox.DisplayMemberPath = "Name"
ExampleTreeBox.SelectedValuePath = "Id"
However, the tree view does not receive dynamic updates unless the collection imple-
ments INotifyCollectionChanged. To have the tree view receive notifications, you use the
ObservableCollection(T) class, which implements INotifyCollectionChanged as well as the
NotificationList(T) you created earlier in this chapter.
The tree view control is capable of showing hierarchical data. Each node in the tree view can
be displayed using other controls, such as a TextBlock, Button, and so on. The children of
the nodes and the controls used to display them can be defined using the HierarchicalData
Template class. The TreeViewBinding window uses a custom node class called TreeViewNode
for each of the nodes. Each child node is exposed through the Nodes property of Tree
ViewNode as a NotificationList(TreeViewNode). The NotificationList(T) class is used instead
of ObservableCollection(T). The NotificationList(T) class lets you receive the items that were
cleared when the Clear method is called. The Text property of the TreeViewNode stores the
text of each node. The following code shows how the tree view is mapped to the custom
TreeViewNode class.
604 Part IV Using Collections with UI Controls
XAML
<TreeView x:Name="ExampleTreeView">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type src:TreeViewNode}"
ItemsSource="{Binding Path=Nodes}">
<TextBlock Text="{Binding Path=Text}" />
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
More Info You can find more information about using the HierarchicalDataTemplate on MSDN
at http://msdn.microsoft.com/en-us/library/system.windows.hierarchicaldatatemplate.aspx.
With the Update button, you can change the text of the currently selected node with the
contents in the Text text box. The tree view is automatically updated to reflect the updated
item. Because each item implements INotifyPropertyChanged, bound controls receive notifi-
cations of property changes when items are updated with the following code.
C#
TreeViewNode node = (TreeViewNode)ExampleTreeView.SelectedItem;
node.Text = TextTextBox.Text;
Visual Basic
Dim node as TreeViewNode = CType(ExampleTreeView.SelectedItem, TreeViewNode)
node.Text = TextTextBox.Text
You can use the Add button to add a new child node to the currently selected node. The tree
view is then automatically updated to reflect the newly added item. The code for adding an
item is as follows.
C#
TreeViewNode node = (TreeViewNode)ExampleTreeView.SelectedItem;
if (node == null)
{
m_nodes.Add(new TreeViewNode(TextTextBox.Text));
}
else
{
node.Nodes.Add(new TreeViewNode(TextTextBox.Text));
}
Visual Basic
Dim node as TreeViewNode = CType(ExampleTreeView.SelectedItem, TreeViewNode)
The newly created node is added to the root if no nodes are currently selected.
You remove the selected item in the TreeView by clicking the Remove button. The following
shows the code for removing an item.
C#
TreeViewNode node = (TreeViewNode)ExampleTreeView.SelectedItem;
if (node.Parent != null)
{
node.Parent.Nodes.Remove(node);
}
else
{
m_nodes.Remove(node);
}
Visual Basic
Dim node As TreeViewNode = CType(ExampleTreeView.SelectedItem, TreeViewNode)
C#
ExampleListView1.ItemsSource = new ListCollectionView(DL.GetDataSource());
ExampleListView2.ItemsSource = new ListCollectionView(DL.GetDataSource());
ExampleListView3.ItemsSource = new BindingListCollectionView(DL.GetDataSourceBL());
Visual Basic
ExampleListView1.ItemsSource = new ListCollectionView(DL.GetDataSource())
ExampleListView2.ItemsSource = new ListCollectionView(DL.GetDataSource())
ExampleListView3.ItemsSource = new BindingListCollectionView(DL.GetDataSourceBL())
Once bound, ListCollectionView can be filtered using the filtering controls present on the
window. The following code shows how the views are filtered when the user clicks the filter
button.
606 Part IV Using Collections with UI Controls
C#
private void FilterButton_Click(object sender, RoutedEventArgs e)
{
ICollectionView view = null;
switch (Convert.ToInt32(cbi.Content))
{
case 1:
view = (ICollectionView)ExampleListView1.ItemsSource;
break;
case 2:
view = (ICollectionView)ExampleListView2.ItemsSource;
break;
}
if (!view.CanFilter)
{
return;
}
view.Filter = null;
PropertyDescriptor pd = (PropertyDescriptor)FilterProperty.SelectedItem;
string text = FilterText.Text;
cbi = (ComboBoxItem)FilterOperator.SelectedItem;
switch (cbi.Content.ToString())
{
case ">":
view.Filter = item => { return Comparer.Default.Compare(pd.GetValue(item),
pd.Converter.ConvertFromString(text)) > 0 ; };
break;
case ">=":
view.Filter = item => { return Comparer.Default.Compare(pd.GetValue(item),
pd.Converter.ConvertFromString(text)) >= 0; };
break;
case "=":
view.Filter = item => { return Comparer.Default.Compare(pd.GetValue(item),
pd.Converter.ConvertFromString(text)) == 0; };
break;
case "<":
view.Filter = item => { return Comparer.Default.Compare(pd.GetValue(item),
pd.Converter.ConvertFromString(text)) < 0; };
break;
case "<=":
view.Filter = item => { return Comparer.Default.Compare(pd.GetValue(item),
pd.Converter.ConvertFromString(text)) <= 0; };
break;
}
}
Chapter 11 Using Collections with WPF and Silverlight Controls 607
Visual Basic
Private Sub FilterButton_Click(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
Dim view As ICollectionView = Nothing
view.Filter = Nothing
C#
void GridViewColumnHeaderClickedHandler(object sender, RoutedEventArgs e)
{
GridViewColumnHeader headerClicked = e.OriginalSource as GridViewColumnHeader;
ListView lv = (ListView)sender;
ICollectionView view = (ICollectionView)lv.ItemsSource;
if (!view.CanSort)
{
return;
}
if (headerClicked != null)
{
if (headerClicked.Role != GridViewColumnHeaderRole.Padding)
{
string header = headerClicked.Column.Header as string;
if (view.SortDescriptions.Count > 0)
{
ListSortDirection direction = ListSortDirection.Ascending;
view.SortDescriptions.Clear();
view.SortDescriptions.Add(new SortDescription(header, direction));
}
else
{
view.SortDescriptions.Add(new SortDescription(header,
ListSortDirection.Ascending));
}
}
}
}
Chapter 11 Using Collections with WPF and Silverlight Controls 609
Visual Basic
Sub GridViewColumnHeaderClickedHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim headerClicked As TryCast(e.OriginalSource, GridViewColumnHeader)
Dim lv As ListView = CType(sender, ListView)
Dim view As ICollectionView = CType(lv.ItemsSource, ICollectionView)
view.SortDescriptions.Clear()
view.SortDescriptions.Add(New SortDescription(header, direction))
Else
view.SortDescriptions.Add(New SortDescription _
(header, ListSortDirection.Ascending))
End If
End If
End If
End Sub
Summary
In this chapter, you saw how to use collections with controls in WPF and Silverlight. The built-
in notification interfaces for WPF and Silverlight can speed up productivity by implementing
common operations that are done between collections and controls. These operations allow
the controls to display collection changes and enable your collections to interact with the users.
Index
A providing synchronization support to, 510
serializing, 522–524
Action(T) method, 225–228 using, 23–26
AddAfter method arrays
doubly linked lists, 60, 64 advantages of, 4
linked lists, 273, 274 allowing users to add items, 10–12
singly linked lists, 34–35 allowing users to remove items, 13–17
AddBefore method Array.IndexOf method, 19
doubly linked lists, 60, 64 array index value, 20
linked lists, 273, 275 ArrayList class, 4, 6
singly linked lists, 34–36 associative. See associative arrays
AddFirst method (linked lists), 273, 275 converting array elements to strings, 82–83
AddIndex method, 559 creating simple, 5–8
AddLast method (linked lists), 273, 276 defined, 3
Add method disadvantages of, 4
arrays and, 10 dynamic, 4, 222
association lists, 92 elements in, 5
calling in BindingSource, 575 helper function to copy contents of, 22
List(T) class, 223–224 helper methods and properties, adding, 17–22
to associate items with keys, 297 implementing queues with, 154–155
for users to associate values with keys, 123 indexing, 5
AddNew method, 541 performing selection sorts on, 25
AddRange method of references, 3
adding to Synchronized Wrapper class, 486 removing items from, 4
List(T) class, 223–224 uses of, 3–4
Add(TKey,TValue,bool) method, 93–94, 124, 126 using to create linked lists, 81
AddToBeginning method using to implement stacks. See stacks
doubly linked lists, 60 of values, 3
singly linked lists, 34 writing sorted lists to console, 26
AddToEnd method Associate method, 111
doubly linked lists, 61 association lists
singly linked lists, 35 advantages/disadvantages of, 88
AgeComparison method (sorting lists), 239 allowing users to associate values with keys, 92–94
algorithms, Quicksort, 232 allowing users to remove keys, 95–98
allOccurrences flag, 17 AssociativeArrayAL(T) class, creating, 89–92
AllowEdit property, 541 helping methods and properties, adding, 98–104
AllowNew property, 541 AssociativeArrayAL(TKey,TValue) class
AllowRemove property, 541 adding collection support to, 417–422
And method (BitArray class), 313–314 adding Key/Value pair support to, 436–440
AND operations, 431 enumeration support for, 385–391
anonymous methods, defined, 219 AssociativeArrayHT(T) class
anonymous types, 452, 457–458 adding fields to, 118
appending items to List(T) class, 223–224 constructors for, 122–123
ApplyFilter method, 566 creating, 119–122
ApplySort method, 553, 568, 573–574 AssociativeArrayHT(TKey,TValue) class
ArrayDebugView class, 8 adding collection support to, 422–427
ArrayEx(T) class adding Key/Value pair support to, 437–440
adding IList(T)/IList support to, 429–433 enumeration support for, 391–396
collection support for, 398–400 associative arrays
definition, changing, 521–522 advantages/disadvantages of, 88
enumeration support for, 349–354 association lists for. See association lists
generic class, 6 AssociativeArrayAL(T) class, creating, 89–92
611
612 associative arrays
LinkedList(T) class
adding nodes to linked lists, 273–276
M
creating new linked lists, 273 Main method (arrays), 26
obtaining information about linked lists, 279–281 mapping values to keys. See hash tables for associative
overview, 272 arrays
removing nodes from, 277–280 m_array field, 352, 376
using, 273 Max method (LINQ queries), 448
LINQ (Language Integrated Query) m_buffer field
additions to .NET language for, 451–453 CircularBuffer(T) class, 357
basic query actions, 442 QueuedArray(T) class, 369
data sources used with, 442–443 m_capacity field, 118
filtering results (where clause), 458–459 m_comparer field (association lists), 90
group clause, 461–463 m_count field, 8, 32, 59, 118
join clause, 463–466 m_current field, 363
let clause, 466–468 m_currentNode field, 387
nesting statements with, 442 m_data field
ordering results (orderby clause), 460–461 DoubleLinkedList(T) class, 183
picking data source and selecting results, 453–458 internal data storage with (queues), 155
query execution with, 443–449 Node class, 57
querying data with and without (examples), 441–442 QueuedLinkedList(T) class, 164
structure for (examples), 449–450 sorting, 554
ListBox control StackedArray(T) class, 178
binding with, 577–578, 600–601 m_end field, 363
setting DisplayMember property of, 539–540 methods
ListChanged event, 543, 552, 584 adding helper methods to doubly linked lists, 76–81
ListChangeType.ItemChange notification, 552 anonymous, 219
ListCollectionView, 598, 605 helper methods, adding to arrays, 17–22
lists helper methods, adding to singly linked lists, 49–54
association lists. See association lists to reverse order of array contents, 80
doubly linked. See doubly linked lists m_head field, 32, 59
list manipulation support (IBindingList interface), Microsoft .NET Framework 32-bit/64-bit applications, 3
541–543 Microsoft Visual Basic project, 6
singly linked. See singly linked lists Microsoft Visual C# class library project, 6
List(T) classes m_index field, 352, 357
appending items to, 223–224 m_list field, 363, 381
checking list contents, 263–269 m_newIndex field, 542, 543
creating, 222–223 m_next field, 31, 57
inserting items into, 230–231 modifying lists, 269–272
List(T)(IEnumerable(T) collection), 223 modulus (mod) operation, 196, 267
List(T)(int size), 223 modulus operator, 112, 403
ListToString method, 232 Monitor class, 478, 487–490
modifying lists, 269–272 MoveNext method
overview, 222 ArrayEx(T) class, 348, 353–354
removing items from, 228–230 AssociativeArrayAL(TKey,TValue) class, 389, 390
searching. See searching List(T) classes AssociativeArrayHT(TKey,TValue) class, 395
sorting. See sorting List(T) classes CircularBuffer(T) class, 359
traversing, 224–228 DoubleLinkedList(T) class, 365–366
List(T) FindAll(Predicate(T) match) method, 257–258 QueuedArray(T) class, 371
List(TOutput) ConvertAll(Converter(T, TOutput) StackedArray(T) class, 378
converter), 269–271 StackedLinkedList(T) class, 383
ListView control, binding with, 601–602 traversing collections with, 139
locking mechanisms (threads), 474–475, 478–480, 487 m_owner field, 31, 57
LockRecursionPolicy.NoRecursion, 500 m_prev field, 57
LockRecursionPolicy.SupportRecursion, 500 m_syncRoot field, marking as nonserializable, 528, 529
locks, upgradeable, 500–503 m_tail field
doubly linked lists, 64
DoublyLinkedList(T) class, 59
to eliminate traversing complete lists, 37
maintaining node references with, 32
Prev property (doubly linked lists) 619
multiline lambda statements, 221 ObservableCollection(T) class
m_updateCode field overview, 593
association lists and, 90 recursive collection change events, 595–596
AssociativeArrayHT(TKey,TValue) class, 393 using, 593–594
CircularBuffer(T) class, 357 oddInts variable (LINQ), 448
to determine changes in collections, 352 OnClear/OnClearComplete methods
DoubleLinkedList(T) class and, 59, 363 CollectionBase class, 318–319
QueuedArray(T) class, 369 DictionaryBase class, 325
QueuedLinkedList(T) class, 164 OnGet method (DictionaryBase class), 325
queues, implementing with arrays, 155 OnInsert/OnInsertComplete methods
StackedArray(T) class, 178, 183 CollectionBase class, 319–323
StackedLinkedList(T) class, 381 DictionaryBase class, 325–326
storing value of before enumeration, 511 notification support and, 543
user list modification and, 8 OnListChanged method, 552
Mutex class, 478 O(n) operations, 222
MyStringComparer.GetHashCode method, 116 OnRemove/OnRemoveComplete methods
CollectionBase class, 321–322
DictionaryBase class, 326–327
N notification support and, 543
NameToIndex method, 107, 109 OnSet/OnSetComplete methods
N elements in arrays, 5 CollectionBase class, 323–324
nested locks, 485 DictionaryBase class, 327–328
.NET collections, using as LINQ data source, 443 OnValidate method
.NET Framework CollectionBase class, 317–318
ArrayList class in, 6 DictionaryBase class, 324–325
hash codes and different versions of, 112 OnXXX/OnXXXComplete methods, 543, 584
tools for thread synchronization, 475–479 operators, modulus, 112
.NET language, LINQ additions to, 451–453 orderby clauses (LINQ), 460–461
Next field, 118 ordering comparer, 218–219
Next method, 181 Or method (BitArray class), 314–315
NextOperation field, 188, 190 outer joins, 465
next pointers (linked lists), 27 Overlaps method (HashSet(T)) class, 336–337
Next property
doubly linked lists, 60–61
nodes and, 34, 42 P
Node class, creating Peek method
in doubly linked lists, 54–57 QueuedLinkedList(T) class, 167
in singly linked lists, 28–30 Queue(T) class, 286
nodes StackedArray(T) class, 181, 185–186
in doubly linked lists, 54, 68–69 to view contents of queues, 159
removing, 48 to view items at top of stacks, 291
removing from LinkedList(T) class, 277–280 performance penalties, 9
non-exclusive locks (thread synchronization), 480–481 PersonAgeComparer class (list sorting), 241
NonSerializable attribute, 513, 516, 521 PersonComparer class (searching lists), 259–260
NotificationList(T) class, 584 PersonNameComparer class (sorting lists), 244
notification support (IBindingList interface), 543–552 Person struct, 236
Not method (BitArray class), 313 Person structure defined in Sort(Comparison(T))
NULL_REFERENCE constant, 118 method, 240
null value, 50, 77 Plate class, defining (stacks example), 187–188
Pop method
QueuedLinkedList(T) class and, 166
O removing items from stacks with, 290–291
Object.Equals(Object) method, 215, 218 removing items from queues with, 158
Object.GetHashCode method, 112 StackedArray(T) class and, 185
objects post-fix Attribute, 513
filtering list of, 459 Predicate(T) method, 247, 251, 257
initializing during creation, 452–453 previous pointers (linked lists), 27
Prev property (doubly linked lists), 61–62
620 prime numbers
StackedLinkedList(T) class
adding collection support to, 415–418
T
enumeration support for, 379–384 Tail method, 52, 78
stacks Tail property, 42, 49–50, 61
adding common functionality, 175–176 T Dequeue() method, 286–287
allowing users to add items to StackedArray(T) T Find(Predicate(T) match)/T FindLast(Predicate(T)
class, 179, 184 match) methods, 247–252
allowing users to remove items from StackedArray(T) This keyword in C#, 20
class, 180, 185 threads
cafeteria plate simulation (example). See cafeteria basics, 469
plate simulation (example) Monitor class, using, 487–490
constructors, creating in StackedArray(T) class, ReaderWriterLockSlim class. See ReaderWriterLockSlim
178–179, 183–184 class
helper methods, adding to StackedArray(T) thread synchronization
class, 180–182, 185–187 defined, 470
serializing Stack classes, 533–534 .NET Framework tools for, 475–479
StackedLinkedList(T) class, 182–183 supporting collection classes with, 480–482
uses of, 175 Synchronized Wrapper class, implementing, 504–510
Stack(T) class Synchronized Wrapper class vs. SyncRoot, 483–486
adding items to stacks, 289 value of (example), 470–473
checking stacks, 291–292 writing code as needed, 474–475
cleaning stacks, 292 ToArray method (LINQ queries), 446
creating stacks, 288–289 ToList method (LINQ queries), 446
in .NET Framework, 175 ToString method, 8
overview, 288 T Peek() method, 286, 291–292
removing items from stacks, 290–291 T Pop() method, 290
start/end pointers (circular buffers), 195, 207 traversing List(T) class, 224–228
State class, 493–495 TreeView control, binding with, 603–605
storage, internal (circular buffers), 195–198 TrimExcess method
streams, serialization and, 516 to change capacity of stacks, 290, 292
strings HashSet(T) class, 338
convert elements of arrays to, 24–25 to modify capacity of lists, 228
converting array elements to, 82–83 to reclaim memory (Queue(T) class), 287
structures as value types, 217 Trim method, 269
subexpressions, storing with let clauses, 466 TrueForAll method, 267–269
SumArray method, 470–473, 490–493, 497 TryEnter method, 479, 488–489
SupportsAdvancedSorting property, 568 TryEnterReadLock method, 496
SupportsChangeNotification flag, 552 TryGetValue method, 104, 146, 299–300
SupportsChangeNotification property, 543 TValue Item[TKey key] { get; }, 300–301
SupportsFiltering property, 563 TValue Item[TKey key] { set; }, 297
SupportsSearching property, 557 two-way data binding
SupportsSorting property, 553 IBindingList interface, implementing. See IBindingList
synchronization, thread. See thread synchronization interface
synchronized collection classes, 511–512 overview, 540
SynchronizedCollection(T) class, 511
SynchronizedKeyedCollection(T) class, 512
SynchronizedReadOnlyCollection(T) class, 512 U
Synchronized Wrapper class, 504–510 UnionWith method (HashSet(T) class), 337
SyncLock statement, 478 UnpackPhoneNumber method, 271
SyncRoot method, 399 Update button (TreeView control), 604
SyncRoot property, 481–483, 483–486 Update Item button
System.Collections.Generic namespace, 215, 218 binding with ListView control, 602
System.Collections.IEqualityComparer interface, 112 ComboBox control, 576
system requirements for exercises, xvi upgradeable locks, using, 500–503
Windows Forms 623
users void Insert(int index, T item), 231
allowing users to add items (StackedArray(T) void InsertRange(int index, IEnumerable collection), 231
class), 179 void IntersectWith(IEnumerable(T) other), 333
allowing users to add items to arrays, 10–12 void OnClear() and void OnClearComplete(), 318
allowing users to add items to CircularBuffer(T) void OnGet(), 325
class, 201–203 void OnInsertComplete(Object key, Object value), 325
allowing users to add items to collections, 34–42 void OnInsert(int index, Object value), 319
allowing users to add items to doubly linked lists, void OnInsert(Object key, Object value), 325
60–68 void OnRemoveComplete(Object key, Object val-
allowing users to add items to QueuedLinkedList(T) ue), 326
class, 165 void OnRemove(Object key, Object value), 326
allowing users to add items to queues, 157–158 void OnSetComplete(Object key, object oldValue, object
allowing users to add items to StackedArray(T) newValue), 327
class, 184 void OnSet(Object key, object oldValue, object
allowing users to associate values with keys, 92–94, newValue), 327
123–130 void OnValidate(object key, object value), 324–325
allowing users to remove items from arrays, 13–17 void OnValidate(object value), 317
allowing users to remove items from CircularBuffer(T) void Push(T item), 289
class, 203–204 void RemoveAt(int index), 230
allowing users to remove items from collections, void RemoveFirst(), 278
42–49 void RemoveLast(), 278–279
allowing users to remove items from doubly linked void RemoveRange(int index, int count), 230
lists, 68–75 void Reverse(), 272
allowing users to remove items from void Reverse(int index, int count), 272
QueuedLinkedList(T) class, 166–167 void SetAll(bool value), 309
allowing users to remove items from queues, 158–159 void Set(int index, bool value), 308
allowing users to remove items from StackedArray(T) void Sort(), 236
class, 180, 185 void Sort(Comparison(T) comparison), 236–239
allowing users to remove keys, 95–98, 130–134 void Sort(IComparer(T) comparer), 240–241
void Sort(int index, int count, IComparer(T)
comparer), 242–246
V void TrimExcess(), 272, 288, 338
values void UnionWith(IEnumerable(T) other), 337
arrays of, 3
ValueCollection class, 437
ValueMember property (ComboBox control), 540–541 W
Values property, 99, 137, 305–306 WaitAll method, 477
variable Operation using named method WaitAny method, 477
(example), 219–220 WaitHandle class, 476, 477
var keyword, 451 Wait method, 479
Visual Basic (Microsoft) websites, for further information
implementing Item property with, 20 Dispose method, 352
lambda expressions in, 221 downloading code samples, xvii
var keyword in, 451 HierarchicalDataTemplate, 603
Visual C# (Microsoft), var keyword in, 451 LINQ data sources, 442
void AddAfter(LinkedListNode(T) node, where clause (LINQ), 458–459
LinkedListNode(T) newNode), 273 Windows Forms
void AddBefore(LinkedListNode(T) node, binding with BindingSource object, 581–582
LinkedListNode(T) newNode), 274 BindingList(T) class, using, 574
void AddFirst(LinkedListNode(T) node), 275–276 BindingSource class, 575–576
void AddLast (LinkedListNode(T) node), 276 binding with ComboBox control, 576–577
void AddRange(IEnumerable(T) collection), 224 binding with DataGridView control/IBindingList-
void Add(T item) (List(T) class), 223–224 View, 580
void Add(TKey key, TValue value), 297 binding with DataGridView/IBindingList control,
void Clear(), 285, 290, 298, 331 578–580
void Enqueue(T item), 285 binding with IBindingList, 578–580
void ExceptWith(IEnumerable(T) other), 337–338 binding with ListBox control, 577–578
void ForEach(Action(T) action), 225–228
624 Windows Forms
X
XmlSerializer, 513
Xor method (BitArray class), 314–315
About the Author
Calvin Janes is currently a Principal Software Consultant with over 20 years of professional
experience. He has been hooked on programming ever since he wrote a 1,000-line BASIC
program in third grade. Since then, he has worked on everything from 3D graphics to devel-
oping software for helicopters and control systems. When not working, he enjoys tinkering
with microcontrollers and robotics, and exploring how they can be applied to the medical
field. He plays several instruments and received an Emergency Medical Technician (EMT-B)
certification while working as a volunteer firefighter.