Chapter 3
Objects and Visual Basic
What's in this chapter?
Object-Oriented Terminology
Composition of an Object
Characteristics of Value Types versus Reference Types
Primitive Types
Commands: If Then, Else, Select Case
Common Value Types (Structures)
Common Reference Types (Classes)
XML literals
Parameter passing ByVal and ByRef
Variable scope
Working with Objects
Understanding Binding
Data type conversions
Creating Classes
Event Handling
Object-Oriented Programming
This chapter takes you through the basic syntax of Visual Basic. With its transition many years ago to .NET, Visual Basic like all native .NET languages, became an object-oriented language. At the time this was a major transition, and even now you continue to talk about how Visual Basic supports the four major defining concepts required for a language to be fully object-oriented:
Chapter 4 discusses these four concepts in detail; this chapter focuses on the syntax that enables you to utilize classes that already implement these concepts. The concepts are then illustrated through a review of the core types that make up Visual Basic, as well as through the creation of a custom class that leverages these core concepts.
Visual Basic is also a component-based language. Component-based design is often viewed as a successor to object-oriented design, so component-based languages have some other capabilities. These are closely related to the traditional concepts of object orientation:
This chapter explains how to create and use classes and objects in Visual Basic. You won't get too deeply into code, but it is important that you spend a little time familiarizing yourself with basic object-oriented terms and concepts.
The baseline is simply a button to initiate the sample code combined with a text box. To test a snippet, you add a new method to MainWindow, and then call this method from the Click event handler for the button. The sample download shows these methods in place, and then when you are ready to test the next method, remove the single quote to activate that method. Once you are done with one sample, comment out that method call and move to the next.
Experienced developers generally consider integers, characters, Booleans, and strings to be the basic building blocks of any language. As noted previously in .NET, all objects share a logical inheritance from the base Object class. This enables all of .NET to build on a common type system. As noted in Chapter 2, Visual Basic builds on the common type system shared across all .NET languages.
Because all data types are based on the core Object class, every variable you dimension can be assured of having a set of common characteristics. However, this logical inheritance does not require a common physical implementation for all variables. This is important because while everything in .NET is based on the Object class, under the covers .NET has two major implementations of types: value and reference.
For example, what most programmers consider to be some of the basic underlying types, such as Integer, Long, Character, and even Byte, are not implemented as classes. This is important, as you'll see when you look at boxing and the cost of transitioning between value types and reference types. The difference between value types and reference types is an underlying implementation difference:
Note that the two implementations are stored in different portions of memory. As a result, value types and reference types are treated differently within assignment statements, and their memory management is handled differently. It is important to understand how these differences affect the software you will write in Visual Basic. Understanding the foundations of how data is manipulated in the .NET Framework will enable you to build more reliable and better-performing applications.
Consider the difference between the stack and the heap. The stack is a comparatively small memory area in which processes and threads store data of fixed size. An integer or decimal value needs the same number of bytes to store data, regardless of the actual value. This means that the location of such variables on the stack can be efficiently determined. (When a process needs to retrieve a variable, it has to search the stack. If the stack contained variables that had dynamic memory sizes, then such a search could take a long time.)
Reference types do not have a fixed size—a string can vary in size from two bytes to nearly all the memory available on a system. The dynamic size of reference types means that the data they contain is stored on the heap, rather than the stack. However, the address of the reference type (that is, the location of the data on the heap) does have a fixed size, and thus can be (and, in fact, is) stored on the stack. By storing a reference only to a custom allocation on the stack, the program as a whole runs much more quickly, as the process can rapidly locate the data associated with a variable.
Storing the data contained in fixed and dynamically sized variables in different places results in differences in the way variables behave. Rather than limit this discussion to the most basic of types in .NET, this difference can be illustrated by comparing the behavior of the System.Drawing.Point structure (a value type) and the System.Text.StringBuilder class (a reference type).
The Point structure is used as part of the .NET graphics library, which is part of the System.Drawing namespace. The StringBuilder class is part of the System.Text namespace and is used to improve performance when you're editing strings.
First, you will examine how the System.Drawing.Point structure is used. To do this, you'll create a new method called ValueType() within your ProVB2012_Ch03 application. This new private Sub will be called from the ButtonTest click event handler. The new method will have the following format (code file: MainWindow.xaml.vb):
Private Sub ValueType() Dim ptX As System.Drawing.Point = New System.Drawing.Point(10, 20) Dim ptY As System.Drawing.Point ptY = ptX ptX.X = 200 TextBox1.Text = "Pt Y = " & ptY.ToString() End Sub
The output from this operation will be Pt Y = {{X = 10, Y = 20}}, is shown in .
When the code copies ptX into ptY, the data contained in ptX is copied into the location on the stack associated with ptY. Later, when the value of ptX changes, only the memory on the stack associated with ptX is altered. Changing ptX has no effect on ptY. This is not the case with reference types. Consider the following code, a new method called RefType, which uses the System.Text.StringBuilder class (code file: MainWindow.xaml.vb):
Private Sub RefType() Dim objX As System.Text.StringBuilder = New System.Text.StringBuilder("Hello World") Dim objY As System.Text.StringBuilder objY = objX objX.Replace("World", "Test") TextBox1.Text = "objY = " & objY.ToString() End Sub
The output from this operation will be ObjY = Hello Test, as shown in , not ObjY = Hello World.
The first example using point values demonstrated that when one value type is assigned to another, the data stored on the stack is copied. This example demonstrates that when objY is assigned to objX, the address associated with objX on the stack is associated with objY on the stack. However, what is copied in this case isn't the actual data, but rather the address on the managed heap where the data is actually located. This means that objY and objX now reference the same data. When that data on the heap is changed, the data associated with every variable that holds a reference to that memory is changed. This is the default behavior of reference types, and is known as a shallow copy. Later in this chapter, you'll see how this behavior varies for strings.
The differences between value types and reference types go beyond how they behave when copied, and later in this chapter you'll encounter some of the other features provided by objects. First, though, take a closer look at some of the most commonly used value types and learn how .NET works with them.
Visual Basic, in common with other development languages, has a group of elements such as integers and strings that are termed primitive types. These primitive types are identified by keywords such as String, Long, and Integer, which are aliases for types defined by the .NET class library. This means that the line
Dim i As Long
is equivalent to the line
im i As System.Int64
The reason why these two different declarations are available has to do with long-term planning for your application. In most cases (such as when Visual Basic transitioned to .NET), you want to use the Short, Integer, and Long designations. If at some point your system decides to allow an integer to hold a larger 64 bit value you don't need to change your code.
On the other hand Int16, Int32, and Int64 specify a physical implementation; therefore, if your code is someday migrated to a version of .NET that maps the Integer value to Int64, then those values defined as Integer will reflect the new larger capacity, while those declared as Int32 will not. This could be important if your code were manipulating part of an interface where changing the physical size of the value could break the interface.
lists the primitive types that Visual Basic 2012 defines, and the structures or classes to which they map:
Primitive Type | .NET Class or Structure |
Byte | System.Byte (structure) |
Short | System.Int16 (structure) |
Integer | System.Int32 (structure) |
Long | System.Int64 (structure) |
Single | System.Single (structure) |
Double | System.Double (structure) |
Decimal | System.Decimal (structure) |
Boolean | System.Boolean (structure) |
Date | System.DateTime (structure) |
Char | System.Char (structure) |
String | System.String (class) |
You can perform certain operations on primitive types that you can't on other types. For example, you can assign a value to a primitive type using a literal:
Dim i As Integer = 32 Dim str As String = "Hello"
It's also possible to declare a primitive type as a constant using the Const keyword, as shown here:
Dim Const str As String = "Hello"
The value of the variable str in the preceding line of code cannot be changed elsewhere in the application containing this code at run time. These two simple examples illustrate the key properties of primitive types. As noted, most primitive types are, in fact, value types. The next step is to take a look at core language commands that enable you to operate on these variables.
The second issue related to that line is that you are concatenating the implicit value of the variable blnTrue with the value of the Environment.Newline constant. Note the use of an ampersand (&) for this action. This is a best practice in Visual Basic, because while Visual Basic does overload the plus (+) sign to support string concatenation, in this case the items being concatenated aren't necessarily strings. This is related to not setting Option Strict to On. In that scenario, the system will look at the actual types of the variables, and if there were two integers side by side in your string concatenation you would get unexpected results. This is because the code would first process the “+” and would add the values as numeric values.
Thus, since neither you nor the sample download code has set Option String to On for this project, if you replace the preceding & with a +, you'll find a runtime conversion error in your application. Therefore, in production code it is best practice to always use the & to concatenate strings in Visual Basic unless you are certain that both sides of the concatenation will always be a string. However, neither of these issues directly affect the use of the Boolean values, which when interpreted this way provide their ToString() output, not a numeric value.
Unfortunately, in the past developers had a tendency to tell the system to interpret a variable created as a Boolean as an Integer. This is referred to as implicit conversion and is related to Option Strict. It is not the best practice, and when .NET was introduced, it caused issues for Visual Basic, because the underlying representation of True in other languages doesn't match those of Visual Basic.
Within Visual Basic, True has been implemented in such a way that when converted to an integer, Visual Basic converts a value of True to -1 (negative one). This different from other languages, which typically use the integer value 1. Generically, all languages tend to implicitly convert False to 0, and True to a nonzero value.
To create reusable code, it is always better to avoid implicit conversions. In the case of Booleans, if the code needs to check for an integer value, then you should explicitly convert the Boolean and create an appropriate integer. The code will be far more maintainable and prone to fewer unexpected results.
There are three sizes of integer types in .NET. The Short is the smallest, the Integer represents a 32-bit value, and the Long type is an eight-byte or 64-bit value. In addition, each of these types also has two alternative types. In all, Visual Basic supports the nine Integer types described in .
A Short value is limited to the maximum value that can be stored in two bytes, aka 16 bits, and the value can range between –32768 and 32767. This limitation may or may not be based on the amount of memory physically associated with the value; it is a definition of what must occur in the .NET Framework. This is important, because there is no guarantee that the implementation will actually use less memory than when using an Integer value. It is possible that in order to optimize memory or processing, the operating system will allocate the same amount of physical memory used for an Integer type and then just limit the possible values.
The Short (or Int16) value type can be used to map SQL smallint values when retrieving data through ADO.NET.
An Integer is defined as a value that can be safely stored and transported in four bytes (not as a four-byte implementation). This gives the Integer and Int32 value types a range from –2147483648 to 2147483647. This range is adequate to handle most tasks.
The main reason to use an Int32 class designation in place of an Integer declaration is to ensure future portability with interfaces. In future 64-bit platforms, the Integer value might be an eight-byte value. Problems could occur if an interface used a 64-bit Integer with an interface that expected a 32-bit Integer value, or, conversely, if code using the Integer type is suddenly passed to a variable explicitly declared as Int32.
Unless you are working on an external interface that requires a 32-bit value, the best practice is to use Integer so your code is not constrained by the underlying implementation. However, you should be consistent, and if using Int32 use it consistently throughout your application.
The Visual Basic .NET Integer value type matches the size of an Integer value in SQL Server.
The Long type is aligned with the Int64 value. The Long has an eight-byte range, which means that its value can range from –9223372036854775808 to 9223372036854775807. This is a big range, but if you need to add or multiply Integer values, then you need a large value to contain the result. It's common while doing math operations on one type of integer to use a larger type to capture the result if there's a chance that the result could exceed the limit of the types being manipulated.
The Long value type matches the bigint type in SQL.
Another way to gain additional range on the positive side of an Integer type is to use one of the unsigned types. The unsigned types provide a useful buffer for holding a result that might exceed an operation by a small amount, but this isn't the main reason they exist. The UInt16 type happens to have the same characteristics as the Character type, while the Uint32 type has the same characteristics as a system memory pointer on a 32-byte system.
However, never write code that attempts to leverage this relationship. Such code isn't portable, as on a 64-bit system the system memory pointer changes and uses the Uint64 type. However, when larger integers are needed and all values are known to be positive, these values are of use. As for the low-level uses of these types, certain low-level drivers use this type of knowledge to interface with software that expects these values, and they are the underlying implementation for other value types. This is why, when you move from a 32-bit system to a 64-bit system, you need new drivers for your devices, and why applications shouldn't leverage this same type of logic.
Just as there are several types to store integer values, there are three implementations of value types to store real number values, shown in .
The Single type contains four bytes of data, and its precision can range anywhere from 1.401298E-45 to 3.402823E38 for positive values and from –3.402823E38 to –1.401298E-45 for negative values.
It can seem strange that a value stored using four bytes (like the Integer type) can store a number that is larger than even the Long type. This is possible because of the way in which numbers are stored; a real number can be stored with different levels of precision. Note that there are six digits after the decimal point in the definition of the Single type. When a real number gets very large or very small, the stored value is limited by its significant places.
Because real values contain fewer significant places than their maximum value, when working near the extremes it is possible to lose precision. For example, while it is possible to represent a Long with the value of 9223372036854775805, the Single type rounds this value to 9.223372E18. This seems like a reasonable action to take, but it isn't a reversible action. The following code demonstrates how this loss of precision and data can result in errors. To run it, a Sub called Precision is added to the ProVB2012_Ch03 project and called from the Click event handler for the ButtonTest control (code file: MainWindow.xaml.vb):
Private Sub Precision() Dim l As Long = Long.MaxValue Dim s As Single = Convert.ToSingle(l) TextBox1.Text = l & Environment.NewLine TextBox1.Text &= s & Environment.NewLine s -= 1000000000000 l = Convert.ToInt64(s) TextBox1.Text &= l & Environment.NewLine End Sub
The code creates a Long that has the maximum value possible, and outputs this value. Then it converts this value to a Single and outputs it in that format. Next, the value 1000000000000 is subtracted from the Single using the -= syntax, which is similar to writing s = s – 1000000000000. Finally, the code assigns the Single value back into the Long and then outputs both the Long and the difference between the original value and the new value. The results, shown in , probably aren't consistent with what you might expect.
The first thing to notice is how the values are represented in the output based on type. The Single value actually uses an exponential display instead of displaying all of the significant digits. More important, as you can see, the result of what is stored in the Single after the math operation actually occurs is not accurate in relation to what is computed using the Long value. Therefore, both the Single and Double types have limitations in accuracy when you are doing math operations. These accuracy issues result from storage limitations and how binary numbers represent decimal numbers. To better address these issues for precise numbers, .NET provides the Decimal type.
The behavior of the previous example changes if you replace the value type of Single with Double. A Double uses eight bytes to store values, and as a result has greater precision and range. The range for a Double is from 4.94065645841247E-324 to 1.79769313486232E308 for positive values and from –1.79769313486231E308 to –4.94065645841247E-324 for negative values. The precision has increased such that a number can contain 15 digits before the rounding begins. This greater level of precision makes the Double value type a much more reliable variable for use in math operations. It's possible to represent most operations with complete accuracy with this value. To test this, change the sample code from the previous section so that instead of declaring the variable s as a Single you declare it as a Double and rerun the code. Don't forget to also change the conversion line from ToSingle to ToDouble. The resulting code is shown here with the Sub called PrecisionDouble (code file: MainWindow.xaml.vb):
Private Sub PrecisionDouble() Dim l As Long = Long.MaxValue Dim d As Double = Convert.ToDouble(l) TextBox1.Text = l & Environment.NewLine TextBox1.Text &= d & Environment.NewLine d -= 1000000000000 l = Convert.ToInt64(d) TextBox1.Text &= l & Environment.NewLine TextBox1.Text &= Long.MaxValue – 1 End Sub
The results shown in look very similar to those from Single precision except they almost look correct. The result, as you can see, is off by just 1. On the other hand, this method closes by using the original MaxValue constant to demonstrate how a 64-bit value can be modified by just one and the results are accurate. The problem isn't specific to .NET; it can be replicated in all major development languages. Whenever you choose to represent very large or very small numbers by eliminating the precision of the least significant digits, you have lost that precision. To resolve this, .NET introduced the Decimal, which avoids this issue.
The Decimal type is a hybrid that consists of a 12-byte integer value combined with two additional 16-bit values that control the location of the decimal point and the sign of the overall value. A Decimal value consumes 16 bytes in total and can store a maximum value of 79228162514264337593543950335. This value can then be manipulated by adjusting where the decimal place is located. For example, the maximum value while accounting for four decimal places is 7922816251426433759354395.0335. This is because a Decimal isn't stored as a traditional number, but as a 12-byte integer value, with the location of the decimal in relation to the available 28 digits. This means that a Decimal does not inherently round numbers the way a Double does.
As a result of the way values are stored, the closest precision to zero that a Decimal supports is 0.0000000000000000000000000001. The location of the decimal point is stored separately; and the Decimal type stores a value that indicates whether its value is positive or negative separately from the actual value. This means that the positive and negative ranges are exactly the same, regardless of the number of decimal places.
Thus, the system makes a trade-off whereby the need to store a larger number of decimal places reduces the maximum value that can be kept at that level of precision. This trade-off makes a lot of sense. After all, it's not often that you need to store a number with 15 digits on both sides of the decimal point, and for those cases you can create a custom class that manages the logic and leverages one or more decimal values as its properties. You'll find that if you again modify and rerun the sample code you've been using in the last couple of sections that converts to and from Long values by using Decimals for the interim value and conversion, your results are accurate.
The default character set under Visual Basic is Unicode. Therefore, when a variable is declared as type Char, Visual Basic creates a two-byte value, since, by default, all characters in the Unicode character set require two bytes. Visual Basic supports the declaration of a character value in three ways. Placing a $c$ following a literal string informs the compiler that the value should be treated as a character, or the Chr and ChrW methods can be used. The following code snippet shows how all three of these options work similarly, with the difference between the Chr and ChrW methods being the range of available valid input values. The ChrW method allows for a broader range of values based on wide character input.
Dim chrLtr_a As Char = "a"c Dim chrAsc_a As Char = Chr(97) Dim chrAsc_b As Char = ChrW(98)
To convert characters into a string suitable for an ASCII interface, the runtime library needs to validate each character's value to ensure that it is within a valid range. This could have a performance impact for certain serial arrays. Fortunately, Visual Basic supports the Byte value type. This type contains a value between 0 and 255 that matches the range of the ASCII character set.
In Visual Basic, the Byte value type expects a numeric value. Thus, to assign the letter “a” to a Byte, you must use the appropriate character code. One option to get the numeric value of a letter is to use the Asc method, as shown here:
Dim bytLtrA as Byte = Asc("a")
You can, in fact, declare date values using both the DateTime and Date types. Visual Basic also provides a set of shared methods that provides some common dates. The concept of shared methods is described in more detail in the next chapter, which covers object syntax, but, in short, shared methods are available even when you don't create an instance of a class.
For the DateTime structure, the Now method returns a Date value with the local date and time. The UtcNow method works similarly, while the Today method returns a date with a zero time value. These methods can be used to initialize a Date object with the current local date, or the date and time based on Universal Coordinated Time (also known as Greenwich Mean Time), respectively. You can use these shared methods to initialize your classes, as shown in the following code sample (code file: MainWindow.xaml.vb):
Private Sub Dates() Dim dtNow = Now() Dim dtToday = Today() TextBox1.Text = dtNow & Environment.NewLine TextBox1.Text &= dtToday.ToShortDateString & Environment.NewLine TextBox1.Text &= DateTime.UtcNow() & Environment.NewLine Dim dtString = #12/13/2009# TextBox1.Text &= dtString.ToLongDateString() End Sub
Running this code results in the output shown in .
As noted earlier, primitive values can be assigned directly within your code, but many developers seem unaware of the format, shown previously, for doing this with dates. Another key feature of the Date type is the capability to subtract dates in order to determine a difference between them. The subtract method is demonstrated later in this chapter, with the resulting Timespan object used to output the number of milliseconds between the start and end times of a set of commands.
Shared Method | Description |
Empty | This is actually a property. It can be used when an empty String is required. It can be used for comparison or initialization of a String. |
Compare | Compares two objects of type String. |
CompareOrdinal | Compares two Strings, without considering the local national language or culture. |
Concat | Concatenates two or more Strings. |
Copy | Creates a new String with the same value as an instance provided. |
Equals | Determines whether two Strings have the same value. |
IsNullorEmpty | This shared method is a very efficient way of determining whether a given variable has been set to the empty string or Nothing. |
Not only have creation methods been encapsulated, but other string-specific methods, such as character and substring searching, and case changes, are now available from String object instances.
The instance method SubString is a powerful method when you want to break out a portion of a string. For example, if you have a string “Hello World” and want only the first word, you would take the substring of the first five characters. There are two ways to call this method. The first accepts a starting position and the number of characters to retrieve, while the second accepts the starting location. The following code shows examples of using both of these methods on an instance of a String (code file: MainWindow.xaml.vb), and the resulting output is the first pair of strings shown in :
' Sub String Dim subString = "Hello World" TextBox1.Text = subString.Substring(0, 5) & Environment.NewLine TextBox1.Text &= subString.Substring(6) & Environment.NewLine TextBox1.Text &= line & Environment.NewLine
These instance methods enable you to justify a String so that it is left- or right-justified. As with SubString, the PadLeft and PadRight methods are overloaded. The first version of these methods requires only a maximum length of the String, and then uses spaces to pad the String. The other version requires two parameters: the length of the returned String and the character that should be used to pad the original String (code file: MainWindow.xaml.vb):
' Pad Left & Pad Right Dim padString = "Padded Characters" TextBox1.Text &= padString.PadLeft("30") & Environment.NewLine TextBox1.Text &= padString.PadRight("30", "_") & Environment.NewLine TextBox1.Text &= line & Environment.NewLine
shows the same string first with the left padded with spaces, then with the right padded with underscores. Note that because the default font on this screen isn't fixed size, the spaces are compacted and the two strings do not appear as the same length.
This instance method on a string enables you to separate it into an array of components. For example, if you want to quickly find each of the different elements in a comma-delimited string, you could use the Split method to turn the string into an array of smaller strings, each of which contains one field of data. As shown in , the csvString is converted to an array of three elements (code file: MainWindow.xaml.vb):
' String Split Dim csvString = "Col1, Col2, Col3" Dim stringArray As String() = csvString.Split(",") TextBox1.Text &= stringArray(0) & Environment.NewLine TextBox1.Text &= stringArray(1) & Environment.NewLine TextBox1.Text &= stringArray(2) & Environment.NewLine TextBox1.Text &= line & Environment.NewLine
To support the default behavior that people associate with the String primitive type, the String class doesn't function in the same way like most other classes. Strings in .NET do not allow editing of their data. When a portion of a string is changed or copied, the operating system allocates a new memory location and copies the resulting string to this new location. This ensures that when a string is copied to a second variable, the new variable references its own copy.
To support this behavior in .NET, the String class is defined as an immutable class. This means that each time a change is made to the data associated with a string, a new instance is created, and the original referenced memory is released for garbage collection. Garbage collection is covered in detail in Chapter 2. You should be aware that this can become a comparatively expensive operation. However, having strings be immutable is important to ensure that the String class behaves as people expect a primitive type to behave. Additionally, when a copy of a string is made, the String class forces a new version of the data into the referenced memory. All of these immutable behaviors ensures that each instance of a string references only its own memory.
Next consider the following code (code file: MainWindow.xaml.vb):
' String Concatenation vs String Builder Dim start = Now() Dim strRedo = "A simple string" For index = 1 To 10000 'Only 10000 times for concatenation strRedo &= "Making a much larger string" Next ' The date processing below uses the built in capability ' to subtract one datetime from another to get the difference ' between the dates as a timespan. This is then output as a ' number of milliseconds. TextBox1.Text &= "Time to concatenate strings: " & (Now().Subtract(start)).TotalMilliseconds().ToString() & " String length: " & strRedo.Length.ToString() & Environment.NewLine TextBox1.Text &= line & Environment.NewLine
This code does not perform well. For each assignment operation on the strMyString variable, the system allocates a new memory buffer based on the size of the new string, and copies both the current value of strMyString and the new text that is to be appended. The system then frees its reference to the previous memory that must be reclaimed by the garbage collector. As this loop continues, the new memory allocation requires a larger chunk of memory. Therefore, operations such as this can take a long time.
To illustrate this, you'll note that the code captures the start time before doing the 10,000 concatenations, and then within the print statement uses the DateTime.Subtract method to get the difference. That difference is returned as an object of type Timespan, between the start time and the print time. This difference is then expressed in milliseconds (refer to ).
However, .NET offers an alternative in the System.Text.StringBuilder object, shown in the following snippet (code file: MainWindow.xaml.vb):
start = Now() Dim strBuilder = New System.Text.StringBuilder("A simple string") For index = 1 To 1000000 '1 million times.... strBuilder.Append("Making a much larger string") Next TextBox1.Text &= "Time to concatenate strings: " & (Now().Subtract(start)).TotalMilliseconds().ToString() & " String length: " & strBuilder.ToString().Length.ToString() & Environment.NewLine TextBox1.Text &= line & Environment.NewLine End Sub
The preceding code works with strings but does not use the String class. The .NET class library contains the System.Text.StringBuilder class, which performs better when strings will be edited repeatedly. This class does not store strings in the conventional manner; it stores them as individual characters, with code in place to manage the ordering of those characters. Thus, editing or appending more characters does not involve allocating new memory for the entire string. Because the preceding code snippet does not need to reallocate the memory used for the entire string, each time another set of characters is appended it performs significantly faster.
Note that the same timing code is used in this snippet. However, for the StringBuilder, the loop executes one million times (versus ten thousand). The increase in the number of iterations was made in order to cause enough of a delay to actually show it requiring more than just one or two milliseconds to complete. Even with 100 times the number of iterations, still illustrates that this is a much more efficient use of system resources.
Ultimately, an instance of the String class is never explicitly needed, because the StringBuilder class implements the ToString method to roll up all of the characters into a string. While the concept of the StringBuilder class isn't new, because it is available as part of the Visual Basic implementation, developers no longer need to create their own string memory managers.
If you ever have to produce output based on a string you'll quickly find yourself needing to embed certain constant values. For example, it's always useful to be able to add a carriage-return line-feed combination to trigger a new line in a message box. One way to do this is to learn the underlying ASCII codes and then embed these control characters directly into your String or StringBuilder object.
Visual Basic provides an easier solution for working with these: the Microsoft.VisualBasic.Constants class. The Constants class, which you can tell by its namespace is specific to Visual Basic, contains definitions for several standard string values that you might want to embed. The most common, of course, is Constants.VbCrLf, which represents the carriage-return line-feed combination. Feel free to explore this class for additional constants that you might need to manipulate string output.
When working with a database, a value for a given column may be defined as Null. For a reference type this isn't a problem, as it is possible to set reference types to Nothing. However, for value types, it is necessary to determine whether a given column from the database or other source has an actual value prior to attempting to assign a potentially null value. The first way to manage this task is to leverage the DBNull class and the IsDBNull function.
This class is part of the System namespace, and you reference it as part of a comparison. The IsDBNull function accepts an object as its parameter and returns a Boolean that indicates whether the variable has been initialized. The following snippet shows two values, one a string being initialized to Nothing and the other being initialized as DBNull.Value (code file: MainWindow.xaml.vb):
Private Sub NullValues() Dim strNothing As String = Nothing Dim objectNull As Object = DBNull.Value TextBox1.Text = "" If IsDBNull(strNothing) Then TextBox1.Text = "But strNothing is not the same as Null." End If If System.DBNull.Value.Equals(objectNull) Then TextBox1.Text &= "objectNull is null." & Environment.NewLine End If End Sub
The output of this code is shown in . In this code, the strNothing variable is declared and initialized to Nothing. The first conditional is evaluated to False, which may seem counterintuitive, but in fact VB differentiates between a local value, which might not be assigned, and the actual DBNull value. This can be a bit misleading, because it means that you need to separately check for values which are Nothing.
The second conditional references the second variable, objectNull. This value has been explicitly defined as being a DBNull.value as part of its initialization. This is similar to how a null value would be returned from the database. The second condition evaluates to True. While DBNull is available, in most cases, developers now leverage the generic Nullable class described in Chapter 7, rather than work with DBNull comparisons.
Finally, note one last limitation of the ParamArray keyword: It can only be used on the last parameter defined for a given method. Because Visual Basic is grabbing an unlimited number of input values to create the array, there is no way to indicate the end of this array, so it must be the final parameter.
The concept of variable scope encapsulates two key elements. In the discussion so far of variables, you have not focused on the allocation and deallocation of those variables from memory. Chapter 2 covers the release of variables and memory once it is no longer needed by an application, so this section is going to focus on the allocation and to some extent the availability of a given variable.
In order to make clear why this is important, consider an allocation challenge. What happens when you declare two variables with the same name but at different locations in the code? For example, suppose a class declares a variable called myObj that holds a property for that class. Then, within one of that class's methods, you declare a different variable also named myObj. What will happen in that method is determined by a concept called: Scope. Scope defines the lifetime and precedence of every variable you declare, and provides the rules to answer this question.
The first thing to understand is that when a variable is no longer “in scope,” it is available to the garbage collector for cleanup. This handles the deallocation of that variable and its memory. However, when do variables move in and out of scope?
.NET essentially defines four levels of variable scope. The outermost scope is global. Essentially, just as your source code defines classes, it can also declare variables that exist the entire time that your application runs. These variables have the longest lifetime because they exist as long as your application is executing. Conversely, these variables have the lowest precedence. Thus, if within a class or method you declare another variable with the same name, then the variable with the smaller, more local scope is used before the global version.
After global scope, the next scope is at the class or module level. When you add properties to a class, you are creating variables that will be created with each instance of that class. The methods of that class will then reference those member variables from the class, before looking for any global variables. Note that because these variables are defined within a class, they are visible only to methods within that class. The scope and lifetime of these variables is limited by the lifetime of that class, and when the class is removed from the system, so are those variables. More important, those variables declared in one instance of a class are not visible in other classes or in other instances of the same class (unless you actively expose them, in which case the object instance is used to fully qualify a reference to them).
The next shorter lifetime and smaller scope is that of method variables. When you declare a new variable within a method, such variables, as well as those declared as parameters, are only visible to code that exists within that module. Thus, the method Add wouldn't see or use variables declared in the method Subtract in the same class.
Finally, within a given method are various commands that can encapsulate a block of code (mentioned earlier in this chapter). Commands such as If Then and For Each create blocks of code within a method, and it is possible within this block of code to declare new variables. These variables then have a scope of only that block of code. Thus, variables declared within an If Then block or a For loop only exist within the constraints of the If block or execution of the loop. Creating variables in a For loop is a poor coding practice and performance mistake and should be avoided.
Variable scope is applicable whether you are working with one of the primitive types, a library class or a custom object. Recall that everything in .NET is represented as an object and all objects have scope, be it at the level of the application root or within a specific method.
Late binding occurs when the compiler cannot determine the type of object that you'll be calling. This level of ambiguity can be achieved using the Object data type. A variable of data type Object can hold virtually any value, including a reference to any type of object. Thus, code such as the following could be run against any object that implements a DoSomething method that accepts no parameters:
Option Strict Off Module LateBind Dim x as Integer = 20 Dim y = DoWork(x) Public Function DoWork(ByVal obj As String) as String Return obj.substring(0, 3) End Function End Module
If the object passed into this routine cannot be converted to a string then method, then an exception will be thrown. Note, however, that you are passing an integer into the method. Late binding allows Visual Basic to determine at run time that the inbound parameter needs to be converted to a string, and Visual Basic will automatically handle this conversion. Keep in mind, however, that late binding goes beyond what is done by Option Strict. For example, it plays into the ability to determine the exact type associated with a LINQ query at run time.
While late binding is flexible, it can be error prone and is slower than early-bound code. To make a late-bound method call, the .NET run time must dynamically determine whether the target object is actually compatible with what you are trying to do. It must then invoke that a conversion on your behalf. This takes more time and effort than an early-bound call, whereby the compiler knows ahead of time that the method exists and can compile the code to make the call directly. With a late-bound call, the compiler has to generate code to make the call dynamically at run time.
When developing software, it is often necessary to take a numeric value and convert it to a string to display in a text box. As you've seen this can be done implicitly via late binding. Similarly, it is often necessary to accept input from a text box and convert this input to a numeric value. These conversions, unlike some, can be done in one of two fashions: implicitly or explicitly.
Implicit conversions are those that rely on the runtime system to adjust the data via late binding to a new type without any guidance. Often, Visual Basic's default settings enable developers to write code containing many implicit conversions that the developer may not even notice.
Explicit conversions, conversely, are those for which the developer recognizes the need to change a variable's type and assign it to a different variable. Unlike implicit conversions, explicit conversions are easily recognizable within the code. Some languages such as C# require that all conversions that might be type unsafe be done through an explicit conversion; otherwise, an error is thrown.
It is therefore important to understand what a type-safe implicit conversion is. In short, it's a conversion that cannot fail because of the nature of the data involved. For example, if you assign the value of a smaller type, Short, into a larger type, Long, then there is no way this conversion can fail. As both values are integer-style numbers, and the maximum and minimum values of a Short variable are well within the range of a Long, this conversion will always succeed and can safely be handled as an implicit conversion:
Dim shortNumber As Short = 32767 Dim longNumber As Long = shortNumber
However, the reverse of this is not a type-safe conversion. In a system that demands explicit conversions, the assignment of a Long value to a Short variable results in a compilation error, as the compiler doesn't have any safe way to handle the assignment when the larger value is outside the range of the smaller value. It is still possible to explicitly cast a value from a larger type to a smaller type, but this is an explicit conversion. By default, Visual Basic supports certain unsafe implicit conversions. Thus, adding the following line will not, when Option Strict is Off, cause an error under Visual Basic:
shortNumber = longNumber
One of the original goals of Visual Basic is to support rapid prototyping. In a rapid prototyping model, a developer is writing code that “works” for demonstration purposes but may not be ready for deployment. This distinction is important because in the discussion of implicit conversions, you should always keep in mind that they are not a best practice for production software.
Keep in mind that even when you choose to allow implicit conversions, these are allowed only for a relatively small number of data types. At some point you'll need to carry out explicit conversions. The following code is an example of some typical conversions between different integer types when Option Strict is enabled:
Dim myShort As Short Dim myUInt16 As UInt16 Dim myInt16 As Int16 Dim myInteger As Integer Dim myUInt32 As UInt32 Dim myInt32 As Int32 Dim myLong As Long Dim myInt64 As Int64 myShort = 0 myUInt16 = Convert.ToUInt16(myShort) myInt16 = myShort myInteger = myShort myUInt32 = Convert.ToUInt32(myShort) myInt32 = myShort myInt64 = myShort myLong = Long.MaxValue If myLong < Short.MaxValue Then myShort = Convert.ToInt16(myLong) End If myInteger = CInt(myLong)
The preceding snippet provides some excellent examples of what might not be intuitive behavior. The first thing to note is that you can't implicitly cast from Short to UInt16, or any of the other unsigned types for that matter. That's because with Option Strict the compiler won't allow an implicit conversion that might result in a value out of range or lead to loss of data. You may be thinking that an unsigned Short has a maximum that is twice the maximum of a signed Short, but in this case, if the variable myShort contained a -1, then the value wouldn't be in the allowable range for an unsigned type.
Just for clarity, even with the explicit conversion, if myShort were a negative number, then the Convert.ToUInt32 method would throw a runtime exception. Managing failed conversions requires either an understanding of exceptions and exception handling, as covered in Chapter 6, or the use of a conversion utility such as TryParse, covered in the next section.
The second item illustrated in this code is the shared method MaxValue. All of the integer and decimal types have this property. As the name indicates, it returns the maximum value for the specified type. There is a matching MinValue method for getting the minimum value. As shared properties, these properties can be referenced from the class (Long.MaxValue) without requiring an instance.
Finally, although this code will compile, it won't always execute correctly. It illustrates a classic error, which in the real world is often intermittent. The error occurs because the final conversion statement does not check to ensure that the value being assigned to myInteger is within the maximum range for an integer type. On those occasions when myLong is larger than the maximum allowed, this code will throw an exception.
Visual Basic provides many ways to convert values. Some of them are updated versions of techniques that are supported from previous versions of Visual Basic. Others, such as the ToString method, are an inherent part of every class (although the .NET specification does not define how a ToString class is implemented for each type).
The following set of conversion methods is based on the conversions supported by Visual Basic. They coincide with the primitive data types described earlier; however, continued use of these methods is not considered a best practice. That bears repeating: While you may find the following methods in existing code, you should strive to avoid and replace these calls.
Each of these methods has been designed to accept the input of the other primitive data types (as appropriate) and to convert such items to the type indicated by the method name. Thus, the CStr class is used to convert a primitive type to a String. The disadvantage of these methods is that they only support a limited number of types and are specific to Visual Basic. If you are working with developers who don't have a long Visual Basic history they will find these methods distracting. A more generic way to handle conversions is to leverage the System.Convert class shown in the following code snippet:
Dim intMyShort As Integer = 200 Dim int = Convert.ToInt32(intMyShort) Dim dt = Convert.ToDateTime("9/9/2001")
The class System.Convert implements not only the conversion methods listed earlier, but also other common conversions. These additional methods include standard conversions for things such as unsigned integers and pointers.
All the preceding type conversions are great for value types and the limited number of classes to which they apply, but these implementations are oriented toward a limited set of known types. It is not possible to convert a custom class to an Integer using these classes. More important, there should be no reason to have such a conversion. Instead, a particular class should provide a method that returns the appropriate type. That way, no type conversion is required. However, when Option Strict is enabled, the compiler requires you to cast an object to an appropriate type before triggering an implicit conversion. Note, however, that the Convert method isn't the only way to indicate that a given variable can be treated as another type.
Most value types, at least those which are part of the .NET Framework, provide a pair of shared methods called Parse and TryParse. These methods accept a value of your choosing and then attempt to convert this variable into the selected value type. The Parse and TryParse methods are available only on value types. Reference types have related methods called DirectCast and Cast, which are optimized for reference variables.
The Parse method has a single parameter. This input parameter accepts a value that is the target for the object you want to create of a given type. This method then attempts to create a value based on the data passed in. However, be aware that if the data passed into the Parse method cannot be converted, then this method will throw an exception that your code needs to catch. The following line illustrates how the Parse function works:
result = Long.Parse("100")
Unfortunately, when you embed this call within a Try-Catch statement for exception handling, you create a more complex block of code. Note that exception handling and its use is covered in Chapter 6; for now just be aware that exceptions require additional system resources for your running code that impacts performance. Because you always need to encapsulate such code within a Try-Catch block, the .NET development team decided that it would make more sense to provide a version of this method that encapsulated that exception-handling logic.
This is the origin of the TryParse method. The TryParse method works similarly to the Parse method except that it has two parameters and returns a Boolean, rather than a value. Instead of assigning the value of the TryParse method, you test it as part of an If-Then statement to determine whether the conversion of your data to the selected type was successful. If the conversion was successful, then the new value is stored in the second parameter passed to this method, which you can then assign to the variable you want to hold that value:
Dim converted As Long If Long.TryParse("100", converted) Then result = converted End If
Whether you are using late binding or not, it can be useful to pass object references around using the Object data type, converting them to an appropriate type when you need to interact with them. This is particularly useful when working with objects that use inheritance or implement multiple interfaces, concepts discussed in Chapter 4.
If Option Strict is turned off, which is the default, then you can write code using a variable of type Object to make an early-bound method call (code file: MainWindow.xaml.vb):
Public Sub objCType(ByVal obj As Object) Dim local As String local = obj local.ToCharArray() End Sub
This code uses a strongly typed variable, local, to reference what was a generic object value. Behind the scenes, Visual Basic converts the generic type to a specific type so that it can be assigned to the strongly typed variable. If the conversion cannot be done, then you get a trappable runtime error.
The same thing can be done using the CType function. If Option Strict is enabled, then the previous approach will not compile, and the CType function must be used. Here is the same code making use of CType (code file: MainWindow.xaml.vb):
Public Sub CType1(ByVal obj As Object) Dim local As String local = CType(obj, String) local.ToLower() End Sub
This code declares a variable of type TheClass, which is an early-bound data type that you want to use. The parameter you're accepting is of the generic Object data type, though, so you use the CType method to gain an early-bound reference to the object. If the object isn't of the type specified in the second parameter, then the call to CType fails with a trappable error.
Once you have a reference to the object, you can call methods by using the early-bound variable local. This code can be shortened to avoid the use of the intermediate variable. Instead, you can simply call methods directly from the data type (code file: MainWindow.xaml.vb):
Public Sub CType2(obj As Object) CType(obj, String).ToUpper() End Sub
Even though the variable you are working with is of type Object and therefore any calls to it will be late bound, you use the CType method to temporarily convert the variable into a specific type — in this case, the type String.
As shown in Chapter 4, the CType function can also be very useful when working with objects that implement multiple interfaces. When an object has multiple interfaces, you can reference a single object variable through the appropriate interface as needed by using CType.
Another function that is very similar to CType is the method DirectCast. The DirectCast call also converts values of one type into another type. It works in a more restrictive fashion than CType, but the trade-off is that it can be somewhat faster than CType:
Dim obj As TheClass obj = New TheClass DirectCast(obj, ITheInterface).DoSomething()
This is similar to the last example with CType, illustrating the parity between the two functions. There are differences, however. First, DirectCast works only with reference types, whereas CType accepts both reference and value types. For instance, CType can be used in the following code:
Dim int As Integer = CType(123.45, Integer)
Trying to do the same thing with DirectCast would result in a compiler error, as the value 123.45 is a value type, not a reference type.
Second, DirectCast is not as aggressive about converting types as CType. CType can be viewed as an intelligent combination of all the other conversion functions (such as CInt, CStr, and so on). DirectCast, conversely, assumes that the source data is directly convertible, and it won't take extra steps to convert it.
As an example, consider the following code:
Dim obj As Object = 123.45 Dim int As Integer = DirectCast(obj, Integer)
If you were using CType this would work, as CType uses CInt-like behavior to convert the value to an Integer. DirectCast, however, will throw an exception because the value is not directly convertible to Integer.
A method similar to DirectCast is TryCast. TryCast converts values of one type into another type, but unlike DirectCast, if it can't do the conversion, then TryCast doesn't throw an exception. Instead, TryCast simply returns Nothing if the cast can't be performed. TryCast works only with reference values; it cannot be used with value types such as Integer or Boolean.
Using TryCast, you can write code like this (code file: MainWindow.xaml.vb):
Public Sub TryCast1 (ByVal obj As Object) Dim temp = TryCast(obj, Object) If temp Is Nothing Then ' the cast couldn't be accomplished ' so do no work Else temp.DoSomething() End If End Sub
If you are not sure whether a type conversion is possible, then it is often best to use TryCast. This function avoids the overhead and complexity of catching possible exceptions from CType or DirectCast and still provides you with an easy way to convert an object to another type.
This chapter uses one class per file in the examples, as this is the most common approach. Returning to the project again access the Project ⇒ Add Class menu option to add a new class module to the project. You'll be presented with the standard Add New Item dialog. Change the name to Person.vb and click Add. The result will be the following code, which defines the Person class:
Public Class Person End Class
With the Person class created, you are ready to start adding code to declare the interface, implement the behaviors, and declare the instance variables.
Fields are variables declared in the class. They will be available to each individual object when the application is run. Each object gets its own set of data — basically, each object gets its own copy of the fields.
Earlier, you learned that a class is simply a template from which you create specific objects. Variables that you define within the class are also simply templates — and each object gets its own copy of those variables in which to store its data.
Declaring member variables is as easy as declaring variables within the Class block structure. Add the following code to the Person class:
Public Class Person Private mName As String Private mBirthDate As Date End Class
You can control the scope of the fields with the following keywords:
Typically, fields are declared using the Private keyword, making them available only to code within each instance of the class. Choosing any other option should be done with great care, because all the other options allow code outside the class to directly interact with the variable, meaning that the value could be changed and your code would never know that a change took place.
Objects typically need to provide services (or functions) that can be called when working with the object. Using their own data or data passed as parameters to the method, they manipulate information to yield a result or perform an action.
Methods declared as Public, Friend, or Protected in scope define the interface of the class. Methods that are Private in scope are available to the code only within the class itself, and can be used to provide structure and organization to code. As discussed earlier, the actual code within each method is called an implementation, while the declaration of the method itself is what defines the interface.
Methods are simply routines that are coded within the class to implement the services you want to provide to the users of an object. Some methods return values or provide information to the calling code. These are called interrogative methods. Others, called imperative methods, just perform an action and return nothing to the calling code.
In Visual Basic, methods are implemented using Sub (for imperative methods) or Function (for interrogative methods) routines within the class module that defines the object. Sub routines may accept parameters, but they do not return any result value when they are complete. Function routines can also accept parameters, and they always generate a result value that can be used by the calling code.
A method declared with the Sub keyword is merely one that returns no value. Add the following code to the Person class:
Public Sub Walk() ' implementation code goes here End Sub
The Walk method presumably contains some code that performs some useful work when called but has no result value to return when it is complete. To make use of this method, you might write code such as this:
Dim myPerson As New Person() myPerson.Walk()
If you have a method that does generate some value that should be returned, you need to use the Function keyword:
Public Function Age() As Integer Return CInt(DateDiff(DateInterval.Year, mBirthDate, Now())) End Function
Note that you must indicate the data type of the return value when you declare a function. This example returns the calculated age as a result of the method. You can return any value of the appropriate data type by using the Return keyword.
Optionally Visual Basic still allows you to return the value without using the Return keyword. Setting the value of the function name itself tells Visual Basic that this value should be used for the return:
Public Function Age() As Integer Age = CInt(DateDiff(DateInterval.Year, mBirthDate, Now())) End Function
Adding the appropriate keyword in front of the method declaration indicates the scope:
Public Sub Walk()
This indicates that Walk is a public method and thus is available to code outside the class and even outside the current project. Any application that references the assembly can use this method. Being public, this method becomes part of the object's interface.
The Private keyword indicates that a method is available only to the code within your particular class:
Private Function Age() As Integer
Private methods are very useful to help organize complex code within each class. Sometimes the methods contain very lengthy and complex code. In order to make this code more understandable, you may choose to break it up into several smaller routines, having the main method call these routines in the proper order. Moreover, you can use these routines from several places within the class, so by making them separate methods, you enable reuse of the code. These subroutines should never be called by code outside the object, so you make them Private.
You will often want to pass information into a method as you call it. This information is provided via parameters to the method. For instance, in the Person class, you may want the Walk method to track the distance the person walks over time. In such a case, the Walk method would need to know how far the person is to walk each time the method is called. The following code is a full version Person class (code file: Person.vb):
Public Class Person Private mName As String Private mBirthDate As Date Private mTotalDistance As Integer Public Sub Walk(ByVal distance As Integer) mTotalDistance += distance End Sub Public Function Age() As Integer Return CInt(DateDiff(DateInterval.Year, mBirthDate, Now())) End Function End Class
With this implementation, a Person object sums all of the distances walked over time. Each time the Walk method is called, the calling code must pass an Integer value, indicating the distance to be walked. The code to call this method would be similar to the following:
Dim myPerson As New Person() myPerson.Walk(42)
The .NET environment provides for a specialized type of method called a property. A property is a method specifically designed for setting and retrieving data values. For instance, you declared a variable in the Person class to contain a name, so the Person class may include code to allow that name to be set and retrieved. This can be done using regular methods (code file: Person.vb):
Public Sub SetName(ByVal name As String) mName = name End Sub Public Function GetName() As String Return mName End Function
Using methods like these, you write code to interact with the object:
Dim myPerson As New Person() myPerson.SetName("Jones") Messagebox.Show(myPerson.GetName())
While this is perfectly acceptable, it is not as nice as it could be with the use of a property. A Property style method consolidates the setting and retrieving of a value into a single structure, and makes the code within the class smoother overall. You can rewrite these two methods into a single property. The Property method is declared with both a scope and a data type. You could add the following code to the Person class (code file: Person.vb):
Public Property Name() As String Get Return mName End Get Set(ByVal Value As String) mName = Value End Set End Property
However the preceding code can be written in a much simpler way:
Public Property Name() As String
This style of defining a property actually creates a hidden backing field called something similar to _Name, which is not defined in the source code but generated by the compiler. For most properties where you are not calculating a value during the get or set, this is the easiest way to define it.
By using a property method instead, you can make the client code much more readable:
Dim myPerson As New Person() myPerson.Name = "Jones" Messagebox.Show(myPerson.Name)
The return data type of the Name property is String. A property can return virtually any data type appropriate for the nature of the value. In this regard, a property is very similar to a method declared using the Function keyword.
By default, the parameter is named Value, but you can change the parameter name to something else, as shown here:
Set(ByVal NewName As String) mName = NewName End Set
In many cases, you can apply business rules or other logic within this routine to ensure that the new value is appropriate before you actually update the data within the object. It is also possible to restrict the Get or Set block to be narrower in scope than the scope of the property itself. For instance, you may want to allow any code to retrieve the property value, but only allow other code in your project to alter the value. In this case, you can restrict the scope of the Set block to Private, while the Property itself is scoped as Public(code file: Person.vb):
Public Property Name() As String Get Return mName End Get Private Set(ByVal Value As String) mName = Value End Set End Property
The new scope must be more restrictive than the scope of the property itself, and either the Get or Set block can be restricted, but not both. The one you do not restrict uses the scope of the Property method.
The Name property you created is an example of a single-value property. You can also create property arrays or parameterized properties. These properties reflect a range, or an array, of values. For example, people often have several phone numbers. You might implement a PhoneNumber property as a parameterized property, storing not only phone numbers, but also a description of each number. To retrieve a specific phone number you would write code such as the following (code file: Person.vb):
Dim myPerson As New Person() Dim homePhone As String homePhone = myPerson.Phone("home")
To add or change a phone number, you'd write the following code:
myPerson.Phone("work") = "555-9876"
Not only are you retrieving and updating a phone number property, you are also updating a specific phone number. This implies a couple of things. First, you can no longer use a simple variable to hold the phone number, as you are now storing a list of numbers and their associated names. Second, you have effectively added a parameter to your property. You are actually passing the name of the phone number as a parameter on each property call.
To store the list of phone numbers, you can use the Hashtable class. The Hashtable is very a standard object, but it allows you to test for the existence of a specific element. Add the following declarations to the Person class (code file: Person.vb):
Public Class Person Public Property Name As String Public Property BirthDate As Date Public Property TotalDistance As Integer Public Property Phones As New Hashtable
You can implement the Phone property by adding the following code to the Person class (code file: Person.vb):
Public Property Phone(ByVal location As String) As String Get Return CStr(Phones.Item(Location)) End Get Set(ByVal Value As String) If Phones.ContainsKey(location) Then Phones.Item(location) = Value Else Phones.Add(location, Value) End If End Set End Property
The declaration of the Property method itself is a bit different from what you have seen:
Public Property Phone(ByVal location As String) As String
In particular, you have added a parameter, location, to the property itself. This parameter will act as the index into the list of phone numbers, and must be provided when either setting or retrieving phone number values.
Because the location parameter is declared at the Property level, it is available to all code within the property, including both the Get and Set blocks. Within your Get block, you use the location parameter to select the appropriate phone number to return from the Hashtable:
Get If Phones.ContainsKey(Location) Then Return Phones.Item(Location) End If Return "" End Get
In this case, you are using the ContainsKey method of Hashtable to determine whether the phone number already exists in the list. When a value exists for a location you return it; however, if no value is stored matching the location, then you return Nothing. Similarly in the Set block that follows, you use the location to update or add the appropriate element in the Hashtable. If a location is already associated with a value, then you simply update the value in the list; otherwise, you add a new element to the list for the value (code file: Person.vb):
Set(ByVal Value As String) If Phones.ContainsKey(location) Then Phones.Item(location) = Value Else Phones.Add(location, Value) End If End Set
This way, you are able to add or update a specific phone number entry based on the parameter passed by the calling code.
Sometimes you may want a property to be read-only so that it cannot be changed. In the Person class, for instance, you may have a read-write property, BirthDate, and a read-only property, Age. If so, the BirthDate property is a normal property, as follows (code file: Person.vb):
Public Property BirthDate() As Date
The Age value, conversely, is a derived value based on BirthDate. This is not a value that should ever be directly altered, so it is a perfect candidate for read-only status.
You could create all your objects without any Property routines at all, just using methods for all interactions with the objects. However, Property routines are obviously attributes of an object, whereas a Function might be an attribute or a method. By implementing all attributes as Properties, using the ReadOnly or WriteOnly attributes as necessary, and having any interrogative methods as Function routines, you create more readable and understandable code.
To make a property read-only, use the ReadOnly keyword and only implement the Get block:
Public ReadOnly Property Age() As Integer Get Return CInt(DateDiff(DateInterval.Year, BirthDate, Now())) End Get End Property
Because the property is read-only, you will get a syntax error also known as a compile time error if you attempt to implement a Set block.
As with read-only properties, sometimes a property should be write-only, whereby the value can be changed but not retrieved.
Many people have allergies, so perhaps the Person object should have some understanding of the ambient allergens in the area. This is not a property that should be read from the Person object, as allergens come from the environment, rather than from the person, but it is data that the Person object needs in order to function properly. Add the following variable declaration to the class (code file: Person.vb):
Public WriteOnly Property AmbientAllergens() As Integer Set(ByVal Value As Integer) mAllergens = Value End Set End Property
To create a write-only property, use the WriteOnly keyword and only implement a Set block in the code. Because the property is write-only, you will get a syntax error if you try to implement a Get block.
Objects can implement a default property, which can be used to simplify the use of an object at times by making it appear as if the object has a native value. A good example of this behavior is the Collection object, which has a default property called Item that returns the value of a specific item, allowing you to write the following:
Dim mData As New HashTable() Return mData(index)
Default properties must be parameterized properties. A property without a parameter cannot be marked as the default. Your Person class has a parameterized property — the Phone property you built earlier. You can make this the default property by using the Default keyword (code file: Person.vb):
Default Public Property Phone(ByVal location As String) As String Get If Phones.ContainsKey(Location) Then Return Phones.Item(Location) End If Return "" End Get Set(ByVal Value As String) If mPhones.ContainsKey(location) Then mPhones.Item(location) = Value Else mPhones.Add(location, Value) End If End Set End Property
Prior to this change, you would have needed code such as the following to use the Phone property:
Dim myPerson As New Person() MyPerson.Phone("home") = "555-1234"
Now, with the property marked as Default, you can simplify the code:
myPerson("home") = "555-1234"
As you can see, the reference to the property name Phone is not needed. By picking appropriate default properties, you can potentially make the use of objects more intuitive.
Both methods and properties enable you to write code that interacts with your objects by invoking specific functionality as needed. It is often useful for objects to provide notification, as certain activities occur during processing. You see examples of this all the time with controls, where a button indicates that it was clicked via a Click event, or a text box indicates that its contents have been changed via the TextChanged event.
Objects can raise events of their own, providing a powerful and easily implemented mechanism by which objects can notify client code of important activities or events. In Visual Basic, events are provided using the standard .NET mechanism of delegates; but before discussing delegates, let's explore how to work with events in Visual Basic.
You are used to seeing code in a form to handle the Click event of a button, such as the following code:
Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click End Sub
Typically, you write your code in this type of routine without paying a lot of attention to the code created by the Visual Studio IDE. However, take a second look at that code, which contains some important things to note.
First, notice the use of the Handles keyword. This keyword specifically indicates that this method will be handling the Click event from the Button1 control. Of course, a control is just an object, so what is indicated here is that this method will be handling the Click event from the Button1 object.
Second, notice that the method accepts two parameters. The Button control class defines these parameters. It turns out that any method that accepts two parameters with these data types can be used to handle the Click event. For instance, you could create a new method to handle the event (code file: MainWindow.vb):
Private Sub MyClickMethod(ByVal s As System.Object, _ ByVal args As System.EventArgs) Handles Button1.Click End Sub
Even though you have changed the method name and the names of the parameters, you are still accepting parameters of the same data types, and you still have the Handles clause to indicate that this method handles the event.
The Handles keyword offers even more flexibility. Not only can the method name be anything you choose, a single method can handle multiple events if you desire. Again, the only requirement is that the method and all the events being raised must have the same parameter list.
One common scenario where this is useful is when you have multiple instances of an object that raises events, such as two buttons on a form (code file: MainWindow.vb):
Private Sub MyClickMethod(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles Button1.Click, Button2.Click End Sub
Notice that the Handles clause has been modified so that it has a comma-separated list of events to handle. Either event will cause the method to run, providing a central location for handling these events.
The WithEvents keyword tells Visual Basic that you want to handle any events raised by the object within the code:
Friend WithEvents Button1 As System.Windows.Forms.Button
The WithEvents keyword makes any event from an object available for use, whereas the Handles keyword is used to link specific events to the methods so that you can receive and handle them. This is true not only for controls on forms, but also for any objects that you create.
The WithEvents keyword cannot be used to declare a variable of a type that does not raise events. In other words, if the Button class did not contain code to raise events, you would get a syntax error when you attempted to declare the variable using the WithEvents keyword.
The compiler can tell which classes will and will not raise events by examining their interface. Any class that will be raising an event has that event declared as part of its interface. In Visual Basic, this means that you will have used the Event keyword to declare at least one event as part of the interface for the class.
Your objects can raise events just like a control, and the code using the object can receive these events by using the WithEvents and Handles keywords. Before you can raise an event from your object, however, you need to declare the event within the class by using the Event keyword.
In the Person class, for instance, you may want to raise an event anytime the Walk method is called. If you call this event Walked, you can add the following declaration to the Person class (code file: Person.vb):
Public Class Person Private msName As String Private mBirthDate As Date Private mTotalDistance As Integer Private mPhones As New Hashtable() Private mAllergens As Integer Public Event Walked()
Events can also have parameters, values that are provided to the code receiving the event. A typical button's Click event receives two parameters, for instance. In the Walked method, perhaps you want to also indicate the total distance that has been walked. You can do this by changing the event declaration:
Public Event Walked(ByVal distance As Integer)
Now that the event is declared, you can raise that event within the code where appropriate. In this case, you'll raise it within the Walk method, so anytime a Person object is instructed to walk, it fires an event indicating the total distance walked. Make the following change to the Walk method (code file: Person.vb):
Public Sub Walk(ByVal distance As Integer) TotalDistance += distance RaiseEvent Walked(TotalDistance) End Sub
The RaiseEvent keyword is used to raise the actual event. Because the event requires a parameter, that value is passed within parentheses and is delivered to any recipient that handles the event.
In fact, the RaiseEvent statement causes the event to be delivered to all code that has the object declared using the WithEvents keyword with a Handles clause for this event, or any code that has used the AddHandler method. The AddHandler method is discussed shortly.
If more than one method will be receiving the event, then the event is delivered to each recipient one at a time. By default, the order of delivery is not defined—meaning you can't predict the order in which the recipients receive the event—but the event is delivered to all handlers. Note that this is a serial, synchronous process. The event is delivered to one handler at a time, and it is not delivered to the next handler until the current handler is complete. Once you call the RaiseEvent method, the event is delivered to all listeners one after another until it is complete; there is no way for you to intervene and stop the process in the middle.
As just noted, by default you have no control over how events are raised. You can overcome this limitation by using a more explicit form of declaration for the event itself. Rather than use the simple Event keyword, you can declare a custom event. This is for more advanced scenarios, as it requires you to provide the implementation for the event itself.
The concept of delegates is covered in detail later in this chapter, but it is necessary to look at them briefly here in order to declare a custom event. Note that creating a fully customized event handler is an advanced concept, and the sample code shown in this section is not part of the sample. This section goes beyond what you would normally look to implement in a typical business class.
A delegate is a definition of a method signature. When you declare an event, Visual Basic defines a delegate for the event behind the scenes based on the signature of the event. The Walked event, for instance, has a delegate like the following:
Public Delegate Sub WalkedEventHandler(ByVal distance As Integer)
Notice how this code declares a “method” that accepts an Integer and has no return value. This is exactly what you defined for the event. Normally, you do not write this bit of code, because Visual Basic does it automatically, but if you want to declare a custom event, then you need to manually declare the event delegate.
You also need to declare within the class a variable where you can keep track of any code that is listening for, or handling, the event. It turns out that you can tap into the prebuilt functionality of delegates for this purpose. By declaring the WalkedEventHandler delegate, you have defined a data type that automatically tracks event handlers, so you can declare the variable like this:
Private mWalkedHandlers As WalkedEventHandler
You can use the preceding variable to store and raise the event within the custom event declaration:
Public Custom Event Walked As WalkedEventHandler AddHandler(ByVal value As WalkedEventHandler) mWalkedHandlers = _ CType([Delegate].Combine(mWalkedHandlers, value), WalkedEventHandler) End AddHandler RemoveHandler(ByVal value As WalkedEventHandler) mWalkedHandlers = _ CType([Delegate].Remove(mWalkedHandlers, value), WalkedEventHandler) End RemoveHandler RaiseEvent(ByVal distance As Integer) If mWalkedHandlers IsNot Nothing Then mWalkedHandlers.Invoke(distance) End If End RaiseEvent End Event
In this case, you have used the Custom Event key phrase, rather than just Event to declare the event. A Custom Event declaration is a block structure with three subblocks: AddHandler, RemoveHandler, and RaiseEvent.
The AddHandler block is called anytime a new handler wants to receive the event. The parameter passed to this block is a reference to the method that will be handling the event. It is up to you to store the reference to that method, which you can do however you choose. In this implementation, you are storing it within the delegate variable, just like the default implementation provided by Visual Basic.
The RemoveHandler block is called anytime a handler wants to stop receiving your event. The parameter passed to this block is a reference to the method that was handling the event. It is up to you to remove the reference to the method, which you can do however you choose. In this implementation, you are replicating the default behavior by having the delegate variable remove the element.
Finally, the RaiseEvent block is called anytime the event is raised. Typically, it is invoked when code within the class uses the RaiseEvent statement. The parameters passed to this block must match the parameters declared by the delegate for the event. It is up to you to go through the list of methods that are handling the event and call each of those methods. In the example shown here, you are allowing the delegate variable to do that for you, which is the same behavior you get by default with a normal event.
The value of this syntax is that you could opt to store the list of handler methods in a different type of data structure, such as a Hashtable or collection. You could then invoke them asynchronously, or in a specific order based on some other behavior required by the application.
Now that you have implemented an event within the Person class, you can write client code to declare an object using the WithEvents keyword. For instance, in the project's MainWindow code module, you can write the following code:
Class MainWindow Private WithEvents mPerson As Person
By declaring the variable WithEvents, you are indicating that you want to receive any events raised by this object. You can also choose to declare the variable without the WithEvents keyword, although in that case you would not receive events from the object as described here. Instead, you would use the AddHandler method, which is discussed after WithEvents.
You can then create an instance of the object, as the form is created, by adding the following code (code file: MainWindow.vb):
Private Sub Window_Loaded_1 (sender As System.Object, _ e As RoutedEventArgs) mPerson = New Person() End Sub
At this point, you have declared the object variable using WithEvents and have created an instance of the Person class, so you actually have an object with which to work. You can now proceed to write a method to handle the Walked event from the object by adding the following code to the form. You can name this method anything you like; it is the Handles clause that is important, because it links the event from the object directly to this method, so it is invoked when the event is raised (code file: MainWindow.xaml.vb):
Private Sub OnWalk(ByVal Distance As Integer) Handles mPerson.Walked MessageBox.Show("Person walked a total distance of " & Distance) End Sub
You are using the Handles keyword to indicate which event should be handled by this method. You are also receiving an Integer parameter. If the parameter list of the method doesn't match the list for the event, then you'll get a compiler error indicating the mismatch.
Finally, you need to call the Walk method on the Person object. Modify the Click event handler for the button (code file: MainWindow.xaml.vb):
Private Sub Button_Click_1(sender As Object, e As RoutedEventArgs) mPerson.Walk(42) End Sub
When the button is clicked, you simply call the Walk method, passing an Integer value. This causes the code in your class to be run, including the RaiseEvent statement. The result is an event firing back into the window's code, because you declared the mPerson variable using the WithEvents keyword. The OnWalk method will be run to handle the event, as it has the Handles clause linking it to the event.
illustrates the flow of control, showing how the code in the button's Click event calls the Walk method, causing it to add to the total distance walked and then raise its event. The RaiseEvent causes the window's OnWalk method to be invoked; and once it is done, control returns to the Walk method in the object. Because you have no code in the Walk method after you call RaiseEvent, the control returns to the Click event back in the window, and then you are done.
Now that you have seen how to receive and handle events using the WithEvents and Handles keywords, consider an alternative approach. You can use the AddHandler method to dynamically add event handlers through your code, and RemoveHandler to dynamically remove them.
WithEvents and the Handles clause require that you declare both the object variable and event handler as you build the code, effectively creating a linkage that is compiled right into the code. AddHandler, conversely, creates this linkage at run time, which can provide you with more flexibility. However, before getting too deeply into that, let's see how AddHandler works.
In MainWindow, you can change the way the code interacts with the Person object, first by eliminating the WithEvents keyword:
Private mPerson As Person
And then by also eliminating the Handles clause:
Private Sub OnWalk(ByVal Distance As Integer) MsgBox("Person walked a total distance of " & Distance) End Sub
With these changes, you've eliminated all event handling for the object, and the form will no longer receive the event, even though the Person object raises it.
Now you can change the code to dynamically add an event handler at run time by using the AddHandler method. This method simply links an object's event to a method that should be called to handle that event. Anytime after you have created the object, you can call AddHandler to set up the linkage (code file: MainWindow.xaml.vb):
Private Sub Window_Loaded_1 (sender As System.Object, _ e As RoutedEventArgs) AddHandler mPerson.Walked, AddressOf OnWalk End Sub
This single line of code does the same thing as the earlier use of WithEvents and the Handles clause, causing the OnWalk method to be invoked when the Walked event is raised from the Person object.
However, this linkage is performed at run time, so you have more control over the process than you would have otherwise. For instance, you could have extra code to determine which event handler to link up. Suppose that you have another possible method to handle the event for cases when a message box is not desirable. Add this code to MainWindow:
Private Sub LogOnWalk(ByVal Distance As Integer) System.Diagnostics.Debug.WriteLine("Person walked a total distance of " & Distance) End Sub
Rather than pop up a message box, this version of the handler logs the event to the output window in the IDE, or in the real world to a log file or event log as shown in Chapter 6. First add a new Setting to the application using the Project Properties. Name the property “NoPopup,” give it the type Boolean and Application scope. You can accept the default value of false. Now you can enhance the AddHandler code to determine which handler should be used dynamically at run time (code file: MainWindow.xaml.vb):
Private Sub Window_Loaded_1 (sender As System.Object, _ e As RoutedEventArgs) If My.Settings.NoPopup Then AddHandler mPerson.Walked, AddressOf LogOnWalk Else AddHandler mPerson.Walked, AddressOf OnWalk End If End Sub
If the setting NoPopup is true, then the new version of the event handler is used; otherwise, you continue to use the message-box handler.
The counterpart to AddHandler is RemoveHandler. RemoveHandler is used to detach an event handler from an event. One example of when this is useful is if you ever want to set the mPerson variable to Nothing or to a new Person object. The existing Person object has its events attached to handlers, and before you get rid of the reference to the object, you must release those references:
If My.Settings.NoPopup Then RemoveHandler mPerson.Walked, AddressOf LogOnWalk Else RemoveHandler mPerson.Walked, AddressOf OnWalk End If mPerson = New Person
If you do not detach the event handlers, the old Person object remains in memory because each event handler still maintains a reference to the object even after mPerson no longer points to the object. While this logic hasn't been implemented in this simple example, in a production environment you would need to ensure that when mPerson was ready to go out of scope you cleaned up any AddHandler references manually.
This illustrates one key reason why the WithEvents keyword and Handles clause are preferable in most cases. AddHandler and RemoveHandler must be used in pairs; failure to do so can cause memory leaks in the application, whereas the WithEvents keyword handles these details for you automatically.
In Visual Basic, classes can implement a special method that is always invoked as an object is being created. This method is called the constructor, and it is always named New.
The constructor method is an ideal location for such initialization code, as it is always run before any other methods are ever invoked, and it is run only once for an object. Of course, you can create many objects based on a class, and the constructor method will be run for each object that is created.
You can implement a constructor in your classes as well, using it to initialize objects as needed. This is as easy as implementing a Public method named New. Add the following code to the Person class (code file: Person.vb):
Public Sub New() Phone("home") = "555-1234" Phone("work") = "555-5678" End Sub
In this example, you are simply using the constructor method to initialize the home and work phone numbers for any new Person object that is created.
You can also use constructors to enable parameters to be passed to the object as it is being created. This is done by simply adding parameters to the New method. For example, you can change the Person class as follows (code file: Person.vb):
Public Sub New(ByVal name As String, ByVal birthDate As Date) mName = name mBirthDate = birthDate Phone("home") = "555-1234" Phone("work") = "555-5678" End Sub
With this change, anytime a Person object is created, you will be provided with values for both the name and birth date. However, this changes how you can create a new Person object. Whereas you used to have code such as
Dim myPerson As New Person()
now you will have code such as
Dim myPerson As New Person("Bill", "1/1/1970")
In fact, because the constructor expects these values, they are mandatory—any code that needs to create an instance of the Person class must provide these values. Fortunately, there are alternatives in the form of optional parameters and method overloading (which enables you to create multiple versions of the same method, each accepting a different parameter list). These topics are discussed later in the chapter.
Operators | Description |
=, <> | Equality and inequality. These are binary operators to support the a = b and a <> b syntax. If you implement one, then you must implement both. |
>, < | Greater than and less than. These are binary operators to support the a > b and a < b syntax. If you implement one, then you must implement both. |
>=, <= | Greater than or equal to and less than or equal to. These are binary operators to support the a >= b and a <= b syntax. If you implement one, then you must implement both. |
IsFalse, IsTrue | Boolean conversion. These are unary operators to support the AndAlso and OrElse statements. The IsFalse operator accepts a single object and returns False if the object can be resolved to a False value. The IsTrue operator accepts a single value and returns True if the object can be resolved to a True value. If you implement one, then you must implement both. |
CType | Type conversion. This is a unary operator to support the CType(a) statement. The CType operator accepts a single object of another type and converts that object to the type of your class. This operator must be marked as either Narrowing, to indicate that the type is more specific than the original type, or Widening, to indicate that the type is broader than the original type. |
+, - | Addition and subtraction. These operators can be unary or binary. The unary form exists to support the a += b and a –= b syntax, while the binary form exists to support a + b and a – b. |
*, /, \, ˆ, Mod | Multiplication, division, exponent, and Mod. These are binary operators to support the a * b, a / b, a \ b, a ˆ b, and a Mod b syntax. |
& | Concatenation. This binary operator supports the a & b syntax. While this operator is typically associated with String manipulation, the & operator is not required to accept or return String values, so it can be used for any concatenation operation that is meaningful for your object type. |
<<, >> | Bit shifting. These binary operators support the a << b and a >> b syntax. The second parameter of these operators must be a value of type Integer, which will be the integer value to be bit-shifted based on your object value. |
And, Or, Xor | Logical comparison or bitwise operation. These binary operators support the a And b, a Or b, and a Xor b syntax. If the operators return Boolean results, then they are performing logical comparisons. If they return results of other data types, then they are performing bitwise operations. |
Like | Pattern comparison. This binary operator supports the a Like b syntax. |
If an operator is meaningful for your data type, then you are strongly encouraged to overload that operator.
Notice that neither the AndAlso nor the OrElse operators can be directly overloaded. This is because these operators use other operators behind the scenes to do their work. To overload AndAlso and OrElse, you need to overload a set of other operators, as shown in :
AndAlso | OrElse |
Overload the And operator to accept two parameters of your object's type and to return a result of your object's type. | Overload the Or operator to accept two parameters of your object's type and to return a result of your object's type. |
Overload IsFalse for your object's type (meaning that you can return True or False by evaluating a single instance of your object). | Overload IsTrue for your object's type (meaning that you can return True or False by evaluating a single instance of your object). |
If these operators are overloaded in your class, then you can use AndAlso and OrElse to evaluate statements that involve instances of your class.
Sometimes it would be nice to be able to pass a procedure as a parameter to a method. The classic scenario is when building a generic sort routine, for which you need to provide not only the data to be sorted, but also a comparison routine appropriate for the specific data.
It is easy enough to write a sort routine that sorts Person objects by name, or to write a sort routine that sorts SalesOrder objects by sales date. However, if you want to write a sort routine that can sort any type of object based on arbitrary sort criteria, that gets pretty difficult. At the same time, because some sort routines can get very complex, it would be nice to reuse that code without having to copy and paste it for each different sort scenario.
By using delegates, you can create such a generic routine for sorting; and in so doing, you can see how delegates work and can be used to create many other types of generic routines. The concept of a delegate formalizes the process of declaring a routine to be called and calling that routine.
In your code, you can declare what a delegate procedure must look like from an interface standpoint. This is done using the Delegate keyword. To see how this works, you will create a routine to sort any kind of data.
To do this, you will declare a delegate that defines a method signature for a method that compares the value of two objects and returns a Boolean indicating whether the first object has a larger value than the second object. You will then create a sort algorithm that uses this generic comparison method to sort data. Finally, you create an actual method that implements the comparison, and then you pass the method's address to the routine Sort.
Add a new module to the project by choosing Project ⇒ Add Module. Name the module Sort.vb, click Add, and then add the following code (code file: Sort.vb):
Module Sort Public Delegate Function Compare(ByVal v1 As Object, ByVal v2 As Object) _ As Boolean End Module
This line of code does something interesting. It actually defines a method signature as a data type. This new data type is named Compare, and it can be used within the code to declare variables or parameters that are accepted by your methods. A variable or parameter declared using this data type could actually hold the address of a method that matches the defined method signature, and you can then invoke that method by using the variable.
Any method with the following signature can be viewed as being of type Compare:
f(Object, Object)
You can write a routine that accepts the delegate data type as a parameter, meaning that anyone calling your routine must pass the address of a method that conforms to this interface. Add the following sort routine to the code module Sort (code file: Sort.vb):
Public Sub DoSort(ByVal theData() As Object, ByVal greaterThan As Compare) Dim outer As Integer Dim inner As Integer Dim temp As Object For outer = 0 To UBound(theData) For inner = outer + 1 To UBound(theData) If greaterThan.Invoke(theData(outer), theData(inner)) Then temp = theData(outer) theData(outer) = theData(inner) theData(inner) = temp End If Next Next End Sub
The GreaterThan parameter is a variable that holds the address of a method matching the method signature defined by the Compare delegate. The address of any method with a matching signature can be passed as a parameter to your sort routine.
Note the use of the Invoke method, which is how a delegate is called from the code. In addition, note that the routine deals entirely with the generic System.Object data type, rather than with any specific type of data. The specific comparison of one object to another is left to the delegate routine that is passed in as a parameter.
Now create the implementation of the delegate routine and call the Sort method. On a very basic level, all you need to do is create a method that has a matching method signature, add the following code to your Sort module:
Public Function PersonCompare(ByVal person1 As Object, _ ByVal person2 As Object) As Boolean End Function
The method signature of this method exactly matches what you defined by your delegate earlier:
Compare(Object, Object)
In both cases, you are defining two parameters of type Object.
Of course, there is more to it than simply creating the stub of a method. The method needs to return a value of True if its first parameter is greater than the second parameter. Otherwise, it should be written to deal with some specific type of data.
The Delegate statement defines a data type based on a specific method interface. To call a routine that expects a parameter of this new data type, it must pass the address of a method that conforms to the defined interface.
To conform to the interface, a method must have the same number of parameters with the same data types defined in your Delegate statement. In addition, the method must provide the same return type as defined. The actual name of the method does not matter; it is the number, order, and data type of the parameters and the return value that count.
To find the address of a specific method, you can use the AddressOf operator. This operator returns the address of any procedure or method, enabling you to pass that value as a parameter to any routine that expects a delegate as a parameter.
The Person class already has a shared method named CompareAge that generally does what you want. Unfortunately, it accepts parameters of type Person, rather than of type Object as required by the Compare delegate. You can use method overloading to solve this problem.
Create a second implementation of CompareAge that accepts parameters of type Object as required by the delegate, rather than of type Person as shown in the existing implementation (code file: Person.vb):
Public Shared Function CompareAge(ByVal person1 As Object, _ ByVal person2 As Object) As Boolean Return CType(person1, Person).Age > CType(person2, Person).Age End Function
This method simply returns True if the first Person object's age is greater than the second. The routine accepts two Object parameters, rather than specific Person type parameters, so you have to use the CType method to access those objects as type Person. You accept the parameters as type Object because that is what is defined by the Delegate statement. You are matching its method signature:
f(Object, Object)
Because this method's parameter data types and return value match the delegate, you can use it when calling the Sort routine. Place a button on the MainWindow form and write the following code behind that button (code file: MainWindow.xaml.vb):
Private Sub CompareAge (ByVal sender As System.Object, _ ByVal e As System.EventArgs) Dim myPeople(4) As Person myPeople(0) = New Person("Fred", #7/9/1960#) myPeople(1) = New Person("Mary", #1/21/1955#) myPeople(2) = New Person("Sarah", #2/1/1960#) myPeople(3) = New Person("George", #5/13/1970#) myPeople(4) = New Person("Andre", #10/1/1965#) DoSort(myPeople, AddressOf Person.CompareAge) End Sub
This code creates an array of Person objects and populates them. It then calls the DoSort routine from the module, passing the array as the first parameter, and the address of the shared CompareAge method as the second parameter. To display the contents of the sorted array in the application's window, you can add the following code (code file: MainWindow.xaml.vb):
Dim myPerson As Person TextBox1.Text = "" For Each myPerson In myPeople TextBox1.Text = TextBox1.Text & myPerson.Name & " " & myPerson.Age & vbCrLf Next
When you run the application and click the button, the application window displays a list of the people sorted by age, as shown in .
What makes this so powerful is that you can change the comparison routine without changing the sort mechanism. Simply add another comparison routine to the Person class (code file: Person.vb):
Public Shared Function CompareName(ByVal person1 As Object, _ ByVal person2 As Object) As Boolean Return CType(person1, Person).Name > CType(person2, Person).Name End Function
Then, change the code behind the button on the form to use that alternate comparison routine (code file: MainWindow.xaml.vb):
Private Sub CompareName Dim myPeople(4) As Person myPeople(0) = New Person("Fred", #7/9/1960#) myPeople(1) = New Person("Mary", #1/21/1955#) myPeople(2) = New Person("Sarah", #2/1/1960#) myPeople(3) = New Person("George", #5/13/1970#) myPeople(4) = New Person("Andre", #10/1/1965#) DoSort(myPeople, AddressOf Person.CompareName) Dim myPerson As Person TextBox1.Text = "" For Each myPerson In myPeople TextBox1.Text = TextBox1.Text & myPerson.Name & " " & myPerson.Age & vbCrLf Next End Sub
When you run this updated code, you will find that the array contains a set of data sorted by name, rather than age, as shown in .
Simply by creating a new compare routine and passing it as a parameter, you can entirely change the way that the data is sorted. Better still, this sort routine can operate on any type of object, as long as you provide an appropriate delegate method that knows how to compare that type of object.
Visual Basic offers a fully object-oriented language with all the capabilities you would expect. This chapter described the basic concepts behind classes and objects, as well as the separation of interface from implementation and data.
The chapter introduced the System.Object class and explained how this class is the base class for all classes in .NET. You were then introduced to concepts such as Value and Reference types as well as the implementation for primitive types. The chapter looked at most of the core primitive types available in Visual Basic and how to convert between types.
You have learned how to use the Class keyword to create classes, and how those classes can be instantiated into specific objects, each one an instance of the class. These objects have methods and properties that can be invoked by the client code, and can act on data within the object stored in member or instance variables.
You also explored some more advanced concepts, including method overloading, shared or static variables and methods, and the use of delegates and lambda expressions.
The next chapter continues the discussion of object syntax as you explore the concept of inheritance and all the syntax that enables inheritance within Visual Basic. You will also walk through the creation, implementation, and use of multiple interfaces — a powerful concept that enables objects to be used in different ways, depending on the interface chosen by the client application.
Also covered in the next chapter is a discussion of objects and object-oriented programming, applying all of this syntax. It explains the key object-oriented concepts of abstraction, encapsulation, polymorphism, and inheritance, and shows how they work together to provide a powerful way to design and implement applications.