Static Local Variables in VB.NET
VB.NET has support for "local static variables". These are variables local to a method, but retain their method call between invocations of the method. The CLR does not support this, so how does VB.NET do it if it runs under the CLR? Just some simple compiler tricks!
Local Static Variables
Consider the following VB.NET method:
Public Shared Sub DoStuff(itemID As Integer)Static lastID As Integer
Console.WriteLine("lastID = {0}", lastID)
Console.WriteLine("Passed in value = {0}", itemID)
Console.WriteLine("---")
lastID = itemID
End Sub
And the code is called:
DoStuff(23)DoStuff(176)
DoStuff(9)
Will produce the following output:
lastID = 0Passed in value = 23
---
lastID = 23
Passed in value = 176
---
lastID = 176
Passed in value = 9
---
As you can see, the value of "lastID" is retained across multiple calls to the method.
Behind the Scenes
What's happening is that the VB.NET compiler creates a static (shared in VB.NET) class-level variable to maintain the value of "lastID". Looking at the IL of the class containing the above method using ILDASM.EXE, we see the following field at the class level:
.field private static specialname int32 $STATIC$DoStuff$0118$lastIDThe VB.NET compiler has given a unique name for the variable that consists of:
$STATIC$<method name>
$<RANDOM value?>$
<Variable_name>
And if we look at the IL for the method, we'll see this variable used in the code:
<LINE#> ldsfld int32 ConsoleApplication1.Module1::$STATIC$DoStuff$0118$lastIDSo when you define a static local variable in a method, you're really just using a static class-level variable (with a rather unique name that you'll never "accidentally" create in VB!). Here's a way to look at it from a pure VB.NET standpoint:
Public Class ConsoleApplication1Private Shared DoStuff_lastID As Integer
Public Shared Sub DoStuff()
Console.WriteLine("lastID = {0}", DoStuff_lastID)
Console.WriteLine("Passed in value = {0}", itemID)
Console.WriteLine("---")
DoStuff_lastID = itemID
End Sub
End Class
Initializing Static Fields
The above sample was pretty simple. Things get a little more complicated when you want to initialize that static variable:
Public Shared Sub DoStuff(itemID As Integer)
Static lastID As Integer = -1
Console.WriteLine("lastID = {0}", lastID)
Console.WriteLine("Passed in value = {0}", itemID)
Console.WriteLine("---")
lastID = itemID
End Sub
Why? Because VB.NET now supports multithreading! That static variable is shared across all instances of the class and there could be multiple threads hitting the "DoStuff" code. How do you make sure that the variable is only initialized once, regardless of how many threads are running? The Monitor class!
Getting into the details of multithreading and the Monitor class is beyond the scope of this article. But here's a snippet from the docs:
The Monitor class controls access to objects by granting a lock for an object to a single thread. Object locks provide the ability to restrict access to a block of code, commonly called a critical section. While a thread owns the lock for an object, no other thread can acquire that lock. You can also use Monitor to ensure that no other thread is allowed to access a section of application code being executed by the lock owner, unless the other thread is executing the code using a different locked object.
If we look at the IL code when initializing the "lastID" static variable to -1, we see another class-level field added to our class:
.field private static specialname class [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.StaticLocalInitFlag $STATIC$DoStuff$0118$lastID$InitThis is an instance of a "helper" class used by VB.NET to control initialization of the static variable. If you look at the disassembled code for this new version of "DoStuff", you'll notice a lot more code. Instead of showing the entire disassembly here, I'll show you the VB.NET equivalent of the IL code (with my own constants added for clarity):
Const STATE_INITIALIZED As Integer = 1Const STATE_INITIALIZING As Integer = 2
Const STATE_UNINITIALIZED As Integer = 0
If $STATIC$DoStuff$0118$lastID$Init.State <> STATE_INITIALIZED Then
Monitor.Enter($STATIC$DoStuff$0118$lastID$Init)
Try
If $STATIC$DoStuff$0118$lastID$Init.State = STATE_UNINITIALIZED Then
$STATIC$DoStuff$0118$lastID$Init.State = STATE_INITIALIZING
$STATIC$DoStuff$0118$lastID = -1
Goto Continue_Code
End If
If $STATIC$DoStuff$0118$lastID$Init.State = STATE_INITIALIZING Then
Throw New IncompleteInitialization()
End If
Finally
$STATIC$DoStuff$0118$lastID$Init.State = STATE_INITIALIZED
Monitor.Exit($STATIC$DoStuff$0118$lastID$Init)
End Try
End If
Continue_Code:
The rest of the code follows as usual. As you can see -- it's a bit more complicated when you want to initialize the static local variable. It first checks to see if the initialization has been done. If it hasn't, it places a lock on the "helper" class (Microsoft.VisualBasic.CompilerServices.StaticLocalInitFlag). Now that it has the lock it checks to make sure the variable has still not been initialized. If not, it sets the state flag to "initializing" and sets our lastID field to -1. There's a "Goto" to exit the try block which will cause the finally block to execute. The finally block places the state of the helper class as initialized so the code won't get hit again and releases the lock on the helper class.
There's also one additional check that should never happen. If the lock is placed on the helper class but the helper class finds it's state as STATE_INITIALIZING, then some other thread has started initializing this variable after we've locked it with the Monitor class. In case this happens, an "IncompleteInitialization" exception is thrown.
There you have it! The secrets behind static local variables.