Recommended Books



ASPAlliance.com Sample Book Chapters

Chapter 3: Basic Programming in Visual Basic .NET

In This Chapter

  • Declaring and Initializing Variables

  • Working with Block-Level Scope

  • Static Variables

  • Using Static Variables

  • Using Arrays

  • Working with New Abstract Data Types

  • Shadow Variables

  • Functions and Subroutines

  • Defining Structures

  • Using Objects

  • Exception Handling

  • Summary

In this chapter, you'll get your first chance to write Visual Basic .NET code unencumbered with the coding practices and idioms of yesterday. We will use Visual Basic .NET code and only Visual Basic .NET code. Chapter 3 provides you with an opportunity to experiment and explore the basic programming concepts that are the molecules of every program.

This chapter looks at variable declaration and initialization, block scope, static variables, and more on arrays and abstract data types. I will also introduce you to the concept of shadow variables, and provide you with some more examples on defining procedures and structures. Near the end of this chapter, we will look at the new garbage collector and the impact it has on using objects. Finally, this chapter wraps up with comprehensive coverage of exception handling. Because you will use exception handling in all of the code you write—though perhaps not all procedures—you need to master the concepts of exception handling early.

Declaring and Initializing Variables

There is one significant difference in the way you write declarations for simple types and object types. That difference is the use of the keyword New. (We'll look at New more in the section "Using Objects.")

Variables can be declared and initialized in Visual Basic .NET on a single line. In addition to resulting in fewer lines of code, this new feature can be applied with some simple rules to make code more robust.

Before we look at several declarative statements, here is an overview of the best practices regarding variables that we will follow throughout this book:

  • Always provide an initial value for variables.

  • Declare and initialize variables in a single statement.

  • Declare variables immediately before they are used.

  • Declare variables in the narrowest scope possible.

  • Use the refactoring "Replace Temp with Query" to keep the number of temporary variables to an absolute minimum.

In each of the subsections, I will demonstrate several declarators supporting each of these best practices. In the final subsection, I will demonstrate the concept of refactoring, and illustrate benefits derived from refactored code.

Initialize Variables

In programming, so many things can go wrong that we need to hedge as often as possible to make things go right. These hedges, like splitting aces in blackjack, are good practices that help us progress deliberately without sweating details. By providing an initial value for variables, we are providing a reference point, or known state, that we can use to evaluate a variable. Essentially, by providing an initial state and controlling the value of variables, we can always evaluate a variable to determine whether its value is within an acceptable range. However, if we never expressed what an acceptable initial value was, we can't test the initial state.

The desirability of a variable having an initial state is exactly why object-oriented languages have constructors and parameterized constructors (the section "Using Objects" in this chapter covers these more). It's so that we can initialize even objects.

The general form of a variable declaration is as follows:

Dim varname As datatype = expression

Note -

Although VB6 code often was encumbered with a pseudo-Hungarian notation prefix for variables, we will not use the Hungarian notation in this book. The first reason is that no single notation existed, resulting in many inconsistencies. A second reason is that we are working with a strongly typed, object-oriented language, which mitigates the need for prefixes. Consequently, a notation would only increase our labor.

We will use a very simple notation in this book (you can adopt it or not). Fields will be prefixed with an F and properties will have an identical name, without the F, as their underlying field values. This convention was borrowed from Object Pascal and helps us name fields and properties consistently and quickly and makes the job of matching fields to properties trivial.


varname is replaced with a suitable variable name. Generally, whole words, no non- standard abbreviations, and no prefixes are used for variable names (see the note). datatype is the name of a class, structure, or ValueType like Double. Expression is a suitable expression for the data type. (The syntax changes slightly for objects and events. See the section "Using Objects" for object declarations and Chapter 8 for an example of WithEvents statements.) Listing 3.1 demonstrates several declarations, including suitable initial values.

Listing 3.1 Several declaration and initialization statements.

 1: Module Module1
 2:
 3:  Sub AHandler(ByVal o As Object, ByVal e As System.EventArgs)
 4:   Debug.WriteLine("Read chapter 9")
 5:  End Sub
 6:
 7:
 8:  Sub Main()
 9:
10:  Dim S As String = Command()
11:  Debug.WriteLine(S)
12:
13:  Dim TodayDate As DateTime = Today()
14:  Debug.WriteLine(TodayDate)
15:
16:  Dim MyArray() As Double = {3.14159, System.Math.Sqrt(2)}
17:  Debug.WriteLine(MyArray(1))
18:
19:  Dim MyHandler As EventHandler = AddressOf AHandler
20:  MyHandler(Nothing, Nothing)
21:
22:  Try
23:   Dim MyException As Exception = New Exception("Raise an exception!")
24:   Throw MyException
25:  Catch e As Exception
26:   Debug.WriteLine(e.StackTrace())
27:  End Try
28:
29:
30:  End Sub
31:
32: End Module

Tip -

If you are debugging in the IDE, you set the command-line arguments by opening the project's properties pages. Choose Project, Properties, Configuration Properties, Debugging to open the view. Modify the command-line argument's value to set the command-line argument.


Listing 3.1 defines some spurious declarators. The examples typify some of the declarations you are likely to encounter in everyday code. Line 10 demonstrates declaring a string and initializing it to the command-line arguments. The value of Command will include all of the arguments that follow the /cmd switch.

Line 13 demonstrates how to initialize a DateTime to today's date. Line 16 demonstrates initializing an array of doubles; MyArray is initialized to contain an approximate value for þ and the square root of 2, returned from System.Math.Sqrt.

Line 19 declares MyHandler as the Delegate type EventHandler and initializes it to the address of the subroutine AHandler. Lines 19 and 20 illustrate that Visual Basic .NET can work with function pointers and introduce the subject of delegates. Delegates are special classes that support function pointers and are used to implement event handling in Visual Basic .NET. Chapter 9, "Understanding Delegates," gives more information on this subject. (For now, suffice it to say that when MyHandler is called on line 20, the subroutine on lines 3 through 5 is actually called.)

Lines 22 to 27 demonstrate declaring and initializing the root exception object. Line 24 demonstrates throwing an exception, which is similar to raising an error in VB6. Lines 25 through 27 demonstrate a Catch block. The last section of this chapter covers exception handling.

Declare and Initialize Variables in a Single Statement

The biggest problem with uninitialized variables is related to pointers in C and C++. Because C/C++ pointers can point to any memory location, an uninitialized C/C++ pointer might be pointing to BIOS memory. Visual Basic .NET has limited support for pointers, so we are able to get away with the default initialization of variables to the null equivalent more often.


Tip -

By default VB .NET initializes numeric types to 0, strings to empty strings, and objects to Nothing.


However, Visual Basic .NET does support delegates and dynamic object creation. It is preferable that these types have an initial value; otherwise, your code will be littered with checks for Nothing or an equivalent. Calling an uninitialized Delegate results in a NullReferenceException, which is cleaner than crashing because of a pointer trashing BIOS memory, but still annoying.

As a convenience, it is easier to have one rule for all cases than to make exceptions to the rule. Thus it is easier to always declare and initialize variables in a single statement.

Declare Variables Immediately Before First Use

There is no compelling reason to declare variables at the top of a procedure. Declaring them immediately before first use makes it easy to track the type and purpose of a local variable.

As an added benefit, if you are able to apply the refactoring "Replace Temp with Query," having the temporary variable close to its point of use makes getting rid of the temporary variable easier too. (See the section "Employ Refactoring: Replace Temp with Query" for more information on refactoring.)

Declare Variables in the Narrowest Scope Possible

Global variables are bad. The reason global variables are bad is because the opportunity for misuse is greatest. The opportunity for misuse of data should be reduced as much as possible; or, simply put, the narrower the scope, the better.

You are familiar with module, class, structure, and procedure scope. You may also use accessibility specifiers to further reduce opportunity for misuse. Accessibility specifiers like Public, Private, Protected, Friend, and Protected Friend are discussed in Chapter 7. Block-level scope has been added to Visual Basic .NET. Block-level scope is the narrowest scope of all. Refer to the section "Working with Block-Level Scope" for more information.

Employ Refactoring: Replace Temp with Query

This book does not cover all the details of refactoring. A great book on refactoring is Martin Fowler's Refactoring: Improving the Design of Existing Code, published by Addison-Wesley. However, where it is suitable to mention refactoring, I will do so.

What Is Refactoring?

Refactoring is an outgrowth of XP (Extreme Programming), which has been popularized by Kent Beck and others. XP includes concepts like pair programming and resolving problems in the structure of code with refactoring. Refactoring stems from a dissertation by William Opdike.

My definition of refactoring is "factoring out common code." The real benefit of refactoring is that it provides a language, or a frame of reference, for talking about improving code, and offers a set of almost algebraic rules to make refactoring an orderly process.

We can't summarize Martin Fowler's book here, so for now, just accept that refactoring is a good thing. Refactoring helps move towards the fewest occurrences of repetitive code and helps achieve greater opportunity for reuse. Refactoring may also lead to a means of measuring the qualitative value of code. Good code is refactored code; bad code often is not.

Replace Temp with Query

"The problem with temps is that they are temporary and local. Because they can be seen only in the context of the method in which they are used, temps tend to encourage longer methods" (Fowler, p. 120). Long methods are less desirable than short methods.

By replacing a temporary variable with a query—a function that returns the value previously maintained by the temporary variable—we shorten the procedure and increase the likelihood that other fragments of code can use the query method instead of reconstructing the temporary variable.

Assume that we have two properties: Sale and TaxRate. TotalSale is defined as the sale plus the cost of taxes. Also, suppose that we have a fragment of code that declares a local temporary variable to store TotalSale:

Dim TotalSale As Decimal = Sale * (1 + TaxRate)
Debug.WriteLine(TotalSale)

The Debug.WriteLine statement plays the role of using the temporary variable.

To replace temporary variables with a query method, first look for temporary variables that are assigned a value just once, as is TotalSale. (If it is assigned values more than once in a procedure, you may need to simplify the procedure or keep the temporary.) Having identified the temporary, perform the following steps to complete the refactoring:

  1. Define a function, adding a Get to the temporary variable name. Define the function to return the calculated value of the two fields. (In the example, Sale and TaxRate represent the two fields.)

  2. Function GetTotalSale() As Decimal
     Return Sale * (1 + TaxRate)End Function
    
  3. Replace the initialization of the temporary with the function call.

  4. Dim TotalSale As Decimal = GetTotalSale
    Debug.WriteLine(TotalSale)
    
  5. Compile and test the modification to ensure that the code behaves identically and yields the same results.

  6. Having confirmed identical behavior, remove the temporary variable and replace the use of the temporary with the call to the query method.

  7. Debug.WriteLine(GetTotalSale())
  8. Compile and test.

The purpose of refactoring is to get the same result with simpler code. Testing after each phase of the refactoring makes it easier to roll back changes if the code behaves differently.

Critical to Extreme Programming and refactoring is the testing process. The benefit of the refactoring "Replace Temp with Query" is that long procedures become shorter, while performing the same operations, and the frequency of debugging the local temporary calculation decreases to one time, in the query method.

The benefit of such refactoring often rises over time. The more times you would have written the calculation for the temporary again, the more savings you achieve.

Working with Block-Level Scope

Scope relates to opportunity for use. When a name is in scope, you can use it; when a name is not in scope, you cannot use it.

There are several layers of scope in Visual Basic .NET. From the broadest to narrowest scope, there is namespace, type (applies equally to class, module, and structure), procedure, and block scope. Things declared in a broader scope are accessible to narrower, subordinate scopes directly. For example, a private class field is accessible to a procedure in the same class and will not cause a name conflict with a field with the same name in another class.

Additionally, narrower-scoped names are accessible to other areas of code using the member-of operator (.). For example, the ToString method is defined as a public method of Object. Because ToString is public, it is accessible outside the Object class, but you have to precede the call to ToString with a class reference and the member-of operator, class.ToString().

Variables declared in a procedure or block have procedure-level or block-level scope, respectively. A block in a procedure is narrower than the containing procedure, so procedure variables are accessible in a block, but block variables are not accessible in the containing procedure. As mentioned in Chapter 2, block scope is new in Visual Basic .NET.

Block-scoped variables are those variables introduced for the first time in a For Next, For Each, While End While, or If Then construct. Any variables introduced with a Dim statement in a block construct are accessible only in that block when Option Explicit is On. Consider the following code fragment, using an arbitrary block to demonstrate block scope behavior:

If ( True ) Then
 Dim I As Integer
 I = 10
End If
I = 5

If Option Explicit is On, you will get a "name is not declared" error reported by the compiler at the last line, assuming that I is not defined in the containing procedure. If Option Explicit is Off, the compiler will report a "block-level variable hides variable in enclosing block error" because the implicit I in the statement I = 5 hides the explicit I in the If Then block. Further, when Option Explicit is Off, if I were first used in the block, then I would be accessible outside the block.

Consider another example:

Option Explicit Off
If (True) Then
 I = 10
End If
Debug.WriteLine(I)

This code writes 10 to the Output window.

If you want a variable to be accessible in an outer scope, move the declaration of the variable to the outer scope, as in this final example:

Option Explicit On
Dim I As Integer
If(True) Then
 I = 10
End If
Debug.WriteLine(I)

The variable I is defined outside the block and consequently is accessible in both the scope where it is defined and the narrower scope of the If Then block.

A second detail to watch out for is that block variables have to be initialized between trips in and out of block scope to prevent unexpected results. When working with block scope, establish a consistent set of rules to prevent problems related to block scope. The following rules, if applied consistently, should preclude any problems:

  • Always set Option Explicit On.

  • Define variables in the narrowest scope possible.

  • Apply the "Replace Temp with Query" refactoring discussed earlier, reducing the number of temporary variables and limiting potential scope-related problems.

  • Always initialize variables, especially variables declared in a block-level scope.

Static Variables

The Static keyword is used to indicate that a variable will maintain its value between successive calls to a procedure. VB6 supported making all procedure variables static by adding the Static keyword to the procedure header. Visual Basic .NET does not support procedure-header use of Static.

Using Static Variables

The mechanics for declaring static variables require that you replace Dim with Static as demonstrated in the following function:

Function GetCounter() As Integer
 Static Counter As Integer = 0
 Counter += 1
 Return Counter
End Function

Each time GetCounter is called, Counter will be incremented by 1. The first time GetCounter is called it will return 1, and each subsequent call to GetCounter will return Counter + 1. (Note the use of the new addition and assignment operator, +=.)


Caution -

You cannot use the compound operators, like +=, in an initialization statement for a variable. For example, Static Counter As Integer += 1 will cause a compiler error.


A function like GetCounter is a good way to get a unique value during the program's run.

Static Variables and Memory

Procedure variables are created in stack memory. The stack memory is temporary memory. Items are added to the stack as local variables are declared in a procedure and removed when the procedure ends. Static variables are actually created in the data segment. The memory for static variables is allocated and remains as long as your program is running, just as variables declared outside of a procedure in a module are.

Using Arrays

Since about 1990, when C++ began gaining popularity, it has been a recommended practice to encapsulate arrays of memory in a class. Storing arrays of data in a class allows the array to perform memory and bounds checking in conjunction with the class and behind the scenes.

Using an unadorned array requires that you manage bounds checking and reallocation of memory (using ReDim). On occasion you may want to use the new array class, but for most new code you will probably find ArrayList easier to use and less error-prone. ArrayList is covered in the section "Working with New Abstract Data Types."

Arrays Are Instances of System.Array

All arrays are instances of System.Array. All members of System.Array were listed in the last chapter in Table 2.10, so we won't repeat that information here. Keep in mind that even simple arrays are classes in Visual Basic .NET.

For additional reference, arrays implement the ICloneable, IList, ICollection, and IEnumerable interfaces.

Declaring Arrays

There are a couple of basic kinds of array declarations. You may declare an array without a fixed number of elements, represented by the empty parentheses (), and optionally include an initializer list. Or, you may declare an array with a fixed number of elements. However, an array with a fixed number of elements specified cannot also have an initializer list.

Dim StringArray() As String = {"This", "is", "a", "test"}
Dim FixedArray(3) As Double

The size of StringArray is not specified but is indicated by the initializer list {} containing the words This, is, a, and test. StringArray will have four elements indexed from 0 to 3. (Remember that Option Base is not supported, so the first element of a Visual Basic .NET array is 0.)


Note -

Remember that Visual Basic .NET does not support the range array using the following syntax:

Dim A(low To high) As datatype

FixedArray contains four elements. In Visual Basic .NET, the number of elements is always n+1. FixedArray is declared as FixedArray(3), resulting in four indexable elements from 0 to 3.

Much of the work we used to have to handcraft in VB6 is already implemented in Visual Basic .NET. The next section demonstrates some of the new capabilities of arrays.

Using Array Methods

In addition to array initializer lists, there are many methods that you can take advantage of that make sorting, copying, and finding elements of an array much easier. Many of these methods are Shared methods (refer to Table 2.10 for details). Listing 3.2 demonstrates several array methods.

Listing 3.2 Demonstration of array methods.

 1: Sub Main()
 2:
 3:   Dim StringArray() As String = {"This", "is", "a", "test"}
 4:   Dim FixedArray(3) As Double
 5:
 6:   Array.Sort(StringArray)
 7:
 8:   Dim E As IEnumerator = StringArray.GetEnumerator()
 9:
10:   While (E.MoveNext())
11:    Debug.WriteLine(E.Current())
12:   End While
13:
14:   Array.Reverse(StringArray)
15:
16:   E.Reset()
17:
18:   While (E.MoveNext())
19:    Debug.WriteLine(E.Current())
20:   End While
21:
22:   Debug.WriteLine(Array.IndexOf(StringArray, "test"))
23:
24:   Dim NewArray(StringArray.Length) As String
25:   Array.Copy(StringArray, NewArray, StringArray.Length)
26:
27:   Debug.WriteLine(Array.IndexOf(NewArray, "a"))
28:
29:
30: End Sub

Lines 3 and 4 repeat the declaration of StringArray and FixedArray from the last section. Line 6 calls the Shared method Sort, passing StringArray as the array to be sorted. After line 6 the strings are ordered as a, is, test, and This. Line 8 declares an IEnumerator interface variable and the enumerator for StringArray is returned. Using enumerators allows you to work with a common interface for enumerating many kinds of data.

The enumerator is positioned before the first element, which makes it convenient to start iterating with MoveNext (see lines 10 and 18). MoveNext returns a Boolean indicating whether or not there is a value at that position. E.Current returns the current element. (Of course you can still use a For loop and an index to iterate over elements of an array, if desired.)

Line 14 sorts the array in descending order with the Reverse method. Again, Reverse is a Shared method, so we pass the array we want reversed as an argument to the method. Line 16 resets the enumerator because we left it pointing past the last element on line 12. Line 22 demonstrates finding an element of an array with the IndexOf method. Line 25 demonstrates copying an array. The number of elements of the source and target arrays must be identical; on line 24 I used the length of StringArray to allocate NewArray. You can also use the ReDim command to resize the target array before copying the array.

Resizing arrays was covered in the section "Resizing Arrays" in Chapter 2.

Multidimensional Arrays

Visual Basic .NET supports multidimensional arrays and nested arrays. A multidimensional array is an array containing a comma-delimited list of array dimensions. A two-dimensional array, for instance, contains two numbers indicating the size of each dimension. Multidimensional arrays are also referred to as matrices.

Chapter 2 demonstrated multidimensional arrays. We won't repeat that information here, but will demonstrate an array of arrays.

We can add any object to the Array type. If we store heterogeneous objects in an array, we can rely on late binding to determine what an element of the array is, or we can perform dynamic type checking and conversion. Because the recommended Option Strict On setting prohibits late binding, I will demonstrate how to convert the elements of the nested arrays to a specific type at runtime. (You also need to know how to perform dynamic type conversions to work with multicast delegates in Chapter 9.)


Note -

Late binding refers to the compiler determining the type of an object at runtime. For example, in VB6 we might have defined a variable as a variant and then assigned an object to it with CreateObject. The following Visual Basic .NET code demonstrates a late bound reference to MS Excel with Option Strict Off and replacing the Variant required in VB6 with the Object required in Visual Basic .NET.

Module Module1
 Private Excel As Object
  Sub Main()
  Excel = CreateObject("Excel.Application")
  Excel.Visible = True
  MsgBox("Stop")
  End SubEnd Module
Early binding is indicated when we add a reference to the object we want to create and declare a variable of that type. An early bound object is an object whose type is known by the compiler at compile time.

Option Strict forces the compiler to do more work and is a recommended setting; thus we give up late bound objects for the benefit of stronger compiler type checking.


To declare an Array object, use the same syntax as any other array and declare the type as Array. Listing 3.3 demonstrates an example with an array defined to contain six elements. To insert a nested array into each indexed position of the array, assign the array object to an indexed position of the containing array. Nested array elements are accessed by indexing the containing array or using an enumerator. To access an element of the nested array, you need to convert the nested array object to a specific type.

Listing 3.3 Nested arrays and dynamic type conversion.

1: Sub Main()
2:
3:  Dim StringArray() As String = {"This", "is", "a", "test"}
4:  Dim AnArray(5) As Array
5:
6:  AnArray(0) = StringArray
7:  Debug.WriteLine(CType(AnArray(0), String())(0))
8: End Sub

From the listing, you can easily determine that declaring the outer, generic array (line 4) is no different than declaring an array of any type. Assigning StringArray to AnArray(0) nests StringArray into the first element of AnArray. Line 7 demonstrates using dynamic type conversion to get the nested array back to a usable form. The code is packed tightly into a single statement; the next fragment breaks line 7 into a more verbose listing.

Dim Elem As Object
Elem = AnArray(0)
Dim Temp() As String
Temp = CType(Elem, String())
Dim S As String
S = Temp(0)
Debug.WriteLine(S)

Line 7 from Listing 3.3 translates into the very verbose seven lines of code in the preceding fragment. AnArray(0) returns an object. CType converts the Object to a string array. Having the string array, we can access elements of the string array, and finally write the value of the string.

Listing 3.3 demonstrates that it is possible to write convoluted, terse code in Visual Basic .NET. However, the code demonstrates what is possible but not necessarily prudent. Sometimes the data structure can make it very easy to store and manage specific kinds of data. If you find yourself coding nested arrays, hide the complexities of managing the data in a class. As a better alternative, consider using one of the new abstract data types rather than a System.Array.

Working with New Abstract Data Types

A revised Collection class similar to the VB6 Collection is defined in the Microsoft. VisualBasic namespace. Visual Basic .NET has added several new Abstract Data Types (ADTs) to ultimately replace the Collection class. The names of the new classes were enumerated in Chapter 2, including an example of using the Stack class. The subsections that follow demonstrate ArrayList, HashTable, SortedList, and Queue.

Members of ArrayList

The ArrayList class is a designed replacement for an unadorned array. Although arrays in Visual Basic .NET are classes, they are designed to work similarly to VB6 arrays. This means you have to resize the arrays and preserve elements manually, which is likely to yield code that is littered with ReDim statements.

Dynamic array sizing is semantical behavior that belongs to an array. In an object- oriented programming language, you would expect that such behavior is defined as part of an array class. Combining storage and capacity management in a single class is what ArrayList has to offer. You will find ArrayList easier to use than a System.Array or the VB6-style collection.

Table 3.1 lists the members of ArrayList.

Table 3.1 ArrayList Members

Name

Description

 

Shared Method

Adapter

Wraps an ArrayList around an object that implements IList

FixedSize

Returns a fixed-size wrapper, allowing elements to be modified but not added or removed

ReadOnly

Returns a read-only wrapper around an ArrayList

Repeat

Returns an ArrayList containing multiple copies of the argument value

Synchronized

Returns a thread-safe list wrapper

 

Instance Property

Capacity

Used to get or change the capacity of an ArrayList

Count

Returns number of elements in the list; capacity may be greater than or equal to count

IsFixedSize

Returns a Boolean indicating whether or not the ArrayList has a fixed size

IsReadOnly

Returns a Boolean indicating whether or not the ArrayList is read-only

IsSynchronized

Returns a Boolean indicating whether or not access to the ArrayList is synchronized

Item

Used to index elements of the ArrayList

SyncRoot

Returns an object used to synchronize ArrayList access

 

Instance Method

Add

Appends an element to the ArrayList, increasing the capacity if necessary

AddRange

Appends elements from an ICollection

BinarySearch

Searches for element using binary algorithm

Clear

Removes all elements

Clone

Performs shallow copy of all elements

Contains

Returns a Boolean indicating whether or not the element is in the ArrayList

CopyTo

Copies all or part of the ArrayList to a one-dimensional System.Array

Equals

Determines whether the argument array references the calling array

GetEnumerator

Returns ArrayList enumerator

GetHashCode

Hashing function inherited from Object

GetRange

Copies range of elements to a new ArrayList

GetType

Returns metaclass of ArrayList; inherited from Object

IndexOf

Returns the index of a particular element of the array list

Insert

Inserts an element into the list at specified index

InsertRange

Inserts a range of elements at specified index

LastIndexOf

Returns the index of the last occurrence of object

Remove

Removes the first instance of argument object

RemoveAt

Removes element at specified index

RemoveRange

Removes a range of elements at specified index

Reverse

Sorts the array in reverse order

SetRange

Copies range of elements over the elements in the ArrayList

Sort

Reorders the data in ascending order

ToArray

Copies elements to System.Array

ToString

Returns name of object

TrimToSize

Sets capacity to number of elements


The next section demonstrates some of the characteristics of ArrayList.

Using ArrayList

The biggest benefit of ArrayList over System.Array is that ArrayList has dynamic capacity management built in. When you use System.Array, you have to make sure there is enough room for an element. If not, you have to add capacity to the array with ReDim. On the other hand, if you use the ArrayList Add, AddRange, Insert, or InsertRange methods, the capacity is adjusted as needed.

ArrayList has significant advantages over VB6 arrays but fewer advantages over Visual Basic .NET System.Array; however, capacity management is enough of a reason to prefer ArrayList over System.Array. Many of the methods in ArrayList are similar to methods in Array (see "Using Array Methods"); therefore, we will not repeat examples of those methods here. Capacity management and adding and managing a range of elements are additional features offered in ArrayList. Let's take a look at examples of using these behaviors. Listing 3.4 demonstrates behaviors of ArrayList that are not found in System.Array.

Listing 3.4 Behaviors of ArrayList not found in System.Array.

 1: Sub DemoSetRange()
 2:
 3:  Dim MyArray As New ArrayList()
 4:
 5:  Dim Array1() As Integer = {0, 1, 2, 3, 4, 5}
 6:
 7:  MyArray.InsertRange(0, Array1)
 8:
 9:  Dim I As Integer
10:
11:  For I = 0 To MyArray.Count - 1
12:   Debug.WriteLine(MyArray(I))
13:  Next
14:
15:  Debug.WriteLine("Contains 3? " & MyArray.Contains(3))
16:
17: End Sub

The example declares an ArrayList named MyArray using one of three possible constructors. The constructor on line 3 takes no parameters. Line 5 allocates a System.Array and initializes the members to the integers 0 through 5. Line 7 demonstrates ArrayList.InsertRange. InsertRange takes a start index and an ICollection object. System.Array implements the ICollection interface, so System.Array is a suitable argument for InsertRange. In fact, any class that implements ICollection (HashTable, Stack, Queue, and SortedList are other examples) is a suitable argument for InsertRange. Lines 11 through 13 demonstrate that elements of an ArrayList can be accessed as if it were a simple array. (Of course you can use the new Enumerator behavior that you saw in Listing 3.2, as well.)

Line 15 demonstrates the Contains method. Contains takes an object, which can be a literal integer like 3, and returns a Boolean indicating whether or not the object is in the ArrayList. In the example, Option Strict is On so the Boolean returned by Contains is printed using the ToString method of the Boolean type.

HashTable

Hash tables use key and value pairs. The key is processed through a hashing function that is designed to generate a unique value that is then used as an index into the hash table to the location containing the value. Hash tables strike a balance between resource usage and speed.

Instead of probing each element for equality to determine whether objects are equal, simply processing the key provides an index to the location that contains the associated value. There is a significant amount of research on hash tables, hashing functions, and key-collision avoidance. (You may have studied some of them in college if you were a computer science major, but the .NET Framework provides a HashTable implemented for you.)

The System.Collections.HashTable class implements a hash table available for you to use. HashTable works externally much like a data dictionary. Provide a unique key and an associated value, and the HashTable takes care of the rest.

Suppose you were storing personnel records in memory for quick access. You might key each record on the Social Security number, and the value would be the personnel record. (For the demonstration, we will simply store a person's name to represent the personnel record.)

Listing 3.5 declares a new instance of a hash table and adds some unique elements to the hash table keyed on pseudo-Social Security numbers. The values stored in the hash table represent the data associated with the keys. (The key is the first argument and the value is the second argument of the Add method.)

Listing 3.5 Storing items in a hash table.

 1: Sub DemoHashTable()
 2:  Dim Hash As New Hashtable()
 3:  Hash.Add("555-55-5555", "Frank Arndt")
 4:  Hash.Add("555-55-5556", "Mary Bonnici")
 5:  Hash.Add("555-55-5557", "Paul Kimmel")
 6:
 7:  Dim Enumerator As IDictionaryEnumerator = Hash.GetEnumerator
 8:
 9:  While (Enumerator.MoveNext())
10:   Debug.WriteLine(Enumerator.Key & "=" & _
11:    Enumerator.Value)
12:  End While
13:
14: End Sub

Tip -

Enumerator objects are read-only. To modify elements of a collection like a HashTable, you can use a For Next loop, indexing the elements of the collection directly.


HashTable uses an IDictionaryEnumerator object to iterate over elements. Line 7 declares an enumerator and lines 9 through 12 iterate over each element displaying the key and value pairs. (.Key and .Value were not defined in the IEnumerator interface; they were added in the IDictionaryEnumerator.)

SortedList

The SortedList ADT is based on the dictionary interface. Recall from the last section that a dictionary is a collection of key (or name) and value pairs. SortedList maintains two internal arrays. One keeps track of keys and the second keeps track of values. As with a hash table, the key values of a sorted list must be unique.

SortedList has methods similar to ArrayList, in addition to the key and value pairs introduced with HashTable. SortedList is defined in the System.Collections namespace. For more information on SortedList, look in the MSDN help files.

Queue

Queue data structures are also referred to as First In First Out (FIFO) data structures. Think of a queue as a line. There is a first in line, a last in line, and everything in between.

Just as Stacks have a language for adding elements to the collection—Push and Pop—to denote adding and removing elements to a Stack, Queue uses the notion of enqueuing and dequeuing. All of the collection-based ADTs work with Objects; hence to enqueue means to add an Object to the queue and to dequeue means to remove an Object from the queue.

Queues are a natural choice when you want the first item put into a collection to be the first item out. Listing 3.6 demonstrates basic queue behavior.

Listing 3.6 Basic queue behavior.

 1: Sub DemoQueue()
 2:
 3:  Dim Q As New Queue()
 4:  Q.Enqueue("One")
 5:  Q.Enqueue("Two")
 6:
 7:  While (Q.Count > 0)
 8:   Debug.WriteLine(Q.Dequeue)
 9:  End While
10:
11: End Sub

The output from Listing 3.6 is One and Two. The elements are dequeued in exactly the same order in which they were enqueued.

Queues implement several of the same COM interfaces as other ADTs defined in the System.Collections namespace, like ICollection, IEnumerable, and ICloneable. For this reason queues have many of the same operations by name as other ADTs.

Assessing the ADTs

For general purpose in-memory storage, ArrayList will suffice. For key and value pairs, use HashTable or SortedList. If you want objects stored and retrieved in the same order, use Queue, and if you want the last element put into a collection to be the first one out, use Stack. The fundamental behaviors of the collection classes are identical. The semantic operations are consistent with the type of data structure.

Shadow Variables

Variables can exist in several places and in broader or narrower scopes. Sometimes this causes problems because it may be unclear to the compiler what the code means. For example, when a variable is defined at the procedure level and a variable with the same name exists in a block within the procedure, the variable in the block hides the variable with the same name in the outer scope. In Visual Basic .NET, this is referred to as shadowing. That is, the narrow scope variable shadows the outer scope variable.

At the block level, the compiler will report an error: "Block-level variable hides a variable in an enclosing block." An example of causation follows:

Sub Main()

 Dim V As Object

 If True Then
  Dim V As Object
  V = Nothing
 End If

End Sub

V in the inner block hides the variable outside of the If...Then block. To resolve this problem, simply rename or remove the block-level variable. However, if the problem exists at the module, class, or structure level, you can use a variety of techniques to clarify your intent.

Suppose Module1 declares a module-level Integer I, and suppose Sub Main declares a local variable I. Within Main all references to I will shadow the variable in the outer, module scope. This scenario is demonstrated by the next code fragment:

Module Module1

 Dim I As Integer

 Sub Main()

  Dim I As Integer
  I = 10
  Debug.WriteLine(Module1.I)
 End Sub

End Module

All references to I in Main refer to the I defined in Main. What if you wanted to modify the outer scope's I variable? In the case of a module, you would simply prefix the reference to I with the module name. Based on this fragment, if we wanted to refer to I in the outer scope, Module1.I would clearly indicate our intent to the compiler.

If the containing entity were a class, we would use the Me (reference to self) object, as in Me.I. The reason we can use the module name is that modules are for all intents and purposes classes with all shared members. The reason we cannot reliably use the class name and have to use Me is that classes are instantiated many times, and each object will have a different name.

Visual Basic .NET also introduces the Shadows keyword. Shadows is used when a subclass reintroduces a name in a superclass, hiding the name in the superclass and that is what you want to happen. I will defer the discussion of Shadows until Chapter 7, "Creating Classes," because it will be beneficial if inheritance and overloading methods are covered before we continue talking about the Shadows qualifier.

Functions and Subroutines

If you are familiar with VB6 functions and subroutines, you have the basics for now. There are some additional changes that you need to familiarize yourself with, including the revision allowing function results to be assigned with the Return keyword.

Chapter 5 provides you ample opportunity to experiment with functions, subroutines, and the new Structure idiom. Chapter 5 also includes some function qualifiers that you may have forgotten about, including Optional and ParamArrays.

Defining Structures

The Type idiom has been replaced with the Structure idiom. Structures are significantly more advanced than types. In addition to the ability to define aggregate data types, Structures allow you to define parameterized constructor, property, and method members.

Chapter 5 combines coverage of functions and subroutines with coverage of the new Structure idiom and examples of defining and using enumerations.

Using Objects

We have talked a lot about objects. If you have programmed in C++, many of the idioms related to object-oriented programming will seem familiar to you, but in this book, we make no assumptions about your experience with object-oriented programming. Although Chapter 7 discusses defining classes in detail, including coverage of inheritance, polymorphism, and encapsulation, this section briefly introduces a few basic concepts, so you will have no trouble working with objects for now.

Visual Basic .NET supports true object-oriented programming. Critical to this is the idea of inheritance and constructors. We'll defer our discussion of inheritance until Chapter 7, but you need to know about constructors now.

What Is a Constructor?

A constructor is a member of a class that has a special role—to initialize the class. In VB6 this role was satisfied by the Class_Initialize method, which was called implicitly when you created an object.

Class_Initialize took no arguments, which meant you were unable to initialize an object with externally passed-in values. Visual Basic .NET replaces Class_Initialize with the constructor New(). New is the constructor subroutine that is responsible for initializing objects. Every object has one constructor that takes no arguments, but New can be overloaded to take one or more arguments depending on the needs of the class.

New is invoked, when you use the New keyword when creating an instance of an object, as in the following:

Dim Button As New Button()

This example creates an instance of a Button component—which replaces the Command control from VB6. Tracing into the Button class would show that the preceding statement traces right into a subroutine named New, the constructor in Visual Basic .NET. Every class can have many constructors and inherits at least one from the Object class. Listing 3.7 demonstrates a constructor call that dynamically adds a button to a form at runtime.

Listing 3.7 A Button component is dynamically constructed with New Button.

 1:  Private Sub Button1_Click(ByVal sender As System.Object, _
 2:   ByVal e As System.EventArgs) Handles Button1.Click
 3: 
 4:   Static NextTop As Integer = 0
 5:   Dim Button As New Button()
 6:   Button.Text = "Button" & Controls().Count
 7:   Button.Top = NextTop
 8:   Controls().Add(Button)
 9:   NextTop = Button.Bottom
10: 
11:  End Sub

When the Click event is invoked the event handler in Listing 3.7 is called. Line 4 declares a static variable that tracks the last bottom location of the most recently created control on line 9. Line 5 constructs a new Button component. Line 6 provides a unique caption for the button, line 7 positions the button, and line 8 adds the button to the form's Controls list.

In addition to demonstrating construction, Listing 3.7 demonstrates dynamic component creation. Refer to Chapter 16, "Designing User Interfaces," for more information on dynamic component creation as a means to create flexible UIs.

Parameterized Constructors

Now that you know a constructor is the term used to refer to the New method, you might ask what a parameterized constructor is. Quite simply, a parameterized constructor is a constructor that takes arguments. Because New is essentially a method, adding arguments to New is the same as adding arguments to any method.

The slight difference in passing arguments to regular methods versus the New constructor method is that the arguments are passed in the parentheses following the data type rather than the New keyword. Here's an example:

Dim MyException As Exception = New Exception("Raise an exception!")

The statement constructs a new Exception object passing the string "Raise an exception!" to the constructor. From the code fragment you might assume that there is a subroutine Exception that takes a String argument, but in reality the New method is getting the argument.


Note -

In C++ and Java the constructor has the same name as the class. Object Pascal, by convention, uses Create for a constructor. These languages pass the arguments to the constructor in parentheses after the constructor name. If Visual Basic .NET were to follow this convention, the construction of the Exception would look like this:

Dim MyException As Exception = New("Raise an exception!") Exception

That seems a little strange. Just remember that New is the constructor and param-eters are being passed to New even though it doesn't look as if this is the case.


Destructors

A destructor is a special method that is used to deinitialize an object. In VB6 the Class_Terminate method was called implicitly when an object was destroyed. Visual Basic .NET implements a garbage collector (GC) rather than explicit object destruction. The GC is a subprogram that runs in the background returning memory assigned to objects back to the memory pool.


Caution -

You can explicitly run the GC, but it isn't recommended that you do so. To force the GC to run, type System.GC.Collect.

The GC is designed to run efficiently during times when your program is idle. Running the GC manually contradicts its intended use.


You cannot tell when the garbage collector will actually run and release your objects, and no equivalent of Class_Terminate is called in Visual Basic .NET.

By convention, a method named Dispose can be implemented to perform any cleanup you may need in your classes, for example, closing an open file. Chapter 7 will demonstrate how to implement and use Dispose; you may encounter examples of it in code between now and Chapter 7.

Exception Handling

Exception handling is a new feature in Visual Basic .NET. Exception handlers are intended to ultimately replace the On Error Goto construct. Exception handlers provide a more robust means of communicating and managing errors and have been available in more advanced languages like C++ and Object Pascal for several years now.

All exception classes are subclassed from the System.Exception class. The basic exception class contains information similar to that found in the VB6 Err object. The differences are that there is no global exception object always available—exception objects are created and raised when needed—and in Visual Basic .NET you can subclass and define your own exception classes.

Try...Catch

The Try...Catch block is used to catch and handle errors for which you can provide a resolution. The basic Try...Catch block takes the following form:

Try
 ' some protected code
Catch
 ' resolution on error
End Catch

The protected code goes in the Try part of the block and the resolution part is written in the Catch block. The Try code is always executed and the Catch block is executed only in the event of an error.

VB6 simply shuts down on an unhandled error. In some circumstances it may be okay to shut down the application, but most of the time, it is another aggravation users don't need. Visual Basic .NET shuts down the application and makes an attempt to run a Just-In-Time debugger.

If an error is important enough to shut down your application, exceptions will at least provide you with an opportunity to do so in an organized manner. For example, the unavailability of a database may be a sufficient reason to shut down the application. However, when possible, if you can program a resolution to the problem and retry the code, exception handlers allow you to do that. If the problem doesn't need a resolution, you might simply want to show the error to the user and keep on trucking.

There are two kinds of exception handling. The first is referred to as the exception handling block, and the second is referred to as the resource protection block. Both are exception-handling blocks, with the difference being the keywords used to define the blocks, and their intended uses. The exception handler designed to handle errors uses the Try...Catch...End Try block and the resource protection exception handler uses the Try...Finally...End Try block. These types are demonstrated in the following sub-sections. Listing 3.8 demonstrates a generic Try...Catch block used to protect against division by zero.


Note -

Recall that division-by-zero for Doubles yields a return value Infinity. Integer division-by-zero raises a DivideByZeroException.


Listing 3.8 Generic exception handler that catches all exceptions.

 1: Public Sub TestGenericException()
 2:
 3:   Dim I As Integer
 4:   Dim Numerator As Integer = 5
 5:   Dim Denominator As Integer = 0
 6:
 7:   Try
 8:    I = Numerator \ Denominator
 9:    Debug.WriteLine(I)
10:   Catch
11:   End Try
12: End Sub

Lines 7 through 11 demonstrate a generic Try...Catch exception-handling block. This code can be used where you would have written an On Error Resume Next in VB6. A Catch statement with no code is referred to as a silent exception. (Basically, a silent exception works similarly to Resume Next. The reason the demonstration code contains more than I = 1/0 is because the Visual Basic .NET compiler can catch direct integer division by zero, whereas the VB6 compiler cannot.)

The Try part of the exception handler is simple enough that if we want to report the division-by-zero error, we might write a literal message that indicates what went wrong. To implement notification behavior, add something like MsgBox("Division by zero error") between the Catch and End Try lines of code.

Catching Specific Exceptions

Except when you prefer a silent exception, you will probably want to catch and respond to specific kinds of exceptions. Catching specific exceptions is supported with a slightly modified syntax.

In Listing 3.8 we know that we are anticipating a potential division-by-zero error. If we want to catch that error specifically, we need to make a minor modification to the Catch block to indicate our intentions. Listing 3.9 demonstrates catching a specific type of exception, the DivideByZeroException.

Listing 3.9 A Catch block for a specific exception.

 1: Public Sub TestGenericException()
 2:
 3:   Dim I As Integer
 4:   Dim Numerator As Integer = 5
 5:   Dim Denominator As Integer = 0
 6:
 7:   Try
 8:    I = Numerator \ Denominator
 9:    Debug.WriteLine(I)
10:   Catch e As System.DivideByZeroException
11:    MsgBox(e.Message)
12:   End Try
13:
14: End Sub

The revision from Listing 3.8 to 3.9 is constrained to lines 10 and 11. The Catch statement is a declarator. Thus the revision to the Catch statement as demonstrated on line 10 declares an object of type System.DivideByZeroException and initializes it with a caught exception. The Message property of the exception object is displayed in a message dialog box.

Based on the revised definition of this Catch block, all other exceptions would be ignored. If you want to catch all exceptions, use Catch without an exception type. If you want to catch specific exceptions, list each type of exception you would like to handle with an additional Catch statement. Listing 3.10 demonstrates catching multiple exceptions.

Listing 3.10 Multiple Catch clauses.

 1: Public Sub BackupFile(ByVal FileName As String)
 2:  Dim F As System.IO.File
 3:  Try
 4:   F.Delete(FileName + ".bak")
 5:   F.Copy(FileName, FileName + ".bak")
 6:
 7:  Catch e As System.UnauthorizedAccessException
 8:   MsgBox(e.Message)
 9:  Catch e As System.IO.FileNotFoundException
10:   MsgBox(e.Message)
11:  End Try
12: End Sub

Listing 3.10 demonstrates two Catch clauses. Line 7 catches the UnauthorizedAccessException that might occur when you attempt to delete or modify a read-only file. Line 9 catches the FileNotFoundException that might occur when you try to copy a file that doesn't exist.

It is impossible to include all possible ways in which the preceding code might be written. For example, you could check to see if the files existed before trying to delete and copy them. As a general rule, if an If...Then conditional check can preclude the problem, use the conditional check. However, the exception handler will catch errors you might not have thought of. In the listing you could check to see if the file exists before attempting to delete the file, but what if the file exists and is write-protected? You would still get an UnauthorizedAccessException.

Think of exception handlers as safety nets for high-wire walkers and an If...Then conditional similar to deciding whether or not you should be on the wire in the first place. If you have to be up on the wire, it's a good idea to have a net. All of the second-guessing in the world won't catch you if you fall off the wire but have decided to forego the net.

From Bjarne Stroustrup (Stroustrup 1994), the inventor of C++ and father of object- oriented languages for the PC, we know the following things about the aim of an exception handler:

  • "An exception handler isn't intended as simply an alternative return mechanism ... but specifically as a mechanism for supporting the construction of fault-tolerant systems."

  • "An exception handler isn't intended to turn every function into a fault-tolerant entity, but rather as a mechanism by which a subsystem can be given a large measure of fault tolerance even if its individual functions are written without regard for overall error-handling strategies."

  • And most importantly, "An exception handler isn't meant to constrain designers to a single 'correct' notion of error-handling, but to make the language more expressive."

Summarized, this means that the application of exception handling, like many things in programming, requires a subjective measure of good taste and experience.

Throughout this book I often include a statement or rationale indicating why an exception handler was used for each unique occurrence, or rationale for an exception block in code.

Raising Exceptions

Exceptions are objects. If you elect to raise an exception to indicate an error condition that a procedure is not going to handle, you create an exception object like any other object and throw it.

To create an exception instance and throw it, write code as demonstrated in the following example:

Throw New Exception("message text")

Throw is a Visual Basic .NET keyword used similarly to the Raise method in VB6. New invokes the constructor for the exception class, and Exception represents any valid exception class subclassed from System.Exception.

If you want to rethrow an exception in an exception block, you can add the Throw statement all by itself in the Catch block. Throw without an explicit exception object raises the last caught exception.

Exception Filters

Catch clauses can have a filter applied that enables you to refine the Catch statement. Consider the example in Listing 3.10. Suppose we only wanted to catch the exception UnauthorizedAccessException if the file exists. We could accomplish this goal by adding a when predicate to the Catch statement:

Catch e As System.UnauthorizedAccessException _
when F.Exists(FileName + ".bak")

Due to the addition of when, this Catch clause will only catch an UnauthorizedAccessException if the backup file exists (perhaps the file exists but is read-only).

Try...Finally

Try...Finally blocks are referred to as resource protection blocks. Although Catch blocks are only invoked upon an exception of the type indicated in the Catch clause—keeping in mind that Catch without an exception predicate catches all exceptions—Finally clauses are always invoked whether there is an exception or not.

Try...Finally blocks are used to ensure that allocated resources are cleaned up. The general order of the Try...Finally statement is illustrated in this algorithmic example:

Allocate resource (for example, open a file)
Try
 Use the resource (for example, add some text to the file)
Finally
 Clean up the resource
End Try

In this example, only Try, Finally, and End Try are literals. Demonstrating the Try...Finally block using the StreamWriter class, we could create a StreamWriter, try to write some text, and close the writer in the Finally block. Listing 3.11 demonstrates both StreamWriter and the Try...Finally statement.

Listing 3.11 A resource protection block protecting a file managed by the StreamWriter object

 1: Public Sub WriteText(ByVal FileName As String, _
   ByVal SomeText As String)
 2:
 3:  Dim W As New System.IO.StreamWriter(FileName, True)
 4:  Try
 5:   W.WriteLine(SomeText)
 6:  Finally
 7:   W.Close()
 8:  End Try
 9:
10: End Sub

As described in the algorithm, the StreamWriter is allocated on line 3. Line 5 uses the protected resource to write SomeText, and the Finally block ensures that the file represented by the StreamWriter object is closed.

Try...Catch and Try...Finally blocks may be combined to protect against exceptions and resource corruption. To this end, you may nest or combine exception-handling blocks as needed for the desired effect. Consider Listing 3.11 again. What if you want to catch the UnauthorizedAccessException and ensure that opened streams were actually closed? We can combine an exception block with a resource protection block as demonstrated in Listing 3.12.

Listing 3.12 A Try...Finally block nested inside a Try...Catch block

 1: Public Sub WriteText(ByVal FileName As String, _
   ByVal SomeText As String)
 2:
 3:  Try
 4:   Dim W As New System.IO.StreamWriter("sample.txt", True)
 5:
 6:   Try
 7:    W.WriteLine("Add some text")
 8:   Finally
 9:    W.Close()
10:   End Try
11:
12:  Catch e As System.UnauthorizedAccessException
13:   MsgBox(e.Message, MsgBoxStyle.Critical)
14:  End Try
15:
16: End Sub

The outer Try...Catch block catches UnauthorizedAccessException for all of the code in between lines 3 and 12. If we get past line 4, the Try...Finally block will ensure that the open StreamWriter is closed on line 9.

You will see many examples of Try...Catch and Try...Finally blocks throughout this book. As with exception-handling blocks, I will elaborate on the rationale behind Try...Finally blocks where it is useful and not redundant to do so.

Summary

Chapter 3 provided you with an opportunity to experiment with some of the new features in Visual Basic .NET. You will use structured exception handling and create objects in every substantial program you write in Visual Basic .NET.

In addition to demonstrating how to use exception-handling blocks, create objects, and pass parameters to constructors, this chapter demonstrated how to declare and initialize variables and use several of the new Abstract Data Types defined in System. Collections.

Chapter 4 will look at how to get more mileage out of your development experience by using macros and the Automation Extensibility Model to customize and extend the Visual Studio IDE.


sponsor links - buy

Submit a chapter

Are you an author or publisher? Interested in seeing a sample chapter of your book on the ASPAlliance.com website? Click here to list your sample chapter.