Many books on VB tell programmers to always use ByVal. They justify this by saying that it is both faster and safer.
You can see that ByVal is slightly faster by the results of the Tests2: Empty functions:
xTestByRef 13.4190000000017 xTestByVal 13.137999999999
With a simple arithmetic expression in the function: xTestAddByRef 15.7870000000039
xTestAddByVal 15.3669999999984
You can also see for that the difference is slight. Be careful when interpreting coding benchmarks, even the simplest can be misleading and it is always wise to profile the real code if you think you have a bottle-neck.
The other claim is that ByVal is safer. This is usually explained by saying that a function in which all arguments are declared ByVal cannot alter the values of the variables in the caller because VB makes copies instead of using pointers. The programmer is then free to make use of the incoming arguments in any way he or she sees fit, in particular they can be assigned new values without disturbing the original values in the caller.
The safety provided by this is generally outweighed by the fact that the compiler is then unable to enforce type checking. In Visual Basic Classic variables are automatically converted between string and numeric types when necessary and possible. If arguments are declared ByVal then the caller can provide a String where a Long was expected. The compiler will silently compile the necessary instructions to convert the String to a Long and carry on. At run time you might, or might not, discover the problem. If the string contained a number then it will be converted and the code will 'work', if it doesn't then the code will fail at the point where the function is called.
Function IntByVal(ByVal z as Double) as Long IntByVal = Int(z)
End Function
Function IntByRef(ByRef z as Double) as Long IntByRef = Int(z) End Function Dim s As String 's = "0-471-30460-3" s = "0" Debug.Print IntByVal(s) 2 Chapter 27.3.1 on page 152
Optimizing Visual Basic
Debug.Print IntByRef(s)
If you try to compile the little snippet of code above you will see that the compiler stops on the line:
Debug.Print IntByRef(s)
it hightlights the s and shows a message box saying 'ByRef argument type mismatch'. If you now comment out that line and run again you will not get an error. Now uncomment this line:
s = "0-471-30460-3"
and comment out this one: 's = "0"
Run the program again. Now the program fails but only at run time with 'Runtime error 13: Type mismatch'.
The moral is:
• use ByRef to cause the compiler to typecheck the arguments in function calls, • never assign to the arguments of a function unless they are output parameters.
Look at the Procedure Parameters3 section in the Coding Standards4 chapter for some suggestions about how to name the arguments so that it is always clear which are in and which are out parameters.
27.3.1 Tests
With Empty Functions
Option Explicit
Private Const mlLOOPS As Long = 100000000 Private mnStart As Double
Private mnFinish As Double
Public Sub main() xTestByRef 1#, 2#
Debug.Print "xTestByRef", mnFinish - mnStart xTestByVal 1#, 2#
Debug.Print "xTestByVal", mnFinish - mnStart End Sub
3 Chapter 31.36 on page 212 4 Chapter 30.5 on page 186
ByRef versus ByVal
Private Sub xTestByRef(ByRef a As Double, ByRef b As Double) Dim lLoop As Long
Dim n As Double mnStart = Timer
For lLoop = 1 To mlLOOPS n = xByRef(a, b) Next lLoop mnFinish = Timer End Sub
Private Sub xTestByVal(ByVal a As Double, ByVal b As Double) Dim lLoop As Long
Dim n As Double mnStart = Timer
For lLoop = 1 To mlLOOPS n = xByVal(a, b) Next lLoop mnFinish = Timer End Sub
Private Function xByRef(ByRef a As Double, ByRef b As Double) As Double End Function
Private Function xByVal(ByVal a As Double, ByVal b As Double) As Double End Function
With Simple Arithmetic
Attribute VB_Name = "modMain" Option Explicit
Private Const mlLOOPS As Long = 100000000 Private mnStart As Double
Private mnFinish As Double
Public Sub main() xTestAddByRef 1#, 2#
Debug.Print "xTestAddByRef", mnFinish - mnStart xTestAddByVal 1#, 2#
Debug.Print "xTestAddByVal", mnFinish - mnStart End Sub
Private Sub xTestAddByRef(ByRef a As Double, ByRef b As Double) Dim lLoop As Long
Dim n As Double mnStart = Timer
For lLoop = 1 To mlLOOPS n = xAddByRef(a, b) Next lLoop
mnFinish = Timer End Sub
Private Sub xTestAddByVal(ByVal a As Double, ByVal b As Double) Dim lLoop As Long
Dim n As Double mnStart = Timer
For lLoop = 1 To mlLOOPS n = xAddByVal(a, b) Next lLoop
mnFinish = Timer End Sub
Private Function xAddByRef(ByRef a As Double, ByRef b As Double) As Double xAddByRef = a + b
Optimizing Visual Basic
End Function
Private Function xAddByVal(ByVal a As Double, ByVal b As Double) As Double xAddByVal = a + b
End Function
27.4 Collections
Collections are very useful objects. They allow you to write simpler code than would otherwise be the case. For instance if you need to hold on to a list of numbers given to you by the user you probably won't know in advance how many will be supplied. This makes it difficult to use an array because you must either allocate an unnecessarily large array so that you are sure that no reasonable user would supply more or you must continually Redim5 the array as new numbers come in.
A Collection lets you avoid all that because it expands as necessary. However, this conve- nience comes at the cost of increased runtime under some circumstances. For many, perhaps most, programs this price is perfectly acceptable but some programs take so long to run that you must try to squeeze the last drop of performance out of every line.
This frequently occurs in programs doing scientific computations (don't tell me C would be a better language for such things because the same constraints and optimizations apply to C and all other languages as well).
One of the reasons a Collection is a convenient tool is because you can use a string as a key and retrieve items by key instead of by index. But every time you ask for an item in a collection the collection must first figure out whether you have supplied an Integer (or Byte or Long) or a String which of course takes a small but finite amount of time. If your application makes no use of the ability to look up items by String key you will get faster programs by using arrays instead because VB doesn't need to waste time checking to see if you supplied a String key instead of a whole number index.
If you want a Collection to simply hold a list of items without keys then you can emulate this behaviour with an array as follows (note that this code is not optimal, optimizing it is left as an exercise for the student):
Public Sub Initialize(ByRef a() As Variant, _ ByRef Count As Long, _ ByRef Allocated As Long) Allocated = 10
Redim a(1 to Allocated) Count = 0
End Sub
Public Sub Add(ByRef NewItem as Variant, _ ByRef a() As Variant, _ ByRef Count As Long, _ ByRef Allocated As Long) Count = Count + 1
If Allocated < Count then
Collections
Allocated = Count
Redim Preserve a(1 to Allocated) End If
a(Count) = NewValue End Sub
To use the above code you must declare an array of Variants and two Longs. Call the Initialize sub to start it all off. Now you can call the Add sub as often as you like and the array will be extended as necessary.
There are a number of things to note about this seemingly simply subroutine:
• Once the array size exceeds 10 a new array is allocated each time an item is added (replacing the original),
• The size of the array (recorded in the Allocated variable) is always the same as the Count when Count exceeds 10,
• No provision is made to delete items,
• the items are stored in the same order as they are added. • all arguments are declared Byref
It is unlikely that any programmer would want to include exactly this routine in production code. Some reasons why are:
• NewItem is declared as Variant but the programmer usually knows the type, • The initial allocation is hard coded using a literal integer,
• The name of the subroutine is too simple, it will probably clash with others in the same namespace.
• No provision is made for removing items.
• Three separate pieces of information must be kept together but they are not bound together.
The performance of the emulated Collection depends on the relative frequency of calls to Add, Item and Remove (see #Exercises6). An optimized version must be optimized for the use to which it will be put. If no lookups by key are performed there is no need to provide a function for it and there is especially no need to provide data structures to make it efficient.
27.4.1 Exercises
• Extend the code presented in this section to emulate the Collection class in more detail. Implement Item and Remove methods, see the VB help files for details of the exact method declarations but do not feel obliged to replicate them exactly.
• Write a test program to exercise all the features and time them so that you can tell when to use a Collection and when to use your emulated version.
• You should be able to think of at least two distinct implementations of Remove. Think about the consequences of different implementations.
Optimizing Visual Basic
• Write an explicit description of the requirements that your version of the Collection class satisfies.
• Compare the behaviour of the built in Collection class and the new class, note any differences and give examples of uses where the differences do and do not matter. • If you haven't already done so implement lookup by key.
• Explain why emulating all of the features of the Collection class using exactly the same interface is unlikely to yield significant improvements in performance for at least one specific use case.