13
\$\begingroup\$

Quick review:

I've been writing API code which receives user input that can come as either an object or a value type - a problem outlined here.

In order to write an unknown data type to a variable, I first came up with this:

Public Sub LetSet(ByRef variable As Variant, ByVal value As Variant)
    If IsObject(value) Then
        Set variable = value
    Else
        variable = value
    End If
End Sub

called like

Dim result As Variant
LetSet result, objectOrValueType()

However an alternate approach that I've come up with is the following:

Public Property Let LetSet(ByRef variable As Variant, ByVal value As Variant)
    If IsObject(value) Then
        Set variable = value
    Else
        variable = value
    End If
End Property

called like

Dim result As Variant
LetSet(result) = objectOrValueType()

which I like because it feels closer to LetSet a = b - the ultimate goal but one that I don't believe is possible.


Both live in standard modules and are functionally equivalent - the return value of objectOrValueType() (some arbitrary function that generates the unknown data) is stored in result.

So I'm wondering which is better and is there anything I can do to improve on either approach? Any other feedback is, of course, also welcome.


NB, just to add some context, I've come across 2 use cases for this:

  1. A buffered list; this class is drip-fed data of unknown kind from some asynchronous process, and merges it into batches of fixed size - then raises an event when a new batch is ready. Because the internal data store is constantly being written to, I can't expose that, so I need some way to copy items to variant arrays for each batch and expose those in the event.
  2. Overloaded arithmetic functions; I've defined some Add method that can take integers and sum them with the + operator. However I'm allowing classes to implement IAddable and define their own response to the Add method; this implementation may return an object. So I need to be able to write the result of this calculation to the return variable of the Add method.

For the former I could use a Collection and avoid the issue altogether as @Freeflow points out in their answer - but the batches are fixed size so an array makes more sense for me. For the latter, I don't see a way around using this approach, as the return type of the function is down to the user, not me.

\$\endgroup\$
1
  • \$\begingroup\$ I'm glad my suggestion of a collection works. If your need is to transfer arrays then you may wish to consider ArrayList (from System.Collections) via adding a reference to mscorlib.dll. \$\endgroup\$
    – Freeflow
    Commented Nov 4, 2019 at 8:46

6 Answers 6

4
\$\begingroup\$

Although you think that the second one is closer to your desired syntax, it is actually somewhat confusing. If LetSet was a function with only one parameter, this would cause a Let coercion on the RHS, if an object is supplied. E.g. if the RHS is a range, the usual behaviour of a Let assignment is that the values in the range are assigned instead of the object. Only because LetSet is parameterized, the object itself is assigned.

So, following the principle of least surprise, I would suggest to use the first version.

\$\endgroup\$
1
  • \$\begingroup\$ I've been wondering about this; IIUC, you're saying the Property version is "surprising" - I agree - because Let myProp = [A1] stores A1.Value while Let LetSet(myProp) = [A1] stores the A1 object . However because the Let syntax is obsolete, there is no indication at the call site telling the user about Let coercion - the surprise is only in the implementation level, not at calling level (if that makes sense). Considering then that the Property syntax is a little clearer at the call site, why might I prefer "least surprise" at implementation level over "clarity" at call level? \$\endgroup\$
    – Greedo
    Commented Nov 12, 2019 at 17:40
4
\$\begingroup\$

I'd name the procedure Assign:

Dim foo As Long
Assign foo, 42

But then, this is involving a stack frame for an otherwise very straightforward operation - the method intending to be generic, it cannot bring any additional logic to the table, and ultimately comes off as redundant.

A Property Let procedure is more problematic though, because such procedures always receive their value/RHS parameters ByVal - regardless of whether or not it says ByRef; M.Doerner's answer gives more details about the implications of this.

I wouldn't use either version of this code, and use explicit built-in keywords (Set) and operators (=) instead, as appropriate. Not knowing what type you're dealing with means you're coding late-bound, which isn't ideal in the first place.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ I think I made it unclear; this isn't intended to replace = and Set in all situations - I have added some use cases to the question to clarify that. Also I don't understand the problem with ByRef - I've specified ByVal explicitly here? Good point on late-bound code, although in some cases I think it's unavoidable (see the update), where the user is providing data to some collection class or API. \$\endgroup\$
    – Greedo
    Commented Nov 4, 2019 at 8:12
3
\$\begingroup\$

If I am not too late to answer this question. The first method which springs to mind is to use a collection to catch the function result.

Dim myResult as Collection
Set myResult=new Collection

myResult.add thefunction()
\$\endgroup\$
1
  • 2
    \$\begingroup\$ It's never too late to post an answer to any CR question! =) \$\endgroup\$ Commented Nov 4, 2019 at 17:49
2
\$\begingroup\$

This first pattern is inspired by Freeflow:

    For Each Item In Array(objectOrValueType)
        Exit For
    Next

The idea is to use For Each Controls loop to make the correct assignment.

Here I use Matt's suggested name Assign to touch up the API Call used in Sancarn's answer

#If Win64 Then
Public Declare PtrSafe Sub Assign Lib "oleaut32.dll" Alias "VariantCopy" (ByRef Target As Variant, ByRef Source As Variant)
#Else
Public Declare Sub Assign Lib "oleaut32.dll" alias "VariantCopy" (ByRef Target As Variant, ByRef Source As Variant)
#End If

Tests

Sub TestAssign()
    Dim Item As Variant
    Dim n As Long
    For n = 0 To 10
        Assign Item, objectOrValueType(n)
        Debug.Print IsObject(Item)
    Next
End Sub

Sub TestForEach()
    Dim Item As Variant
    Dim n As Long
    For n = 0 To 10
        For Each Item In Array(objectOrValueType(n))
            Exit For
        Next
        Debug.Print IsObject(Item)
    Next
End Sub

Sub TestCast()
    Dim Item As Variant
    Dim n As Long
    For n = 0 To 10
        Cast Item, objectOrValueType(n)
        Debug.Print IsObject(Item)
    Next
End Sub

Sub Cast(ByRef Target As Variant, ByRef Source As Variant)
    For Each Target In Array(Source)
        Exit For
    Next
End Sub

Function objectOrValueType(ByVal n As Long) As Variant
    If n Mod 2 = 0 Then
        objectOrValueType = Range("A1")
    Else
        Set objectOrValueType = Range("A1")
    End If
End Function
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Despite language specifications saying otherwise, a For Each loop variable will not have a reference to the last item if the loop runs to completion - this Exit For loop-assignment is kind of a hack... \$\endgroup\$ Commented Nov 5, 2019 at 15:38
1
\$\begingroup\$

The body of either function:

If IsObject(value) Then
    Set variable = value
Else
    variable = value
End If

... can be written more concisely and clearly as a single thought:

Public Property Let LetSet(ByRef variable As Variant, ByVal value As Variant)
    If IsObject(value) Then Set variable = value Else variable = value
End Property
\$\endgroup\$
1
\$\begingroup\$

I am stumbling over this old thread again. I feel this issue is still not completely resolved.

' IsObject approach
If IsObject(value) Then
    Set variable = value
Else
    variable = value
End If
' LetSet approach
Public Property Let LetSet(ByRef variable As Variant, ByVal value As Variant)
    If IsObject(value) Then Set variable = value Else variable = value
End Property

A certain difference between the two approaches is that in the IsObject approach, the value gets evaluated twice: once in the IsObject term and once in the actual Set/Let statement. If value is a property, this might be a significant additional computation effort, and it might even lead to different results! In my view, it is a clear advantage of the LetSet approach, that it evaluates value only once. 1:0 for LetSet.

On the other hand, I learned the hard way that LetSet can be tricky. If you pass a property as the first parameter, you may be surprised to find that LetSet does not change it at all!

This is a consequence of a general problem with ByRef parameters: If you pass a variable ByRef, its value might get changed by the function/procedure. Fine, but if you pass a property ByRef, changes in the function do not affect the property value! Greedo wisely called the first parameter variable to avoid that. Never pass a property to it!

One problem is that the desired assignment is not performed, the even bigger problem is that the developer does not get any notice of that. VBA just implicitly performs a ByVal call.

LetSet inherits this ByRef-Property-problem. For LetSet, this is particularly severe because it is thought to directly replace the IsObject approach. But the IsObject approach works for variables AND properties, hence it is reasonable to assume that LetSet will do so as well. It does not!

I am using LetSet in my development, but it is a daily dose of pain to pollute my beautiful code with this suboptimal solution.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.