0

Given the following code: I can not seem to successfully pass a Range Object Variable from one sub-function to another. I spent an entire day researching, and experimenting before I swallowed pride and came here.

Please read the comments below, and reply with any ideas you have regarding why the LAST two lines will not behave.

Public Sub doSomethingToRows(ROI As Range)
*'do Something with the cell values within the supplied range*

End Sub
'
Public Sub testDoAltRows()

    Dim RegionOfInterest As Range       'is this an object or not?

    '*The following yields: Class doesn't support Automation (Error 430)*
    '*Set RegionOfInterest = New Worksheet 'this just gives an error*

    Set RegionOfInterest = Worksheets("Sheet1").Range("A1")
    RegionOfInterest.Value = 1234.56        '*okay, updates cell A1*

    Set RegionOfInterest = Worksheets("Sheet1").Range("B5:D15")
    RegionOfInterest.Columns(2).Value = "~~~~~~"    '*okay*

    'doSomethingToRows (RegionOfInterest)   'why do I get "OBJECT IS REQUIRED" error?
    doSomethingToRows (Worksheets("Sheet1").Range("B5:C15")) 'but this executes okay
End Sub
Mathieu Guindon
  • 69,817
  • 8
  • 107
  • 235
HadziJo
  • 57
  • 1
  • 5
  • 7
    Remove the parentheses. They're forcing a call to the default member so you're actually trying to pass a Variant. – Comintern Feb 08 '17 at 18:55

3 Answers3

5

From the msdn documentation of the Call keyword statement,

Remarks

You are not required to use the Call keyword when calling a procedure. However, if you use the Call keyword to call a procedure that requires arguments, argumentlist must be enclosed in parentheses. If you omit the Call keyword, you also must omit the parentheses around argumentlist. If you use either Call syntax to call any intrinsic or user-defined function, the function's return value is discarded.

To pass a whole array to a procedure, use the array name followed by empty parentheses.

From a practical standpoint, even though Subs can be called with or without the "Call" keyword, it makes sense to pick one way and stick with it as part of your coding style. I agree with Comintern - it is my opinion, based on observation of modern VBA code, that using the "Call" keyword should be considered deprecated. Instead, invoke Subs without parenthesis around the argument list.

And now the answer to the important question:

Why does your code throw an error?

Take for example the following Subroutine:

Public Sub ShowSum(arg1 As Long, arg2 As Long)
    MsgBox arg1 + arg2
End Sub

We have established that, if not using the Call keyword, Subs must be invoked like so:

ShowSum 45, 37

What happens if it were instead called like ShowSum(45, 37)? Well, you wouldn't even be able to compile as VBA immediately complains "Expected =". This is because the VBA parser sees the parenthesis and decides that this must be a Function call, and it therefore expects you to be handling the return value with an "=" assignment statement.

What about a Sub with only one argument? For example:

Public Sub ShowNum(arg1 As Long)
    MsgBox arg1
End Sub

The correct way to call this Sub is ShowNum 45. But what if you typed this into the VBA IDE: ShowNum(45)? As soon as you move the cursor off of the line, you'll notice that VBA adds a space between the Sub name and the opening parenthesis, giving you a crucial clue as to how the line of code is actually being interpreted:

ShowNum (45)

VBA is not treating those parenthesis as if they surrounded the argument list - it is instead treating them as grouping parenthesis. MOST of the time, this wouldn't matter, but it does in the case of Objects which have a default member.

To see the problem this causes, try running the following:

Dim v As Variant
Set v = Range("A1")
Set v = (Range("A1"))  '<--- type mismatch here

Notice that you get a "Type Mismatch" on the marked line. Now add those two statements to the watch window and look at the "Type" column:

+-------------+-----+--------------+
| Expression  |Value|     Type     |
+-------------+-----+--------------+
|Range("A1")  |     |Object/Range  |
|(Range("A1"))|     |Variant/String|
+-------------+-----+--------------+

When you surround an Object with grouping parenthesis, its default property is evaluated - in the case of the Range object, it is the Value property.

So it's really just a coincidence that VBA allowed you to get away with "putting parenthesis around the argumentlist" - really, VBA just interprets this as grouping parenthesis and evaluates the value accordingly. You can see by trying the same thing on a Sub with multiple parameters that it is invalid in VBA to invoke a Sub with parenthesis around the argument list.

@PaulG

Try this:

Public Sub Main()
    Debug.Print TypeName(Range("A1"))
    Debug.Print TypeName((Range("A1")))
End Sub
Blackhawk
  • 5,984
  • 4
  • 27
  • 56
  • 1. I agree with the MS Doc in yellow. Like I said in my earlier post you can call it two ways. If you use the Call you need the (). If not then you don't, Period. 2. You're call Set v = (Range("A1")) has brackets around it. Excel is trying to evaluate it, yes I get that but thats because you have supplied EXTRA brackets around it. His call DOES NOT supply extra brackets around it, Thats why you are getting the type exception. You are trying to say you are returning an object when you are FORCING the compiler to evaluate the function which will call the default parameters. – PaulG Feb 09 '17 at 18:38
  • 3. His code does not have any additional parameter around the invocation. Excel is passing the Range object into the parameter. Your example is a totally different invocation. Its forcing it to behave differently. It can't be calling the default parameter because its casting it to a Range. You would get a type exception if it did. – PaulG Feb 09 '17 at 18:40
  • 1
    @PaulG The OP's incorrect code is `doSomethingToRows (Worksheets("Sheet1").Range("B5:C15"))`. `doSomethingToRows` is a Sub, and therefore it needs to be called without parenthesis around the argument list, like this `doSomethingToRows Worksheets("Sheet1").Range("B5:C15")`. However, as you can see they included parenthesis around the Range, which causes VBA to evaluate the Range's default property. – Blackhawk Feb 09 '17 at 18:43
  • How come my example where I call them two different ways works? The function expects a Range object. There is no way it is not a range object. You cannot cast the default property of a range object to a Range object. There is no default property being called. Your extra bracketed example is forcing this, which he does not do. – PaulG Feb 09 '17 at 18:45
  • @PaulG ah, I see the confusion! Yes, VBA does attempt implicit conversion of arguments to the types of the expected parameters. If it succeeds, you don't have any errors and it works as expected. – Blackhawk Feb 09 '17 at 19:01
  • 1
    @PaulG For your `DisplaySomething(ByVal Data As String)` example, if you call *without* parenthesis, VBA takes the Range object and attempts (successfully) an implicit conversion to String. If you call *with* the extraneous parenthesis, the evaluation of the parenthesized expression evaluates the Range object's default parameter, yielding a Variant containing a String or Empty or Double, etc. Then VBA attempts to implicitly convert this to a String (the type of the Sub parameter). If this is successful, the subroutine runs and no error occurs. – Blackhawk Feb 09 '17 at 19:02
  • Nice: What about the invocation to the declation DisplaySomething(ByVal Data As Range). as he has defined in his post. It has to accept a Range object or we get a type error. Both calls succeed so both calls are of type Range. Because they are of type range, the default parameter cannot have been called because this would be a different type. – PaulG Feb 09 '17 at 19:07
  • Anyway, all that aside, I still refer to my original post. I don't think this merited a mark down since he posted a solution (valid in my opinion, but not everyone else). If he was incorrect, someone should have just comented.corrected his error. :) – PaulG Feb 09 '17 at 19:09
  • 1
    @PaulG The OP only calls his function once, incorrectly - which other call are you referring to? If you mean his answer, that is covered by the MSDN documentation: If you use `Call`, you must also then provide parenthesis around the argument list. VBA interprets these parenthesis as part of the invocation and *not* grouping parenthesis. However, if you invoke a Sub without `Call`, you must *omit* parenthesis. If you include parenthesis, they are not interpreted by VBA as part of the sub invocation. (also, the downvote wasn't me :P) – Blackhawk Feb 09 '17 at 19:17
  • Seriously? that is just not in question, I agree. BUT you can call them both ways. Call requires params, No Call doesn't. Lovely. My question is if his function is Sub DisplaySomething(ByVal Data As Range) - notice the RANGE bit. It is invoked both ways in my example, one with a Call with params and the other without. Both work, no errors, no type exceptions. The type passed in has to be a Range type for both calls. The issue I have is, there is no default parameter being called. To me its impossible. Unless you can explain that. But hey lets just move on. :) – PaulG Feb 09 '17 at 19:23
  • @PaulG you called it with and without the `Call` keyword, both correctly by the msdn documentation. Try calling it incorrectly like this: `DisplaySomething(r)`, without the `Call` keyword. – Blackhawk Feb 09 '17 at 19:29
  • 2
    @PaulG I will leave it at that - we've commented the poor OP's question to heck and back :P But I will leave you with one more resource: the [VBA Language Specification (VBAL)](http://interoperability.blob.core.windows.net/files/MS-VBAL/[MS-VBAL].pdf). See section 5.5 for more information on how VBA performs implicit conversion. – Blackhawk Feb 09 '17 at 19:30
  • Lol that was his issue. He missed off the Call. You're not supposed to call it like that, which he corrected by putting the call in front. My code doesn't call it like that. Its an incorrect call. Mine is not. Take a look. So how does the calls which I've called CORRECTLY not get a compilation/run error. Its because the types passed in are Range. No parameter invokation. – PaulG Feb 09 '17 at 19:39
  • 2
    @PaulG the point & the matter is, extraneous parens cause their contents to be evaluated **before** the result is passed to the called procedure (and possibly coerced to the parameter's type). Also, no offense, but Blackhawk and Comintern know their VBA inside out (quite literally), and `Call` is deprecated/obsolete syntax that has absolutely [zero valid use case](http://stackoverflow.com/a/22325284/1188513) in any code written this century. I'm not sure what all this arguing is about, but I'm flagging this whole mess for a cleanup. – Mathieu Guindon Feb 09 '17 at 19:52
  • No offence caused. It was me that said the coersion was happening. Please go ahead and clean up or remove anything belonging to me on this post. BTW I know my VBA as well. – PaulG Feb 09 '17 at 20:04
  • Comments are not for extended discussion; this conversation has been [moved to chat](http://chat.stackoverflow.com/rooms/135350/discussion-on-answer-by-blackhawk-how-do-i-pass-a-range-obj-variable-to-a-sub-in). – Bhargav Rao Feb 10 '17 at 07:07
-1

okay, I knew after I posted this question I'd be struck by lighting and receive an answer.

When passing an object VARIABLE to a sub-function and wishing to use parentheses "()", one must use CALL! Thus the correction to my code sample is:

**CALL doSomethingToRows(RegionOfInterest)**

Thank you!

HadziJo
  • 57
  • 1
  • 5
  • 6
    Call is actually obsolete syntax left over from the 1980s. Just take the parentheses off the argument. – Comintern Feb 08 '17 at 19:02
  • Why was that marked down? There is nothing wrong with using call SomeFunc() with parenthesis. This was marked down for this? Using call is a matter of preference, albeit not necessary. I actually prefer to use it because the code is looks cleaner to me. Actually +1 for finding your issue, and posting it. By the way you can pass your Range object Byval unless you wish to reassign the variable. The reference of the Range object is copied and not the huge Range object. – PaulG Feb 08 '17 at 21:17
  • @PaulG in VBA, if you're calling a "Sub" (does not return a value), you need to omit parenthesis around the parameters. If you are calling a "Function", you must include them. – Blackhawk Feb 08 '17 at 21:29
  • 2
    @PaulG to be more specific, since the parenthesis are not considered part of the Sub function call, they are instead treated as grouping parenthesis, telling VBA to evaluate everything inside of them before proceeding with the rest of that line of code. Unfortunately, if an Object has a default property, VBA treats the parenthetic Object as a call of its default member. In this case, the default member of Range is Value of type Variant. Once VBA has evaluated a Range to obtain its Value (in my simple test, a String), it cannot be then converted back to a Range so VBA complains. – Blackhawk Feb 08 '17 at 21:46
  • @Blackhawk. That isn't true. You can invoke a Sub in two ways: 1. Call SomeRoutine(SomeObject) 2. SomeRoutine SomeObject. – PaulG Feb 09 '17 at 09:42
  • I understand Default properties and all that. No idea what you are getting at there mind. When I invoke a routined defined as something like: Public Sub DisplaySomeRange(Byval Data as Range). The Data object is a range. The reference on the call stack is a reference to a range. In the routine it is a range and I code against it as a range. Nothing to do with Default properties. That is used when the Range object is invoked inside the Routine. Is that not true? – PaulG Feb 09 '17 at 09:52
  • 2
    @PaulG - It has nothing to do with the parameter declaration - it has to do with the how the call is made. When you use `Call`, the parentheses are treated as enclosing an argument list - otherwise the grammar would be ambiguous, i.e. `Call Foo Bar`. When `Call` is omitted, the parentheses are treated as an expression, and as Blackhawk points out, these are evaluated *before the function call* and passed `ByVal`. If it's a reference type, the default member is resolved. You can demo this with `Debug.Print TypeName((Range("A1")))`. – Comintern Feb 09 '17 at 14:09
  • 1
    @PaulG My experience is the same as Comintern's - that Call should be considered deprecated - so I didn't include it in my comment. I will try to clarify in an answer. – Blackhawk Feb 09 '17 at 16:25
  • 2
    @PaulG take a look at the answer I posted - I give a couple of example in there to see how putting parenthesis around an Object can invoke its default property. – Blackhawk Feb 09 '17 at 17:11
-3

Maybe we're talking about different things, but here's an example to make it a bit clearer what I mean.

Option Explicit

Sub TestDisplay()
Dim r As Range

'Create some range object
Set r = Range("A1")
'Invoke with Call.
Call DisplaySomething(r)
'Invoke without Call.
DisplaySomething r

End Sub

Sub DisplaySomething(ByVal Data As Range)
Debug.Print "Hi my type is " & TypeName(Data)
End Sub

Both calls work perfectly. One with Call and the other without.

Edit: @Conintern. Thanks for explaining that. I see what is meant now.

However, I still respectively disagree. If I declare the following:

Function DisplaySomething(ByVal Data As String)
DisplaySomething = "Hi my type is " & TypeName(Data)
End Function

and invoke it:

Debug.print DisplaySomething(Range("A1"))

I believe that Excel has been clever and converted to a string. It can do that by invoking the Default Parameter and can convert to a string.

However, as in the original parameter example, If I declare the following:

Function DisplaySomething(ByVal Data As Range)
DisplaySomething = "Hi my type is " & TypeName(Data)
End Function

There is no call on the Default Parameter, however it is called, because Excel was able to resolve it to that type.

Function DisplaySomething(ByVal Data As Double)
DisplaySomething = "Hi my type is " & TypeName(Data)
End Function

will return a double because it was able to coerce to a double.

Indeed in those examples the Default was called. But in this example we are defining as Range. No Default called there however it is invoked - brackets or no brackets.

I believe this is more to do with Excel and data coercion. Similar to the following:

Public Function Test(ByVal i As String) As Integer
Test = i
End Function

and invoking with: Debug.print Test("1")

BTW, yes I know this isn't an object without a Default parmeter. Im pointing out data coercion. Excel does its best to resolve it.

Could be wrong mind you...

PaulG
  • 1,051
  • 7
  • 9