Function caller code VBA - arrays

Problem
When you want to look directly at the arguments of your UDF (not their values, which can be passed directly, but the formula that gave these values), you can use Application.Caller.Formula and parse out the arguments to find out.
Is there any way to see the line of VBA code which called a Function, so that you can parse out its arguments in a similar way?
Background
A while ago I created a UDF which was essentially another approach to array functions*. What I wanted to do was take some statement which evaluates to True/False
LEN(A1)>LEN(B1)
And evaluate it over an array. So say the above function was placed in cell A1, then to evaluate over the array A1:A100 would be the same as creating the array
{LEN(A1)>LEN(B1),LEN(A2)>LEN(B2),[...]} 'you may recognise this as an array formula ={LEN(A1:A100)>LEN(B1:B100)}
*For context, this was before I knew about array formulae
I was frustrated with the syntax of certain array-handling Excel functions, like COUNTIF, which takes the arguments in the following form
COUNTIF(range_To_Evalueate_Over, "string_Representing_Boolean_Test")
The string argument presents the following limitations
Not any boolean returning statement can be used as a test; there is no way of looking at properties of the range which you evaluate over other than their values
So you can't use functions like LEN() to get more data about the range
You can not reference other cells relative to the range (Like B1 relative to A1)
The string is static at runtime, you cannot step-into the function to see what the string will evaluate to for a given cell from the range you are evaluating
I much prefer the versatility of the conditional formatting formulae. They take the form of array formulae, where any offsets (B1 relative to A1) are calculated relative to the TL cell of the range that the conditional formatting is applied to.
That prompted me to create a UDF which has a structure like this
evaluateOverRange(range_to_evalute_over As Range, boolean_test_on_TL_Cell As Boolean) As Boolean() 'returns an array equal in size to the evaluate range
Used like
evaluateOverRange(A1:A100,LEN(A1)<LEN(B1))
Note
Boolean test is not a string, so can be evaluated step by step in Excel
Boolean test is guaranteed to be Boolean thanks to type declaration
Boolean test is relative to the first cell (A1) in the evaluate range (A1:A100)
I.e. B1 is replaced with A1.Offset(0,1)
Since boolean_test_on_TL_Cell is not a string, it tells us nothing about the actual test, it just passes the result of the test on the A1, it is actually useless within the UDF so is ignored
To obtain the test string "LEN(A1)<LEN(B1)", the Application.Caller.Formula is read, and the relevant argument of evaluateOverRange is parsed out
In order to evaluate some worksheet function over an array in VBA, you can use the Evaluate method
Dim colA As Range: Set colA = [A1:A100] 'range_to_evaluate_over in my udf
Dim cellA As Range
Dim cellB as Range
Dim outputArray(1 To 100) As Boolean
For i = 1 To 100
Set cellA = colA(i)
Set cellB = cellA.Offset(0,1) 'all cells that arent the TL cell in colA (i.e., not A1) are set relative to the top left cell
outputArray(i) = Evaluate("LEN(" & cellA.Value & ")>LEN(" & cellB.Value ")")
Next i
Right, so all that was for worksheet functions, and somewhat pointless given array functions do the same thing. But now I want to use the same approach within VBA.
Specifically, I want to filter an array of custom classes based on some function of their properties, using actual VBA Boolean returning code rather than a string.
Sub FilterMyClassArray() 'Prints how many items in arrayToFilter whose properties match certain conditions
Dim arrayToFilter(1 To 100) As New myClass
Dim filteredArray() As myClass
Dim tlClass As myClass 'pretend class used only for intellisense and to create
boolean test
Set filteredArray = filterClassArray(arrayToFilter, tlClass.PropertyA > 3 And
tlClass.PropertyB = "hat")
Debug.Print "Number left after filtering:" ; Ubound(filteredArray)
End Sub
Function filterClassArray(ByVal inutArray() As myClass, classTest As Boolean) As myClass 'returns an output array which is equal to the input array filtered by some test
'Somehow get what classTest actually was
'Evaluate classTest over each item in inputArray
'If boolean test evaluates to true, add to output array, otherwise skip
End Function
I imagine some manipulation of the code modules will be required (both to get the string of code which represents the test, and to actually evaluate it), but I want to check feasibility before I dig too deep.

I've been having a think about this and a solution might be possible if you'd be prepared to use something approximating a Linq syntax.
If I understand the requirements correctly, you need to:
obtain a string value for each property name,
record the evaluation and ultimately run it as a string,
have intellisense access to the properties, and
have the ability to debug the evaluation on each iteration.
Regarding #1 and #3, the only way of doing this in VBA would be to code the values manually. If you code them in your class then the class can become cumbersome and some might say it compromises the single responsibility principle (https://en.wikipedia.org/wiki/Single_responsibility_principle). If you code them in a separate 'container' (eg class, type, collection, etc.), then there's a risk of some being missed or of corruption if you change the property names. An Interface class might mitigate these issues.
For #2, I can't see any way around it: the evaluation must be entered as a string. An enum (and associated intellisense) might alleviate things a bit though.
Item #4 is purely a coding architecture issue.
First the syntax
I'm sure there are VBA solutions on the internet which implement a pretty decent mock-up of Linq, but further down is a skeleton version to give you the idea. The end result is that your query syntax could look like this:
Dim query As cLinq
Dim p As INameable
Dim arrayToFilter(1 To 100) As INameable
Dim filteredArray() As INameable
Set query = New cLinq
With query
.SELECT_USING_INTERFACE p
.FROM arrayToFilter
.WHERE p.PropertyA, EQUAL_TO, 3
.AND_WHERE p.PropertyB, EQUAL_TO, "hat"
filteredArray = .EXECUTE
End With
The interface
As far as VBA is concerned an interface is really just a class module with a list of properties and methods that you want a class to implement. In your case, I've created a class and called it INameable, with the following sample code to match your example:
Option Explicit
Public Property Get PropertyA() As Long
End Property
Public Property Let PropertyA(RHS As Long)
End Property
Public Property Get PropertyB() As String
End Property
Public Property Let PropertyB(RHS As String)
End Property
Your MyClass class then implements this interface. For the sake of consistency, I've called the class cMyClass:
Option Explicit
Implements INameable
Private mA As Long
Private mB As String
Private Property Let INameable_PropertyA(RHS As Long)
mA = RHS
End Property
Private Property Get INameable_PropertyA() As Long
INameable_PropertyA = mA
End Property
Private Property Let INameable_PropertyB(RHS As String)
mB = RHS
End Property
Private Property Get INameable_PropertyB() As String
INameable_PropertyB = mB
End Property
I've created a second class, called cNames, which also implements the interface, and this one produces the string names of the properties. As a quick and dirty method it just stores the name of the last property used:
Option Explicit
Implements INameable
Private mName As String
Private Property Let INameable_PropertyA(RHS As Long)
End Property
Private Property Get INameable_PropertyA() As Long
mName = "PropertyA"
End Property
Private Property Let INameable_PropertyB(RHS As String)
End Property
Private Property Get INameable_PropertyB() As String
mName = "PropertyB"
End Property
Public Property Get CurrentName() As String
CurrentName = mName
End Property
You wouldn't have to use an interface and some might argue it's not necessary or even correct to do so, but at least it gives you an idea of how it could be implemented if you went this route.
The Linq class
The final class is really just a helper class to create the intellisense syntax you need and to process the evaluation. It's by no means thorough, but might get you started if the idea appealed to you. I've called this class cLinq:
Option Explicit
'Enumerator to help with intellisense.
Public Enum Operator
EQUAL_TO
GREATER_THAN
LESS_THAN
GREATER_OR_EQUAL_TO
LESS_OR_EQUAL_TO
NOT_EQUAL_TO
End Enum
Private mP As cNames
Private mQueries As Collection
Private mByAnd As Boolean
Private mFromArray As Variant
Public Sub SELECT_USING_INTERFACE(p As INameable)
'Insantiate the name of properties class.
Set mP = New cNames
Set p = mP
End Sub
Public Sub FROM(val As Variant)
'Array containing objects to be interrogated.
mFromArray = val
End Sub
Public Sub WHERE(p As Variant, opr As Operator, val As Variant)
'First query.
Set mQueries = New Collection
AddQuery opr, val
End Sub
Public Sub AND_WHERE(p As Variant, opr As Operator, val As Variant)
'Subsequent query using AND.
mByAnd = True
AddQuery opr, val
End Sub
Public Sub OR_WHERE(p As Variant, opr As Operator, val As Variant)
'Subsequent query using OR.
mByAnd = False
AddQuery opr, val
End Sub
Public Function EXECUTE() As Variant
Dim o As Object
Dim i As Long
Dim result As Boolean
Dim matches As Collection
Dim output() As Object
'Iterate the array of objects to be checked.
Set matches = New Collection
For i = LBound(mFromArray) To UBound(mFromArray)
Set o = mFromArray(i)
result = EvaluatedQueries(o)
If result Then matches.Add o
Next
'Transfer matched objects to an array.
ReDim output(0 To matches.Count - 1)
i = LBound(output)
For Each o In matches
Set output(i) = o
i = i + 1
Next
EXECUTE = output
End Function
Private Function EvaluatedQueries(o As Object) As Boolean
Dim pep As Variant, val As Variant
Dim evalString As String
Dim result As Boolean
For Each pep In mQueries
'Obtain the property value by its string name
val = CallByName(o, pep(0), VbGet)
'Build the evaluation string.
evalString = ValToString(val) & pep(1)
'Run the evaluation
result = Evaluate(evalString)
'Exit the loop if AND or OR conditions are met.
If mQueries.Count > 1 Then
If (mByAnd And Not result) Or (Not mByAnd And result) Then Exit For
End If
Next
EvaluatedQueries = result
End Function
Private Sub AddQuery(opr As Operator, val As Variant)
Dim pep(1) As Variant
'Create a property/evaluation pair and add to collection,
'eg pep(0): "PropertyA", pep(1): " = 3"
pep(0) = mP.CurrentName
pep(1) = OprToString(opr) & ValToString(val)
mQueries.Add pep
End Sub
Private Function OprToString(opr As Operator) As String
'Convert enum values to string operators
Select Case opr
Case EQUAL_TO
OprToString = " = "
Case GREATER_THAN
OprToString = " > "
Case LESS_THAN
OprToString = " < "
Case GREATER_OR_EQUAL_TO
OprToString = " >= "
Case LESS_OR_EQUAL_TO
OprToString = " <= "
Case NOT_EQUAL_TO
OprToString = " <> "
End Select
End Function
Private Function ValToString(val As Variant) As String
Dim result As String
'Add inverted commas if it's a string.
If VarType(val) = vbString Then
result = """" & val & """"
Else
result = CStr(val)
End If
ValToString = result
End Function

Related

Building a library function for arrays of unspecified element type

Appending an element to the end of an array is slightly involved as it requires first testing if the array is Nothing, then if not, testing if it is empty and finally invoking the ReDim statement with or without Preserve (specifying Preserve in all cases won't hurt). Having to do this in one of my applications hundreds of times for arrays having elements of a variety of types, most of them home-grown Structure types, would make the availability of a library function very desirable. The ArrayAppend method shown below solves the problem for arrays with specific element types, integer in this case, but how could ArrayAppend be phrased for arrays with elements of a general type? I tried to pass the argument " _ar() as Object ", but no luck. Perhaps I have gone overboard with the code I am submitting, but I felt it ought to be complete enough to compile and am prepared to stand corrected.
Option Strict On
Option Explicit On
Public Class CUtils
Public Shared Function ArrayAppend(ByVal _ar() As Integer,
ByVal _element As Integer) As Integer()
'
If Not _ar Is Nothing Then
Dim Len As Integer = _ar.Length
If Len = 0 Then
ReDim _ar(0)
_ar(0) = _element
Else
ReDim Preserve _ar(Len)
_ar(_ar.Length - 1) = _element
End If
Else
ReDim _ar(0)
_ar(0) = _element
End If
Return _ar
'
End Function ' ArrayAppend
'
Public Shared Sub ArrayShow(ByVal _ar() As Integer)
'
If Not _ar Is Nothing Then
For Each item As Integer In _ar
If item = _ar.Last Then
Console.Write(item & vbCrLf)
Else
Console.Write(item & " ,")
End If
Next
Else
Console.WriteLine("Nothing")
End If
'
End Sub ' ArrayShow
'
End Class ' CUtils
Option Strict On
Option Explicit On
Module MyApp
Sub Main()
Dim arInt() As Integer = Nothing
Console.WriteLine("1. Appending to a Nothing array of integers:")
CUtils.ArrayShow(arInt)
arInt = CUtils.ArrayAppend(arInt, 987)
CUtils.ArrayShow(arInt)
Console.WriteLine("2. Appending to an existing non-empty array of integers:")
arInt = {1, 2, 3}
CUtils.ArrayShow(arInt)
arInt = CUtils.ArrayAppend(arInt, 987)
CUtils.ArrayShow(arInt)
Console.WriteLine("3. Appending to an existing empty array of integers:")
arInt = {}
CUtils.ArrayShow(arInt)
arInt = CUtils.ArrayAppend(arInt, 987)
CUtils.ArrayShow(arInt)
End Sub
End Module ' MyApp
This is exactly what generics are for. You have presumably used a generic class like List(Of T) before, where you get to specify what T is at run time. You can also write a generic method without it being in a generic class. In this case, you can do that and then fix the type of the parameters at run time. I would also recommend that you write it as an extension method, so you can then call it on an array instance as though it were an instance method:
Imports System.Runtime.CompilerServices
Public Module ArrayExtensions
<Extension>
Public Sub Append(Of T)(ByRef source As T(), element As T)
'If source is Nothing then new upper bound is zero.
'Otherwise, new upper bound is current Length, i.e. current upper bound + 1.
Dim upperBound = If(source?.Length, 0)
ReDim Preserve source(upperBound)
source(upperBound) = element
End Sub
End Module
Sample usage:
Dim strings As String() = Nothing
Dim integers As Integer() = Nothing
strings.Append("Hello")
integers.Append(123)
Console.WriteLine("strings: " & String.Join(", ", strings))
Console.WriteLine("integers: " & String.Join(", ", integers))
strings.Append("World")
integers.Append(456)
Console.WriteLine("strings: " & String.Join(", ", strings))
Console.WriteLine("integers: " & String.Join(", ", integers))
Output:
strings: Hello
integers: 123
strings: Hello, World
integers: 123, 456
Because that method is generic - the (Of T) is what makes it generic - you can call it with any type you like, as long as the array and the new value you pass are the same type, because they are both declared as type T in the parameters.
The fact that the array parameter is declared ByRef means that any changes you make to that parameter will be reflected in the calling code. That means that the array you called the method on will be replaced with the new array you're creating in the method.
Because this is an extension method, calling it on Nothing will not throw a NullReferenceException. This:
strings.Append("Hello")
is just syntactic sugar and what's really happening is this:
Append(strings, "Hello")

Initializing Object with Arrays VBA

I'm trying to create a class with arrays in it, and I'm having issues creating the class for it...
CLASS:
Private pST(0 To 2) As String
Public Property Get ST() As String
ST() = pST()
End Property
Public Property Let ST(value() As String) '<---- ERROR HERE
pST() = value()
End Property
CODE RUN:
Sub test()
Dim foo As cPurchaseOrder
Set foo = New cPurchaseOrder
foo.ST(0) = "test"
Debug.Print foo.ST(0)
End Sub
THE ERROR:
Compile error:
Definitions of property procedures for the same property are inconsistent, or property procedure has an optional parameter, a ParamArray, or an invalid Set final parameter.
THE QUESTION:
How can I properly initialize a class with arrays as variables?
EDIT: in relation to Mat's Mug response
CLASS CHANGED:
Private pST As Variant
Public Property Get STContent(ByVal index As Long) As String
STContent = pST(index)
End Property
Public Property Let STContent(ByVal index As Long, ByVal value As String)
pST(index) = value
End Property
Private Sub Class_Initialize()
ReDim pST(0 To 2)
End Sub
CODE RUN TO TEST:
Sub test()
Dim foo As cPurchaseOrder
Set foo = New cPurchaseOrder
foo.STContent(0) = "test" '<--- Type mismatch here
Debug.Print foo.STContent(0)
End Sub
Your getter would need to return a String() array for the types to be consistent:
Public Property Get ST() As String()
However I wouldn't recommend exposing an array like this. First because assigning typed arrays is rather painful, second because the setter (Property Let) is actually cheating here:
Public Property Let ST([ByRef] value() As String)
Unless you specify ByVal explicitly, a parameter is always passed ByRef in VBA... except there's this quirk about Property Let - the RHS/value parameter is always passed ByVal at run-time.
And arrays can only ever be passed ByRef.
Therefore, a property that gets (or assigns, actually) a whole array doesn't make much sense.
A better way would be to encapsulate the array (I'd make it a Variant though), and expose its contents (not the array itself) through an indexed property:
Private internal As Variant 'String array
'...
Public Property Get Content(ByVal index As Long) As String
Content = internal(index)
End Property
Public Property Let Content(ByVal index As Long, ByVal value As String)
internal(index) = value
End Property
You have a lot of issues there.
First, your Property Get needs to return a String array. Second, your array needs to be dynamic, or you need to rewrite the whole thing so that you pass an index value to it, otherwise there is no way to indicate which value you are passing to the array. So, for example, using a dynamic array:
Private pST() As String
Public Property Get ST() As String()
ST = pST
End Property
Public Property Let ST(value() As String)
pST() = value()
End Property
and the calling code:
Sub test()
Dim foo As cPurchaseOrder
Set foo = New cPurchaseOrder
Dim asData() As String
ReDim asData(0)
asData(0) = "test"
foo.ST = asData
Debug.Print foo.ST()(0)
End Sub
Unfortunately, I couldn't be sure form the original what the intent was.
It is getting late here but give it a try. In the module:
Option Explicit
Sub Test()
Dim foo As cPurchaseOrder
Set foo = New cPurchaseOrder
foo.AddValueToSt "test", 1
Debug.Print foo.ST(1)
End Sub
In the Class:
Option Explicit
Private pST
Public Property Get ST() As Variant
ST = pST
End Property
Public Property Let ST(value As Variant)
pST = value
End Property
Public Function AddValueToSt(value As Variant, position As Long)
pST(position) = value
End Function
Private Sub Class_Initialize()
ReDim pST(2)
End Sub
This is my way to use the Factory Method Pattern. When I say "my way", for me this pattern is translated to "Whenever some OOP requires more than 5 minutes of thinking simply add a function."

VB.NET: Using an iterator, ObjArray.Select(Function(a) a.Property), to drive a parameterized constructor that creates a different object array

Suppose I have the following class declaration:
Public Class MyObjectR
Private mStr As String
Public Sub New(ByVal _Var1 As String)
mStr = _Var1
End Sub
Public Property MyProperty As String
Get
Return mStr
End Get
Set(value As String)
mStr = value
End Set
End Property
Public Shared Widening Operator CType(ByVal _Initializer As String) As MyObjectR
Return New MyObjectR(_Initializer)
End Operator
End Class
I can initialize an array of these, in this case conveniently using the widening operator to achieve a very readable initialization:
Dim u As MyObjectR() = New MyObjectR() {"a", "b", "c", "d"}
At this point, I have an array of MyObjectR.
Suppose I now add the following class declaration to my scope:
Public Class MyObjectS
Private mStr As String
Public Sub New(ByVal _Var1 As String)
mStr = _Var1
End Sub
End Class
Now, I'd like to create and initialize a new array of MyObjectS with the same count as in my prior array of MyObjectR and where each MyObjectS is initialized by using a specific property of MyObjectR.
As implied in the following line of code (which isn't acceptable syntax for VB.NET):
Dim v As MyObjectS() = New MyObjectS() {u.Select(Function(a) a.MyProperty)}
Here, I'd like to somehow use the iterator to feed successive use of the parameterized New of MyObjectS to create an array of them. Of course, the above line of syntax isn't correct.
(For anyone considering the idea, I need to add that I do NOT want to involve the use of widening operators here [even if possible] because MyObjectS might very well be a class object I don't control and also do not want to modify with methods. For example, it might be a ListViewItem [that does have a parameterized New accepting String.] So it's not necessarily an object of my own that I can tinker around with.)
Clearly, the syntax isn't quite as easy as I'd like to see in VB.NET. But I imagine that somewhere in VB.NET the syntax does exist and I'm simply ignorant about it.
Here's a different option I also tried without success (or expecting success):
Dim v As MyObjectS() = New MyObjectS() {u.Select(Function(a) a.MyProperty).ToArray}
Just for completeness, I know I can easily do it this way:
Dim w As List(Of MyObjectS) = New List(Of MyObjectS)
For Each i As String In u.Select(Function(a) a.MyProperty)
w.Add(New MyObjectS(i))
Next
Dim v As MyObjectS() = w.ToArray
But I'd like to learn any new syntax that may perform a similar operation without the need for creating a List(Of T) to do it. There are some good performance reasons, if no other.
You need just one line of code
Dim v As MyObjectS() = u.Select(Function(a) New MyObjectS(a.MyProperty)).ToArray()
You simply ask the Select iterator to build a new MyObjectS for each element of the input sequence using the MyObjectR.MyProperty as parameter to MyObjectS constructor. At the end you can materialize the resulting sequence with ToArray.

VBA: Arrays and Global Variable Declarations

I need to declare an array in VBA that will be used by every function. However, I cannot declare it as a global as I would do in C++.
My code is as follows:
Option Explicit
Dim test(0 to 10) as String
test(0) = "avds"
test(1) = "fdsafs"
....
The following conceptualizes what I am trying to do.
public function store() as boolean
Worksheets("test").cells(1,1) = test(0)
End Function
How can I achieve this functionality?
For global declaration, change Dim to Public like so:
Public test(0 to 10) as String
You can call this like (assuming it is in Module1, else change Module1 to whatever you've named it):
Module1.test(0) = "something"
Or simply:
test(0) = "something"
Why wouldn't you create everything in a class? That's the reason why classes where invented after all.
Consider the Class1 definition
Option Explicit
Private m_data() As String
Private Sub Class_Initialize()
ReDim m_data(0 To 10)
End Sub
Private Sub Class_Terminate()
Erase m_data
End Sub
Public Property Get Count() As Integer
Count = UBound(m_data) - LBound(m_data) + 1
End Property
Public Property Get Data(index As Integer) As String
Data = m_data(index)
End Property
Public Property Let Data(index As Integer, value As String)
m_data(index) = value
End Property
Public Function Store(rng As Range) As Boolean
Store = (rng.value = m_data(0))
End Function
You can add all the functions you want that can access your array just like Store().
with the test code in a worksheet of
Public Sub Test()
Dim c As New Class1
c.Data(0) = "January"
Debug.Print c.Store(Cells(1, 1))
End Sub
You can also cache the location of the cell where it is referencing, or used an assumed named argument and only supply a reference to the worksheet once after class initialization.
You can use the Public keyword to declare a variable that you need to access in any module.
Remember that in vba you cannot declare variables or code outside of procedures.
See here for more information
I have a recommendation that is a bit lighter than a class (although class is a great recommendation)
Option 1
Define your desired constant array as a delimited string constant:
Public Const cstrTest = "String 1;String 2; String 3; String 4; String 5; String 6"
Next, whenever you need it just use Split to create an array with minimal code:
Dim arrStrings
arrStrings = Split (cstrTest, ";")
Option 2
You might replace (or combine with Option 1) a simple public function
Public Function constStringArray() As String()
constStringArray = Split (cstrTest, ";")
End Function
So then, in use...
Dim arrStrings
'Option 1 example
arrStrings = Split (cstrTest, ";")
'Option 2 example
arrStrings = constStringArray()
one can do it (with global initialization) via Static Property quite straight-forward without creating a class or string parsing - as described in detail and with examples here

VBA - Returning array from Property Get

If arrays are returned by reference, why doesn't the following work:
'Class1 class module
Private v() As Double
Public Property Get Vec() As Double()
Vec = v()
End Property
Private Sub Class_Initialize()
ReDim v(0 To 3)
End Sub
' end class module
Sub Test1()
Dim c As Class1
Set c = New Class1
Debug.Print c.Vec()(1) ' prints 0 as expected
c.Vec()(1) = 5.6
Debug.Print c.Vec()(1) ' still prints 0
End Sub
You don't have a let property. Also, the get property is returning the entire array, rather than just the element in question. Change the return type of Property Get from Double() to just plain Double. Add Property Let. Note that it takes two inputs, but only one is passed to it. The last variable (MyValue, in this case) is assumed to get it's value from whatever is after the = sign. Put a break point somewhere early in Test1() and see how the values are affected in the Locals window. Compare the variables created by the original code versus my code:
'Class1 class module
Private v() As Double
Public Property Get Vec(index As Long) As Double
Vec = v(index)
End Property
Public Property Let Vec(index As Long, MyValue As Double)
v(index) = MyValue
End Property
Private Sub Class_Initialize()
ReDim v(0 To 3)
End Sub
' end class module
'Begin module
Sub Test1()
Dim c As Class1
Set c = New Class1
Debug.Print c.Vec(1) ' prints 0 as expected
c.Vec(1) = 5.6
Debug.Print c.Vec(1) ' prints 5.6
End Sub
'End module
In VBA, arrays are never returned by reference unless they are returned through a ByRef parameter. Furthermore, whenever you use = to assign an array to a variable, you've made a new copy of the array, even if you're assigning it to a ByRef argument inside of a procedure, so you're pretty much out of luck trying to make this work.
Some alternative are...
Use a VBA.Collection instead of an array.
Make your own class that encapsulates an array and exposes procedures for indirectly accessing and manipulating the internal array.
I want to suggest another nice way to do this using a Collection and a static Property without the need to use a class:
imagine you want to have the xlCVError enum as an array (or collection), e.g. to loop through it on errors and handle it based on the actual error.
The following is initialized once on access:
'from https://stackoverflow.com/a/56646199/1915920
Static Property Get XlCVErrorColl() As Collection
Dim c As Collection 'will be already initalized after 1st access
'because of "Static Property" above!
Set XlCVErrorColl = c
If Not c Is Nothing Then Exit Property
'initialize once:
Set c = New Collection
c.Add XlCVError.xlErrDiv0
c.Add XlCVError.xlErrNA
c.Add XlCVError.xlErrName
c.Add XlCVError.xlErrNull
c.Add XlCVError.xlErrNum
c.Add XlCVError.xlErrRef
c.Add XlCVError.xlErrValue
Set XlCVErrorColl = c
End Property
Turning this into an array or implementing it as an array is straight forward, but collections seem to be more useful to me, with the disadvantage that their elements are not implicitely typed/(compile-time-)type checked.
So this would e.g. turn it into an (read-only) array (with the in-mem-copy-disadvantage mentioned in other answers/comments):
'from https://stackoverflow.com/a/56646199/1915920
Static Property Get XlCVErrorArr() As XlCVError()
Dim a() As XlCVError
XlCVErrorArr = a
If UBound( a ) > 0 Then Exit Property
'initialize once:
Dim c As Collection: Set c = XlCVErrorColl
ReDim a(c.Count)
Dim i As Integer: For i = 1 To c.Count
a(i) = c(i)
Next i
XlCVErrorArr = a
End Function
So transforming the example from Clayton Ss answer into a static, modifiable module property using some array it would be:
'module (no class required)
'from https://stackoverflow.com/a/56646199/1915920
Private v() As Double
Static Property Get Vec(index As Long) As Double
If UBound(v) < 3 Then 'initialize once:
ReDim v(0 To 3) 'one could initialize it with anyting after here too
end if
Vec = v(index)
End Property
Public Property Let Vec(index As Long, MyValue As Double)
v(index) = MyValue
End Property

Resources