Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning Visual Basic 2005 Express Edition - From Novice To Professional (2006)

.pdf
Скачиваний:
387
Добавлен:
17.08.2013
Размер:
21.25 Mб
Скачать

330

C H A P T E R 1 3 L I S T S A N D G E N E R I C S

away from it: arrays certainly have their uses, but they are definitely not the flexible way of working with data in the world of Visual Basic.

On the other hand, take a look at a list box. With the ListBox.Items collection (that’s the key word here—collection), you can add items into the list, remove items, and even sort items. That’s what collections do, but unfortunately they have limitations as well.

Take a look at this line of code to add a custom object to a collection and then get it out again:

myListBox.Items.Add("My New String")

newString =CType(myListBox.Items(myListBox.Items.Count-1), String)

The big no-no here is the cast that has to take place. You see, collections store objects, as in System.Object. They don’t store any specific type. What this means is that you have to put up with the performance cost of casting from a System.Object to the specific type you are interested in, at runtime. More problematic, though, is that with untyped collections you don’t get compile-time checking. For example, if I had a string array called myArray and tried to do this

myArray(0) = 123

my program wouldn’t even run. Instead, I’d get a nice compile-time error telling me that myArray is a string array and so I shouldn’t add numbers to it. Now, imagine I had a collection of strings and tried to do this with it:

Dim myInt As Integer = CType(myListBox.Items(0), Integer)

That would compile just fine, because you can treat an integer as an object (you can, but you shouldn’t for reasons covered earlier), but at runtime the program would crash. It’s much nicer to get compile-time errors than it is to have your pride and joy crash unceremoniously in front of your users, isn’t it. The crash, of course, would occur because the collection holds strings, so when I try to cast a string to an integer I get an error. It’s also very, very slow. Doing this in a loop over 10,000 items would take a long time because behind the scenes .NET needs to check every access into the collection to make sure that I’m not screwing up the data types I’m working with.

So, although arrays are a little inflexible, they are nowhere near as dangerous as collections. Well, that’s how it used to be, anyway.

In the .NET Framework 2.0 we get a new feature called generics, and a brand new set of collections in the new System.Collections.Generic namespace that provide all the power of arrays (including compile-time type checking), but with all the flexibility of collections. Let’s take a look at how that works.

C H A P T E R 1 3 L I S T S A N D G E N E R I C S

331

Introducing Generics

Microsoft calls generics “parameterized types,” which even I find confusing. I prefer to think of them as classes Of something. For example, instead of the old-fashioned .NET 1.0 collections of objects, generics let you have collections of strings, or collections of integers, or collections of employees, and so on. The obvious use for them is in dealing with nice big lists of stuff, and so Microsoft very kindly provides a bunch of generics lists in the System.Collections.Generic namespace. They are listed in Table 13-1.

Table 13-1. Types of Collections in the System.Collections.Generic Namespace

System.Collections.Generic

Description

List

A nice simple list, which can be sorted.

SortedList

A special list that holds two “things” for every item: a key and a

 

value. Think of it as a social security number and a person’s name.

 

The number is the key and is always unique. A sorted list will sort

 

based on that key.

Dictionary

Another key-value type of list that enables you to instantly access

 

any item based on its key.

SortedDictionary

Has all the benefits of a SortedList (in that it sorts items), coupled

 

with the wonderful instant item access of a Dictionary.

LinkedList

A special list that enables each item to point forward to the next, or

 

backward to the previous, a bit like an iron chain.

Queue

As with a real-world queue, you can get at only the first item in the

 

queue. The first one to enter the queue is the first one out as well.

Stack

Think of a stack of plates in a restaurant. You can easily get at only

 

the last plate added to the stack, so Stack works in a last in, first

 

out way.

 

 

Declaring any of these for use in code requires you to use the special generic’s way of working. For example, if you wanted to declare a list of aliens in space game, you would do this:

Dim aliens As New List(Of Alien)

The type the list is going to hold is specified using the Of keyword inside parentheses. In this case then, you have created a list that will hold objects of type Alien.

Once you have the list created, you’ll find that it works like a very natural, and powerful, array. Rather than give the whole game away right now, let’s dive in and take a look at each type with some examples.

332

C H A P T E R 1 3 L I S T S A N D G E N E R I C S

Lists and SortedLists

As I gave away in the preceding section, creating an instance of a generic list is quite easy, but it can take a little getting used to. The easiest way to become familiar with the syntax is this. First, you’ll need to add an Imports line to the top of the source file you are working with, to let Visual Basic know that you are going to work with classes from the

System.Collections.Generic namespace.

Imports System.Collections.Generic

Module Module1

:

:

End Module

Next, declare a list like any other kind of object. For example:

Dim aliens As New List

As you type this in, IntelliSense will kick in and encourage you to finish off the declaration by specifying the type to use in the generic list:

Dim aliens As New List(Of Alien)

With the list created, items can be added into the list by calling the Add() method on it:

aliens.Add( reallyBadAlien )

Similarly, items can be removed from the list by calling Remove():

aliens.Remove( reallyBadAlien )

Notice, though, that with a call to Remove() you really need to pass in the actual object to remove. This isn’t always convenient, though, so the generic List lets you also remove based on an index:

aliens.RemoveAt( 12 )

The preceding code line would of course remove the 13th item from the list; list items, like array items, are numbered starting at 0. Should you need to completely empty a list, a quick call to the aptly named Clear() will do just that:

aliens.Clear()

There is also a RemoveAll() method, but strangely it doesn’t actually remove all items from a list. It uses something called a predicate to locate matching items in the list and removes just those items. We’ll look at how this works in a moment.

C H A P T E R 1 3 L I S T S A N D G E N E R I C S

333

The Add() and Remove() methods then add most of the functionality that people soon start to wish arrays had. However, lists can do so much more than arrays, particularly when it comes to finding things.

Finding Items in a List

There are seven methods that you can use to find items in a list: Contains(), Exists(),

Find(), FindAll(), FindIndex(), FindLast(), and FindLastIndex().

Contains() is perhaps the simplest of them all. If you pass in an object to Find(), it will return True or False if the list contains an identical item (in terms of the values in the public properties of the object). You have to be careful with this, though. Let’s take a look at why.

Try It Out: The Problem with Contains()

We’ll stick with console-based applications for now to focus on the semantics of working with the generic collections rather than building complex user interfaces, so go ahead and create a new console application in Visual Basic 2005 Express.

First, add a class into Module1.vb for the wonderful canonical Employee class:

Module Module1

Sub Main()

End Sub

End Module

Public Class Employee

Public Name As String

Public Number As String

Private m_id As Integer

Public Sub New(ByVal name As String, ByVal number As String) Me.Name = name

Me.Number = number

m_id = New Random().Next(10000) End Sub

334 C H A P T E R 1 3 L I S T S A N D G E N E R I C S

Public Overrides Function ToString() As String

Return String.Format("Employee Num: {0} Name: {1} id({2})", _

Me.Number, Me.Name, m_id)

End Function

End Class

It’s a pretty simple class, as you can see. There are a couple of public fields in there to expose the user’s name and social security number (no, I wouldn’t recommend you work with social security numbers this blithely in a real application), and a private member that holds a unique internal ID. The class has a single constructor that is used to set up these three variables and a simple ToString() implementation to print out the employee information as text. So far, so good.

Now add some code into the Main() function to actually use this Employee class in a generic List (don’t forget to add an Imports statement to the very top of the source as well):

Imports System.Collections.Generic

Module Module1

Sub Main()

Dim staff As New List(Of Employee)

Dim newHire As New Employee("John Smith", "1101") staff.Add(newHire)

If staff.Contains(newHire) Then Console.WriteLine( _

"Yes the list contains the item we just added" _ + vbCrLf + " {0}", _

newHire.ToString())

End If

newHire = New Employee("John Smith", "1101") If staff.Contains(newHire) Then

Console.WriteLine("The list also contains John")

Else

Console.WriteLine("Nope, can't find John Smith") End If

C H A P T E R 1 3 L I S T S A N D G E N E R I C S

335

Console.ReadLine()

End Sub

End Module

Before you run this, let me explain what’s going on. First a generic List is created to hold Employee objects. Notice that the code includes “Of Employee” to “type” the generic list to Employee objects only.

Next, a new Employee is created and added to the list. As I mentioned in the intro, you can add items into a list just by calling the Add() method.

Next, you call the Contains() method. Naturally, because you just added the object into the list, Contains() returns True to indicate that the item is in the list. So far this should all make sense. Now we come to the problem with Contains().

The next block of code creates a new Employee object and fills in the employee name through the constructor. You’re going to use this new Employee object to effectively search the List for an item. The problem is, Contains() will fail.

Go ahead and run the code and you’ll see the output in Figure 13-1.

Figure 13-1. Contains() fails to find the second object.

The reason that Contains() fails is that it is looking for an exact object match; it’s not checking properties and data within an object, but is instead searching through a list to see whether the exact same object you pass to Contains() is already in the list. Sometimes this way of working is useful. For example, you might be building a list to hold staff members who have been selected to play on the softball team. Before trying to add a staff member to the list, you could call Contains() to see whether that person is already on the team, and then add that person if not.

336

C H A P T E R 1 3 L I S T S A N D G E N E R I C S

For the vast majority of code, though, Contains() really isn’t that useful. Instead, what you’ll need is a more flexible method of finding any object based on certain criteria. For example, find all the aliens in a video game that are still alive and need updating, or find all staff members in a certain department, or simply find that guy called Peter Wright on the payroll. That’s exactly where the Find() methods come into play.

The Find() Methods

I mentioned before that there are seven Find () methods available to you when you work with a generic collection: Find(), FindAll(), FindLast(), FindIndex(), FindLastIndex(),

Exists(), and Contains(). You work with all these methods in exactly the same way. First, you create a search method, like this:

Function FindEmployeeByName(ByVal employeeToCheck As Employee) As Boolean Return employeeToCheck.EmployeeName.Equals( _

employeeToFind.EmployeeName) End Function

This method obviously works only with Employee objects, but the basic format would look the same if you were searching for integers:

Function FindSomeNumber(ByVal numberToCheck As Integer) As Boolean Return (numberToCheck = numberToFind)

End Function

Both methods simply compare the value passed in as a parameter to some other value, usually an instance variable in the class containing the method.

With the search method created, all that remains is to pass the address of the method to the List.Find() method.

Dim matchedEmployee As Employee = staff.Find(AddressOf FindEmployeeByName);

This calls the Find() method on a generic list and tells the Find() method to use the predicate you set up.

The Find() method itself just iterates through all the items in the list and for each item calls your predicate. In this case that means that for every item in the list, your

FindEmployeeByNamePredicate() (searcher) will get called. Find() will pass your function the current item in the list, and it’s up to your code in the function to return a True or False value based on whether the item passed in is the one that you were looking for.

The net result, with Find() anyway, is that when a match is made, the matching item from the list is returned. If no match is made, null is returned.

C H A P T E R 1 3 L I S T S A N D G E N E R I C S

337

So, let’s put all that in English. The Find() methods don’t actually do very much. Their job is simply to move through each item in a list and call a function that you create (the predicate) to do the searching. Your job in searching a list is to create the function and return True or False to show Find() whether the item it passed was the one you were looking for.

Let’s take a look at a couple of these Find() methods with some real code.

Try It Out: Using the Find() Methods

Create a new console application. In this you’ll create a generic Employee list and then search for an employee by name, and also find all the employees who work in a specific department.

The first thing to do is to add an Employee class to the project. Add in Employee.vb to the solution in the usual way and key in the following code to create the Employee class itself:

Public Class Employee

Private _employeeNumber As Integer

Public EmployeeName As String

Public Department As String

Public ReadOnly Property EmployeeNumber() As Integer

Get

Return _employeeNumber

End Get

End Property

Public Sub New(ByVal name As String, ByVal department As String) Me.EmployeeName = name

Me.Department = department _employeeNumber = New Random().Next()

End Sub

Public Sub New()

' A do-nothing constructor

End Sub

End Class

338

C H A P T E R 1 3 L I S T S A N D G E N E R I C S

The class is straightforward. It has a constructor that takes in parameters to initialize the properties of the class as soon as an instance is created. It also has a constructor that does nothing—you’ll see that in use shortly.

The class also has three members: EmployeeName and Department are standard public fields, and _employeeNumber is a private variable you just set up with a random number to make each instance of the class different from another internally. This isn’t necessary; it’s just to make sure that you are unlikely to end up with two instances of a class with the same public properties that are completely identical (the random number will normally be different).

Drop back into the Module1.vb code now. You need to initialize a generic Employee list with a bunch of items, and then you’ll call out to two of your own methods to find first one specific employee and then a bunch of employees in a specific department. As before, don’t forget to add the Imports line to the top of the source code so that you are able to work with generic lists.

Imports System.Collections.Generic

Module Module1

Sub Main()

Dim staff As New List(Of Employee) staff.Add(New Employee("Peter Wright", "IT"))

staff.Add(New Employee("Heather Wright", "Usability")) staff.Add(New Employee("Dominic Shakeshaft", "Editorial")) staff.Add(New Employee("Grace Wong", "Management")) staff.Add(new Employee("Gary Cornell", "Management))

FindAnEmployee(staff, "Peter Wright")

FindStaffInDepartment(staff, "Management")

Console.ReadLine()

End Sub

End Module

Because you added a constructor that can take parameters to the Employee class, it’s quite easy to initialize the Employee list simply passing in a bunch of new Employee objects.

C H A P T E R 1 3 L I S T S A N D G E N E R I C S

339

With the list created, you just hand off control to two functions that you have yet to write: the FindAnEmployee() function looks for a specific employee in the list, while the FindStaffInDepartment() method lists all employees who work in a specific part of the business. Let’s go ahead and code these up, in the Module1.vb source file:

Module Module1

Private _employeeToFind As Employee

Sub Main()

Dim staff As New List(Of Employee) staff.Add(New Employee("Peter Wright", "IT"))

staff.Add(New Employee("Heather Wright", "Usability")) staff.Add(New Employee("Dominic Shakeshaft", "Editorial")) staff.Add(New Employee("Grace Wong", "Management")) staff.Add(New Employee("Gary Cornell", "Management"))

FindAnEmployee(staff, "Peter Wright")

FindStaffInDepartment(staff, "Management")

Console.ReadLine()

End Sub

Sub FindAnEmployee(ByVal staff As List(Of Employee), ByVal name As String) _employeeToFind = New Employee()

_employeeToFind.EmployeeName = name

If staff.Exists(AddressOf FindEmployeeByName) Then Dim matchedEmployee As Employee = _

staff.Find(AddressOf FindEmployeeByName) Console.WriteLine(" {0} works in {1}", _

matchedEmployee.EmployeeName, matchedEmployee.Department)

End If End Sub

Function FindEmployeeByName(ByVal employeeToCheck As Employee) As Boolean Return (employeeToCheck.EmployeeName.Equals( _

_employeeToFind.EmployeeName)) End Function

End Module