When should an Excel VBA variable be killed or set to Nothing? - variables

I've been teaching myself Excel VBA over the last two years, and I have the idea that it is sometimes appropriate to dispose of variables at the end of a code segment. For example, I've seen it done in this bit adapted from Ron de Bruin's code for transferring Excel to HTML:
Function SaveContentToHTML (Rng as Range)
Dim FileForHTMLStorage As Object
Dim TextStreamOfHTML As Object
Dim TemporaryFileLocation As String
Dim TemporaryWorkbook As Workbook
...
TemporaryWorkbook.Close savechanges:=False
Kill TemporaryFileLocation
Set TextStreamOfHTML = Nothing
Set FileForHTMLStorage = Nothing
Set TemporaryWorkbook = Nothing
End Function
I've done some searching on this and found very little beyond how to do it, and in one forum post a statement that no local variables need to be cleared, since they cease to exist at End Sub. I'm guessing, based on the code above, that may not be true at End Function, or in other circumstances I haven't encountered.
So my question boils down to this:
Is there somewhere on the web that explains the when and why for variable cleanup, and I just have not found it?
And if not can someone here please explain...
When is variable cleanup for Excel VBA necessary and when it is not?
And more specifically... Are there specific variable uses (public variables?
Function-defined variables?) that remain loaded in memory for longer
than subs do, and therefor could cause trouble if I don't clean
up after myself?

VB6/VBA uses deterministic approach to destoying objects. Each object stores number of references to itself. When the number reaches zero, the object is destroyed.
Object variables are guaranteed to be cleaned (set to Nothing) when they go out of scope, this decrements the reference counters in their respective objects. No manual action required.
There are only two cases when you want an explicit cleanup:
When you want an object to be destroyed before its variable goes out of scope (e.g., your procedure is going to take long time to execute, and the object holds a resource, so you want to destroy the object as soon as possible to release the resource).
When you have a circular reference between two or more objects.
If objectA stores a references to objectB, and objectB stores a reference to objectA, the two objects will never get destroyed unless you brake the chain by explicitly setting objectA.ReferenceToB = Nothing or objectB.ReferenceToA = Nothing.
The code snippet you show is wrong. No manual cleanup is required. It is even harmful to do a manual cleanup, as it gives you a false sense of more correct code.
If you have a variable at a class level, it will be cleaned/destroyed when the class instance is destructed. You can destroy it earlier if you want (see item 1.).
If you have a variable at a module level, it will be cleaned/destroyed when your program exits (or, in case of VBA, when the VBA project is reset). You can destroy it earlier if you want (see item 1.).
Access level of a variable (public vs. private) does not affect its life time.

VBA uses a garbage collector which is implemented by reference counting.
There can be multiple references to a given object (for example, Dim aw = ActiveWorkbook creates a new reference to Active Workbook), so the garbage collector only cleans up an object when it is clear that there are no other references. Setting to Nothing is an explicit way of decrementing the reference count. The count is implicitly decremented when you exit scope.
Strictly speaking, in modern Excel versions (2010+) setting to Nothing isn't necessary, but there were issues with older versions of Excel (for which the workaround was to explicitly set)

I have at least one situation where the data is not automatically cleaned up, which would eventually lead to "Out of Memory" errors.
In a UserForm I had:
Public mainPicture As StdPicture
...
mainPicture = LoadPicture(PAGE_FILE)
When UserForm was destroyed (after Unload Me) the memory allocated for the data loaded in the mainPicture was not being de-allocated. I had to add an explicit
mainPicture = Nothing
in the terminate event.

Related

Can I safely check when a COM object has been released in VBA?

I've been reviewing some code for creating weak-references in VBA by manually dereferencing object pointers without calling IUnknown::AddRef, and I've found a bug that I can't explain. I could come up with a minimal reproducible example using the pure API calls, but I think it's easier just to demonstrate using the WeakReference class from that review. Here's how I can crash Excel:
Dim strongRef As Range
Set strongRef = [A1]
Dim weakRef As New WeakReference
weakRef.Object = strongRef
Debug.Assert strongRef.address = weakRef.Object.address 'fine
Set strongRef = Nothing
Debug.Assert weakRef.Object Is Nothing 'fine if step through, crashes if F5
This is buggy behaviour; the WeakReference class is designed in such a way that once the parent reference is destroyed, the weak reference should return Nothing rather than attempt blindly to dereference the parent ObjPtr which would now be pointing to an invalid object instance. The way it does this is explained in detail in the linked question, but essentially it caches the parent object's VTable pointer, then uses this to check the VTable pointer is still valid before every dereference. Basically the class relies on the fact that when the parent object goes out of scope, its memory is reclaimed and so the VTable pointer is overwritten with something else.
That should stop this kind of bug. However it doesn't, and I'm wondering why...
It was my understanding that
Set strongRef = Nothing
Calls IUnknown::Release
This sets the ref count to zero, the object goes out of scope
The object is responsible for releasing its own instance memory, so it uses the this pointer (first arg to IUnknown::Release) to zero the instance memory (including the VTable pointer) and free it for use by the VBA memory allocator again
Finally the value at VarPtr(strongRef) is set to zero to indicate it is a null object reference
However I think the bug is happening because the instance memory is not reset as soon as the reference count hits zero, so perhaps VBA's implementation of IUnknown::Release marks the memory as "dirty" to be cleared up at a later date by an asynchronous garbage collector? I'm just guessing here. The thing is, if I step through the code line by line then it works fine, or if you hold the WeakReference in a child class then it works fine (see the examples in the linked post).
UPDATE
I just tried, with a custom VBA class for strongRef, e.g.
Class1
Option Explicit
Static Property Get address() As Double
Dim value As Double
If value = 0 Then value = [RandBetween(1,1e10)]
address = value
End Property
...then I don't get a crash! So it's definitely something to do with specific implementations of IUnknown::Release and is probably why the author of that code never noticed the bug.

Declaring Variables Memory Leaks

I am wondering what would be the most correct way to deal with memory when using VBScript. Should declare all variables right before I use them? The beginning of the program? I understand global vs local, however in my script all variables are local. I know that memory leaks will never be a problem when writing in VBScript 99.9% of the time, but I am also curious as to the 'best' way to clear and release memory within a script. By 'best' I mean, the timing of clearing variables/objects (right after you are done using them vs the end of the script), etc.
An example:
Dim fso: Set fso = CreateObject("Scripting.FileSystemObject")
Dim arrList : Set arrList = CreateObject("System.Collections.ArrayList")
Dim objDict: Set objDic = CreateObject( "Scripting.Dictionary")
Dim objEmail : Set objEmail = CreateObject("CDO.Message")
Dim someArray(), x, y, z, item
It's best practice to declare all variables, but not for the reason you assume. VBScript is sufficiently good at cleaning up after itself, so memory leaks usually aren't an issue. Most of the time you don't even need to release objects (Set var = Nothing) because they're automatically destroyed when leaving the context.
The reason why you still want to declare your variables is that you want to use Option Explicit in your scripts (which enforces variable declarations), so that you can avoid problems due to mistyped or otherwise uninitialized variables. Without Option Explicit VBScript would automagically create missing variables and initialize them with an empty/zero value. Silly example:
Dim foo : foo = 3
Dim bar : bar = 1
Do
bar = bar + fo 'mistyped variable, initilized as empty/0
Loop Until bar > 10
WScript.Echo bar
Running the above would create an infinite loop. If you add Option Explicit the script will instead immediately terminate with a runtime error:
C:\path\to\your.vbs(5, 3) Microsoft VBScript runtime error: Variable is undefined: 'fo'
The VBScript garbage collector runs at the end of every line to clear implicit variables and at the end of every procedure (end sub, end function, and end property) to clear explicit variables. Objects are similar but have added constraints. It works similar to VBA's garbage collector. By contrast JScript waits until 30,000 objects have gone out of scope before running and freeing memory.
An implicit variable is an unnamed variable - msgbox LCase(UCase("String") has two implicit variables - the result of UCase("String") and that is passed to LCase(implicitVar1) which returns implicitVar2 which is passed to Msgbox. An Explict variable is declared either by DIM or just by using it as in A=5 which creates an explicit variable called A.
VBScript, on the other hand, has a much simpler stack-based garbage collector. Scavengers are added to a stack when they come into scope, removed when they go ou t of scope, and any time an object is discarded it is immediately freed.
https://blogs.msdn.microsoft.com/ericlippert/2003/09/17/how-do-the-script-garbage-collectors-work/
VBScript’s garbage collector is completely different. It runs at the end of every statement and procedure, and does not do a search of all memory. Rather, it keeps track of everything allocated in the statement or procedure; if anything has gone out of scope, it frees it immediately
https://blogs.msdn.microsoft.com/ericlippert/2004/12/22/t4-vbscript-and-the-terminator/
Also
https://blogs.msdn.microsoft.com/ericlippert/2004/04/28/when-are-you-required-to-set-objects-to-nothing/
https://blogs.msdn.microsoft.com/ericlippert/2004/03/01/syntax-semantics-micronesian-cults-and-novice-programmers/
The CPU is a stack based machine (and VBScript a stack based virtual machine). When the CPU calls a function the calling program puts the parameters on the stack and the return address, adjust the stack frame and does a jump. The callee function creates local variables on the stack and also places the return value on it. When it returns the stack pointer is adjusted back to where it was which automatically frees all the above.

Storing range reference in a global variable

I'm using several named ranges located in different worksheets. I need to read from and write to those ranges in many situations throughout my VBA code.
So my question is: what is the proper way to store those range references in global variables for quick access? Or is there a better way?
I tried declaring global variables:
Public WS_BOARD As Worksheet
Public RNG_BOARD As Range
and initializing them on Workbook_Open:
Private Sub Workbook_Open()
Set WS_BOARD = Worksheets("BOARD")
Set RNG_BOARD = WS_BOARD.Range("NR_BOARD")
End Sub
This works okay except that if my code crashes those global variables are reset to Nothing and cannot be used any further (i.e. in worksheet event handlers).
Of course, I can always use something like
Worksheets("BOARD").Range("NR_BOARD")
everywhere in my code but I think it will affect performance because it obviously needs to lookup objects using string names, let alone it being not DRY.
One way is to "load" them once into memory and then pass them as arguments into other subs/functions.
Passing Variables By Reference And By Value
Second, you can declare them as module-scope variables (something like public, but only in the module you declare them).
Scope of variables in Visual Basic for Applications
Third, your way.
Scope of variables in Visual Basic for Applications
Every worksheet has a Name property (the tab caption) and a CodeName property. If you use the CodeName in your code, you don't need a variable. Like a userform, a sheet's code name is auto instantiated when you use it. Select the worksheet under your project in the Project Explorer (Ctrl+R), go to the Propeties (F4) and change the (Name) to a meaninful CodeName. The Project Explorer will then show the sheet as
wshDataEntry (Data Entry)
if you made the CodeName wshDataEntry and the tab said Data Entry.
For Ranges, I used defined names in the spreadsheet. It's like a global variable that never goes out of scope. If you have, say, and interest rate cell that you need to read in various procedure, name it Interest_Rate. Then at the top of every procedure where you need it
Set rInterest = wshDataEntry.Range(gsNMINTEREST)
where gsNMINTEREST is a global string variable holding "Interest_Rate". If the cell ever moves, you only need to move your named range for the code to work. If you change the name to "LiborPlusFour" you only need to update your global variable. Pretty DRY.
Yes there is a slight performance hit making VBA look up the named range every time. I wouldn't worry about that unless you've calculated a performance problem and identified it as the drag.
One way that I keep global variables (of course I'm very judicious using them) in scope is an Initialize procedure.
Public Sub Initialize()
If gclsApp Is Nothing Then
Set gclsApp = New CApp
'do some other setup
End If
End Sub
Then I call the Initialize procedure at the top of any module I need it. If it's Not Nothing, then it doesn't reset the variable. I'm not sure the performance implications of checking for Nothing and just looking up a named range, though.
Global variables are generally not a good idea. In some cases they're necessary, but this isn't one of those cases.
I don't think you should worry about the performance impact of looking up a few ranges by their string name. I have sheets with hundreds of named ranges, and I have never, ever observed this having a negative impact on performance.
Do you have specific evidence indicating that this is slowing down your code? If not, then don't waste time worrying about it.
I often do this to define ranges:
Type RangesType
board As Range
plank As Range
wood As Range
End Type
Function GetRanges() As RangesType
With GetRanges
Set .board = Range("board")
Set .plank = Range("plank")
Set .wood = Range("wood")
End With
End Function
Then I use them like this:
Sub something()
Dim rngs As RangesType: rngs = GetRanges
rngs.board = "My favourite board"
rngs.wood = "Chestnut"
'etc.
End Sub
Yes, ranges will get fetched by their name repeatedly if you have many subs, and no, this isn't likely to have any noticeable effect on performance.

MS Access: Assign variable for Form

I am trying to set up a global variable (as form) and set it = Form_MyForm
I have used the Form Load event to make the assignment and since it is a global variable I am expecting all my procedures can use this variable of mine without me retyping the assignment in different procedures
Problem is that sometimes it works while at others times it fails to recognise my variable, at which point I have to close my form and re-open it to refresh the assignment
I looked at the many Event for Access Forms but not sure what they are and how they can be helpful to my situation
Thanks for your help guys!
Define your global variable(s) in an Access module (not behind any form). This allows any object (form, report, macro etc.) to have access to them.
Initialize the variables inside a Public Function or Public Sub within the module and have the function/sub called by the database's opening form (i.e., main menu or switchboard).
Now, variables can be used in expressions, queries, VBA, and other areas.
**If the global variable needs to be redefined by various parameters, set that up in the function/sub and re-call it from a specific trigger event.
Had the same problem with user defined global variables, any time there is an error thrown or you switch to the development environment, you lose the set value in the variable.
You could try using a Session Variable, from the TempVar collection, which I find to be much more stable. It is stays set until you close the Database or you unset it.
MyFormName = me.Form.Name
'Load the data into Session variables
TempVars.Add "SessMyFormName", MyFormName
'Use the value elsewhere
SelectedForm = TempVars![SessMyFormName]
'Remove All Session Variables Set Earlier
TempVars.RemoveAll 'Destroy Session

What is the lifetime of a global variable in excel vba?

I've got a workbook that declares a global variable that is intended to hold a COM object.
Global obj As Object
I initalize it in the Workbook_Open event like so:
Set obj = CreateObject("ComObject.ComObject");
I can see it's created and at that time I can make some COM calls to it.
On my sheet I have a bunch of cells that look like:
=Module.CallToComObject(....)
Inside the Module I have a function
Function CallToComObject(...)
If obj Is Nothing Then
CallToComObject= 0
Else
Dim result As Double
result = obj.GetCalculatedValue(...)
CallToComObject= result
End If
End Function
I can see these work for a bit, but after a few sheet refreshes the obj object is no longer initialized, ie it is set to Nothing.
Can someone explain what I should be looking for that can cause this?
Any of these will reset global variables:
Using "End"
An unhandled runtime error
Editing code
Closing the workbook containing the VB project
That's not necessarily an exhaustive list though...
I would suggest a 5th point in addition to Tim's 4 above: Stepping through code (debugging) and stopping before the end is reached. Possibly this could replace point number 3, as editing code don't seem to cause global variable to lose their values.