1

I am studying/learning the bahavior of ByVal and ByRef when it comed to working with a call object. So I created this class PersonModel

Private Type TPerson
    firstName As String
    lastName As String
End Type

Private this As TPerson

Public Property Get firstName() As String
    firstName = this.firstName
End Property

Public Property Let firstName(ByVal strNewValue As String)
    this.firstName = strNewValue
End Property

Public Property Get lastName() As String
    lastName = this.lastName
End Property

Public Property Let lastName(ByVal strNewValue As String)
    this.lastName = strNewValue
End Property

Public Property Get fullName() As String
    fullName = this.firstName & " " & this.lastName
End Property

And I made a standard module trying to see how the value of an object be affected if it's passed as ByVal or ByRef in s subroutine. Here's the code in standard module

Private passedPerson As PersonModel

Public Sub StructureType()
    Dim Object1 As PersonModel
    Dim Object2 As PersonModel

    Set Object1 = New PersonModel
    With Object1
        .firstName = "Max"
        .lastName = "Weber"
        Debug.Print .fullName 'gives Max Weber
    End With

    Set Object2 = Object1   'Object2 references Object1

    Debug.Print Object2.fullName 'gives Max Weber

    passByVal Object1
    ' First Call
    Debug.Print passedPerson.fullName  'gives Max Weber

    With Object2
        .firstName = "Karl"
        .lastName = "Marx"
        Debug.Print .fullName  'gives Karl Marx
    End With

    'Second Call
    Debug.Print passedPerson.fullName 'gives Karl Marx

End Sub

Private Sub passByVal(ByVal person As PersonModel)
    Set passedPerson = New PersonModel
    Set passedPerson = person
End Sub

I was just expecting that in the second call part of the code Debug.Print passedPerson.fullName will give me an unchanged value of "Max Weber". But instead, it's giving the new value "Karl Marx". Even if I change the code of the procedure passByVal to:

Private Sub passByVal(ByVal person As PersonModel)
    Dim newPerson As PersonModel
    Set newPerson = New PersonModel
    Set newPerson = person

    Set passedPerson = newPerson
End Sub

Second call part of the code Debug.Print passedPerson.fullName is still giving "Karl Marx". Regardless of changing ByVal to ByRef, it's still giving the same result.

I have two questions: 1. Is this how it should really work? 2. What am I doing wrong if my aim is to keep the value of the variable passedPerson to "Max Weber"?

Mathieu Guindon
  • 69,817
  • 8
  • 107
  • 235
  • 1
    Possible duplicate of [ByRef vs ByVal Clarification](https://stackoverflow.com/questions/4383167/byref-vs-byval-clarification) – Peter Duniho Mar 28 '19 at 18:32
  • Classic misunderstanding. See https://stackoverflow.com/questions/4383167/byref-vs-byval-clarification for specifics, but you are confusing the `ByRef` semantics with "passing a reference `ByVal`". In particular, your `PersonModel` is a "reference type", and so the whole object isn't what's passed when using `ByVal`, but instead the _reference_ to the object. So the method has access to the same exact instance that the caller is using, and so changes to the instance are visible by both. – Peter Duniho Mar 28 '19 at 18:46
  • AFAIK, VBA doesn't provide a mechanism to declare "value type" types, like VB.NET does. Both `Type`- and `Class`-declared types are reference types.If you want value-type semantics, then you might need to use VB.NET (which you can do by writing a standalone .NET program that uses Access automation...most of your existing Access could should work as-is, called through automation from the VB.NET program). In that case, you would make your type a value type (i.e. `Structure` in VB.NET, not `Class`). – Peter Duniho Mar 28 '19 at 18:46
  • 1
    If you want value-type semantics but still write only VBA code inside Access, then you'll need to create a new instance (i.e. copy) of the object to pass to your method, so that when the method changes it, your original instance doesn't. – Peter Duniho Mar 28 '19 at 18:46
  • So does it mean that a class instance sent to a function **will always be** of `ByRef`? And this is in VBA. If that is so, is there a way to send/pass the class instance as `ByRef` so it will retain its value? – DJ Villareal Mar 28 '19 at 18:46
  • No...it does not mean the instance will always be `ByRef`. It just means that the **value** being passed is a reference, not the whole object. You could pass a reference **value** using `ByRef`, and that would allow the called method to modify the _reference_ itself, which it turn would cause the caller's variable to be modified to refer to a completely different instance. – Peter Duniho Mar 28 '19 at 18:48

1 Answers1

3

An object variable isn't an object: it's a programming construct that we use to keep a reference to one - the actual object lives not in our code, but in the VBA runtime context.

Dim objRef1 As Object
Set objRef1 = New Collection
Debug.Print ObjPtr(objRef1)

Dim objRef2 As Object
Set objRef2 = objRef1
Debug.Print ObjPtr(objRef2)

This should output the same address, twice: both variables are pointing to the same object: changing the properties of that object with one...

objRef1.Add 42

...will affect the very same object that the other also points to:

Debug.Print objRef2.Count ' prints 1 even though .Add was called against objRef1

Passing an object ByRef (it's the implicit default) means you're passing a reference to the object pointer, so it can be simplified to passing the pointer itself: the ByRef parameter is now a local variable in its own local scope, pointing to an object to which the caller also has a reference.

Public Sub CallingCode()
    Dim objRef1 As Object
    Set objRef1 = New Collection
    PassByReference objRef1
    Debug.Print objRef1.Count ' error 91, the object reference is gone!
End Sub

Private Sub PassByReference(ByRef thing As Object)
    thing.Add 42
    Set thing = Nothing
End Sub

Because the reference is being passed, setting it to Nothing in either procedure will bring that object's reference count to 0, and the object gets destroyed. Here the object is destroyed and then accessed, which raises error 91.

Passing an object ByVal means you're passing a copy of the reference to the object pointer - it's a distinct reference to the very same object:

Public Sub CallingCode()
    Dim objRef1 As Object
    Set objRef1 = New Collection
    PassByValue objRef1
    Debug.Print objRef1.Count ' 1
End Sub

Private Sub PassByValue(ByVal thing As Object)
    thing.Add 42
    Set thing = Nothing
End Sub

Here the local copy is being set to Nothing, but since the calling code also has a reference to that object, the object itself isn't getting destroyed - so the element 42 was added to the collection, and Debug.Print outputs 1.

And that's exactly what's going on with your PersonModel: passing it ByVal gives you a local copy of the object pointer, pointing to the exact same object as the calling code - ByVal doesn't deep-clone entire objects, it simply makes a new reference to the same involved object. Hence, modifying that object's properties affects the exact same object regardless of whether the pointer to that object is passed by value or by reference.

Mathieu Guindon
  • 69,817
  • 8
  • 107
  • 235
  • Wow, would love to hear about that downvote. FWIW I did spend about as long searching for a duplicate in the VBA tag as I spent writing this answer. Proposed dupe is VB.NET and introduces concepts about value vs reference types that have nothing to do with VBA. – Mathieu Guindon Mar 28 '19 at 19:34
  • Thank you so much for this very clear explanation. Thank you for taking time writing this answer and explaining what's happening. – DJ Villareal Mar 28 '19 at 19:36