Directives

11.1 Early bind objects whenever possible.

When an interface call is made on an object (such as calling a method or setting a property), a number of things happen. Even though you reference a member's name in code, it's not what the common language runtime (CLR) uses to access an interface member. Each interface element (property, method, or event) of an object has an associated unique identifier. These IDs are what are actually used by the .NET common language runtime to reference an interface member. Visual Basic automatically assigns these IDs to your members when you compile a component.

As a Visual Basic .NET developer, you'll probably never actually use a member ID to invoke a member of an object you'll always reference a member's name instead. The calls you make to a member using its name must be cross-referenced to the proper ID, however, before the member can be invoked. This process (known as binding) has two forms: early binding and late binding.

An object can contain any number of properties, methods, and events every object is different. When you write code to manipulate an object, Visual Basic .NET needs to understand the interface of the object or your code won't work. Resolving the interface members and their arguments occurs when your object variable is bound to an object. Binding can occur at run time or at compile time, depending on the form of binding used. Both types of binding have benefits, but, in general, early binding is superior because code that uses late-bound objects requires considerably more work by Visual Basic .NET than code that uses early-bound objects. As a result, early binding object references results in significantly better performance under most conditions.

Late Binding

When you dimension a variable as type Object, as shown in the following code, you are using late binding.

Dim obj As Object obj = New MyObject() 

When you late-bind an object, the binding occurs at run time when the New keyword is used to instantiate an object. When you reference an interface member in code, such as by calling a method or setting a property, Visual Basic .NET needs to obtain the ID of the member in order to successfully invoke it. This involves querying the object for the interface member and its associated ID, preparing any arguments that need to be supplied to the member, and finally invoking the member using the ID and passing associated arguments. These steps require a great deal of overhead and therefore adversely affect the performance of the application.

Late binding is not the preferred method of binding. There are, however, some occasions where late binding is desirable. You might want to use late binding when

  • You want to write code that can work with many different types of objects.

  • A type library is not available for an object.

    Note

    You won't be able to use Option Strict in a project that uses late binding. Using Option Strict promotes more reliable code, and you should consider this when deciding whether or not to use late binding.


     

Early Binding

If Visual Basic can determine the internal ID for a member at compile time, there is no need to query the object for its interface members and IDs at run time. Instead, the ID can be saved within the compiled application, resulting in considerably faster calls to object members. When an object variable is declared so that the IDs of the object can be resolved at design time, the object variable is said to be early bound.

Note

Early binding occurs at compile time; late binding occurs at run time.


 

The following are important reasons to use early binding:

  • Speed.

  • Speed.

  • More speed.

  • Objects, their properties, and their methods appear in the IntelliSense drop-down lists (the lists that appear when you type a period after typing an object's name).

  • Increased code stability due to the fact that the compiler can check for syntax and reference errors in your code. Visual Basic .NET is able to validate that a given member exists and that you have supplied the proper type, count, and order of parameters at design time.

  • Dynamic help in the integrated development environment (IDE) is available when working with the object because, once again, Visual Basic .NET is able to identify and understand the object's structure at design time.

For early binding to take place, an object variable must be declared as a specific object type (that is, not using As Object).

Incorrect:
Private Sub DrawText(ByVal TextToDraw As String)    Dim objGraphics As Object    Dim objFont As Object     ' Create a graphics object using the memory bitmap.    objGraphics = Graphics.FromImage(m_objDrawingSurface)    ' Create a new font object.    objFont = New System.Drawing.Font("Arial", _                     mc_FontSize, FontStyle.Bold)    ' Draw the user's text.    objGraphics.DrawString(TextToDraw, objFont, _                           System.Drawing.Brushes.Red, _                           mc_TextX, mc_TextY)    ' Clean up.    objGraphics.Dispose()    ' Force the form to paint itself. This triggers the Paint event.     Me.Invalidate() End Sub 
Correct:
Private Sub DrawText(ByVal TextToDraw As String)    Dim objGraphics As Graphics    Dim objFont As Font    ' Create a graphics object using the memory bitmap.    objGraphics = Graphics.FromImage(m_objDrawingSurface)    ' Create a new font object.    objFont = New System.Drawing.Font("Arial", _                     mc_FontSize, FontStyle.Bold)    ' Draw the user's text.    objGraphics.DrawString(TextToDraw, objFont, _                           System.Drawing.Brushes.Red, _                           mc_TextX, mc_TextY)    ' Clean up.    objGraphics.Dispose()    ' Force the form to paint itself. This triggers the Paint event.     Me.Invalidate() End Sub 

11.2 Use .NET objects rather than API calls whenever possible.

In previous editions of Visual Basic, you often had to turn to the Windows API in order to get "under the hood" and accomplish tasks that simply could not be done with Visual Basic itself. The .NET Framework is composed of a comprehensive object model that encapsulates most of the Windows API; if you're interested in calling a Windows API function, chances are good that there is a .NET Framework object that accomplishes the same thing.

Windows APIs are unmanaged code; they use different data types than do .NET applications, and they support no type libraries they're not even COM objects. One result of this fact is that Windows APIs don't support enumerations, and calling an API often means looking up required constants in some form of documentation (not in the context-sensitive online help, however). The .NET Framework, on the other hand, provides much of the same functionality of the Windows API (including additional capabilities such as GDI+ functions) wrapped in objects that are more intuitive (usually) than API function calls, run as managed code, share the same type system as all other .NET applications, support type libraries, and have integrated help.

Note

In general, you should use a .NET object instead of calling the Windows API whenever possible.


 

Perhaps the biggest drawback of using .NET Framework objects over the Windows API is that a wealth of books and sample code illustrates how to do things with the API (not to mention a lot of legacy code that uses the Windows API), while comparatively little details the equivalent functions using the .NET Framework. I expect this to change, perhaps even rather quickly. In the meantime, finding the appropriate .NET substitute for a Windows API call will largely be a process of discovery.

Incorrect:
Private Declare Function GetSystemDirectory _                             Lib "kernel32" _                             Alias "GetSystemDirectoryA" _                             (ByVal lpBuffer As String, _                             ByVal nSize As LongAs Long        Dim lngReturn As Long    Dim strBuffer As String = ""    Dim strPath As String    Dim strSystemFolder As String     ' Pad the buffer for the API call.    strBuffer = strBuffer.PadLeft(255, "0")    ' The API call returns the number of characters in the path.    lngReturn = GetSystemDirectory(strBuffer, strBuffer.Length)    strSystemFolder = strBuffer.Substring(0, lngReturn) 
Correct:
   Dim strSystemFolder As String    strSystemFolder = System.Environment.SystemDirectory() 

Tip

I highly recommend you check out the documentation on System.Environment; it supports a lot of useful properties and methods.


 

11.3 Expose public properties, not public variables.

Any variable declared as Public within a class becomes a "property" of the class. (Actually, the behavior is similar to a property, but such a variable is called a field.) For example, suppose you have the following statement in the Declarations section of a class:

Public Length As Long 

Clients could manipulate the field using code such as this:

obj.Length = 255 

Although this works, serious limitations make this approach less than desirable. These concerns include the following:

  • You can't execute code when a field value changes. For example, what if you wanted to write the new value of length to a database? Because the client application can access the variable directly, you have no way of knowing when the value of the variable changes.

  • There is no way to implement data validation on a field. For example, you couldn't prevent a user from passing a value to the Length field that falls outside some acceptable range.

New to Visual Basic .NET is the ability to create a read-only field. You do this by using the ReadOnly keyword like this:

Public ReadOnly MyField As Integer 

You might be tempted to do this; however, this approach has an important drawback. If you ever need to let a user modify the value or you ever need to run code when the property value is retrieved, you'll have to create a property in place of the field. Doing this will break compatibility with existing clients. If you create a property to begin with, you only have to change the implementation within the property procedure and you won't break compatibility in doing so.

When you need to create a member whose value can be changed, or a member that needs to execute code when called, you have no choice but to create a property rather than declare a public field. The best long-term approach is simply not to use fields, but rather to always create properties.

Incorrect:
Public Class Class1    ' Expose Length as a field.     Public Length As Integer     End Class 
Correct:
Public Class Class1    Private m_intLength As Integer    Public Property Length() As Integer       Get          Return m_intLength       End Get       Set(ByVal Value As Integer)          ' Data validation code goes here...                   m_intlength = Value       End Set    End Property End Class 

11.4 Always close what you open.

At times, you might find yourself using some form of resource within a class, such as a COMM port. Whenever you open such a resource, be sure to close it appropriately. Usually, the best time to do this is as soon as you're finished with the resource. However, you might need to keep the resource open for the life of the object. In this case, your best bet is to close the resource in your object's Dispose event. You might also consider implementing some sort of Close method on your class so that clients can close the resource when they no longer need it.

Incorrect:
Public Overloads Sub Dispose()    ' Let the ADO.NET objects go out of scope... End Sub 
Correct:
Public Overloads Sub Dispose()    ' Close all open ADO.NET objects.    m_dstContacts.Close()    m_cnADOConnection.Close() End Sub 

11.5 Use Overloads to create a property or method with the same name as another property or method but with a different argument list.

In my opinion, one of the most exciting new object-oriented features is the ability to overload procedures. When you overload a procedure, you create a procedure with the same name as an existing procedure, but one that accepts a different set of arguments. If you've ever had to come up with creative names for two nearly identical procedures that do the same thing but use different arguments, then you'll appreciate this feature.

To use Overloads to create procedures with the same name, each overloaded procedure must have a different argument list. The argument lists can differ in the number of arguments, their types, or both; modifiers such as ByVal and ByRef don't count as a differentiation, nor do return types. The difference in the arguments list is what enables the compiler to choose which overloaded procedure to invoke in a given situation.

Incorrect:
Public Sub DrawText(ByVal strText As String)    ' Code to draw the text goes here. End Sub Public Sub DrawNumber(ByVal sngNumber As Single)    ' Code to draw the number goes here. End Sub 
Correct:
Public Overloads Sub Draw(ByVal strText As String)    ' Code to draw the text goes here. End Sub Public Overloads Sub Draw(ByVal sngNumber As Single)    ' Code to draw the number goes here. End Sub 
Practical Application
11.5.1 Always include the keyword Overloads when overloading procedures.

It's not required that you include the Overloads keyword when defining overloaded procedures. However, it greatly adds to the readability of the code because it quickly tells a reader that there are other procedures with the same name.

Important

If you specify the Overloads keyword for one of a set of overloaded procedures, you must supply it for all of procedures with the same name.


 

Incorrect (this code does work, however):
Public Sub Draw(ByVal strText As String)    ' Code to draw the text goes here. End Sub Public Sub Draw(ByVal sngNumber As Single)    ' Code to draw the number goes here. End Sub 
Correct:
Public Overloads Sub Draw(ByVal strText As String)    ' Code to draw the text goes here. End Sub Public Overloads Sub Draw(ByVal sngNumber As Single)    ' Code to draw the number goes here. End Sub 

11.6 Create Dispose methods for all of your objects.

As I mentioned earlier in the section on garbage collection, you can never be certain when an object will be destroyed and the resources it uses (such as system objects or database connections) released. This means you can no longer rely on placing cleanup code in a class's Terminate event and expect it to execute when the last reference to an object is released. In fact, classes in Visual Basic .NET don't even have a Terminate event. MSDN offers this bit of information:

The only potential disadvantage of traced garbage collection is the interval between the release of the last reference that can be reached by running code, and the moment when the garbage collector detects this condition. On a lightly loaded system with a great deal of memory, considerable time may elapse before your component's destructor is called.

Don't let the potential disadvantage text fool you this is a serious issue that you need to take into account when designing and consuming objects. The solution to this problem is twofold:

  • Create a Dispose method for each of your objects and place cleanup code in that method.

  • Call the Dispose method on each and every object that has one, once you are finished with the object.

    Note

    When creating shared components, you must be careful about the code you place in a custom Dispose method. For example, if the component is using a resource on behalf of multiple clients, releasing the resource when a client calls Dispose could cause problems for the remaining clients that are sharing the object.


     

Some sources recommend adding a custom Dispose method only to components that use limited or expensive resources. This approach has a number of problems, including the following:

  • Calling Dispose on objects that support it is a must, and you need to get into the habit of doing this. If some of your objects have a custom Dispose method and some don't, you and other developers might miss calling Dispose for an object that actually needs it called.

  • What happens if you modify a component such that you now need to execute in a custom Dispose method when you didn't previously? If the object has already been distributed without a custom Dispose method, all client code would have to be modified to call Dispose. If you had previously implemented a custom Dispose method even if it didn't actually do anything you could easily insert the necessary code without negatively affecting existing client code.

For these reasons, I highly recommend that you add custom Dispose methods to all of your objects, whether the Dispose methods actually do something or not. Also, if you consume an object that has a Dispose method, you should always call the method at the appropriate time.

Note

If your object holds references to other objects that have Dispose methods, you should call those methods from your own Dispose method.


 

Adding a Dispose method is easy; you just define the method like this:

Public Sub Dispose()    ' Insert cleanup code here. End Sub 

If your object inherits from System.ComponentModel.Component, the object already has a default implementation of Dispose, which is why I keep referring to the method you create as a "custom Dispose method." In this case, you'll have to overload the new method like this:

   Public Overloads Sub Dispose()       ' Insert cleanup code here.     End Sub 

The compiler will tell you if you need to overload the method, as shown in Figure 11-1.

Figure 11-1. When you need to overload a Dispose method, the .NET compiler will tell you so.

graphics/f11ln01.jpg

Caution

When consuming COM objects from within Visual Basic .NET, pay careful attention to the resources used by the object. COM objects were built around a reference counting scheme that insured an object was destroyed when its last reference was released, and cleanup code was often placed in the class's Terminate event. Because .NET uses garbage collection instead, you might run into problems with some COM objects not freeing their resources in a timely manner.


 

11.7 Create constructors for a class whenever possible.

The object-oriented features of Visual Basic .NET provide a mechanism for you to add constructors to your classes. In the past, a user had to create an instance of a class and then set values as shown here:

Dim objContact As New Contact() objContact.FirstName = "James" objContact.LastName = "Foxall" 

One of the drawbacks to this approach was that you could not force the client to provide initial values in order to create an object. This meant you had to write additional code to make sure required information had been sent to the class by way of a property or method. Constructors allow a client to pass initialization information to the class when creating a new object from the class. With a constructor, you can create a class that accepts (and even requires) initialization data. For example, using the same Contact object with a constructor, a client could create a new instance of the class with the following single statement:

Dim objContact As New Contact("James", "Foxall") 

You can even overload the constructor to allow different argument lists to be passed. For example, you could provide a generic constructor (allowing the user to use New while providing no arguments) and a specific constructor that accepts arguments. You could also leave out the generic implementation so that the user is forced to provide initialization data. Figure 11-2 illustrates the compile error that occurs if you attempt to create a new object instance without initialization data when using a class that requires a constructor.

Figure 11-2. You can force clients to provide initialization data.

graphics/f11ln02.jpg

To create a constructor, you define a procedure within the class called New, assigning it the appropriate access level. (For instance, you can create a Private constructor that can be called only from within the class itself.) For example, to create the constructor for the fictitious Contact class discussed earlier, you could use the following procedure:

Public Sub New(ByVal FirstName As StringByVal LastName As String)    ' Perform initialization code with the supplied parameters here. End Sub 

Note that if you were to add this procedure to a class, it would force the client code to specify the initialization data; a client could not instantiate an instance of the class without the initialization data using a statement such as

Dim objContact As New Contact()

. To provide a generic implementation (one that requires no initialization data), you would have to add the following procedure in addition to the one shown above:

 

Public Sub New()    ' Place code for generic initialization here. End Sub 

You can do some interesting things with the various implementations of New in a class. For instance, you can have the generic implementation call a more specific implementation like this:

Public Sub New()    ' Call another constructor with default initialization values.     MyClass.New("John", "Doe") End Sub 

Note

You cannot use the keyword Overloads on constructors, even though creating multiple constructors is indeed creating overloaded methods.


 

You can create as many constructors as you need to, as long as each has a unique argument list. For example, you could add the following constructor to the Contact class along with the two already defined:

Public Sub New(ByVal ContactName As String)    ' Perform initialization code with a full name supplied. End Sub 

You should consider adding a constructor to a class when

  • The class requires specific pieces of data. In this case, consider leaving out a generic implementation and supplying only constructors that require the needed information.

  • There exists common data used by the class. If client code typically sets the same properties each time an object instance is created, providing a constructor that accepts the data makes writing the client code a little easier and more efficient.

  • You need to implement some sort of instance counter. In this case, you could increment a counter in the constructor and decrement the counter in the Dispose method (if you can be sure that clients will call Dispose properly) or in the Finalize method discussed in the next section.

    Note

    A constructor serves the same purpose as the Initialize event of Visual Basic 6 albeit with much more functionality and flexibility.


     

Practical Application
11.7.1 Create the proper visibility for a class.

In Visual Basic 6, the visibility of a class was determined by its Instancing property. This has been changed in Visual Basic .NET. Now, the access modifiers you use for the class itself, as well as its constructors, determine the visibility of the class. Table 11-1 illustrates the Instancing values of Visual Basic 6 and their appropriate implementation in Visual Basic .NET.

 

Table 11-1. Visual Basic .NET Class Visibility Settings

Visual Basic 6 Instancing

Visual Basic .NET Implementation

Private

Set the class declaration to Friend.

PublicNotCreatable

Set the class declaration to Public and all constructors (Sub New) to Friend.

SingleUse

Not supported.

GlobalSingleUse

Not supported.

MultiUse

Set the class declaration to Public, and declare the constructors as Public.

GlobalMultiUse

Not supported.

 

For example, to create a class available outside a project, but one where objects derived from the class can be instantiated only from within the project, you could use code like this:

Public Class Contact    Friend Sub New()       ' Initialize the object here.     End Sub End Class 

Note

This practical application doesn't include correct/incorrect examples.


 

11.8 Add a finalizer to a class only when necessary.

If you developed classes in Visual Basic 6, you've undoubtedly used the Terminate event at some point in time. The Terminate event in Visual Basic 6 executes when the last reference to an object is released right before the object is destroyed. This event was used to execute cleanup code for the object. As I explained earlier, Visual Basic .NET uses a garbage collection scheme to destroy objects and reclaim their resources. Under garbage collection, you can never be assured of when an object is destroyed, and there is no Terminate event. However, Visual Basic .NET supplies a mechanism for similar behavior: the Finalize method.

Every class has a default implementation of Finalize, which is called by the garbage collector before actually destroying an object. Once again, you can never be sure when this will happen, but at least you can execute specific code when it does. If you don't have any code that needs to execute when an object is destroyed, there's no need to create a custom Finalize method in your class. In fact, it's desirable not to have a custom Finalize method because of the negative impact it can have on performance.

The reason that a custom Finalize event negatively affects performance has to do with the way the garbage collector deals with objects that have a custom implementation of Finalize. When such an object is created, the garbage collector places an entry in the finalization queue, an internal structure that manages objects with custom Finalize methods. Of course, it takes some resources to accomplish this task. When an object with a custom Finalize method is no longer used, the garbage collector must make two passes in order to reclaim the resources used by the object. The first pass of the garbage collector reclaims resources of all objects that don't have custom Finalize methods (objects that don't appear in the finalization queue). During this pass, the garbage collector is unable to collect objects in the finalization queue. Instead, these objects are removed from the finalization queue and marked as ready for finalization. A special run-time thread then becomes active and calls the Finalize method for each of the objects marked ready for collection and removes them from the list of marked objects. A future garbage collection will then determine that the objects are ready to be collected (they no longer appear in the finalization queue or the list of marked objects ready for finalization). At this time, the objects will be collected. As you can imagine, this entire process takes resources and actually increases the amount of time that occurs before such an object is finally destroyed by the garbage collector.

That said, if you positively must execute code when an object is destroyed (such as cleanup code to release any unmanaged resources still being held), a custom Finalize method is the way to go. To create a custom Finalize method in a class, declare a procedure like this:

Protected Overrides Sub Finalize()    ' Cleanup code goes here. End Sub 

Caution

You should avoid referencing other objects in your finalize code because the nature of garbage collection is such that finalizers aren't executed in any special order. If you reference objects in finalizing code, the objects might have already been finalized and collected, in which case the finalizing code would fail.


 

Note the use of the keyword Protected in this code sample. This prevents client code from calling your Finalize method directly which should never be allowed to occur. You can imagine the havoc wreaked by freeing resources as though the object were being destroyed, when in fact the object is still being used. For this reason, if you happen to use an object that makes its Finalize method available to you, don't call it. If you want to allow a user to initiate cleanup code, implement a Dispose method.

Note

For an interesting experiment, create a class and add a custom Finalize method containing only a Stop statement. Then create a button on a form that instantiates an object derived from the class and immediately discards the object by setting the object variable to Nothing. See how long it takes before the Finalize method is actually called you might be very surprised, as I was.


 

11.9 Force garbage collection only when necessary.

The garbage collector will scan for and reclaim resources automatically when a request for memory cannot be satisfied using available memory. There might be times, however, when you want to force garbage collection. For example, say your code just released a large number of objects that used a considerable amount of resources. If you have a beefy machine, garbage collection might not be triggered right away because the garbage collector might determine that your application still has enough memory available to it. However, you might be interested in reclaiming all of those resources right away and therefore would like to force a garbage collection. To force garbage collection, call the Collect method of System.GC like this:

System.GC.Collect() 

As you can see, forcing garbage collection is a trivial task. As such, you might be tempted to do this regularly within your code. It's usually best to let the garbage collector determine when to reclaim resources, however. The garbage collector itself consumes processor time and other resources, and forcing it to collect at regular intervals when it's really not necessary to do so could negatively impact the performance of your application (and the entire system as well).

11.10 Use With End With to increase performance and make code more readable.

Whenever you write a set of code that repeatedly references the same object, consider placing the code in a With block. When you use a With block, your code will be more efficient, with the added bonus of less typing and more readability.

Incorrect:
Private Sub btnAddNew_Click(ByVal sender As System.Object, _                             ByVal As System.EventArgs) _                             Handles btnAddNew.Click    ' Create a new contact record, populating it with the     ' data entered by the user.    m_rstContacts.AddNew()    m_rstContacts.Fields("ContactName").Value = _          txtNewContactName.Text    m_rstContacts.Fields("State").Value = txtNewState.Text    m_rstContacts.Update()    ' Show the newly created record.     Call ShowCurrentRecord() End Sub 
Correct:
Private Sub btnAddNew_Click(ByVal sender As System.Object, _                             ByVal As System.EventArgs) _                             Handles btnAddNew.Click    ' Create a new contact record, populating it with the     ' data entered by the user.     With m_rstContacts       .AddNew()       .Fields("ContactName").Value = txtNewContactName.Text       .Fields("State").Value = txtNewState.Text       .Update()    End With     ' Show the newly created record.     Call ShowCurrentRecord() End Sub 
Practical Application
11.10.1 Eliminate as many "dots" as possible in a With statement.

When creating a With End With block, use the lowest-level object possible.

Incorrect:
With frmCallingForm    ' Create a random number for X coordinate of the text.    intTextX = Int(((.ClientRectangle.Width - 20) - 0 + 1) _               * Rnd() + 0)    ' Create a random number for Y coordinate of the text.    intTextY = Int(((.ClientRectangle.Height - 20) - 0 + 1) _               * Rnd() + 0) End With 
Correct:
With frmCallingForm.ClientRectangle    ' Create a random number for X coordinate of the text.    intTextX = Int(((.Width - 20) - 0 + 1) * Rnd() + 0)    ' Create a random number for Y coordinate of the text.    intTextY = Int(((.Height - 20) - 0 + 1) * Rnd() + 0) End With 

Note

In previous editions of Visual Basic, it was recommended that you not nest With End With structures. This is no longer taboo in Visual Basic .NET.



Practical Standards for Microsoft Visual Basic. NET
Practical Standards for Microsoft Visual Basic .NET (Pro-Developer)
ISBN: 0735613567
EAN: 2147483647
Year: 2005
Pages: 84

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net