Who Drank My Beer?
I was attending a .Net user group event recently, and chatting with some group members. One of the attendees remarked, “The thing I really I hate about VB is that ByVal doesn’t pass my objects by value, the compiler should catch that and make you use ByRef. It’s really misleading.” I am paraphrasing the statement, so I hope the person who made it won’t call me on the details.
I immediately thought “Huh? ByVal does pass objects by value, but it’s the reference that’s passed that way, not the object itself!” If you had the same reaction, you may not want to read on, but if not this article may clarify some things for you. While the concepts are not particularly advanced, you’d be amazed by the number of developers (and I’m not talking beginners here) I’ve met that are unaware of what happens when passing different types ByVal.
The ByVal Key Word
Let’s start with a short review of the ByVal key word. It’s been around since VB 3 and maybe even earlier so we’re all familiar with it, right? According to the .NET framework documentation:
“The ByVal keyword indicates that an argument is passed in such a way that the called procedure or property cannot change the value of a variable underlying the argument in the calling code”
Many developers interpret this to mean that any variable passed to a procedure ByVal can’t be changed by the procedure. This is a correct interpretation, but in order to “own” the concept, one must consider the difference between the two variable types supported by .NET
Value Types Vs. Reference Types
A fundamental distinction in the Common Type System is the “value type” vs. the “reference type” variable. Managed code can allocate memory on the heap or on the stack. When an instance of a value type is created, it is stored directly on the stack. Memory for a value type is allocated when it is declared, and freed when the variable is no longer accessible. In other words, an instance of a value type’s lifetime is determined by the lifetime of the variable that created it. Reference types are allocated and released differently, as we’ll see later.
If we declare an integer and set it to the value 21, the value is stored on the stack. The diagram below represents the result of the statement Dim intLegalDrinkingAge as integer = 21. For the sake of simplicity, we’ll refer to the place on the stack that the memory is allocated as location “A”
|
Application Memory |
|
Stack |
Heap |
|
A- intLegalDrinkingAge [21] |
|
Figure 1 (memory allocation for a value type)
When intLegalDrinkingAge (a value type) is passed to a procedure by reference, the value is accessed directly. The called procedure may modify the intLegalDrinkingAge variable. While this is something most of us would have liked to do as teenagers, as programmers we may not want this to happen. We want to other procedures to promise not to change intLegalDrinkingAge, so we declare the parameter that accepts intLegalDrinkingAge ByVal.
Public Function CanPersonPurchaseBeer(ByVal LegalDrinkingAge _
As Integer, ByVal BirthDate As DateTime) As Boolean
The ByVal key word tells the compiler to create a copy of the variable on the stack and pass the copy to the procedure. The figure below illustrates this. When the program runs, the value in location A is copied to location B, and location B is passed to CanPersonPurchaseBeer(). When CanPersonPurchaseBeer returns, the memory at location B is freed and the memory at location A remains in tact.
|
Application Memory |
|
Stack |
Heap |
|
A- intLegalDrinkingAge [21] |
|
|
B- intLegalDrinkingAge [21] |
|
Figure 2 (memory allocation for a value type passed ByVal)
Reference types allocate memory on the both the heap and the stack. When an instance of a reference type is created, the instance is stored on the heap, and a reference to the instance is stored on the stack. Memory on the heap is managed by the CLR’s garbage collector. The lifetime of an instance of a reference type is determined by the variable that created it and by the CLR. Let’s take a look at the memory allocation that occurs for reference types. Imagine that we have a class called “Beer”, and the following code.
Dim myBeer As New Beer
Dim yourBeer As Beer = myBeer
Figure 3 below shows the memory allocation for these statements. For the first statement, an instance of the Beer class is created and placed on the heap. We’ll refer to the location on the heap as “Ha”. The myBeer variable is placed on the stack, and valued with a pointer to the location of the Beer Object. When the second line is executed, the yourBeer variable is placed on the stack (location Sb), and valued with a pointer to the Beer Object created in the first line. So myBeer and yourBeer point to the same object. You probably knew this already, and you may not be keen on the idea of sharing a beer with me anyway, but this concept is important to understanding the rest of the story.
|
Application Memory |
|
Stack |
Heap |
|
Sa- myBeer [Ha] |
Ha [Beer Object] |
|
Sb- yourBeer[Ha] |
|
Figure 3 (memory allocation for a reference type)
Now let’s say that I pass myBeer to you by value.
Public Sub PassBeerByVal()
HoldBeer(myBeer)
If myBeer.PercentFull = 0 Then
MessageBox.Show("You Drank My Beer!")
End If
End Sub
Public Sub HoldBeer(ByVal passedBeer As Beer)
‘chug a lug
passedBeer.DrinkAll()
End Sub
Before HoldBeer is called, the stack variable for myBeer is copied (see fig 4). The copy is then passed to HoldBeer(). Did you catch that? The beer object (Ha) was not copied, the reference to the beer object (Sa) was copied! The copied reference (Sb) points to the same object as the original (Sa). When the DrinkAll method is called on passedBeer, and control returns to PassBeerByVal, we will find that myBeer has been modified. The called procedure was able to modify the beer object on the heap, because it had a reference to it. That may seem unfair, and I’ve heard more than one developer suggest that the heap object should be copied. That’s not a bad idea, but it would require the compiler to copy not only the object on the heap, but any other objects it references or contains. This type of copy is often referred to as a “deep copy” or “clone”, and if you want it done, you’ll need to implement it yourself. The implementation Microsoft chose makes sense, if you think about the potential performance implications of doing a deep copy. Deep copies could end up allocating lots of memory on the heap, and this memory only gets cleaned up when the garbage collector gets around to it.
|
Application Memory |
|
Stack |
Heap |
|
Sa- myBeer [Ha] |
Ha [Beer Object] |
|
Sb- passedBeer[Ha] |
|
Figure 4 (memory allocation for a reference type passed ByVal)
It might seem that ByVal is of little value for reference types, but that’s not the case. ByVal does not guarantee that the public properties and methods of the Beer Object (Ha) will not be used by HoldBeer(), but it does promise that after the call, myBeer will still refer to the same object (Ha). This can be just as useful, and The .NET framework itself takes advantage of it in a form’s Closing event.
Form1_Closing(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs)
The CancelEventArgs object (e) is passed by value, but we are able to set e.Cancel = True, letting the form know not to close (see the .NET SDK for details if you’re not familiar with this event.
Hopefully, at this point you have a clearer understanding of the way ByVal works for reference types, and an appreciation for why it works the way that it does. Things can get much more complex if we introduce remoting into the picture, but that’s beyond the scope of this article.
I’ve written a simple demo that I think is a fun way to illustrate the concept to your friends. The Demo consists of a single form with several buttons…
The “New Beer” button creates a beer object after prompting you for your favorite brand. You may take a sip from your beer, or ask someone to hold it for you. You may use the HoldByVal button to pass your beer to the holder by value, or the HoldByRef button to pass it by reference. A look at the underlying code will show you that both buttons perform the same code (the only difference is whether the beer is passed by reference or by value). The code randomly selects one of four people to hold your beer. After that, a random action is selected. The beer you passed may be sipped, completely drank, or replaced with a fresh “Sam Adams” before it is returned to you. As you may suspect by now, when you pass your beer by value, you will never actually get a fresh beer back, but you’ll probably find that someone drank at least some of it!
Dim ht As New Hashtable
ht.Add(1, "Joe")
ht.Add(2, "Mary")
ht.Add(3, "Phil")
ht.Add(4, "Sam")
Dim randObj As New Random
Dim intI As Integer = randObj.Next(1, 4)
m_GuiltyParty = ht.Item(intI)
intI = randObj.Next(1, 4)
Try
Select Case intI
Case 1
passedBeer.Sip()
Case 2
passedBeer = New Beer("Sam Adams")
Case Else
passedBeer.DrinkAll()
End Select
Catch
End Try
RefreshIndicator()
The full code for the form is below, Just save it as form1.vb and add it to your project. Have fun, and don’t drink too much!!
Public Class Form1
Inherits System.Windows.Forms.Form
#Region " Windows Form Designer generated code "
Public Sub New()
MyBase.New()
'This call is required by the Windows Form Designer.
InitializeComponent()
'Add any initialization after the InitializeComponent() call
End Sub
'Form overrides dispose to clean up the component list.
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then
If Not (components Is Nothing) Then
components.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub
'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
Friend WithEvents beerIndicator As System.Windows.Forms.ProgressBar
Friend WithEvents cmdHoldByValue As System.Windows.Forms.Button
Friend WithEvents lblBrand As System.Windows.Forms.Label
Friend WithEvents cmdNewBeer As System.Windows.Forms.Button
Friend WithEvents cmdSip As System.Windows.Forms.Button
Friend WithEvents cmdHoldByRef As System.Windows.Forms.Button
Friend WithEvents cmdWho As System.Windows.Forms.Button
Private Sub InitializeComponent()
Me.beerIndicator = New System.Windows.Forms.ProgressBar
Me.cmdHoldByValue = New System.Windows.Forms.Button
Me.cmdHoldByRef = New System.Windows.Forms.Button
Me.cmdNewBeer = New System.Windows.Forms.Button
Me.cmdSip = New System.Windows.Forms.Button
Me.lblBrand = New System.Windows.Forms.Label
Me.cmdWho = New System.Windows.Forms.Button
Me.SuspendLayout()
'
'beerIndicator
'
Me.beerIndicator.Location = New System.Drawing.Point(32, 72)
Me.beerIndicator.Name = "beerIndicator"
Me.beerIndicator.Size = New System.Drawing.Size(256, 23)
Me.beerIndicator.TabIndex = 0
Me.beerIndicator.Value = 100
'
'cmdHoldByValue
'
Me.cmdHoldByValue.Location = New System.Drawing.Point(24, 136)
Me.cmdHoldByValue.Name = "cmdHoldByValue"
Me.cmdHoldByValue.Size = New System.Drawing.Size(112, 23)
Me.cmdHoldByValue.TabIndex = 1
Me.cmdHoldByValue.Text = "Hold ByVal"
'
'cmdHoldByRef
'
Me.cmdHoldByRef.Location = New System.Drawing.Point(24, 176)
Me.cmdHoldByRef.Name = "cmdHoldByRef"
Me.cmdHoldByRef.Size = New System.Drawing.Size(112, 23)
Me.cmdHoldByRef.TabIndex = 2
Me.cmdHoldByRef.Text = "Hold ByRef"
'
'cmdNewBeer
'
Me.cmdNewBeer.Location = New System.Drawing.Point(24, 40)
Me.cmdNewBeer.Name = "cmdNewBeer"
Me.cmdNewBeer.Size = New System.Drawing.Size(112, 23)
Me.cmdNewBeer.TabIndex = 3
Me.cmdNewBeer.Text = "New Beer"
'
'cmdSip
'
Me.cmdSip.Location = New System.Drawing.Point(152, 40)
Me.cmdSip.Name = "cmdSip"
Me.cmdSip.Size = New System.Drawing.Size(112, 23)
Me.cmdSip.TabIndex = 4
Me.cmdSip.Text = "Sip"
'
'lblBrand
'
Me.lblBrand.Location = New System.Drawing.Point(32, 104)
Me.lblBrand.Name = "lblBrand"
Me.lblBrand.Size = New System.Drawing.Size(256, 23)
Me.lblBrand.TabIndex = 5
'
'cmdWho
'
Me.cmdWho.Location = New System.Drawing.Point(24, 224)
Me.cmdWho.Name = "cmdWho"
Me.cmdWho.Size = New System.Drawing.Size(224, 23)
Me.cmdWho.TabIndex = 6
Me.cmdWho.Text = "Who messed with my beer?"
'
'Form1
'
Me.AutoScaleBaseSize = New System.Drawing.Size(6, 15)
Me.ClientSize = New System.Drawing.Size(312, 273)
Me.Controls.Add(Me.cmdWho)
Me.Controls.Add(Me.lblBrand)
Me.Controls.Add(Me.cmdSip)
Me.Controls.Add(Me.cmdNewBeer)
Me.Controls.Add(Me.cmdHoldByRef)
Me.Controls.Add(Me.cmdHoldByValue)
Me.Controls.Add(Me.beerIndicator)
Me.Name = "Form1"
Me.Text = "Form1"
Me.ResumeLayout(False)
End Sub
#End Region
Private m_Beer As Beer
Private m_GuiltyParty As String
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
NewBeer("Budweiser")
End Sub
Private Sub cmdNewBeer_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdNewBeer.Click
NewBeer(InputBox("What Brand?", "Brand Selector", "Budweiser"))
End Sub
Private Sub NewBeer(ByVal sBrand As String)
m_Beer = New Beer(sBrand)
Me.beerIndicator.Value = m_Beer.PercentFull
Me.lblBrand.Text = m_Beer.BrandName
End Sub
Private Sub cmdSip_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdSip.Click
Try
m_Beer.Sip()
RefreshIndicator()
Catch
MessageBox.Show("Your Beer is Empty!")
End Try
End Sub
Private Sub cmdHoldByValue_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdHoldByValue.Click
HoldBeerByVal(m_Beer)
End Sub
Private Sub cmdHoldByRef_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdHoldByRef.Click
HoldBeerByRef(m_Beer)
End Sub
Private Sub HoldBeerByVal(ByVal passedBeer As Beer)
Dim ht As New Hashtable
ht.Add(1, "Joe")
ht.Add(2, "Mary")
ht.Add(3, "Phil")
ht.Add(4, "Sam")
Dim randObj As New Random
Dim intI As Integer = randObj.Next(1, 4)
m_GuiltyParty = ht.Item(intI)
intI = randObj.Next(1, 4)
Try
Select Case intI
Case 1
passedBeer.Sip()
Case 2
passedBeer = New Beer("Sam Adams")
Case Else
passedBeer.DrinkAll()
End Select
Catch
End Try
RefreshIndicator()
End Sub
Private Sub HoldBeerByRef(ByRef passedBeer As Beer)
Dim ht As New Hashtable
ht.Add(1, "Joe")
ht.Add(2, "Mary")
ht.Add(3, "Phil")
ht.Add(4, "Sam")
Dim randObj As New Random
Dim intI As Integer = randObj.Next(1, 4)
m_GuiltyParty = ht.Item(intI)
intI = randObj.Next(1, 4)
Try
Select Case intI
Case 1
passedBeer.Sip()
Case 2
passedBeer = New Beer("Sam Adams")
Case Else
passedBeer.DrinkAll()
End Select
Catch
End Try
RefreshIndicator()
End Sub
Private Sub RefreshIndicator()
Me.beerIndicator.Value = m_Beer.PercentFull
Me.lblBrand.Text = m_Beer.BrandName
End Sub
Private Sub cmdWho_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdWho.Click
MessageBox.Show("'" & m_GuiltyParty & "' is Guilty!")
End Sub
End Class
Public Class Beer
Private m_sBrand As String
Private m_iPercentFull As Integer
Public Sub New(ByVal sBrand As String)
m_sBrand = sBrand
m_iPercentFull = 100
End Sub
Public Function DrinkAll()
m_iPercentFull = 0
End Function
Public Function Sip()
If Me.PercentFull <= 0 Then
Throw New Exception("Beer is Empty!")
End If
PercentFull = PercentFull - 10
End Function
Public ReadOnly Property BrandName() As String
Get
Return m_sBrand
End Get
End Property
Public Property PercentFull() As Integer
Get
Return m_iPercentFull
End Get
Set(ByVal Value As Integer)
Select Case Value
Case Is > 100
m_iPercentFull = 100
Case Is < 0
m_iPercentFull = 0
Case Else
m_iPercentFull = Value
End Select
End Set
End Property
End Class