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 = 0
Passed 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$lastID

The 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$lastID

So 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 ConsoleApplication1
    Private 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$Init

This 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 = 1
Const 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.

4 Comments

  • Thanks Patrick! It's always good to know what's going on :-)

  • This item is now a bit old, but I just wanted to add a comment after reading TrackBack's blog.

    Properties must be read/write to be serialized or the serializer cannot set the value when deserializing. An error will occur if a property is declared as ReadOnly as in TrackBack's example.

    I don't understand his solution of removing the static declaration. Am I missing something? Isn't the point to find a solution so that static variables can still be used?

  • This works if the function DoStuff is shared. If it isn't shared, it won't work.

  • Could you explain what you mean by "won't work"? I just tried this in VS 2003 (.NET 1.1) with an instance method (not shared) and it worked fine.

Comments have been disabled for this content.