11
\$\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\$
  • \$\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 Nov 4 '19 at 8:46
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.

|improve this answer|||||
\$\endgroup\$
  • \$\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 Nov 12 '19 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.

|improve this answer|||||
\$\endgroup\$
  • 1
    \$\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 Nov 4 '19 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()
|improve this answer|||||
\$\endgroup\$
  • 2
    \$\begingroup\$ It's never too late to post an answer to any CR question! =) \$\endgroup\$ – Mathieu Guindon Nov 4 '19 at 17:49
1
\$\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
|improve this answer|||||
\$\endgroup\$
  • \$\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\$ – Mathieu Guindon Nov 5 '19 at 15:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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