Class Get Function Error ByRef - vba

I have a custom class called a cCavity. One of the many properties of the class is a string array called pAdjacency, which contains the string name of a node at each index (Format is C[# of Node]). I am trying to create edge names by using two node names in conjunction with one another. Whenever I try to call the adjacency GET function for the class objects, I get a ByRef argument type mismatch, and I cannot see why.
Class Get function:
Public Property Get Adjacency(Index As Integer) As String
Adjacency = pAdjacency(Index)
End Property
Section of Code that is getting an error:
Sub CalculateEdges(cCavities() As cCavity, dEdges As Scripting.Dictionary)
'Dim i as integer
For i = 1 To UBound(cCavities)
If cCavities(i).AdjacencySize > MaxEdges Then MaxEdges = cCavities(i).AdjacencySize
'Dim j as Integer
For j = 1 To cCavities(i).AdjacencySize
dEdges.Add cCavities(i).Name & cCavities(i).Adjacency(j), 0 ' The error is taking place here, with the .Adjacency(j)
Next j
Next i
End Sub
The error in question is: "Compile error: ByRef argument type mismatch"
Am I missing something obvious? The argument j should be an integer, and I have tried explicitly defining it as such while trying to figure out what was happening.

First specify Option Explicit and declare all your variables, then make your parameters ByVal where it makes sense: object pointers and values can be passed by value, arrays must be passed ByRef.
So start with this:
Public Property Get Adjacency(ByVal Index As Long) As String
Adjacency = pAdjacency(Index)
End Property
Now, that 4-liner snippet is dereferencing cCavities(i) 5 times. Have you considered introducing a local variable, and dereferencing the object once?
For i = LBound(cCavities) To UBound(cCavities)
Dim foo As cCavity
Set foo = cCavities(i)
For j = 1 To foo.AdjacencySize
Dim edge As String
edge = foo.Name & foo.Adjacency(j)
Next
Next
Now, does that foo assignment work? If so, congrats, you now have IntelliSense on the cCavity member calls, and more efficient code.

It's a compile error, not a run-time error. If j is a Variant (as it will be if it's not explicitly declared), the compiler won't let it through. Declare all of your variables with their proper types.
Edit: for some reason, the compiler only complains about this at run-time, but it's still a compile error.

Related

Dealing with error message narrowing from type object to type string

I am getting this error message when converting code from .Net 2.0 to .Net 4.5:
Option strict on disallows narrowing from type 'object' to type
'string' in copying the value of 'ByRef' parameter 'ParamValue' back
to the matching argument.
The code looks like this:
Public Shared Function TheFunction(ByRef x As Object ) As Integer
TheFunction = 5
// ultimately called like this: SqlCommand.Parameters.AddWithValue("field", x)
End Function
Private Function AFunction(ByVal x As String) As Boolean
Dim cnt As Integer = TheFunction(x)
End Function
I have googled for answers and it seems the suggestion is to change the TheFunction.
I am constrained in that I cannot change TheFunction.
I can turn off strict, but I would rather put in a good fix for this problem like copying x to a different variable and passing that variable in.
Would this work?
Dim boxedObject as Object = CType(x, Object)
Dim cnt As Integer = TheFunction(boxedObject)
x = CType(boxedObject, String)

What's a legit use case for a function call to be on left-hand side of assignment?

There's a compile error in VBA that reads as follows:
Compile error:
Function call on left-hand side of assignment must return Variant or Object.
To produce this compile error, you just need a function:
Public Function Foo(ByVal x As Integer) As Integer
Foo = x
End Function
..And then a function call on the left-hand side of an assignment - for example:
Public Sub Test()
Foo(42) = 12
End Sub
If I remove the As Integer return type from the function's signature, I get a function that returns an implicit Variant, and the compiler is satisfied - but now there's a run-time error 424 / "Object Required".
So I make the function return an actual object:
Public Function Foo(ByVal x As Integer) As Object
Dim result As Collection
Set result = New Collection
result.Add x
Set Foo = result
End Function
Public Sub Test()
Foo(42) = 12
End Sub
Now at run-time the error is 438 / "Object doesn't support this property or method" - obviously, that Test method makes no sense whatsoever.
I cannot for the life of me think of anything that would be a valid, legitimate (and warranted?) use of a function call on the LHS of an assignment.
That compile error exists for a reason, so there must be a valid use case. What is it?
If the function were to return a Variant, and that Variant contains an Object (i.e. the Variant type is VT_DISPATCH), and that object has a default "property put", then the assignment is valid, and the assignment is set to that default property.
In all other cases, it isn't.
VB variant is analogous to dynamic in C#, meaning that the type is dynamically resolved at run time. So it would make sense that you can assign a value to a variant type (i.e. the return value of a function that returns a variant type).
I imagine the use case is very narrow and 99.9% of the time, if you're trying to do this, you're doing something wrong.
https://msdn.microsoft.com/VBA/Language-Reference-VBA/articles/function-call-on-left-hand-side-of-assignment-must-return-variant-or-object

Reference and Value types

I have read that String was a "reference type", unlike integers. MS website
I tried to test its behavior.
Sub Main()
Dim s As New TheTest
s.TheString = "42"
Dim z As String = s.GimmeTheString
z = z & "000"
Dim E As String = s.TheString
s.GimmeByref(z)
end sub
Class TheTest
Public TheString As String
Public Function GimmeTheString() As String
Return TheString
End Function
Public Sub GimmeByref(s As String)
s = TheString
End Sub
End Class
So I expected :
z is same reference as TheString, thus TheString would be set to "42000"
Then Z is modified by reference by GimmeByref thus Z is set to whatever TheString is
Actual result:
Z="42000"
E="42"
TheString="42"
What point am I missing?
I also tried adding "ByRef" in GimmeByRef : yes obviously the GimmeByRef does work as expected, but it also does if I put everything as Integer, which are said to be "Value type".
Is there any actual difference between those types?
The confusion comes about because regardless of type, argument passing in VB is pass by value by default.
If you want to pass an argument by reference, you need to specify the argument type as ByRef:
Public Sub GimmeByref(ByRef s As String)
You also need to understand the difference between mutating a value and re-assigning a variable. Doing s = TheString inside the method doesn’t mutate the value of the string, it reassigns s. This can obviously be done regardless of whether a type is a value or reference type.
The difference between value and reference types comes to bear when modifying the value itself, not a variable:
obj.ModifyMe()
Strings in .NET are immutable and thus don’t possess any such methods (same as integers). However, List(Of String), for instance, is a mutable reference type. So if you modify an argument of type List(Of String), even if it is passed by value, then the object itself is modified beyond the scope of the method.
Strings are immutable, every time you do a change it creates a new "reference" like if New was called.
A String object is called immutable (read-only), because its value
cannot be modified after it has been created. Methods that appear to
modify a String object actually return a new String object that
contains the modification. Ref
Your code basically does something like this:
Sub Main()
Dim a, b As String
a = "12"
b = a
a = a & "13"
Console.WriteLine(a) ' 1213
Console.WriteLine(b) ' 12
Console.ReadLine()
End Sub

How can I assign a Variant to a Variant in VBA?

(Warning: Although it might look like one at first glance, this is not a beginner-level question. If you are familiar with the phrase "Let coercion" or you have ever looked into the VBA spec, please keep on reading.)
Let's say I have an expression of type Variant, and I want to assign it to a variable. Sounds easy, right?
Dim v As Variant
v = SomeMethod() ' SomeMethod has return type Variant
Unfortunately, if SomeMethod returns an Object (i.e., a Variant with a VarType of vbObject), Let coercion kicks in and v contains the "Simple data value" of the object. In other words, if SomeMethod returns a reference to a TextBox, v will contain a string.
Obviously, the solution is to use Set:
Dim v As Variant
Set v = SomeMethod()
This, unfortunately, fails if SomeMethod does not return an object, e.g. a string, yielding a Type Mismatch error.
So far, the only solution I have found is:
Dim v As Variant
If IsObject(SomeMethod()) Then
Set v = SomeMethod()
Else
v = SomeMethod()
End If
which has the unfortunate side effect of calling SomeMethod twice.
Is there a solution which does not require calling SomeMethod twice?
In VBA, the only way to assign a Variant to a variable where you don't know if it is an object or a primitive, is by passing it as a parameter.
If you cannot refactor your code so that the v is passed as a parameter to a Sub, Function or Let Property (despite the Let this also works on objects), you could always declare v in module scope and have a dedicated Sub solely for the purpose of save-assigning that variable:
Private v As Variant
Private Sub SetV(ByVal var As Variant)
If IsObject(var) Then
Set v = var
Else
v = var
End If
End Sub
with somewhere else calling SetV SomeMethod().
Not pretty, but it's the only way without calling SomeMethod() twice or touching its inner workings.
Edit
Ok, I mulled over this and I think I found a better solution that comes closer to what you had in mind:
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
[...] I guess there just is no LetSet v = ... statement in VBA
Now there is: LetSet v, SomeMethod()
You don't have a return value that you need to Let or Set to a variable depending of its type, instead you pass the variable that should hold the return value as first parameter by reference so that the Sub can change its value.
Dim v As Variant
For Each v In Array(SomeMethod())
Exit For 'Needed for v to retain it's value
Next v
'Use v here - v is now holding a value or a reference
You could use error trapping to reduce the expected number of method calls. First try to set. If that succeeds -- no problem. Otherwise, just assign:
Public counter As Long
Function Ambiguous(b As Boolean) As Variant
counter = counter + 1
If b Then
Set Ambiguous = ActiveSheet
Else
Ambiguous = 1
End If
End Function
Sub test()
Dim v As Variant
Dim i As Long, b As Boolean
Randomize
counter = 0
For i = 1 To 100
b = Rnd() < 0.5
On Error Resume Next
Set v = Ambiguous(b)
If Err.Number > 0 Then
Err.Clear
v = Ambiguous(b)
End If
On Error GoTo 0
Next i
Debug.Print counter / 100
End Sub
When I ran the code, the first time I got 1.55, which is less than the 2.00 you would get if you repeated the experiment but with the error-handling approach replaced by the naïve if-then-else approach you discussed in your question.
Note that the more often the function returns an object, the less function calls on average. If it almost always returns an object (e.g. that is what it is supposed to return but returns a string describing an error condition in certain cases) then this way of doing things will approach 1 call per setting/ assigning the variable. On the other hand -- if it almost always returns a primitive value then you will approach 2 calls per assignment -- in which case perhaps you should refactor your code.
It appears that I wasn't the only one with this issue.
The solution was given to me here.
In short:
Public Declare Sub VariantCopy Lib "oleaut32.dll" (ByRef pvargDest As Variant, ByRef pvargSrc As Variant)
Sub Main()
Dim v as Variant
VariantCopy v, SomeMethod()
end sub
It seems this is similar to the LetSet() function described in the answer, but I figured this'd be useful anyway.
Dim v As Variant
Dim a As Variant
a = Array(SomeMethod())
If IsObject(a(0)) Then
Set v = a(0)
Else
v = a(0)
End If

How to assign a value to a variable of type Double, that has been passed as Object?

I am trying to assign a value to global variable, which has a Property of type Double. This Property is passed as Object and the assignment fails.
In the example code below, the value is never assigned to the actual object, but only locally:
Public Class Form1
Friend Home As New Building
Private Sub AssignValues() Handles Me.Load
'Objects of different types are added to a list
Dim listObjects As New List(Of Object)
listObjects.Add(Home.Surface)
'All the Objects in listObjects are assigned a value that
'is stored as String
For Each o As Object In listObjects
SetProperty(o, "45.6")
Debug.Print("Surface = " & Home.Surface.ToString)
Next
End Sub
Private Sub SetProperty(ByRef Variable As Object, ByVal Value As String)
Select Case Variable.GetType
Case GetType(Double)
Variable = CDbl(Value)
Case Else
'...
End Select
End Sub
End Class
Public Class Building
Dim _surface As Double = 0
Public Property Surface As Double
Get
Return _surface
End Get
Set(ByVal value As Double)
_surface = value
End Set
End Property
End Class
The program invariably outputs Surface = 0 instead of 45.6. What am I doing wrong?
I tried to pass the Variable as reference, as suggested here, but without success. I also read about using Reflection, but there ought to be something simpler than that...
When your adding home.surface to the list, your adding a copy of the double to the list and then adjusting that copy. Stick a watch on "o" and see how it changes whilst home.surface remains the same.
If you want to use reflection, try something along these lines.
Dim prop As Reflection.PropertyInfo = o.GetType().GetProperty("Surface")
prop.SetValue(o, 45.6)
With Variable.GetType you will get always Object, because this is the type of Variable. What you can do with an Object is converting/casting it into a different type (like Double).
The best way to determine the "original type" from where the Object comes would be including an additional variable telling it. Another option might be converting the given Object into the target Type and see if it is not nothing/does not trigger an error. But this second option is not too accurate, mainly when dealing with "equivalent types" like Doubles/Integers.