Visual Basic

Declaring Your Intentions

The answer to the next question might well depend on whether your primary language is Basic, Pascal, or C. What will be the data type of the variable tmpVarB in each of the following declarations?

  Dim tmpVarB, tmpVarA As Integer
  Dim tmpVarA As Integer, tmpVarB

The first declaration, if translated into C, would produce a data type of integer for tmpVarB. The second declaration, if translated into Pascal, would also produce a data type of integer for tmpVarB. Of course in Visual Basic, either declaration would produce a data type of Variant, which is the default data type if none is explicitly assigned. While this is obvious to an experienced Visual Basic developer, it can catch developers by surprise if they're accustomed to other languages.

Another declaration surprise for the unwary concerns the use of the ReDim statement. If you mistype the name of the array that you are attempting to redim, you will not get any warning, even if you have Option Explicit at the top of the relevant module or class. Instead you will get a totally new array, with the ReDim statement acting as a declarative statement. In addition, if another variable with the same name is created later, even in a wider scope, ReDim will refer to the later variable and won't necessarily cause a compilation error, even if Option Explicit is in effect.

Born again

When declaring a new object, you can use either of the following methods:

  ' Safer method
  Dim wgtMyWidget As Widget
  Set wgtMyWidget = New Widget
  ' Not so safe method
  Dim wgtMyWidget As New Widget

The second method of declaring objects is less safe because it reduces your control over the object's lifetime. Because declaring the object as New tells Visual Basic that any time you access that variable it should create a new object if one does not exist, any reference to wgtMyWidget after it has been destroyed will cause it to respawn.

  ' Not so safe method
  Dim wgtMyWidget As New Widget
  wgtMyWidget.Name = "My widget"
  Set wgtMyWidget = Nothing
  If wgtMyWidget Is Nothing Then
      Debug.Print "My widget doesn't exist"
  Else
      Debug.Print My widget exists"
  End If

In the situation above, wgtMyWidget will always exist. Any reference to wgtMyWidget will cause it to be born again if it doesn't currently exist. Even comparing the object to nothing is enough to cause a spontaneous regeneration. This means that your control over the object's lifetime is diminished, a bad situation in principle.

Safe global variables

In the restricted and hermetically sealed world of discrete components, most developers dislike global variables. The major problem is that global variables break the valuable principle of loose coupling, in which you design each of your components to be reused by itself, with no supporting infrastructure that needs to be re-rigged before reuse is a possibility. If a component you write depends upon some global variables, you are forced to rethink the context and use of these global variables every time you reuse the component. From bitter experience, most developers have found that using global variables is much riskier than using local variables, whose scope is more limited and more controllable.

You should definitely avoid making the code in your classes dependent on global data. Many instances of a class can exist simultaneously, and all of these objects share the global data in your program. Using global variables in class module code also violates the object-oriented programming concept of encapsulation, because objects created from such a class do not contain all of their data.

Another problem with global data is the increasing use of multithreading in Visual Basic to perform faster overall processing of a group of tasks that are liable to vary significantly in their individual execution time. For instance, if you write a multithreaded in-process component (such as a DLL or OCX) that provides objects, these objects are created on client threads; your component doesn't create threads of its own. All of the objects that your component supplies for a specific client thread will reside in the same "apartment" and share the same global data. However, any new client thread will have its own global data in its own apartment, completely separate from the other threads. Sub Main will execute once for each thread, and your component classes or controls that run on the different threads will not have access to the same global data. This includes global data such as the App object.

Other global data issues arise with the use of MTS with Visual Basic. MTS relies heavily on stateless components in order to improve its pooling and allocation abilities. Global and module-level data mean that an object has to be stateful (that is, keep track of its state between method invocations), so any global or module-level variables hinder an object's pooling and reuse by MTS.

However, there might be occasions when you want a single data item to be shared globally by all the components in your program, or by all the objects created from a class module. (The data created in this latter occasion is sometimes referred to as static class data.) One useful means of accomplishing this sharing is to use locking to control access to your global data. Similar to concurrency control in a multiuser database environment, a locking scheme for global data needs a way of checking out a global variable before it's used or updated, and then checking it back in after use. If any other part of the program attempts to use this global variable while it's checked out, an assertion (using Debug.Assert) will trap the problem and signal the potential bug.

One method of implementing this locking would be to create a standard (non-class) module that contains all of your global data. A little-known fact is that you can use properties Get/Let/Set even in standard modules, so you can implement all your global data as private properties of a standard module. Being a standard module, these variables will exist only once and persist for the lifetime of the program. Since the variables are actually declared as private, you can use a locking scheme to control access to them. For example, the code that controls access to a global string variable might look something like this:

  'Note that this "public" variable is declared Private
  'and is declared in a standard (non-class) module.
  Private gsMyAppName As String
  Private mbMyAppNameLocked As Boolean
  Private mnMyAppNameLockId As Integer
  Public Function MyAppNameCheckOut() As Integer
  'Check-out the public variable when you start using it.
  'Returns LockId if successful, otherwise returns zero.
      Debug.Assert mbMyAppNameLocked = False
      If mbMyAppNameLocked = False Then
          mbMyAppNameLocked = True
          mnMyAppNameLockId = mnMyAppNameLockId + 1
          MyAppNameCheckOut = mnMyAppNameLockId
      Else
          'You might want to raise an error here too,
          'to avoid the programmer overlooking the return code.
          MyAppNameCheckOut = 0
      End If
  End Function
  Property Get MyAppName(ByVal niLockId As Integer) As String
  'Property returning the application name.
  'Assert that lock id > 0, just in case nobody's calling CheckOut!
  'Assert that lock ids agree, but in production will proceed anyway.
  'If lock ids don't agree, you might want to raise an error.
      Debug.Assert niLockId > 0
      Debug.Assert niLockId = mnMyAppNameLockId
      MyAppName = gsMyAppName
  End Property
  Property Let MyAppName(ByVal niLockId As String, ByVal siNewValue As Integer)
  'Property setting the application name.
  'Assert that lock id > 0, just in case nobody's calling CheckOut!
  'Assert that lock ids agree, but in production will proceed anyway.
  'If lock ids don't agree, you might want to raise an error.
      Debug.Assert niLockId > 0
      Debug.Assert niLockId = mnMyAppNameLockId
      gsMyAppName = siNewValue
  End Property
  Public Function MyAppNameCheckIn() As Boolean
  'Check-in the public variable when you finish using it
  'Returns True if successful, otherwise returns False
      Debug.Assert mbMyAppNameLocked = True
      If mbMyAppNameLocked = True Then
          mbMyAppNameLocked = False
          MyAppNameCheckIn = True
      Else
          MyAppNameCheckIn = False
      End If
  End Function

The simple idea behind these routines is that each item of global data has a current LockId, and you cannot use or change this piece of data without the current LockId. To use a global variable, you first need to call its CheckOut function to get the current LockId. This function checks that the variable is not already checked out by some other part of the program and returns a LockId of zero if it's already being used. Providing you receive a valid (non-zero) LockId, you can use it to read or change the global variable. When you've finished with the global variable, you need to call its CheckIn function before any other part of your program will be allowed to use it. Some code using this global string would look something like this:

  Dim nLockId As Integer
  nLockId = GlobalData.MyAppNameCheckout
  If nLockId > 0 Then
      GlobalData.MyAppName(nLockId) = "New app name"
      Call GlobalData.MyAppNameCheckIn
  Else
      'Oops! Somebody else is using this global variable
  End If

This kind of locking scheme, in which public data is actually created as private data but with eternal persistence, prevents nearly all the problems mentioned above that are normally associated with global data. If you do want to use this type of scheme, you might want to think about grouping your global routines into different standard modules, depending on their type and use. If you throw all of your global data into one huge pile, you'll avoid the problems of global data, but miss out on some of the advantages of information hiding and abstract data types.