Passing variants/arrays as arguments for a function - vba

Example code;
Sub functiontester()
Dim testdata As Variant
Dim answer As Long
Dim result1 As Long
testdata = Sheets("worksheet1").Range("E1:E2").Value
result1 = testfunction(testdata)
result2 = testfunction2(testdata(2, 1))
End Sub
Function testfunction(stuff As Variant) As Long
testfunction = stuff(2, 1)
End Function
Function testfunction2(num As Long) As Long
testfunction2 = num
End Function
So from my days in python I'd expect result1 & result2 to both run fine, however this is not the case in VBA and if you try to run this you get
"compile error: Byref argument type mismatch" from result2; which I assume has something to do with limits of calculating values inside arguments of functions
So my question is: is there an easy way to make result2 work so that the variant reference just resolves to the specified element?

testdata(2, 1) likely will be of type Double, not Long.
You can use
CLng(testdata(2, 1))
to cast it to a Long.
So:
result2 = testfunction2(CLng(testdata(2, 1)))
should be fine

"compile error: Byref argument type mismatch" Actually refers to the fact it won't implicitly convert the datatype for you, because ByRef arguments (the default) are expected to be writeable. If converted arguments are written to , it gets lost when you return from the function/subroutine because the converted values are only temporary, they're not in any variable outside of the called function.
You can get around this complaint by making the receiving parameter ByValwhich means that it shouldn't be writable anyway:
Function testfunction2(ByVal num As Long) As Long

Related

Why does my function assume a missing argument is there?

I have a function which updates a form, "LoadingInterface". The function looks like this:
Private Sub updateLoadingBar(Optional tekst As String, Optional barOnePerc As Long, Optional barTwoPerc As Long)
If Not IsMissing(tekst) Then
LoadingInterface.Label1.Caption = tekst
End If
If Not IsMissing(barOnePerc) Then
LoadingInterface.Bar.Width = barOnePerc * 1.68
LoadingInterface.prosent.Caption = barOnePerc & "%"
LoadingInterface.prosent.Left = barOnePerc * 1.68 / 2 - 6
End If
If Not IsMissing(barTwoPerc) Then
LoadingInterface.SubBar.Width = barTwoPerc * 1.68
End If
LoadingInterface.Repaint
End Sub
I then call the function like this, expecting it to only update the textfield, since the other two arguments are missing.
Call updateLoadingBar(tekst:="Test")
This works fine for updating Label1, but unfortunately the other two values are updated too - it seems that not including any values in the function-call makes VBA assume the two variables values are 0. What's more, it appears that the IsMissing function does not detect that the two values are missing when the function is called, which is the bigger problem. Stepping through the code using F8 confirms that all the if-statements are indeed entered.
Is there any way to make the code skip the two lowermost if-statements in my function, if no values are provided for the parameters barOnePerc and barTwoPerc?
IsMissing only works if the argument is declared as a Variant.
I don't think you can validly distinguish between 0 and no passed parameter for the Long. In that case you would need to declare as Variant in the signature. You can later cast if required.
I guess you could put a default (unlikely number) and test for that. Note: I wouldn't advise this. This just screams "Bug".
IsMissing:
IsMissing returns a Boolean value indicating whether an optional Variant argument has been passed to a procedure.
Syntax: IsMissing(argname)
The required argname argument contains the name of an optional Variant
procedure argument.
Remarks: Use the IsMissing function to detect
whether or not optional Variant arguments have been provided in
calling a procedure. IsMissing returns True if no value has been
passed for the specified argument; otherwise, it returns False.
Both methods:
Option Explicit
Public Sub Test()
RetVal
RetVal2
End Sub
Public Function RetVal(Optional ByVal num As Long = 1000000) As Long
If num = 1000000 Then
MsgBox "No value passed"
RetVal = num
Else
MsgBox "Value passed " & num
RetVal = num
End If
End Function
Public Function RetVal2(Optional ByVal num As Variant) As Long
If IsMissing(num) Then
MsgBox "No value passed"
Else
MsgBox "Value passed " & num
RetVal2 = CLng(num)
End If
End Function

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)

Is there a VBA equivalent (or way to replicate) passing parameters as 'Out' like C#?

I generally use VBA but have been reading up on programming techniques in The C# Programming Yellow Book which, obviously, is more specific to C#. Anyway, it mentions a technique of passing parameters using the Out keyword.
I already know that VBA supports byVal and byRef and am fairly certain there is no direct equivalent for Out. Passing parameters using Out is subtly different to passing parameters by Ref.
This Answer https://stackoverflow.com/a/388781/3451115 seems to give a good explanation of the difference between Out & Ref.
The Ref modifier means that:
The value is already set and
The method can read and modify it.
The Out modifier means that:
The Value isn't set and can't be read by the method until it is set.
The method must set it before returning.
In the code base that I've inherited there are several places where values are assigned to variables using methods that accept parameters byRef. It seems to me that while passing byRef does the job, passing by Out would be safer... So (and here is the question) is there a way of safely / reliably replicating Out in VBA?
In my first iteration (original question) I imagined that the code would have a pattern like:
Sub byOutExample(byRef foo As String)
' Check before running code:
' foo must = vbNullString
If foo <> vbNullString then Err.Raise(someError)
' Do Something to assign foo
foo = someString
' Check before exiting:
' foo must <> vbNullString
If foo = vbNullString then Err.Raise(someError)
End Sub
Other considerations: is it worth doing, is there a better way, what could go wrong?
Edit: I noticed in the comments for the above definition of Ref vs Out that the passed parameter need not be null, nothing, empty etc. it can be preassigned - the main criteria seems that it is re-assigned.
In light of #ThunderFrame's answer below and the comment that a parameter passed by Out can be pre-assigned (and used), perhaps the following is a better approach:
Sub byOutExample(ByRef foo As String)
Dim barTemp As String
barTemp = foo
' Do Something to assign a new value to foo (via barTemp)
barTemp = someString
' Must assign new variable
foo = barTemp
End Sub
In which case would it be true to say that, as long as foo only appears in the 2 locations shown above, the above code is an accurate way to replicate passing a parameter by Out in VBA?
The answer is unequivocally 'no' you cannot replicate the C# out parameter modifier in VBA. From https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/out-parameter-modifier:
Variables passed as out arguments do not have to be initialized before
being passed in a method call. However, the called method is required
to assign a value before the method returns.
These aspects simply don't exist in VBA. All variables in VBA are initialised with default values, ie the concept of an unitialised variable does not exist in VBA, so point 1 isn't possible; and the compiler cannot object if a specified parameter has not had a value assigned within the procedure, so point 2 isn't possible either.
Even the coding patterns in your example would rely on the Do Something to assign foo not to resolve to the relevant data type's default value (which is obviously not the same as being unitialised). The following, for example, would wrongly throw an error:
Public Sub Main()
Dim income As Long, costs As Long
Dim result As Long
income = 1000
costs = 500
ProcessSpend income, costs, result
End Sub
Private Sub ProcessSpend(income As Long, costs As Long, ByRef outValue As Long)
Const TAX_RATE As Long = 2
Dim netCosts As Long
Dim vbDefaultValue As Long
netCosts = costs * TAX_RATE
outValue = income - netCosts
If outValue = vbDefaultValue Then Err.Raise 5, , "Unassigned value"
End Sub
So we're really left with the question of is there a way of getting close to the characteristics of out in VBA?
Unitialised variables: the closest I can think of are a Variant or Object type which by default initialise to Empty and Nothing respectively.
Assign value within the procedure: the simplest way would be to test if the address of the assigning procedure matches your desired procedure address.
It's all leaning towards a helper class:
Option Explicit
Private mNumber As Long
Private mTargetProc As LongPtr
Private mAssignedInProc As Boolean
Public Sub SetTargetProc(targetProc As LongPtr)
mTargetProc = targetProc
End Sub
Public Sub SetNumber(currentProc As LongPtr, val As Long)
mAssignedInProc = (currentProc = mTargetProc)
mNumber = val
End Sub
Public Property Get Number() As Long
If mAssignedInProc Then
Number = mNumber
Else
Err.Raise 5, , "Unassigned value"
End If
End Property
And then the previous example would look like this:
Public Sub Main()
Dim income As Long, costs As Long
Dim result As clsOut
income = 1000
costs = 500
ProcessSpend income, costs, result
Debug.Print result.Number
End Sub
Private Sub ProcessSpend(income As Long, costs As Long, outValue As clsOut)
Const TAX_RATE As Long = 2
Dim netCosts As Long
If outValue Is Nothing Then
Set outValue = New clsOut
End If
outValue.SetTargetProc AddressOf ProcessSpend
netCosts = costs * TAX_RATE
outValue.SetNumber AddressOf ProcessSpend, income - netCosts
End Sub
But that's all getting very onerous... and it really feels as if we are trying to force another language's syntax onto VBA. Stepping back a little from the out characteristics and developing in a syntax for which VBA was designed, then a function which returns a Variant seems the most obvious way to go. You could test if you forgot to set the 'out' value by checking if the function returns an Empty variant (which suits point 1 and 2 of the out characteristics):
Public Sub Main()
Dim income As Long, costs As Long
Dim result As Variant
income = 1000
costs = 500
result = ProcessedSpend(income, costs)
If IsEmpty(result) Then Err.Raise 5, , "Unassigned value"
End Sub
Private Function ProcessedSpend(income As Long, costs As Long) As Variant
Const TAX_RATE As Long = 2
Dim netCosts As Long
netCosts = costs * TAX_RATE
'Comment out the line below to throw the unassigned error
ProcessedSpend = income - netCosts
End Function
And if you wanted the option of passing in a pre-assigned value, then could just define an optional argument as a parameter to the function.
You can pseudo enforce an out type parameter in VBA by passing it in ByRef, and then checking that it is Nothing (or the default value for a value type) before continuing, much as you have done with the String in your example.
I wouldn't impose the exit condition - sometimes an empty string is a perfectly valid return value, as is a Nothing reference.

Converting a Variant (Integer) parameter to Variant (Long)

I have a Variant parameter passed to a function, and this parameter is essentially an Integer:
Function Foo(vNum As Variant) As Long
vNum = 50000
When I call this function using:
Dim A As Integer
A = 2000
Foo(A)
I get an Overflow error (6).
This seems to have something to do with vNum being 'classed' as an integer variant when I pass a number smaller than 32767 (the upper limit of integers), which then causes overflow when attempting to assign a number larger than 32767.
My question is, how do I cast or convert this 'Integer' Variant into one that will accept Longs?
I tried casting: vNum = CLng(50000) and vNum = CVar(50000),
and using dummy variables:
Function Foo(vNum As Variant) As Long
Dim vTest As Variant
vTest = 50000
vNum = vTest
All of these still generated an Overflow error (6).
Would appreciate any help thanks!
I think you should just define your function like this:
Function Foo(ByVal vNum As Variant) As Long
If you don't specify ByVal, your parameter is passed ByRef, and any operation you make to the parameter is made on the "original" variable, hence an integer.
If you just set vNum as a Long you dont get that problem.
Then it can contain a number to 2,147,483,648
Function Foo(vNum As Long) As Long
'do stuff
End Function

ByRef in VB.NET

I have written the following code in VB.NET:
Dim obj As Object
obj = "00"
Test(obj)
MsgBox(obj)
Private Sub Test(ByRef num As Integer)
End Sub
Private Sub Test(ByVal num As Integer)
End Sub
When the value "00" is passed "ByRef" in the method "Test" it converts to 0. But if the value "00" is passed "ByVal" it keeps the same value as "00". How the passed value is being converted only depending of the signature?
In VB6 although the default passing type is "ByRef", still the same code keeps the same value("00")
Could anybody explain the reason behind this contradictory behaviour in VB6 and VB.NET?
The way you are doing it, the ByRef changes the type of the object from string to integer. By default, integer do not have trailling "0" when covnerted to strings.
This example below might help you understand what is hapenning.
Sub Main()
Dim o1 As Object = "00"
Dim o2 As Object = "00"
Console.WriteLine(o1.GetType().ToString())
Test1(o1)
Console.WriteLine(o1.GetType().ToString())
Console.WriteLine(o2.GetType().ToString())
Test2(o2)
Console.WriteLine(o2.GetType().ToString())
Console.ReadLine()
End Sub
Sub Test1(ByVal num As Integer)
End Sub
Sub Test2(ByRef num As Integer)
End Sub
Output
System.String
System.String
System.String
System.Int32
I suggest you always turn Option Strict On, this will remove a lot of confusion.
The object is of type System.String. It cannot be passed ByRef to a method, it is of the wrong type. So the compiler has to work around it and rewrites the code:
Dim obj As Object
obj = "00"
Dim $temp As Integer
$temp = CInt(obj)
Test($temp)
obj = $temp '' <=== Here
MsgBox(obj)
The indicated statement is the one that changes the object from a string to an integer. Which, converted again to a string by the MsgBox() call, produces "0" instead of "00".
Notable is that C# does not permit this and generate a compile error. This rewriting trick is rather nasty, if the method itself changes the original object then you'll have a very hard time guessing what is going on since that doesn't change the passed argument value.
ByRef means that value passes by reference and in function will be used the same value what has been sent.
ByVal means that value passes by value (function creates a copy of passed value) and you use only copy of value.