Partilhar via


Generics

This chapter is excerpted from Programming Visual Basic 2008: Build .NET 3.5 Applications with Microsoft's RAD Tool for Business by Tim Patrick, published by O'Reilly Media

Programming Visual Basic 2008

Logo

Buy Now

When I was in high school, my family sometimes shopped at a local grocery warehouse named Fedmart. Signs on the window clearly stated that there was "no connection between Fedmart and the federal government," but people continued to shop there anyway. They had these small $1 frozen cheese pizzas that my mom would buy in bulk for me and my friends, teenage boys who didn't care much about what went down the esophagus.

Most of the store stocked the typical grocery products, but there was one aisle near the south border of the store that sold only "generic" products. Walking into this section was like walking into a black-and-white television; all of the product labels were plain clear or white, with simple black lettering. And they were cheap. They did the job, but just barely. You would never want to run out of name-brand ketchup in the middle of a celebratory barbeque with your friends, and offer up a bottle of generic ketchup as a replacement. Somehow I remember clearly reading the black lettering on the white label of that watery ketchup substitute, about how it met the federal ketchup standards. At that moment I had an epiphany, a sudden realization that would change the way I thought about life in these United States forever: the government has a federal ketchup standard!

Sadly, Fedmart closed down before I finished my senior year, leaving a vacuum in the generic ketchup and aluminum foil marketplace. But as a Visual Basic programmer, you can still gain access to generics, through .NET's generics technology. Generics-the ability to use placeholders for data types-first appeared in Visual Basic 2005 and the related .NET Framework 2.0. This chapter provides you with the "specifics" on generics.

What Are Generics?

In .NET, "generics" is a technology that lets you define data type placeholders within types or methods. Let's say you needed to define a class to track customer data, but you didn't want to enforce a specific format on the customer "ID" value. Part of your code needs to interact with customer objects using an Integer ID value, while another part of the code will use an alphanumeric key for the customer. You might ask, "Why don't you just include both types of identifiers as distinct fields in your customer record?" That wouldn't work because I am trying to come up with a reasonably simple example and answering that question would just distract me. So, here's the numeric version of the class:

Class CustomerWithNumberID
   Public ID As Integer
   Public FullName As String
End Class

Here's the variation that uses a string ID:

Class CustomerWithStringID
   Public ID As String
   Public FullName As String
End Class

Of course, you could define ID as System.Object, and stick anything you wanted in that field. But System.Object is considered "weakly typed," and there is nothing to stop you from mixing in Integer and String ID values for different instances in an array of customer objects.

What you want is a system that lets you define the class generically, and hold off on specifying the data type of ID until you actually create an instance of the class, or a complete collection of related class instances. With such a system, you could define a general-purpose version of the customer class.

Class CustomerWithSomeID
   Public ID As <DatatypePlaceholder>
   Public FullName As String
End Class

Later, when it was time to create an instance, you could tell the language which data type to use for the placeholder.

Dim oneCustomer As CustomerWithSomeID(replacing _
   <DatatypePlaceholder> with Integer)

This is what generics let you do. Here's the actual Visual Basic syntax that defines the nonspecific customer class:

Class CustomerWithSomeID(Of T)
   Public ID As T
   Public FullName As String
End Class

The general placeholder, T, appears in a special Of clause, just after the class name. (You don't have to name the placeholder T, but it's become a tradition when presenting sample code using generics.) As a data type, T can be used anywhere within the class definition where you don't want to define the data type upfront. The class, and its ID member, are now ready for instantiation with an actual replacement data type for T. To create a new instance, try this code:

Dim numberCustomer As CustomerWithSomeID(Of Integer)

When (Of Integer) is attached to the end of the class definition, Visual Basic acts as though you actually declared a variable for a class that had an Integer member named ID. In fact, you did. When you create an instance of a generic class, the compiler defines a separate class that looks like a non-generic class with all of the placeholders replaced.

Dim customer1 As New CustomerWithSomeID(Of Integer)
Dim customer2 As New CustomerWithSomeID(Of Integer)
Dim customer3 As New CustomerWithSomeID(Of String)

These lines define two instances of CustomerWithSomeID(Of Integer), and one instance of CustomerWithSomeID(Of String). customer1 and customer2 are truly instances of the same data type, but customer3 is an instance of a completely different data type. Assignments between customer1 and customer2 will work, but you can't mix either of them with customer3 without performing an explicit conversion.

' ----- This works just fine.
customer1 = customer2

' ----- This will not compile.
customer3 = customer1

As true compile-time data types generated automatically by the compiler, they exhibit all of the personality of other non-generic classes. Even Visual Studio's IntelliSense properly detects the substituted data type. Figure 16.1, "Congratulations, Mr. and Mrs. Generic: it's an Integer" includes a tool tip, just to the right of the instance member selection list, that properly identifies the customer1.ID member as Integer.

Figure 16.1. Congratulations, Mr. and Mrs. Generic: it's an Integer

Congratulations, Mr. and Mrs. Generic: it's an Integer

Within the class definition, the T placeholder can appear anywhere, even within argument lists and local variable declarations.

Class SomeClass(Of T)
   Public Function TransformData(ByVal sourceData As T) As T
      ' ----- Add generic transormation code here.
      Dim workData As T
      ...
   End Function
End Class

Generics work with structures and interfaces as well.

Structure SomeStructure(Of T)
   Public GenericMember As T
End Structure

Interface ISomeInterface(Of T)
   Sub DoWorkWithData(ByVal theData As T)
End Interface

Variations of Generic Declaration

If there were a minimum federal government data type placeholder requirement, the implementation of generics just described would certainly meet it. It's kind of nice to postpone the definition of data types until the last minute. But .NET generics don't stop there.

Multiple Placeholders

Generic placeholders-also known as type parameters-are like those knives you buy on late-night TV. You don't get one; you get more! As many as you need, it turns out. Each generic class can include multiple placeholders by adding them to the initial Of clause.

Class MultiTypes(Of T1, T2)
   Public Member1 As T1
   Public Member2 As T2
End Class

As before, you aren't required to use the boring names T1 and T2. Whatever names you choose, include them as a comma-separated list just after the Of keyword. When you're ready to create an instance, replicate the comma-delimited list in the same order, but using actual types. In this statement, Integer replaces T1, and String replaces T2:

Dim useInstance As MultiTypes(Of Integer, String)

Data Type and Interface Constraints

The type parameters you include in a generic, such as T, accept any valid data type, including Integer, String, System.Windows.Forms.Form, or your own custom types. That is, T can be replaced by anything that derives from System.Object, which is everything. You can even imagine the statement:

Class SomeClass(Of T)

being replaced by:

Class SomeClass(Of T As System.Object)

adding the As clause to make it look like other Visual Basic declarations. Well, you can stop imagining and start acting: placeholders support the As clause. If you don't include an As clause, Visual Basic assumes you mean As System.Object, but you can follow As with any type you want.

Class FormOnlyClass(Of T As System.Windows.Forms.Form)

By adding a specific class with the As clause, you enforce a constraint on the generic type, a limitation that must be met to use the type. In this case, the constraint says, "You may supply any class value for T as long as it is or it derives fromSystem.Windows.Forms.Form." This means you can create an instance of FormOnlyClass using one of your application's forms, but not using non-Form classes.

' ----- This works.
Dim usingForm As FormOnlyClass(Of Form1)

' ----- This doesn't work.
Dim usingForm As FormOnlyClass(Of Integer)

When you add a constraint to a type parameter, it impacts the features you can use with that type parameter. Consider this generic class destined to work with forms, but not declared that way:

Class WorkWithForms(Of T)
   Public Sub ChangeCaption(ByVal whichForm As T, _
         ByVal newCaption As String)
      ' ----- The following line will not compile.
      whichForm.Text = newCaption
   End Sub
End Class

In this class, the assignment to whichForm.Text will fail because the WorkWithForms class does not know that you plan to use it with forms. It only knows that you plan to use T, and T is, by default, of type System.Object. There's no Text property in the System.Object class; I checked.

If we change the definition of WorkWithForms to accept Form objects, the outlook for compiling this code changes dramatically.

Class WorkWithForms(Of T As Windows.Forms.Form)
   Public Sub ChangeCaption(ByVal whichForm As T, _
         ByVal newCaption As String)
      ' ----- Yes! It now compiles.
      whichForm.Text = newCaption
   End Sub
End Class

Since T has to be a Form type or something derived from Form, Visual Basic knows that all the members of the Form class, including Text, are available to all things T. Therefore, the assignment to whichForm.Text works.

In addition to classes, you can also use interfaces to constrain your generic types.

Class ThrowAwayClass(Of T As IDisposable)

Instances of ThrowAwayClass can be created as needed, but only if the type supplied with the declaration implements the IDisposable interface.

' ----- This works. Pens use IDisposable.
Dim disposablePen As ThrowAwayClass(Of System.Drawing.Pen)

' ----- This doesn't work, since the Integer data type
'       doesn't implement IDisposable.
Dim disposableNumber As ThrowAwayClass(Of Integer)

But wait, there's more! See, I told you it was like shopping for knives on TV. Besides your run-of-the-mill types and interfaces, you can also follow the As clause on the generic placeholder with the New keyword.

Class SomeClass(Of T As New)

The As New clause says to the generic type, "Accept any type for T, but only if that type includes a constructor that requires no arguments." That is, T must include a default constructor. Once defined, you'll be able to create new instances of T-whatever type it actually turns out to be-in your generic type.

Class SomeClass(Of T As New)
   Public Sub SomeSub(  )
      Dim someVariable As New T
   End Sub
End Class

If your generic class includes multiple type parameters, each parameter can include its own As class with a distinct type or interface constraint.

Simultaneous Constraints

It's nice that each of those knives you purchased can slice a watermelon, but what if you want to chop wood with that same knife, or use it to upgrade that electrical work you've been postponing? You're looking for a multifunctional tool, just like you find in each generic placeholder. If you need one placeholder to include a constraint for a specific class, an interface, and "New" all at once, you can do it. After the As keyword, include the multiple constraints in curly braces.

Class SomeClass(Of T As {Windows.Forms.Form, _
   IDisposable, New})

Now, any type you supply in the Of clause when creating an instance of this class must meet all of the constraints, not just one of them. And here's something new: you can include more than one interface constraint at a time.

Class SomeClass(Of T As {ISerializable, IDisposable})

And you can still include a class constraint and the New constraint, even with those multiple interfaces. (You can't include more than one class constraint for a single placeholder.) If your generic type includes multiple type parameters, each of them can have its own multiple constraints set.

Nesting Generic Types

Generic types can include their own nested types.

Class Level1(Of T1)
   Public Level1Member As T1
   Class Level2(Of T2)
      Public Level2Member1 As T1
      Public Level2Member2 As T2
   End Class
End Class

You can nest the generics as deeply as you need.

Non-Generic Types with Generic Members

If generic types seem a little scary or overwhelming, don't fret. You don't have to create a full generic type to use the new generic features. You can add generic support to just a single method within an otherwise normal class.

Class SomeClass
   ' ----- The class itself does not have the generic
   '       Of clause, so it's not generic. But...

   Public Shared Sub ReverseValues(Of T) _
         (ByRef first As T, ByRef second As T)
      ' ----- This method is generic with its own Of clause.

      ' ----- Reverse the contents of two variables.
      Dim holdFirst As T

      holdFirst = first
      first = second
      second = holdFirst
   End Sub
End Class

Generic methods are useful when you need to have a local variable of the placeholder's type within the method (as is done with holdFirst here), but you don't know the type in advance. Using this shared ReverseValues method works like any other method, with the extra Of clause stuck in.

Dim x As Integer = 5
Dim y As Integer = 10
SomeClass.ReverseValues(Of Integer)(x, y)
MsgBox(x)  ' Displays 10

If you will be using the placeholder for one or more of the method arguments, Visual Basic will infer the type based on the passed value. If Visual Basic is able to guess the type in this way, you don't even need the Of clause when calling the generic method.

SomeClass.ReverseValues(x, y)

As with generic types, generic methods allow you to add constraints to the placeholders.

Overloading Generic Types and Members

Earlier I mentioned how the compiler essentially creates separate classes for each instance variation of a generic class that you create. This means that these two instances actually use completely different and generally unrelated classes:

Dim numberVersion As SomeClass(Of Integer)
Dim textVersion As SomeClass(Of String)

So, SomeClass(Of Integer) and SomeClass(Of String) are completely different classes, even though they have the same base name. In a way, Visual Basic is overloading the class name for you, letting you use it in two (or more) different ways.

Generics also let you get involved in the class-overloading game. Normally, you can only create a single class with a given name-inside a particular namespace, that is. But with generics, you can reuse a class name, as long as the placeholders used among the classes are different enough, either in their number or in their applied constraints.

Class SomeClass(Of T1)
   ' ----- This is a generic class with one placeholder.
End Class

Class SomeClass(Of T1, T2)
   ' ----- This is a completely different generic
   '       class with two placeholders.
End Class

Visual Basic will figure out which version to use based on the Of clause you include with the instance declaration.

Dim simpleVersion As SomeClass(Integer)
Dim complexVersion As SomeClass(Integer, String)

Generics and Collections

Generics really shine in the area of collections. The initial release of .NET had, among the thousands of possibly useful classes, a set of "collection" classes, all in the System.Collections namespace. Each collection lets you stuff as many other object instances as you want inside that collection, and retrieve them later. The collections differ in how you stuff and retrieve, but they all allow you to stick any type of object in the collection.

One of the collection classes is the System.Collections.Stack class. Stacks let you store objects like pancakes: the first object you add to the stack goes on the bottom, and each one you add goes on top of the previous object. When you're ready to eat a pancake-I mean, remove an item-it comes off the top. (This "last in, first out" system is sometimes called "LIFO.") The Push and Pop methods manage the addition and removal of objects.

Dim numberStack As New Collections.Stack
numberStack.Push(10)
numberStack.Push(20)
numberStack.Push(30)
MsgBox(numberStack.Pop(  ))    ' Displays 30
MsgBox(numberStack.Pop(  ))    ' Displays 20
MsgBox(numberStack.Pop(  ))    ' Displays 10

There is also a Peek method that looks at the topmost item, but doesn't remove it from the stack. The thing with stacks (and other similar collections) is that you don't have to put just one type of object into the stack. You can mix any ol' types of objects you want.

Dim numberStack As New Collections.Stack
numberStack.Push(10)                 ' Integer
numberStack.Push("I'm sneaking in.") ' String
numberStack.Push(Me.Button1)         ' Control

The stack doesn't care, since it's just treating everything as System.Object. But what if you needed to ensure that only integers were put into the stack? What if you wanted to limit a stack to any specific data type, but didn't want to write separate stack classes for each possible type?

This sure sounds like a job for generics to me. It sounded that way to Microsoft, too. So, it added a bunch of new generic collections to the framework. They appear in the System.Collections.Generic namespace. There are a few different classes in this namespace, including classes for linked lists, queues, chocolate chip cookies, and dictionaries. And hey, there's a class called Stack(Of T). That's just what we need.

Dim numberStack As New Collections.Generic.Stack(Of Integer)
numberStack.Push(10)
numberStack.Push(20)
numberStack.Push(30)

Now, if we try to add anything other than an Integer to numberStack, an error occurs.

' ----- This won't work.
numberStack.Push("I'll try again.")

Generic Nullable Types

Back in Chapter 6, Data and Data Types, I introduced nullable types, a way to allow Nothing to be used with value types.

Dim numberOrNothing As Integer?

Although you can't tell from that source code line, nullable types are actually implemented using generics. The full version of numberOrNothing's declaration is:

Dim numberOrNothing As Nullable(Of Integer)

Visual Basic simply provided a shortcut for this syntax through the ? suffix. You can use either syntax to declare your nullable instances.

Summary

Having generics available for .NET development really makes Visual Basic even more flexible and useful than all the hype you heard about it. You always had the ability to use placeholders for data-they're called variables. Generics provide that same placeholder functionality, but with data types instead of just plain data.

When you control all development aspects of an application, you might think that generics aren't for you. After all, you're not going to let an Integer variable slip into a collection of dates. But they are quite handy for enforcing standards within your code, which is always good.

Project

When a patron checks out a book or other library item, the due date is automatically calculated based on a number of days stored in the CodeMediaType.CheckoutDays database field. But what happens if that calculated date is a holiday, and the library is closed? The patron might not be able to return the book until the next day, and would incur a fine. This fine, though small, could start a chain reaction in the patron's life that would lead to poverty, despair, and an addiction to soap operas. Fortunately, this can all be avoided by adding a list of holidays to the project. If an item's return date falls on a documented holiday, the program adjusts the date forward until it finds a non-holiday date.

Note

Load the Chapter 16, Generics (Before) Code project, either through the New Project templates or by accessing the project directly from the installation directory. To see the code in its final form, load Chapter 16, Generics (After) Code instead.

Managing Holidays

As a small, standalone application that fully manages its own data, there isn't necessarily a pressing need for generics in the Library application. However, generics provide more advantages than just limiting the types of data stored in a class or collection. They also enhance data conversion and IntelliSense support, since Visual Basic can tell immediately, for instance, what type of data will appear in a collection.

We'll store all holidays managed by the Library Project in the Holiday database table. The contents of this table will seldom change, and will be frequently accessed during the checkout process. To speed things up, we'll cache the data inside the application. And to simplify management of that cache, we'll store the holidays in a generic collection.

First, let's create the class that holds a single holiday entry. Add a new class to the project through the Project → Add Class menu command, and give it the name HolidaySet.vb. The familiar structure of an empty class appears.

Public Class HolidaySet

End Class

The Holiday database table includes two main fields used in calculating holidays: EntryType and EntryDetail. Let's store these as members of the class, and add a flag that ensures the entry is valid.

Note

Insert Chapter 16, Generics, Snippet Item 1.

Private HolidayType As String
Private HolidayDetail As String
Private IsValid As Boolean

We'll populate these private members through the class constructor.

Note

Insert Chapter 16, Generics, Snippet Item 2.

Public Sub New(ByVal entryType As String, _
      ByVal entryDetail As String)
   ' ----- Create a new holiday entry instance.
   HolidayType = Left(Trim(UCase(entryType)), 1)
   HolidayDetail = entryDetail

   ' ----- See if the details are valid.
   IsValid = True
   Select Case HolidayType
      Case "A"
         ' ----- The detail should be in mm/dd format.
         IsValid = IsDate(entryDetail & "/2004")
      Case "E"
         ' ----- The detail is a number from 1 to 7.
         If (Val(entryDetail) < 1) Or _
            (Val(entryDetail) > 7) Then IsValid = False
      Case "O"
         ' ----- The detail should be a valid date.
         IsValid = IsDate(entryDetail)
      Case Else
         ' ---- Invalid. This should never happen.
         IsValid = False
   End Select
End Sub

Clearly, the holiday entries have a coding system all their own, and it wouldn't be fair to force code elsewhere in the application to deal with all the complexities of holiday date comparisons. So, let's add a public method to the class that indicates whether a given date matches the holiday stored in an instance.

Note

Insert Chapter 16, Generics, Snippet Item 3.

Public Function IsHoliday(ByVal whatDate As Date) As Boolean
   ' ----- Given a date, see if it matches the entry
   '       type in this instance.
   Dim buildDate As String

   ' ----- If this record is invalid, then it is never a
   '       holiday match.
   If (IsValid = False) Then Return False

   Select Case HolidayType
      Case "A"
         ' ----- Annual.
         buildDate = HolidayDetail & "/" & Year(whatDate)
         If (IsDate(buildDate)) Then
            Return CBool(CDate(buildDate) = whatDate)
         Else
            ' ----- Must be 2/29 on a non-leap-year.
            Return False
         End If
      Case "E"
         ' ----- Day of the week.
         Return CBool(Val(HolidayDetail) = _
            Weekday(whatDate, FirstDayOfWeek.Sunday))
      Case "O"
         ' ----- See if this is an exact one-time match.
         Return CBool(CDate(HolidayDetail) = whatDate)
   End Select
End Function

We're done with that class. Now we just need a place to keep our cached holiday records. The System.Collections.Generic namespace includes a few different collection classes that we could use. Since the only thing we really need to do with the holidays once they are in the collection is scan through them, looking for matches, the standard no-frills list seems best. Its class name is List(Of T), and its primary feature, according to the .NET documentation, is that it lets you access members by index. That's fine.

Open the General.vb file and find where the global variables appear, somewhere near the top. Then add a definition for the global collection that will store all the holidays.

Note

Insert Chapter 16, Generics, Snippet Item 4.

Public AllHolidays As Collections.Generic.List( _
   Of Library.HolidaySet)

There it is! There it is! The Of clause. This is a generic collection. Yeah! OK, party's over; let's move on.

Locate the InitializeSystem method, still in the General.vb file, and add the code that will initialize the global holiday cache.

Note

Insert Chapter 16, Generics, Snippet Item 5.

AllHolidays = New Collections.Generic.List(Of HolidaySet)

That's it for infrastructure. Let's add some routines that access this generic list. We need a routine that will tell us, True or False, whether a given date (the planned due date of a library item) matches any of the holidays or not. Add the function IsHolidayDate to General.vb.

Note

Insert Chapter 16, Generics, Snippet Item 6.

Public Function IsHolidayDate(ByVal whatDate As Date) _
      As Boolean
   ' ----- See if the given date is a holiday.
   Dim oneHoliday As Library.HolidaySet

   ' ----- Scan through the holidays, looking for a match.
   For Each oneHoliday In AllHolidays
      If (oneHoliday.IsHoliday(whatDate)) Then Return True
   Next oneHoliday

   ' ----- Not a holiday.
   Return False
End Function

This routine, IsHolidayDate, shows where generics really come in handy. It's all in the For Each statement that the magic occurs. In a normal collection, we wouldn't be sure what type of items were stored in the collection, be they HolidaySet or String or Integer. Well, we would know since we are the developer, but Visual Basic plays dumb in this area, and assumes you mixed up the data types in one collection.

But because we tied the AllHolidays collection to the HolidaySet class using the Of HolidaySet clause, Visual Basic now understands that we are only going to store items of HolidaySet in the AllHolidays collection. That means we don't have to explicitly convert items retrieved from the collection to the HolidaySet data type. If we weren't using a generic class, the code would look something like this:

Dim scanHoliday As System.Object
Dim oneHoliday As Library.HolidaySet

For Each scanHoliday In AllHolidays
   oneHoliday = CType(scanHoliday, Library.HolidaySet)
   If (oneHoliday.IsHoliday(whatDate)) Then Return True
Loop

Since non-generic collections boil everything down to System.Object, we would have to explicitly convert each collection object to HolidaySet using CType or a similar conversion function. But with a generic collection, Visual Basic takes care of it for us.

We still need to cache the holidays from the database, so add a RefreshHolidays method to General.vb that does this.

Note

Insert Chapter 16, Generics, Snippet Item 7.

Public Sub RefreshHolidays(  )
   ' ----- Load in the list of holidays.
   Dim sqlText As String
   Dim dbInfo As SqlClient.SqlDataReader
   Dim newHoliday As Library.HolidaySet

   On Error GoTo ErrorHandler

   ' ----- Clear the current list of holidays.
   AllHolidays.Clear(  )

   ' ----- Get the holidays from the database.
   sqlText = "SELECT * FROM Holiday"
   dbInfo = CreateReader(sqlText)
   Do While dbInfo.Read
      newHoliday = New Library.HolidaySet( _
         CStr(dbInfo!EntryType), CStr(dbInfo!EntryDetail))
      AllHolidays.Add(newHoliday)
   Loop
   dbInfo.Close(  )
   Return

ErrorHandler:
   GeneralError("RefreshHolidays", Err.GetException(  ))
   On Error Resume Next
   If Not (dbInfo Is Nothing) Then _
      dbInfo.Close(  ) : dbInfo = Nothing
   Return
End Sub

You've seen a lot of code like this already, code that loads records from a database table into the program. I won't sport with your intelligence by explaining it to you line by line.

There are two places where we need to call RefreshHolidays: when the program first starts up, and later whenever changes are made to the list of holidays. We won't worry about other users changing the list; we'll just focus on when the local application updates the list. First, open the sometimes-hidden ApplicationEvents.vb file, and add this code to the MyApplication_Startup event handler, just after the existing call to LoadDatabaseSettings( ).

Note

Insert Chapter 16, Generics, Snippet Item 8.

RefreshHolidays(  )

One down, and one to go. Open the MainForm.vb file, and locate the AdminLinkHolidays_LinkClicked event handler. This is the handler that lets the user edit the list of holidays. Add the same RefreshHolidays( ) line to the end of this routine.

Note

Insert Chapter 16, Generics, Snippet Item 9.

' ----- Reload the holidays if they changed.
RefreshHolidays( )

As you can see right in this routine, we already added the editor to manage the list of holidays. The only thing left to do is to actually access the holiday list when checking out items. We'll do that in a future chapter.