[Previous] [TOC] [Next]

User-Defined Types

The rehabilitation of UDTs was the biggest surprise for me in version 6 of Visual Basic. It had looked as if UDTs were being gradually squeezed out of the language. In particular, the new language features such as classes, properties, and methods did not seem to include UDTs. Before version 6, it was not possible to

  1. have a UDT as a public property of a class or form.
  2. pass a UDT as a parameter ByVal to a sub or function.
  3. have a UDT as a parameter to a public method of a class or form.
  4. have a UDT as the return type of a public method of a class or form.
  5. place a UDT into a Variant.

But this has suddenly changed and now it is possible in version 6 to perform most of these to a greater or lesser extent. In this chapter, I am really only concentrating on the last point, that of placing a UDT into a Variant.

Restrictions are imposed on the sorts of UDTs that can be placed in a Variant. They must be declared within a public object module. This rules out their use within Standard EXE programs, as these do not have public object modules. This is a Microsoft ActiveX-only feature. Internally, the Data portion of the Variant structure is always a simple pointer to an area of memory where the UDT's content is sitting. The Type is always 36. This prompts the question of where and how the meta-data describing the fields of the UDT is kept. Remember that all other Variant subtypes are self-describing, so UDTs must be, too. The way it works is that from the Variant you can also obtain an IRecordInfo interface pointer. That interface has functions that return everything you want to know about the UDT.

We are able to improve substantially on the nesting ability demonstrated earlier with Variant arrays. While it is still impossible to have a member field of a UDT be that UDT itself-a hierarchy that is commonly needed-you can use a Variant and sidestep the circular reference trap. The following code shows a simple example of an employee structure (Emp) in an imaginary, not-so-progressive organization (apologies for the lack of originality). The boss and an array of workers are declared as Variant-these will all in fact be Emps themselves. GetEmp is just a function that generates Emps.

' In Class1
  Public Type Emp
      Name As Variant
      Boss As Variant
      Workers() As Variant
   End Type
  ' Anywhere Class1 is visible:
  Sub main()
      Dim a As Emp
      a.Name = "Adam"
      a.Boss = GetEmp(1)
      a.Workers = Array(GetEmp(2), GetEmp(3))
  End Sub
  Private Function GetEmp(ByVal n) As Emp
      Dim x As Emp
      x.Name = "Fred" & n
      GetEmp = x
  End Function

Note that this code uses the ability to return a UDT from a function. Also, the Array function always creates an array of Variants, so this code now works because we can convert the return value of GetEmp to a Variant.

Interface Inviolability

If you're like me, you may well have experienced the frustration of creating ActiveX components (in-process or out-of-process, it doesn't matter) and then realizing you need to make a tiny upgrade.

You don't want to change the interface definition because then your server is no longer compatible, the CLSID has changed, and you get into all the troublesome versioning complexity. Programs and components that use your component will all have problems or be unable to automatically use your upgraded version.

There isn't a lot you can do about this. Visual Basic imposes what is a very good discipline on us with its version compatibility checking, though it is sometimes a bitter pill to swallow.

In this respect, the flexibility gained by using Variants for properties and methods' parameters can be a great headache saver.

One drawback to this is that Visual Basic does not know at compile time the actual type of Workers, so you might write errors that will not be found until run time, such as the following:
a.Workers.qwert = 74

Accessing an invalid property like this will not be caught at compile time. This is analagous to the behavior of using Variants to hold objects described earlier. Similarly, the VarType of a.Workers is 8204-vbArray + vbVariant. Visual Basic does not know what is in this array. If we rewrote the above code like this:

' In Class1
  Public Type Emp
      Name As Variant
      Boss As Variant
      Workers As Variant
   End Type
  ' Anywhere Class1 is visible:
  Sub main()
      Dim a As Emp
      ReDim a.Workers(0 To 1) As Emp
      a.Name = "Adam"
      a.Boss = GetEmp(1)
      a.Workers(0) = GetEmp(2)
      a.Workers(1) = GetEmp(3)
  End Sub

This time the VarType of a.Workers is 8228-vbArray + vbUserDefinedType. In other words, Visual Basic knows that Workers is an array of Emps, not an array of Variants. This has similarities to the late-bound and early-bound issue with objects and classes. (See "How Binding Affects ActiveX Component Performance" in the Visual Basic Component Tools Guide.) At compile time, however, the checking of valid methods and properties is still not possible because the underlying declaration is Variant.

The alternative way of implementing this code would be to create a class called Emp that had other Emps within it-I'm sure you've often done something similar to this. What I find interesting about the examples above is the similarity they have with this sort of class/object code-but no objects are being created here. We should find performance much improved over a class-based approach because object creation and deletion still take a relatively long time in Visual Basic. This approach differs slightly in that an assignment from one Variant containing a UDT to another Variant results in a deep copy of the UDT. So in the above examples, if you copy an Emp, you get a copy of all the fields and their contents. With objects, you are just copying the reference and there is still only one underlying object in existence. Using classes rather than UDTs for this sort of situation is still preferable given the many other advantages of classes, unless you are creating hundreds or thousands of a particular object. In this case, you might find the performance improvement of UDTs compelling.
MORE ON PASSING PARAMETER BY REFERENCE

You might be wondering, "Why should I avoid passing parameters by reference? It's often very useful." In many situations, passing parameters by reference is indicative of bad design. Just as using global variables is bad design but can be the easy or lazy way out, passing parameters by reference is a shortcut that often backfires at a later date.

Passing parameters by reference is a sign that you don't have the relationships between your functions correct. The mathematical model of a function is of the form:

x = f (a,b,c,..)

where the function acts on a,b,c, and so on to produce result x. Both sides of the equal sign are the same value. You can use either x or f(a,b,c,...) interchangeably.

Likewise in Visual Basic, functions can be used as components in expressions, as this example shows:

x = Sqr(Log(y))

This is not quite the case in a Visual Basic program, because the function does something in addition to returning a value. But it's still most useful to think of the return value x as the result of what that function f does. But if x contains the result, the result cannot also be in a, b, or c. In other words, only x is changed by the function. This is my simplistic conceptual model of a function, and it is at odds with the notion of passing by reference. Passing a parameter by reference often indicates one of the following:

Functions are trying to do more than one task

This is going to lead to larger functions than need be, functions that are more complex than need be, and functions that are not as useful as they could be. You should break down the functions so that each one does only one task, as in the mathematical model.

A new class needs to be defined

If the function needs to return two related values, say an X and a Y value for a coordinate, create a class or UDT to hold the object that these values relate to, and return that. If the values are not sufficiently related to be able to define a class, you are almost certainly doing too much in the one function. As an example, this

GetCenter(f As Form, ByRef X, ByRef Y)

would be better written as

Set p = GetCenter(f As Form)

where p is an object of class Point. Alternatively

p = GetCenter(f as Form)

here p is a Variant UDT.

Functions are returning some data and some related meta-data

By meta-data I mean a description of the data returned. Functions won't need to return meta-data if you use only self-describing data types. For example, functions that return an array or a single element, depending upon some argument, should return a Variant, which can hold either, and the caller can use IsArray to determine what sort of data is returned.

[Previous] [TOC] [Next]