In This Chapter
• Understanding Collections Architecture
• Working with Nongeneric Collections
• Working with Generic Collections
Applications often require working with data. Until now you got information on how to store data within classes and structures, but it is common to need to create groups of data. In .NET development, this is accomplished using special classes known as collections, which enable storing a set of objects within one class. The .NET Framework provides a lot of collections, each one specific to a particular need or scenario. The main distinction is between nongeneric collections and generic collections, but all of them can store a set of objects. Collections are not merely groups of data. They are the backend infrastructure for engines such as the ADO.NET Entity Framework and LINQ, so understanding collections is an important task. At a higher level, collections’ infrastructure is offered by the System.Collections
namespace and by some interfaces that bring polymorphism to collections. After you have stored data in collections, it is not uncommon for you to analyze the content of such collections; this task is often performed via (but not limited to) For..Each
or For..Next
loops. Continuing the goal of helping developers write better and more responsive code, the .NET Framework 4.5 brings to Visual Basic a feature called iterators. Iterators make loops more responsive through a background asynchrony. In this chapter you learn about the .NET Framework’s collections, both nongeneric and generic ones. You also learn how to create and consume custom collections, and you get an overview of concurrent collections. Finally, you learn about iterators in Visual Basic 2012, an important new addition to the language.
Collections are special classes that can store sets of objects; the .NET Framework offers both nongeneric and generic collections. Whatever kind of collection you work on, collections implement some interfaces. The first one is ICollection
that derives from IEnumerable
and provides both the enumerator (which enables For..Each
iterations) and special members, such as Count
(which returns the number of items within a collection) and CopyTo
(which copies a collection to an array). Collections also implement IList
or IDictionary
; both inherit from ICollection
and expose members that enable adding, editing, and removing items from a collection. The difference is that IDictionary
works with key/value pairs instead of single objects as IList
. The previously mentioned interfaces are about nongeneric collections. If you work with generic collections, the collections implement the generic counterpart of those interfaces such as ICollection(Of T)
, IList(Of T),
and IDictionary(Of TKey, TValue)
. It’s important to understand this implementation because this provides similarities between collections so that you can learn members from one collection and be able to reuse them against other kinds of collections.
Use the Object Browser
Remember that the Object Browser tool can provide thorough information on objects’ architecture, meaning that if you don’t remember which members are exposed by a collection, this tool can be your best friend.
The System.Collections
namespace defines several nongeneric collections. The namespace also defines interfaces mentioned in the previous section. Here you learn to work with nongeneric collections so that in the next section you can easily understand why generic ones are more efficient.
System.Collections.ArrayList
is the most common nongeneric collection in the .NET Framework and represents an ordered set of items. By ordered, we mean that you add items to the collection one after the other, which is different from sorting. ArrayList
collections can store any .NET type because, at a higher level, it accepts items of type Object
. The following code shows how you can create a new ArrayList
and can set the number of items it can contain by setting the Capacity
property:
Dim mixedCollection As New ArrayList
mixedCollection.Capacity = 10
Adding new items to an ArrayList
is a simple task. To do this, you invoke the Add
method that receives as an argument the object you want to add, as demonstrated by the following snippet:
mixedCollection.Add(35)
mixedCollection.Add("35")
mixedCollection.Add("Alessandro")
mixedCollection.Add(Date.Today)
The preceding code adds an Integer
, two Strings
, and a Date
. Of course, you can also add composite custom types. Always be careful when using an ArrayLists
. The first two items’ value is 35, but the first one is an Integer
whereas the second is a String
. If you want to compare such items or iterate them, you should explicitly convert from String
to Integer,
or vice versa. This would require boxing/unboxing operations. You work with collections of a single type, so I suggest you avoid nongeneric collections in favor of generic ones. For example, if you have to work with a set of strings, you should use the List(Of String)
. The second part of this chapter discusses generic collections.
Why You Should Prefer Generics
The second part of this chapter is about generic collections. You should use this kind of collection for two reasons; the first is that generic collections are strongly typed and prevent the possibility of errors that can be caused when working with items of type Object
. The second reason is that each time you add a value type to a nongeneric collection, the type is first subject to boxing (that was discussed in Chapter 4, “Data Types and Expressions”), which can cause performance overhead and does not provide the best object management.
You can also add a block of items in a single invocation using the AddRange
method. The following code shows how you can add an array of strings to the existing collection:
Dim anArray() As String = {"First", "Second", "Third"}
mixedCollection.AddRange(anArray)
Add
and AddRange
add items after the last existing item. You might want to add items at a specified position. This can be accomplished via the Insert
and InsertRange
methods whose first argument is the position; the second one is the object. The following code demonstrates this:
mixedCollection.Insert(3, "New item")
mixedCollection.InsertRange(3, anArray)
You can remove items from the collection using one of two methods. Remove
enables you to remove a specific object, and RemoveAt
enables you to remove the object at the specified position:
'Removes the string "35"
mixedCollection.RemoveAt(1)
'Removes 35
mixedCollection.Remove(35)
One interesting method is TrimToSize
; it enables you to resize the collection based on the number of items effectively stored. For example, consider the following code:
Dim mixedCollection As New ArrayList
mixedCollection.Capacity = 10
mixedCollection.Add(32)
mixedCollection.Add("32")
mixedCollection.Add("Alessandro")
mixedCollection.Add(Date.Today)
mixedCollection.TrimToSize()
When created, the ArrayList
can store up to 10 items. After the TrimToSize
invocation, Capacity
’s value is 4, which is the number of items effectively stored. If you have a large collection, the ArrayList
provides a BinarySearch
method that enables searching the collection for a specified item, returning the index of the item itself:
'Returns 2
Dim index As Integer = mixedCollection.BinarySearch("Alessandro")
If the item is not found within the collection, the return value is a negative number. If the collection contains more than one item matching the search criteria, BinarySearch
returns only one item—which is not necessarily the first one. This collection also exposes other interesting members that are summarized in Table 16.1.
You can access an item from the ArrayList
using the index as follows:
Dim anItem As Object = mixedCollection(0)
You need to perform a conversion into the appropriate type, which is something you can accomplish inline if you already know the type:
Dim anItem As Integer = CInt(mixedCollection(0))
If the conversion fails, an InvalidCastException
is thrown. The ArrayList
implements the IList
interface and thus can take advantage of the enumerator; therefore, you can iterate it via a For..Each
loop as follows:
For Each item As Object In mixedCollection
Console.WriteLine(item.ToString)
Next
You just need to pay attention to the fact that each item is treated as an Object
, so you must be aware of conversions. The ArrayList
collection provides members that you find in other kinds of collections. This is the reason the most common members are discussed here.
The System.Collections.Queue
collection works according to the FIFO (First-In, First-Out) paradigm, meaning that the first item you add to the collection is the first pulled out of the collection. Queue
exposes two methods: Enqueue
adds a new item to the collection, and Dequeue
removes an item from the collection. Both methods receive an argument of type Object
. The following code provides an example:
Sub QueueDemo()
Dim q As New Queue
q.Enqueue(1)
q.Enqueue(2)
'Returns
'1
'2
Console.WriteLine(q.Dequeue)
Console.WriteLine(q.Dequeue)
End Sub
You just need to invoke the Dequeue
method to consume and automatically remove an item from the collection. You can also invoke the Peek
method, which returns the first item from the collection without removing it. Be careful when adding items to a queue because you are working in a fashion that is not strongly typed. If you plan to work with objects of a specified type (for example, you need a collection of Integer
), consider using a Queue(Of T)
that behaves the same way except that it is strongly typed. The collection also exposes a Count
property that returns the number of items in the collection. The constructor provides an overload that enables specifying the capacity for the collection.
The System.Collections.Stack
collection mimics the same-named memory area and works according to the LIFO (Last-In, First-Out) paradigm meaning that the last item you add to the collection is the first that is pulled out from the collection. Stack
exposes three important methods: Push
adds an item to the collection, Pop
enables the consuming of and removing of an item from the collection, and Peek
returns the top item in the collection without removing it. The following is an example:
Dim s As New Stack
s.Push(1)
s.Push(2)
'Returns 2 and leaves it in the collection
Console.WriteLine(s.Peek)
'Returns 2 and removes it
Console.WriteLine(s.Pop)
'Returns 1 and removes it
Console.WriteLine(s.Pop)
As for Queue
, here you work with Object
items. Although this enables pushing different kinds of objects to the collection, it is not a good idea. You should use the generic counterpart (Stack(Of T)
) that enables you to work with a single type in a strongly typed fashion. The Stack
collection also exposes the Count
property, which returns the number of objects that it stores.
The System.Collections.HashTable
collection can store items according to a key/value pair, where both key and value are of type Object
. The HashTable
peculiarity is that its items are organized based on the hash code of the key. The following code provides an example:
Dim ht As New Hashtable
ht.Add("Alessandro", "Del Sole")
ht.Add("A string", 35)
ht.Add(3.14, New Person)
'Number of items
Console.WriteLine(ht.Count)
'Removes an item based on the key
ht.Remove("Alessandro")
Items within a HashTable
can be accessed by the key, as shown in the last line of code in the preceding example. It also offers two methods named ContainsKey
and ContainsValue
that check whether a key or a value exists within the collection, as demonstrated here:
'Checks if a key/value exists
Dim checkKey As Boolean = ht.ContainsKey("A string")
Dim checkValue As Boolean = ht.ContainsValue(32)
Note that if you do not check whether a key already exists, as in the preceding example, and attempt to add a key that already exists, then an ArgumentException
is thrown. This is because keys must be unique. A single item within the collection is of type DictionaryEntry
that exposed two properties, Key
and Value
. For example, you can iterate a HashTable
as follows:
'iterate items
For Each item As DictionaryEntry In ht
Console.WriteLine("{0} {1}", item.Key, item.Value)
Next
HashTable
also offers two other properties named Keys
and Values
that return an ICollection
containing keys in the key/value pair and values in the same pair, respectively. This is demonstrated here:
'iterate keys
For Each key As Object In ht.Keys
Console.WriteLine(key)
Next
It is recommend that you use a strongly typed Dictionary(Of T, T)
that provides more efficiency. (And this suggestion is appropriate when discussing other dictionaries.)
The System.Collections.Specialized.ListDictionary
collection works exactly like HashTable
but differs in that it is more efficient until it stores up to 10 items. It is not preferred after the item count exceeds 10.
The System.Collections.Specialized.OrderedDictionary
collection works like HashTable
but differs in that items can be accessed via either the key or the index, as demonstrated by the following code:
Dim od As New OrderedDictionary
od.Add("a", 1)
'Access via index
Dim item As DictionaryEntry = CType(od(0), DictionaryEntry)
Console.WriteLine(item.Value)
The System.Collections.SortedList
collection works like HashTable
but differs in that items can be accessed via the key or the index items are automatically sorted based on the key. For example, look at the following code:
Dim sl As New SortedList
sl.Add("Del Sole", 2)
sl.Add("Alessandro", 1)
For Each item As String In sl.Keys
Console.WriteLine(item)
Next
It sorts items based on the key; therefore, it produces the following result:
Alessandro
Del Sole
The System.Collections.Specialized.HybridDictionary
collection is a dynamic class in that it implements a ListDictionary
until the number of items is small and then switches to HashTable
if the number of items grows large. Technically, it works like HashTable
.
The System.Collections.Specialized.StringCollection
is similar to the ArrayList
collection except that it is limited to accepting only strings. The following code provides an example:
Dim stringDemo As New StringCollection
stringDemo.Add("Alessandro")
stringDemo.Add("Del Sole")
'Returns True
Dim containsString As Boolean = stringDemo.Contains("Del Sole")
stringDemo.Remove("Alessandro")
You can use the same members of ArrayList
and perform the same operations.
The System.Collections.Specialized.StringDictionary
collection works like the HashTable
collection but differs in that it accepts only key/value pairs of type String
, meaning that both keys and values must be String
. The following is a small example:
Dim stringDemo As New StringDictionary
stringDemo.Add("Key1", "Value1")
stringDemo.Add("Alessandro", "Del Sole")
'Simple iteration
For Each value As String In stringDemo.Values
Console.WriteLine(value)
Next
You can recall the HashTable
collection for a full member listing.
The NameValueCollection
behaves similarly to the StringDictionary
collection, but it differs in that NameValueCollection
enables accessing items via either the key or the index. The following code snippet provides a brief demonstration:
Dim nv As New NameValueCollection
nv.Add("First string", "Second string")
Dim item As String = nv(0)
The Add
method also provides an overload accepting another NameValueCollection
as an argument.
The System.Collections.BitArray
collection enables storing bit values represented by Boolean values. A True
value indicates that a bit is on (1), whereas a False
value indicates that a bit is off (0). You can pass to the BitArray
arrays of Integer
numbers or Boolean values, as demonstrated by the following code:
Dim byteArray() As Byte = New Byte() {1, 2, 3}
'Length in zero base
Dim ba As New BitArray(byteArray)
For Each item As Object In ba
Console.WriteLine(item.ToString)
Next
This code produces the output shown in Figure 16.1, which is a human-readable representation of the bits in the collection.
The constructor also accepts a length argument that enables specifying how large the collection must be.
System.Collections.Specialized.BitVector32
has the same purpose of BitArray
but differs in two important elements; the first one is that BitVector32
is a structure that is a value type and therefore can take advantage of a faster memory allocation. On the other hand, the collection manages only 32-bit integers. All data is stored as 32-bit integers that are affected by changes when you edit the collection. The most important (shared) method is CreateMask
that enables creating a mask of bits. The method can create an empty mask (which is typically for the first bit) or create subsequent masks pointing to the previous bit. When done, you can set the bit on or off by passing a Boolean value. The following code provides an example:
'Passing zero to the constructor
'ensures that all bits are clear
Dim bv As New BitVector32(0)
Dim bitOne As Integer = BitVector32.CreateMask
Dim bitTwo As Integer = BitVector32.CreateMask(bitOne)
Dim bitThree As Integer = BitVector32.CreateMask(bitTwo)
bv(bitOne) = True
bv(bitTwo) = False
bv(bitThree) = True
The Data
property stores the actual value of the collection, as demonstrated here:
'Returns 5 (the first bit + the second bit = the third bit)
Console.WriteLine(bv.Data)
If you instead want to get the binary representation of the data, you can use the name of the instance. The following code demonstrates this:
Console.WriteLine(bv)
This code produces the following result:
BitVector32{00000000000000000000000000000101}
Members and Extension Methods
Collections provide special members, such as properties and methods, and are extended by most of the built-in extension methods. Providing a thorough discussion on each member is not possible; IntelliSense can point you in the right direction to provide explanations for each member. Each member has a self-explanatory identifier; therefore, it is easy to understand what a member does when you understand the high-level logic. In this chapter you are introduced to the most important members from collections so that you can perform the most common operations.
The .NET Framework offers generic counterparts of the collections described in the previous section. Moreover, it offers new generic collections that are specific to particular technologies such as WPF. In this section, you learn to work with generic built-in collections and how you can take advantage of a strongly typed fashion. Generic collections are exposed by the System.Collections.Generic
namespace, but a different namespace is explained.
The System.Collections.Generic.List(Of T)
collection is a generic ordered list of items. It is a strongly typed collection, meaning that it can accept only members of the specified type. It is useful because it provides support for adding, editing, and removing items within the collection. For example, imagine you need to store a series of Person
objects to a collection. This can be accomplished as follows:
Dim person1 As New Person With {.FirstName = "Alessandro",
.LastName = "Del Sole",
.Age = 35}
Dim person2 As New Person With {.FirstName = "XXXXX",
.LastName = "ZZZZZZZZ",
.Age = 44}
Dim person3 As New Person With {.FirstName = "YYYYY",
.LastName = "DDDDDDDD",
.Age = 18}
Dim personList As New List(Of Person)
personList.Add(person1)
personList.Add(person2)
personList.Add(person3)
You notice that the List(Of T)
has lots of members in common with its nongeneric counterpart—the ArrayList
. This is because the first one implements the IList(Of T)
interface, whereas the second one implements IList
. The following code shows how you can access an item within the collection using the IndexOf
method and how you can remove an item invoking the Remove
method, passing the desired instance of the Person
class:
'Returns the index for Person2
Dim specificPersonIndex As Integer = personList.IndexOf(person2)
'Removes person3
personList.Remove(person3)
The List(Of T)
still provides members such as Capacity
, AddRange
, Insert
, and InsertRange
, but it exposes a method named TrimExcess
that works like the ArrayList.TrimToSize
. Because the List(Of T)
implements the IEnumerable(Of T)
interface, you can then iterate the collection using a classic For..Each
loop, but each item is strongly typed, as demonstrated here:
For Each p As Person In personList
Console.WriteLine(p.LastName)
Next
If you need to remove all items from a collection, you can invoke the Clear
method, and when you do not need the collection anymore, you assign it to Nothing
:
personList.Clear()
personList = Nothing
In this case, the code simply assigns Nothing
to the list because this does not implement the IDisposable
interface. If it did, you have to be sure to invoke the Dispose
method before clearing and destroying the collection to avoid locking resources. There are also other interesting ways to interact with a collection, such as special extension methods. Extension methods are discussed in Chapter 20, “Advanced Language Features.” For now, you need to know that they are special methods provided by the IEnumerable(Of T)
interface that enables performing particular operations against a collection. For example, the Single method enables retrieving the unique instance of a type that matches the specified criteria:
'Returns a unique Person whose LastName
'property is Del Sole
Dim specificPerson As Person = personList.Single(Function(p) _
p.LastName = "Del Sole")
This method receives a lambda expression as an argument that specifies the criteria. Lambda expressions are also discussed in Chapter 20. Another interesting method is FindAll
, which enables the generation of a new List(Of T)
containing all the type instances that match a particular criteria. The following snippet retrieves all the Person
instances whose LastName
property starts with the letter D:
'Returns a new List(Of Person) storing
'all Person instances whose LastName starts
'with "D"
Dim specificPeople = personList.FindAll(Function(p) _
p.LastName.StartsWith("D"))
As usual, IntelliSense can be your best friend in situations such as this. Because all members from each collection cannot be described here, that technology can help you understand the meaning and the usage of previously mentioned members whose names are always self-explanatory.
Investigating Collections at Debug Time
Chapter 5, “Debugging Visual Basic 2012 Applications,” discussed the Data Tips features of the Visual Studio debugger. They are useful if you need to investigate the content of collections while debugging, especially if you need to get information on how collections and their items are populated.
Visual Basic 2012 implements a language feature known as collection initializers. This feature works like the object initializers, except that it is specific for instantiating and populating collections inline. To take advantage of collection initializers, you need to use the From
reserved keyword enclosing items within brackets, as demonstrated in the following code:
'With primitive types
Dim listOfIntegers As New List(Of Integer) From {1, 2, 3, 4}
The preceding code produces the same result as the following:
Dim listOfIntegers As New List(Of Integer)
listOfIntegers.Add(1)
listOfIntegers.Add(2)
listOfIntegers.Add(3)
listOfIntegers.Add(4)
You can easily understand how collection initializers enable writing less code that’s more clear. This feature can also be used with any other .NET type. The following code snippet shows how you can instantiate inline a List(Of Person)
:
'With custom types
Dim person1 As New Person With {.FirstName = "Alessandro",
.LastName = "Del Sole",
.Age = 35}
Dim person2 As New Person With {.FirstName = "XXXXX",
.LastName = "ZZZZZZZZ",
.Age = 44}
Dim person3 As New Person With {.FirstName = "YYYYY",
.LastName = "DDDDDDDD",
.Age = 18}
Dim people As New List(Of Person) From {person1,
person2,
person3}
The code also shows how you can take advantage of implicit line continuation if you have long lines for initializations. When you have an instance of the new collection, you can normally manipulate it. Of course, this feature works with any other collection type.
Non Generic Collections
Collection initializers are also supported by nongeneric collections using the same syntax.
The System.Collections.ObjectModel.ReadOnlyCollection(Of T)
is the read-only counterpart of the List(Of T)
class. Being read-only, you can add items to the collection only when you create an instance, but then you cannot change it. The constructor requires an argument of type IList(Of T)
so that the new collection will be generated starting from an existing one. The following code demonstrates how you can instantiate a new ReadonlyCollection
:
Dim person1 As New Person With {.FirstName = "Alessandro",
.LastName = "Del Sole",
.Age = 35}
Dim person2 As New Person With {.FirstName = "XXXXX",
.LastName = "ZZZZZZZZ",
.Age = 44}
Dim person3 As New Person With {.FirstName = "YYYYY",
.LastName = "DDDDDDDD",
.Age = 18}
Dim people As New List(Of Person) From {person1, person2, person3}
Dim readonlyPeople As New ReadOnlyCollection(Of Person)(people)
As an alternative, you can create a ReadonlyCollection
invoking the List(Of T).AsReadOnly
method, as shown in the following code:
'Same as above
Dim readonly As ReadOnlyCollection(Of Person) = people.AsReadOnly
Invoking AsReadOnly
produces the same result of creating an explicit instance.
The System.Collections.Generic.Dictionary(Of TKey, TValue)
collection is the generic counterpart for the HashTable
. Each item within a Dictionary
is a key/value pair; therefore, the constructor requires two arguments (TKey
and TValue
) where the first one is the key and the second one is the value. The following code shows instantiating a Dictionary(Of String, Integer)
in which the String
argument contains a person’s name and the Integer
argument contains the person’s age:
Dim peopleDictionary As New Dictionary(Of String, Integer)
peopleDictionary.Add("Alessandro", 35)
peopleDictionary.Add("Stephen", 27)
peopleDictionary.Add("Rod", 44)
A single item in the collection is of type KeyValuePair(Of TKey, TValue),
and both arguments reflect the collection’s ones. For a better explanation, take a look at the following iteration that performs an action on each KeyValuePair
:
For Each item As KeyValuePair(Of String, Integer) In peopleDictionary
Console.WriteLine(item.Key & " of age " & item.Value.ToString)
Next
The previous code will produce the following output:
Alessandro of age 35
Stephen of age 27
Rod of age 44
Each KeyValuePair
object has two properties, Key
and Value
, which enable separated access to parts composing the object. You can then manipulate the Dictionary
like you would other collections.
The System.Collections.Generic.SortedDictionary(Of TKey, TValue)
works exactly like the Dictionary
collection, except that items are automatically sorted each time you perform a modification. You can rewrite the code shown in the section about the Dictionary(Of TKey, TValue)
collection as follows:
Dim peopleDictionary As New SortedDictionary(Of String, Integer)
peopleDictionary.Add("Alessandro", 35)
peopleDictionary.Add("Stephen", 27)
peopleDictionary.Add("Rod", 44)
For Each item As KeyValuePair(Of String, Integer) In peopleDictionary
Console.WriteLine(item.Key & " of age " & item.Value.ToString)
Next
This code will produce the following result:
Alessandro of age 35
Rod of age 44
Stephen of age 27
Notice how the result has a different order than the one we added items to the collection with. In fact, items are sorted alphabetically. This collection performs sorting based on the Key
part.
The System.Collections.ObjectModel.ObservableCollection(Of T)
is a special collection that is typically used in WPF, Silverlight, and Windows Store applications. Its main feature is that it implements the INotifyPropertyChanged
interface and can therefore raise an event each time its items are affected by any changes, such as adding, replacing, or removing. Thanks to this mechanism, the ObservableCollection
is the most appropriate collection for the WPF and Silverlight data-binding because it provides support for two-way data-binding in which the user interface gets notification of changes on the collection and is automatically refreshed to reflect those changes. Although a practical example of this scenario is offered in the chapters about WPF, this section shows you how the collection works. The ObservableCollection
is exposed by the System.Collections.ObjectModel
namespace, meaning that you need to add an Imports
directive for this namespace. Notice that in the .NET Framework 4.5 the ObservableCollection
has been moved to the System.dll assembly from the WindowsBase.dll assembly. This makes it usable not only in WPF applications, but also in other kinds of applications (and this is why you do not need to add any reference manually as you would do in previous versions). Now consider the following code snippet:
Dim people As New ObservableCollection(Of Person)
AddHandler people.CollectionChanged, AddressOf CollectionChangedEventHandler
The people variable represents an instance of the collection, and its purpose is to store a set of Person
class instances. Because the collection exposes a CollectionChanged
event, which enables intercepting changes to items in the collection, we need an event handler to understand what is happening. The following code shows an example of event handler implementation:
Private Sub CollectionChangedEventHandler(ByVal sender As Object,
ByVal e As Specialized.
NotifyCollectionChangedEventArgs)
Select Case e.Action
Case Is = Specialized.NotifyCollectionChangedAction.Add
Console.WriteLine("Added the following items:")
For Each item As Person In e.NewItems
Console.WriteLine(item.LastName)
Next
Case Is = Specialized.NotifyCollectionChangedAction.Remove
Console.WriteLine("Removed or moved the following items:")
For Each item As Person In e.OldItems
Console.WriteLine(item.LastName)
Next
End Select
End Sub
The System.Collections.Specialized.NotifyCollectionChangedEventArgs
type exposes some interesting properties for investigating changes on the collection. For example, the Action
property enables you to understand if an item was added, moved, replaced, or removed by the collection via the NotifyCollectionChangedAction
enumeration. Next, the collection exposes other interesting properties such as NewItems
, which returns an IList
object containing the list of items that were added, and OldItems
, which returns an IList
object containing the list of items that were removed/moved/replaced. The NewStartingIndex
and the OldStartingIndex
provide information on the position where changes (adding and removing/replacing/moving, respectively) occurred. Now consider the following code, which declares three new instances of the Person
class and then adds them to the collection and finally removes one:
Dim person1 As New Person With {.FirstName = "Alessandro",
.LastName = "Del Sole",
.Age = 35}
Dim person2 As New Person With {.FirstName = "XXXXX",
.LastName = "ZZZZZZZZ",
.Age = 44}
Dim person3 As New Person With {.FirstName = "YYYYY",
.LastName = "DDDDDDDD",
.Age = 18}
people.Add(person1)
people.Add(person2)
people.Add(person3)
people.Remove(person1)
The ObservableCollection
works like the List one; therefore, it exposes methods such as Add
, Remove
, RemoveAt
, and so on and supports extension methods. The good news is that each time a new item is added or an item is removed, the CollectionChanged
event is raised and subsequently handled. Because of our previous implementation, if you run the code, you get the following output:
Added the following items:
Del Sole
Added the following items:
ZZZZZZZZ
Added the following items:
DDDDDDDD
Removed or moved the following items:
Del Sole
Because of its particularity, the ObservableCollection(Of T)
can also be useful in scenarios different from WPF, Silverlight, and WinRT, though they remain the best places where you can use it.
As for the List(Of T)
and also for the ObservableCollection(Of T)
, there is a read-only counterpart named ReadonlyObservableCollection(Of T)
that works like the ReadonlyCollection(Of T)
plus the implementation of the CollectionChanged
event. The collection is also exposed by the System.Collections.ObjectModel
namespace.
Think of the System.Collections.Generic.LinkedList(Of T)
collection as a chain in which each ring is an item in the collection that is linked to the others. In other words, an item is linked to the previous one and the next one and points to them. Each item in the collection is considered as a LinkedListNode(Of T)
, so if you decide to create a LinkedList(Of Person)
, each Person
instance will be represented by a LinkedListNode(Of Person)
. Table 16.2 summarizes the most common methods and properties for the collection over the ones that you already know (derived from IList(Of T)
).
The following code provides an example of creating and consuming a LinkedList(Of Person)
collection (see comments for explanations):
Dim person1 As New Person With {.FirstName = "Alessandro",
.LastName = "Del Sole",
.Age = 35}
Dim person2 As New Person With {.FirstName = "XXXXX",
.LastName = "ZZZZZZZZ",
.Age = 44}
Dim person3 As New Person With {.FirstName = "YYYYY",
.LastName = "DDDDDDDD",
.Age = 18}
'Creates a new LinkedList
Dim linkedPeople As New LinkedList(Of Person)
'Creates a series of nodes
Dim node1 As New LinkedListNode(Of Person)(person1)
Dim node2 As New LinkedListNode(Of Person)(person2)
Dim node3 As New LinkedListNode(Of Person)(person3)
'The first item in the collection
linkedPeople.AddFirst(node1)
'The last one
linkedPeople.AddLast(node3)
'Add a new item before the last one and after
'the first one
linkedPeople.AddBefore(node3, node2)
'Removes the last item
linkedPeople.RemoveLast()
'Gets the instance of the last item
'(person2 in this case)
Dim lastPerson As Person = linkedPeople.Last.Value
'Determines if person1 is within the collection
Dim isPerson1Available As Boolean = linkedPeople.Contains(person1)
The most important difference between this collection and the other ones is that items are linked. This is demonstrated by an Enumerator
structure exposed by every instance of the collection that enables moving between items, as demonstrated in the following code snippet:
Dim peopleEnumerator As LinkedList(Of Person).
Enumerator = linkedPeople.GetEnumerator
Do While peopleEnumerator.MoveNext
'Current is a property that is of type T
'(Person in this example)
Console.WriteLine(peopleEnumerator.Current.LastName)
Loop
This code demonstrates that items in the collections are linked and that each one points to the next one (see Figure 16.2).
The .NET Framework offers generic versions of the Queue
and Stack
collections, known as System.Collections.Generic.Queue(Of T)
and System.Collections.Generic.Stack(Of T)
. Their behavior is the same as nongeneric version, except that they are strongly typed. Because of this, you already know how to work with generic versions, so they are not discussed here.
Built-in collections are good for most scenarios. In some situations you might need to implement custom collections. You have two options: creating a collection from scratch or recur to inheritance. The first choice can be hard. You create a class implementing the ICollection(Of T)
and IList(Of T)
(or IDictionary
) interfaces, but you need to manually write code for performing the most basic actions onto items. The other choice is inheriting from an existing collection. This is a good choice for another reason: You can create your custom base class for other collections. Imagine you want to create a custom collection that stores sets of FileInfo
objects, each one representing a file on disk. It would not be useful to reinvent the wheel, so inheriting from List(Of T)
is a good option. The following code inherits from List(Of FileInfo)
and extends the collection implementing a new ToObservableCollection
method, which converts the current instance into an ObservableCollection(Of FileInfo)
and overrides ToString
to return a customized version of the method:
Public Class FileInfoCollection
Inherits List(Of FileInfo)
Public Overridable Function ToObservableCollection() As _
ObservableCollection(Of FileInfo)
Return New ObservableCollection(Of FileInfo)(Me)
End Function
Public Overrides Function ToString() As String
Dim content As New StringBuilder
For Each item As FileInfo In Me
content.Append(item.Name)
Next
Return content.ToString
End Function
End Class
Now you have a strongly typed collection working with FileInfo
objects. Plus, you extended the collection with custom members.
The .NET Framework 4.5 includes the Task Parallel Library (TPL), which offers support for multicore CPU architectures. The library exposes specific generic collections, via the System.Collections.Concurrent
namespace that was introduced by .NET 4.0. Table 16.3 gives you a list of the new classes.
You get an overview of these collections in the appropriate chapters, but for completeness, you now have a full list of available collections.
The concept of iterators is not new in modern programming language—for example, Visual C# has had iterators since version 2005. However, version 2012 is the first time this feature becomes part of the Visual Basic programming language. Technically speaking, iterators are used to step through a collection and iterator functions perform custom iterations over a collection. They can have only an IEnumerable
, IEnumerable(Of T)
, IEnumerator
, or IEnumerator(Of T)
as their return type. Iterators use the new Yield
keyword to return the item that is currently iterated to the caller code; then the iteration stops at that point. After Yield
executes, the iteration restarts from that point. With this kind of approach, an iteration over a collection is more responsive and makes items in the collection immediately available to the caller code right after they have been returned. If this technical explanation can seem difficult, let’s provide an easier one. Imagine you have a For..Each
loop that iterates through a collection and returns a list of elements; such a loop is invoked from within a method that needs to make further elaborations over the list of elements returned by the loop. This is a common situation, and the caller method needs to wait until the For..Each
loop completes the iteration and populates the list of elements before it can use it. If the collection that For..Each
is iterating is very large, this task can take a lot of time and the caller code will need to wait a while. With iterators, things change for the better—in fact, if you use iterators to loop through a collection, every item in the collection will be immediately sent to the caller via Yield
so that the caller can start its elaboration, while the loop is still in progress and goes to completion. In this way an application is faster and more responsive. In the next paragraphs you get more detailed explanations about iterators via some code examples.
Iterator Functions’ Scope
Because they are methods, iterator functions can have the same scope of methods. So they can be Private
, Public
, Friend
, Protected
, or Protected Friend
(except for when they are implemented in Module, where they can be Private
, Friend
, or Public
).
Iterators have plenty of usages, and this chapter describes them all in detail. Before studying the many ways iterators can be useful, though, a better idea is understanding why they allow you to write better and more responsive code. Let’s imagine we have a collection of items of type Person
and a method that iterates through the collection the way you know. Listing 16.1 demonstrates how to reproduce this simple scenario in code.
Public Class Person
Public Property FirstName As String
Public Property LastName As String
Public Property Age As Integer
End Class
Public Class People
Inherits List(Of Person)
End Class
Module Module1
Dim somePeople As New People
Sub Main()
'Populates the collection with a million items, for demo purposes
For i As Integer = 1 To 1000000
somePeople.Add(New Person With {.FirstName = "First Name: " & i.ToString,
.LastName = "Last Name: " & i.ToString,
.Age = i})
Next
For Each item As Person In GetPeople()
Console.WriteLine("{0}, {1} of age: {2}", item.FirstName,
item.LastName, item.Age.ToString)
Next
Console.ReadLine()
End Sub
Function GetPeople() As IEnumerable(Of Person)
Dim p As New People
For Each item As Person In somePeople
p.Add(item)
Threading.Thread.Sleep(10)
Next
Return p
End Function
End Module
Notice how a sample collection called People
is created and populated with a million items for demo purposes. The GetPeople
method iterates through the People
collection and returns the result to the caller code (the Main
method in this example). The code execution is delayed for 10 milliseconds (via Threading.Thread.Sleep
) to force a small delay and provide a demonstration of performance improvement with iterators (in-memory collections are faster than other collections such as database tables, where you could instead face long running query operations). If you run this code, you will see that GetPeople
needs to populate the local p
collection before returning it as a result to the caller, so you will have to wait for this before being able to iterate the result and see the list of elements in the Console window. We can now rewrite the GetPeople
method using iterators, as follows:
Iterator Function GetPeople() As IEnumerable(Of Person)
For Each item As Person In somePeople
Yield item
Threading.Thread.Sleep(10)
Next
End Function
This is an iterator function. It requires the Iterator
modifier in its definition and returns an object of type IEnumerable(Of T)
, where T
is now Person
. If you run the edited code, you will see how every item of type Person
is returned from GetPeople
immediately and made available to the caller code for further elaborations, while the iteration continues its progress. Figure 16.3 shows a moment of the operation progress. In other words, you will not have to wait for all the iteration to complete before elaborating items that it stores. As a consequence, the whole process is faster and the application is more responsive.
Measuring Performances
You can add a System.Diagnostics.StopWatch
object to the code and use its Start
and Stop
method to measure how much time the process takes and make comparisons between both implementations of GetPeople
.
Now that you have understood the benefits of iterators, it is time to discuss them in more detail.
You can return elements from iterator functions with multiple Yield
invocations. The following code demonstrates how to perform a loop over an iterator that returns a collection of strings:
Iterator Function StringSeries() As IEnumerable(Of String)
Yield "First string"
Yield "Second string"
Yield "Third string"
'...
End Function
Sub Main()
For Each item As String In StringSeries()
Console.WriteLine(item)
Next
Console.ReadLine()
End Sub
You can also use a single Yield
statement inside a For..Each
or For..Next
loop. The following code demonstrates how to retrieve the list of odd numbers in a sequence of integers:
Iterator Function OddNumbers(first As Integer,
last As Integer) As IEnumerable(Of Integer)
For number As Integer = first To last
If number Mod 2 <> 0 Then
'is odd
Yield number
End If
Next
End Function
Sub Main()
'Prints 1, 3, 5, 7, 9
For Each number As Integer In OddNumbers(1, 10)
Console.WriteLine(number)
Next
Console.ReadLine()
End Sub
In this case the caller code in the Sub Main
will not need to wait for a collection of integers to be populated before the iteration occurs; instead, each number that comes from the Yield
invocation is immediately printed to the Console window.
You can break the execution of an iterator by using either an Exit Function
or a Return
statement. The following snippet demonstrates how to exit from an iterator function:
Iterator Function StringSeries() As IEnumerable(Of String)
Yield "First string"
Yield "Second string"
Exit Function 'Return is also accepted
Yield "Third string"
'...
End Function
Unlike Visual C#, in Visual Basic the Yield
statement can be executed from within a Try
statement of a Try..Catch..Finally
block. The following code demonstrates how to rewrite the OddNumbers
iterator function including error handling:
Iterator Function OddNumbers(first As Integer,
last As Integer) As IEnumerable(Of Integer)
Try
For number As Integer = first To last
If number Mod 2 <> 0 Then
'is odd
Yield number
End If
Next
Catch ex As Exception
Console.WriteLine(ex.Message)
Finally
Console.WriteLine("Loop completed.")
End Try
End Function
As you can see, the Yield
statement is allowed inside the Try
block.
Iterators can also be implemented as anonymous methods. These are discussed in more detail in Chapter 20, but for now all you need to know is that they are methods with no name and are defined on-the-fly inside a code block. The following code snippet demonstrates how to implement the previous iterator as an anonymous method:
'Type of oddSequence is inferred by the compiler
Dim oddSequence = Iterator Function(first As Integer,
last As Integer) As IEnumerable(Of Integer)
Try
For number As Integer = first To last
If number Mod 2 <> 0 Then
'is odd
Yield number
End If
Next
Catch ex As Exception
Console.WriteLine(ex.Message)
Finally
Console.WriteLine("Loop completed.")
End Try
End Function
For Each number As Integer In oddSequence(1, 10)
Console.WriteLine(number)
Next
As you can see, the iterator is defined as an in-line method without an explicit name and its result is assigned to a variable whose type is inferred by the compiler (in this case IEnumerable(Of Integer)
).
Iterators can be useful in creating classes that act like lists. By implementing the IEnumerable
interface and the GetEnumerator
method of this interface, you can write efficient code that takes advantage of iterator functions to yield the content of a list. Listing 16.2 demonstrates how to create a class called BottlesOfWine
that stores a list of bottles of wine given a name and a color of the wine (red or white). Internally it uses a List(Of BottleOfWine)
instance, but instead of invoking the usual GetEnumerator
method of the List
class (provided by the IEnumerable
interface that the List
implements), it returns the list of bottles via an iterator function. It also returns a list of bottles based on their color.
Public Class BottleOfWine
Property Brand As String
Property Color As WineColor
End Class
Public Enum WineColor
Red
White
End Enum
Public Class BottlesOfWine
Implements IEnumerable(Of BottleOfWine)
Private _bottles As New List(Of BottleOfWine)
Public Function GetEnumerator1() As IEnumerator Implements IEnumerable.GetEnumerator
Return Me._bottles.GetEnumerator
End Function
Public Iterator Function GetEnumerator() As IEnumerator(Of BottleOfWine) Implements IEnumerable.GetEnumerator(Of BottleOfWine)
'Use Yield instead of List.GetEnumerator
For Each bottle As BottleOfWine In Me._bottles
Yield bottle
Next
End Function
Private Iterator Function BottlesByColor(ByVal color As WineColor) As IEnumerable _
(Of BottleOfWine)
For Each bottle As BottleOfWine In Me._bottles
If bottle.Color = color Then
Yield bottle
End If
Next
End Function
Public ReadOnly Property RedBottles As IEnumerable(Of BottleOfWine)
Get
Return BottlesByColor(WineColor.Red)
End Get
End Property
Public ReadOnly Property WhiteBottles As IEnumerable(Of BottleOfWine)
Get
Return BottlesByColor(WineColor.White)
End Get
End Property
Public Sub AddRedBottle(ByVal name As String)
Me._bottles.Add(New BottleOfWine With {.Brand = name, .Color = WineColor.Red})
End Sub
Public Sub AddWhiteBottle(ByVal name As String)
Me._bottles.Add(New BottleOfWine With {.Brand = name, .Color = WineColor.White})
End Sub
End Class
By returning with Yield
the result of both the GetEnumerator
and of BottlesByColor
methods, the code is more responsive. You can then use the code easily—for example, creating a list of bottles of red wine and then iterating the list in a way that is efficient because the result takes advantage of iterators, as in the following code:
Dim bottles As New BottlesOfWine
bottles.AddRedBottle("Chianti")
bottles.AddRedBottle("Cabernet")
'Result is returned via an iterator function
For Each bottle As BottleOfWine In bottles
Console.WriteLine(bottle.Brand)
Next
Iterators are an Opportunity, Not the Rule
Iterators provide an additional opportunity to write efficient code, but of course they are not necessary everywhere. You typically use iterators with large collections of objects and over long-running operations. So, they are not a new rule for iterating collections. For this reason, in this book you will find code examples for collections that are not based on iterators, which is instead the general rule. In particular circumstances, samples based on iterators are offered.
Applications often require data access. You store data within classes and structures, but often you need to group a set of data and collections to help you in this task. The .NET Framework offers both nongeneric collections (such as ArrayList
, Queue
, Stack
, and HashTable
) and generic ones (such as List(Of T)
, ObservableCollection(Of T)
, Dictionary(Of TKey, TValue)
, Queue(Of T),
and Stack(Of T)
). In both cases you can add, remove, and edit items within collections using the same members (due to the interfaces implementations). Moreover, with the feature of collection initializers, you can instantiate and populate collections inline. Although you typically use collections for manipulating data, the .NET Framework provides some special read-only collections, such as ReadonlyCollection(Of T)
. You learned to create custom collections, which is not an uncommon scenario, and finally you learned a new efficient way to iterate over collections via iterator functions and the Yield
keyword.