Login   /   Register

VBScript Nitpicking (the good kind) - part.1

Rate this article
     2 votes, average: 1 out of 52 votes, average: 1 out of 52 votes, average: 1 out of 52 votes, average: 1 out of 52 votes, average: 1 out of 5
Loading ... Loading ...
March 30th, 2008 by Yaron Assa

A very refreshing debate at SQAForums has really opened my eyes on the small details involved with Objects, Scopes and Procedure calls. I thought it was a good idea to write a summary of what went on there, and I took the opportunity and added a very detailed background about many basic terms that may be useful to VBScript and programming beginners.

It might seem that the matters I’ll cover are nitpicking in the worse sense – useless, meaningless deliberation over situations that will never happen. However, I can assure you that these situations do happen, and when they do, you’ll get a script that’s inconsistent and impossible to debug. So it is nitpicking, but it’s the useful kind :)

Before we begin, I have to thank Terry Horwath for correcting some misperceptions I had about objects and references. His contribution has led me to dig deeper into the VBScript engine and find out some of the inconsistencies I’ll talk about today.


Background

First of all, some background about the terms I’ll use in this article. It will be pretty detailed, so if you’re familiar with ByRef, ByVal, scope, pointers, objects, references and procedure calls, feel free to jump ahead to Objects and Scope Leaks.

Pointers and objects: Oversimplifying a bit, a pointer is a kind of an arrow, pointing to a specific area in the memory. While you cannot create a pointer in VBScript out of thin air, many of our everyday operations in VBScript are actually done by pointers. For example, the command Set oDic = CreateObject("Scripting.Dictionary") doesn’t actually put the dictionary object in the variable oDic. It creates the object, stores it in memory, and sets oDic as a pointer to that object.

So if after the previous command we execute Set oDic2 = oDic, we haven’t actually created another object. We’ve just copied the old pointer into a new variable name, and now both variables point TO THE SAME OBJECT. This means that changes made on the object via oDic will be reflected in oDic2 – oDic.Add "some", "thing" will result in oDic2 having that value.

It is extremely important to understand that oDic and oDic2 are just conduits leading to the same place. oDic.Add doesn’t perform the add action on oDic (or on oDic2 for that matter), it performs it on the actual object that’s stored in memory.

Another important example that illustrates the difference between the actual object in memory to a variable holding a reference to it is the subject of destroying objects. We usually use the command Set oDic = Nothing to get rid of any object that "was" in oDic (nullifying or destroying it). However, we now know that oDic doesn’t contain an actual object that can be nullified, but only the pointer to it. So actually, Set oDic = Nothing only severed the link between oDic and that actual dictionary object. Now the situation is even worse, as we have an object taking up resources (memory, file locks etc), but completely inaccessible (as nothing points to its location in memory). So how can we actually destroy an object and free the resources it took? Enters the garbage collector.

(Update: This section was corrected according to Eric, who was kind enough to put the time and effort to give his remarks)

After every VBScript statement, VBScript builds and updates a stack (list) of all the local objects used in memory. When the code leaves the scope (e.g. a sub-procedure ends), the garbage collector runs through the stack, attempting to destroy all the objects. However, objects that still have "live" pointers to or from them cannot be released, and are left to consume system resources "forever". So if oDic was the last "live" pointer to the dictionary object, exiting the sub-procedure will initiate a chain reaction that will lead to the removal of the actual dictionary object from memory.

A point to note is that the garbage collector algorithm is not immune to circular reference. So if we have two local variables that point to each other (e.g. two dictionaries, each referring to the other as a dictionary value), the garbage collector will attempt to destroy them, find out that each still has a "live pointer", and leave them untouched. Of course that for us, both objects will be lost forever, since variable we has access to points to them.

Scope: Simply put, it’s from where a specific piece of code can be "seen" or used. Usually when you write a statement in QTP, you usually write it in the scope of the current action you’re in.

This means that if you define a variable within an action, you cannot access it from the other actions (they cannot "see" it), but you CAN access it from functions that are called by that action. For another example, if you define a variable within a function, it isn’t accessible by ANYONE outside that specific function (=outside the function scope).

And as an opposite example, if you attach an external VBS to your test, and define a variable in that VBS, but not in a specific function, it will be defined in "the global scope", which means that any function, sub-procedure or action will "see" it, and will be able to access it.

It’s important to notice the a-symmetry: while the function can "see" all the scopes from the "outside world" (e.g. action scope and global scope), no one can see into the function’s own scope.

You can read more about QTP scopes in Terry’s namespaces article.

ByVal VS ByRef: When calling a function or a sub, we usually ignore the distinction between the two ways for passing parameters to them (ByVal / ByRef), but it can become crucial in certain situations. We’ll illustrate the two ways through an example. Let’s say that we have a variable called sMessage which holds the text "The field value: Something", and the following function:

Function Report(Message)
    Message = Replace(Message, "The field Value: ", "")
    Msgbox (sMessage)
End Function
 

The function is pretty straightforward: it strips everything but the actual field value from the original message, and reports it to the user.

What happens if we define the function to receive the parameter ByRef(by changing the function’s declaration to: Function Report(ByRef Message) )? ByRef means that we’re Message will contain a reference to the original memory slot holding the value, which in our case is the variable sMessage. Oversimplifying, it’s like passing a pointer to the original value.

For this particular function, it would mean that actions performed on Message (within the scope of the function), will be reflected in sMessage (outside the function’s scope). So after we call the function (and send over sMessage as a parameter), sMessage will be changed to "Something".

What happens if we define the function to receive the parameter ByVal (by changing the function’s declaration to: Function Report(ByVal Message) )? Simply put, the function will create A NEW VARIABLE, with a copy of the value of the in-parameter. It will then use the new variable for any operations executed within the scope of the function. This means that no changes made to Message could ever "leak" out of the functions scope, and will never affect sMessage.

Even from this simple example you can realize the catastrophic possibilities inherent in the ByRef method. Since changes within the function "leak out", we must be extra careful not to destroy or alter valuable data that resides outside the function’s scope. On the other side, ByRef can be an excellent method for returning two or more values from a function (which usually has only one return value). Consider this:

Function IsDBError(ByRef sMessage)
    If inStr(sMessage, "DB") < 0 Then
        IsDBError = False
        Exit Function
    End If
 
   sMessage = Replace(sMessage, "Some junk from error message", "")
 
   IsDBError = True
 
End Function
 

This function can return two values – Is the error message referring a DB error (Boolean), and if it is a DB error, a cleaner message, not cluttered with junk text (String). We use our power to leak out of the function’s scope to our advantage, and increase the function’s power and capabilities.

Objects and Scope Leaks

Let’s imagine the following situation:

Sub AddValue(ByVal oDicationay)
   oDicationay.Add "Some", "Thing"
End Sub
 
Set oDic = CreateObject("Scripting.Dictionary")
 
Call AddValue(oDic)
 
Msgbox oDic.Containes("Some") 'What will this print &ndash; True or False?
 

Let’s see what’s going on here. We have a function that receives a dictionary object, and change it within the function’s scope. As we’ve seen in our ByVal example, since the function will create a new copy of the incoming parameter and perform the action on it, there will be no scope leak, and oDic should not contain the key "Some".

Well, I thought so, but I was wrong. This is where Terry had opened my eyes – oDic will contain the new key, although there hasn’t been any scope leak. As we’ve said, ByVal parameters are copied into a new variable within the function, but we haven’t taken into account what exactly is being copied.

As we’ve seen in the pointers section of the background, oDic isn’t really the dictionary object. It’s just a reference (pointer) to the actual dictionary object in memory. So indeed, oDic is copied into a new variable oDictionary, but that changes nothing – they both still point to the same dictionary object. So changes made through oDictionay will be reflected in oDic as they both lead to the same object.

So much for now. In part 2, we’ll examine the sneaky ways ByRef and ByVal can effect object parameters, as well as the different ways to initiate a function / sub-procedure call, and the mess they cause

Posted in VBScript Techniques

Leave a Reply

You must be logged in to post a comment.

This article was viewed 1493 times