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
VarPtrs and dereferencing them manually, or some clever redim preserve stuff with arrays), I think the easiest workaround is to ditch
Booleans in favour of
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
Boolclass 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!)
'@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
'@PredeclaredId; I'm using this class as a factory for instances of itself - it'll probably go into an addin as
PublicNotCreatableso requires a factory of some sort. It has 2 constructor methods,
.Self; I like using
With New Blahstatements in the constructors like in this answer, but I thought the additional
.Selfmethod 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
.Itemmethod. 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 to
Createan indistinguishable instance of the class. I feel a default member should represent what a class is - a
Boolis 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:
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)