Context
You can probably skim most of this, it's unlikely to be useful for a review and is just for background info. Also download the files
I've tried to create a Boolean
object - some truthy reference type which can be used in place of VBA's builtin Boolean
. I have 2 motivations for this:
I need an object which encapsulates a Boolean value, so that it can be passed around by reference, even to procedures expecting a Boolean ByVal.
In one project, I'm trying to have child subs signal something to a parent caller. The caller passes a flag to the child, and the child can choose to modify this flag's value. I won't justify here why the flag is used rather than say, a function return, or a global variable or the child calling a method of the parent; that will come when I post the rest of the project. For now my challenge (and what I want reviewed) is to determine what sort of data structure/ approach should be used for that flag.
Ordinarily a perfect candidate for such a flag would be a
ByRef Boolean
. A caller creates a variable with a value at some address. It passes the address to the child, the child can choose to modify the value at the address, and the parent's copy of the variable receives those modifications. The parent could keep track of all the flags in a Boolean array.However this approach is messy when the number of children is dynamic; in order for the parent to keep track of all the flags, it needs some dynamic array - a Collection perhaps? That won't work, because when a primitive value type like a Boolean flag is added to a Collection, the Collection keeps the value of, not the reference to (address in memory of) the flag. So the child can't use the address it receives to modify the value of the flag in the collection, since the copy of the flag in the collection lives at a different address entirely.
Now whilst you could do some clever stuff (like keeping a collection of
VarPtr
s and dereferencing them manually, or some clever redim preserve stuff with arrays), I think the easiest workaround is to ditchBoolean
s in favour ofBool
objects.Objects are reference types meaning they can be added to Collections and the child process can still modify them. (This is because the value of an object variable is that object's address, so when Collections discard the variable address and store the contents of a variable, they are still storing the address of an object. Therefore a Collection can be used to keep track of references to objects, instead of values of variables).
Summary: Objects are always passed to procedures and saved as references. Booleans/ primitives are only sometimes stored as references, sometimes their raw values are saved. That's a problem because it means you can't use Collections/Dictionaries to store the boolean flags which is a real headache, but they can be used to store object references.
Sub test()
Dim a As Object, b As Boolean
Set a = CreateObject("Scripting.Dictionary")
a.Add Key:="flag1", Item:=b
Debug.Assert b = a.Item("flag1") 'fine
childRoutine b 'editing b should also edit the value in the dictionary if passed by reference
Debug.Assert b = a.Item("flag1") 'fails
End Sub
Sub test2()
Dim a As Object, b As New Bool
Set a = CreateObject("Scripting.Dictionary")
a.Add Key:="flag1", Item:=b
Debug.Assert b = a.Item("flag1") 'fine
childRoutine b 'editing b also edits the value in the dictionary since objects are always passed by reference
Debug.Assert b = a.Item("flag1") 'fine again
End Sub
Sub childRoutine(ByRef flag As Variant)
flag = Not flag 'make some change
End Sub
I want object oriented data types a bit like python has, but more VBA idiomatic.
I've been planning to replace many of the mundane data types in VBA with some more jazzy object-y ones which will eventually allow me to create classes which determine their own responses to operations. This
Bool
class is a sort of template for such object oriented data types, and explores the data storage aspects; how my class can be used in place of the datatype it encapsulates. Later I'll look at stuff like operator overloading and inheritance and other object stuff.
Since this class is going to be used in both projects, it doesn't make sense to post it exclusively as part of either of them. Especially for the latter I want this to act as a strong foundation, so any criticism or feedback, however minor is useful.
Here's the main class (not much code for all that chatting!)
Bool.cls
'@Folder("API.Utils")
'@ModuleDescription("Boolean object that can be passed byRef")
'@PredeclaredID
Option Explicit
'NOTE RtlCopyMemory would be faster (as source and dest won't overlap) but is not exposed to VBA
''@Description("API: Destination and Source can be byVal pointers or byRef variables, length is LenB(dataType)")
#If Win64 Then
Private Declare PtrSafe Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByRef destination As Any, ByRef source As Any, ByVal length As Long)
Private Declare PtrSafe Sub ZeroMemory Lib "kernel32.dll" Alias "RtlZeroMemory" (ByRef destination As Any, ByVal length As Long)
#Else
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByRef destination As Any, ByRef source As Any, ByVal length As Long)
Private Declare Sub ZeroMemory Lib "kernel32.dll" Alias "RtlZeroMemory" (ByRef destination As Any, ByVal length As Long)
#End If
Private Type tBool
Value As Boolean
End Type
Private this As tBool
'@Description("Truthy encapsulated value")
'@DefaultMember
Public Property Get Value() As Boolean
Value = this.Value
End Property
Public Property Let Value(ByVal newVal As Boolean)
this.Value = newVal
End Property
'@Description("Create instance of Bool class from objPtr")
Public Function FromPtr(ByVal pData As LongPtr) As Bool 'TODO fails in VB6 as no LongPtr
Dim result As New Bool
CopyMemory result, pData, LenB(pData)
Set FromPtr = result
ZeroMemory result, LenB(pData) ' free up memory, equiv: CopyMemory result, 0&, LenB(pData)
End Function
'@Description("Class Constructor takes Boolean or Boolean-like values")
Public Function Create(ByVal initialValue As Variant) As Bool
Dim result As New Bool
result.Value = CBool(initialValue)
Set Create = result
End Function
Worth noting;
'@PredeclaredId
; I'm using this class as a factory for instances of itself - it'll probably go into an addin asPublicNotCreatable
so requires a factory of some sort. It has 2 constructor methods,Create
andFromPtr
- No
.Self
; I like usingWith New Blah
statements in the constructors like in this answer, but I thought the additional.Self
method or equivalent would clutter the interface and lead to additional confusion given the next point '@DefaultMember
; going against all of Rubberduck's advice, I've used a default member which is not a.Item
method. But here I think it's justified, as the class is meant only to be a wrapper for actual data; the class must have an intrinsic encapsulated value in order to operate correctly. And that value is all that is required toCreate
an indistinguishable instance of the class. I feel a default member should represent what a class is - aBool
is the value of the encapsulated variable, the point in a Collection specified by a key/ index is the item located there. A Range is not simply the value in the cell, it is also the address and the formatting and ... that's why Range's default member is so confusing.
As well as feedback on the approach and functionality of the class, I'd really appreciate some insight on best practices regarding layout, documentation and structure of the project, as such I've included all the comments/attributes and here are some unit tests:
UnitTests.bas
Option Explicit
Option Private Module
'@TestModule
'@Folder("API.Utils.Tests")
Private Assert As Rubberduck.PermissiveAssertClass
Private Fakes As Rubberduck.FakesProvider
'@ModuleInitialize
Private Sub ModuleInitialize()
'this method runs once per module.
Set Assert = New Rubberduck.PermissiveAssertClass
Set Fakes = New Rubberduck.FakesProvider
End Sub
'@ModuleCleanup
Private Sub ModuleCleanup()
'this method runs once per module.
Set Assert = Nothing
Set Fakes = Nothing
End Sub
'@TestInitialize
Private Sub TestInitialize()
'this method runs before every test in the module.
End Sub
'@TestCleanup
Private Sub TestCleanup()
'this method runs after every test in the module.
End Sub
'@TestMethod("Uncategorized")
Private Sub DefaultPropertyLetGet()
On Error GoTo TestFail
'Arrange:
Dim b As New Bool
b.Value = False
'Act:
b = True
'Assert:
Assert.AreEqual True, b.Value
Assert.AreEqual True, (b) 'should just be b - this is an issue with the assert class
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
'@TestMethod("Uncategorized")
Private Sub ClassConstructor()
On Error GoTo TestFail
'Arrange:
Dim a As Bool, b As Bool, c As Bool
'Act:
Set b = Bool.Create(True)
Set a = Bool.Create(False)
Set c = Bool.Create(a) 'implicit conversion with CBool
'Assert:
Assert.AreEqual True, b.Value
Assert.AreEqual False, a.Value
Assert.AreEqual a.Value, c.Value
Assert.AreNotSame a, c 'c only has the same value as a
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
'@TestMethod("Uncategorized")
Private Sub AssigningByReferenceCanOverwrite()
On Error GoTo TestFail
'Arrange:
Dim base As Bool, copy As Bool
'Act:
Set base = Bool.Create(True)
Set copy = Bool.FromPtr(ObjPtr(base))
copy = False
'Assert:
Assert.AreEqual False, base.Value
Assert.AreSame base, copy
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
'@TestMethod("Uncategorized")
Private Sub InvalidConstructionRaisesTypeMismatchError()
Const ExpectedError As Long = 13 'type mismatch
On Error GoTo TestFail
'Arrange:
Dim b As Bool
'Act:
Set b = Bool.Create("Not a boolean!")
Assert:
Assert.Fail "Expected error was not raised"
TestExit:
Exit Sub
TestFail:
If Err.Number = ExpectedError Then
Resume TestExit
Else
Resume Assert
End If
End Sub
I'm not sure if I'm using these quite right; I wrote them all after I'd written the class itself. Additionally, I've had to qualify the default member explicitly with b.Value
in the tests; I believe this is a bug/ incorrect behaviour in the Rubberduck.PermissiveAssertClass
(I made a comment to that effect in the repository)
MSForms.ReturnBoolean
? \$\endgroup\$