this is the code that I got at the end with the help of Latch, this would be an easy example of a subrutine that changes values in global collection in a recursive way, and now is completly funtional
Public List_of_data As New Collection
Sub main()
ClearCollection List_of_data
List_of_data.Add 7.5
List_of_data.Add 1
Recursive_function List_of_data
End Sub
Public Sub ClearCollection(parmCol As Collection)
Set parmCol = New Collection
End Sub
Public Function Recursive_function(ByRef List_of_data As Collection)
Dim x1
x1 = List_of_data.Item(1)
Dim x2
x2 = List_of_data.Item(2)
If x1 > x2 Then
ClearCollection List_of_data
List_of_data.Add x1
List_of_data.Add x2 + 1
Call Recursive_function(List_of_data)
End If
End Function
You are passing an argument to a function that takes none. Changing your function to:
Public Function Calculo_de_Dientes_Epicicloidales(byref lista_de_datos)
will yeild the result you are looking for.
Also, instead of looping to empty your collection, you could simply do:
Public Sub ClearCollection(parmCol As Collection)
set parmCol = new collection
End Sub
You cannot "call" a function. A function provides a result; you need to set some variable equal to your function. If you just need the code to run, and don't need it to provide you with a specific variable result, then make it a sub, and not a function.
Also, do you have "End Function" below the last line you have copied for us?
Related
Often I have to write structures where logic is performed on each member variable for a class. For example, in this code I am finding anything that has changed between the object and another object, then returning the changes:
Public Class Foo
Public B1 As Boolean?
Public B2 As Boolean?
...
Public B1000 As Boolean?
Function GetChanges(F as Foo) As Foo
Dim Changes As New Foo()
If Not B1.Equals(F.B1) Then
B1 = F.B1
Changes.B1 = F.B1
End If
If Not B2.Equals(F.B2) Then
B2 = F.B2
Changes.B2 = F.B2
End If
...
If Not B1000.Equals(F.B1000) Then
B1000 = F.B100
Changes.B1000 = F.B1000
End If
Return Changes
End Function
End Class
As you can see, the same "If Not X.Equals(F.X) ..." has to be copy pasted many many times.
Here is a second example where I am taking the changes calculated before, and using them to update the value of the Foo object.
Sub Update(Changes as Foo)
If Changes.B1.HasValue Then
B1 = Changes.B1
End If
If Changes.B2.HasValue Then
B2 = Changes.B2
End If
...
If Changes.B1000.HasValue Then
B1000 = Changes.B1000
End If
End Sub
This kind of structure isn't something I'm happy with. It's fragile, it's ugly, it's repetitive. Is there something I can do about it?
One option is to pass the 3 members by ref, but the problem is you still have these 3 calls to the same variable, which is still repeditive:
Public Shared Sub CheckChange(Of T)(ByRef OldV As T, NewV As T, ByRef Change As T)
If Not OldV.Equals(NewV) Then
OldV = NewV
Change = NewV
End If
End Sub
Function GetChanges(F as Foo) As Foo
Dim Changes As New Foo()
CheckChange(B1, F.B1, Changes.B1)
CheckChange(B2, F.B2, Changes.B2)
...
CheckChange(B1000, F.B1000, Changes.B1000)
return Changes
End Function
It would be great if I could do something like:
Sub CheckChanges(F as Foo, ByRef Changes as Foo, Member as ???)
If Not Member.Equals(F.Member) Then
Member = F.Member
Changes.Member = F.Member
End If
End Sub
Function GetChanges(F as Foo) As Foo
Dim Changes As New Foo()
CheckChange(F, Changes, B1)
CheckChange(F, Changes, B2)
...
CheckChange(F, Changes, B1000)
return Changes
End Function
I'm not even sure what that kind of feature would be called. Relative member referencing? Anyway, is there any kind of feature (this, or otherwise) that would make the code more robust when having to run the same logic on multiple members?
On the subject of the question, code in situations like this can often be simplified by using a bit of Reflection. I am yet to test this but I think that this method should work:
Private Sub UpdateIfNotEqual(source As Foo, changes As Foo, fieldName As String)
Dim fooType = GetType(Foo)
Dim field = fooType.GetField(fieldName)
Dim currentValue = field.GetValue(Me)
Dim sourceValue = field.GetValue(source)
If Not currentValue.Equals(sourceValue) Then
field.SetValue(Me, sourceValue)
field.SetValue(changes, sourceValue)
End If
End Sub
You'd call it like so:
UpdateIfNotEqual(F, Changes, NameOf(B1))
UpdateIfNotEqual(F, Changes, NameOf(B2))
'...
UpdateIfNotEqual(F, Changes, NameOf(B1000))
If the fields really were numbered then you could even use a loop:
For i = 1 To 1000
UpdateIfNotEqual(F, Changes, $"B{i}")
Next
The drawback is that Reflection is very slow. It may not be a big deal for this much data but only testing would tell you for sure.
Reflection
Class Foo
Public B1 As Boolean
Public B2 As Boolean
Public B3 As Boolean
Public C1 As Integer = Rnd() * 100
Function GetChanges(F As Foo) As Foo
Dim Changes As New Foo()
For Each field As System.Reflection.FieldInfo In F.GetType.GetFields.Where(Function(x) x.Name.StartsWith("B"))
If field.GetValue(F) <> Me.GetType.GetField(field.Name).GetValue(F) Then
Changes.GetType.GetField(field.Name).SetValue(Changes, field.GetValue(F))
Else
'????????
End If
Next
Return Changes
End Function
End Class
PropertyDescriptor
Imports System.ComponentModel
Class Foo
Property B1 As Integer
Property B2 As Boolean
Property Text As String
Function GetChanges(F As Foo) As Foo
Dim Changes As New Foo()
Dim pdc As PropertyDescriptorCollection = TypeDescriptor.GetProperties(F)
For Each pd As PropertyDescriptor In pdc
If pd.IsReadOnly = False Then
If pd.IsBrowsable = True Then
'add more conditions if needed
Changes.GetType.GetProperty(pd.Name).SetValue(Changes, pd.GetValue(F), Nothing)
End If
End If
Next
Return Changes
End Function
End Class
In a VBA class module (let's say in Excel or Access), I wrote a function SomeFunction() returning a value.
If I call this from another function/sub in the class, should I call it:
a) this way: myVar = SomeFunction or
b) this way: myVar = Me.SomeFunction ?
I think both work, so except for the writing style and clarifying SomeFunction is part of the class, does it make any difference?
Both are indeed valid, but way B should be preferred since it's more explicit what you're doing.
Consider the following (valid) class code:
Public Property Get SomeValue() As Integer
SomeValue = 5
End Property
Public Property Get AnotherValue() As Integer
Dim SomeValue As Integer
SomeValue = 3
AnotherValue = SomeValue 'Returns 3
Debug.Print Me.SomeValue 'Returns 5
End Property
Because you can (but shouldn't) do this in VBA, it's a good practice to use Me. to make it clear you're using a class property and not a variable.
As far as I know - It does not make any difference.
However, if you use Me. in the class, you can use the Intellisense to see the available subs, functions and properties, which could be a bit handy:
However, I prefer not to use the Me.
If you are having the following in a module:
Public Function Foo()
Foo = 5
End Function
Sub TestMe()
Dim cls As New Klasse1
cls.TestMe
End Sub
And then the following in Klasse1:
Sub TestMe()
Debug.Print Modul1.Foo
Debug.Print Me.Foo
Debug.Print Foo
End Sub
Function Foo()
Foo = 10
End Function
it is visible that the existense of Me. is just syntax sugar.
(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
Please take a look at sample1 below:
Public Class LocalVariable
Public Sub Run()
Dim TestVariable As Integer
TestVariable = Method1(TestVariable)
TestVariable = Method2(TestVariable)
TestVariable = Method3(TestVariable)
End Sub
Private Function Method1(ByVal x As Integer) As Integer
Return x + 1
End Function
Private Function Method2(ByVal x As Integer) As Integer
Return x + 2
End Function
Private Function Method3(ByVal x As Integer) As Integer
Return x + 3
End Function
End Class
and sample 2 below:
Public Class InstanceVariable
Dim TestVariable As Integer
Public Sub Run()
Method1()
Method2()
Method3()
End Sub
Private Sub Method1()
TestVariable = TestVariable + 1
End Sub
Private Sub Method2()
TestVariable = TestVariable + 2
End Sub
Private Sub Method3()
TestVariable = TestVariable + 3
End Sub
End Class
The outcome is obviously the same after each program runs i.e. TestVariable=6. Every example I find online and at work uses sample 1. Surely this is a misuse of instance variable as TestVariable should be shared across functions? Therefore an instance variable should be used.
The two samples don't mean the same thing.
The difference is what happens if you call Run() more than once over the life of the program. The Run() method in sample 2 never resets TestVariable, so it will continue to get larger and larger. In sample 1, the result will always be 6 because TestVariable is a new variable with each call to the function. Which is more correct depends entirely on what you're trying to do.
There is a third option
All else being equal, I also recommend the sample 1 approach from those two options. However, instance vs local variable is not the distinction. There's no reason sample 1 couldn't also use an instance variable with those method definitions. So our third option would look like this:
Public Class InstanceVariableWithSampleOneFunctions
Dim TestVariable As Integer
Public Sub Run()
TestVariable = Method1(TestVariable)
TestVariable = Method2(TestVariable)
TestVariable = Method3(TestVariable)
End Sub
Private Function Method1(ByVal x As Integer) As Integer
Return x + 1
End Function
Private Function Method2(ByVal x As Integer) As Integer
Return x + 2
End Function
Private Function Method3(ByVal x As Integer) As Integer
Return x + 3
End Function
End Class
This uses the instance variable from sample 2 with the methods from sample 1. I'll call it sample 3.
This cuts better to the heart of your question, because now sample 3 has the same behavior as sample 2. Whether you should choose 1 or 2 depends on which behavior you need. But whether you should choose 2 or 3 depends on the merits of the coding style. Both 2 and 3 rely on an instance variable in the Run() method, but 2 also uses an instance variable in the additional methods, while 3 uses a local variable.
I can say that at this point, comparing 2 and 3, I definitely prefer sample 3. The methods from sample 3 have more of a functional style: accept an input, return an output. This gives them a higher level of abstraction, which makes it easier to refactor sample 3 to do things like move those methods elsewhere... say, to a utility class where one set of methods can be shared with both samples 1 and 3. Since you mentioned threading, typically this style makes it easier, not harder, to do multi-threading correctly.
One concrete example how this method style is better is that it's composable. This attribute allows me to re-write sample 3's Run() method like this and be confident of getting the same results:
Public Sub Run()
TestVariable = Method3(Method2(Method1(TestVariable)))
End Sub
How do I get a public array whose values are set within a subroutine and do not get cleared at the end of the sub in which they were set?
I tried to get:
Public GlobalArray() as Variant
Sub One()
ReDim GlobalArray(0 to 2)
GlobalArray=Array("0","1","2")
End Sub
Sub Two()
Check = GlobalArray(2)
End Sub
such that Check = 2, but I get thrown an error in sub Two complaining about a lack of values in GlobalArray (in fact, even sub One complains that there is no GlobalArray to put things in).
Basically, I have a procedure (One) pulling data from disparate sources, doing some stuff with it, letting the user do some things in Excel, and then running a new subroutine (Two) that uses both the user's input and some of the arrays from sub One.
The Public GlobalArray() variable must be declared in a module. It will not work if it is declared at the top of either a Worksheet or the ThisWorkbook module. Try:
'// Must be declared in a module
Public GlobalArray() As Integer
'// These routines can be in worksheet/thisworkbook modules along side events etc Or in a module
Sub One()
ReDim GlobalArray(0 To 2)
GlobalArray(0) = 0
GlobalArray(1) = 1
GlobalArray(2) = 2
End Sub
Sub Two()
check = GlobalArray(2)
MsgBox (check)
End Sub
Instead of a public variable you could pass it to the second function:
Sub One()
Dim GlobalArray(0 To 2) As Integer
GlobalArray(0) = 0
GlobalArray(1) = 1
GlobalArray(2) = 2
Two GlobalArrayToMe:=GlobalArray
End Sub
Sub Two(ByRef GlobalArrayToMe() As Integer)
check = GlobalArrayToMe(2)
MsgBox (check)
End Sub
This is not VBA and won't compile: GlobalArray=("0","1","2")
You could instead use: GlobalArray = Array("0", "1", "2")
but that requires declaring Public GlobalArray() As Variant
Otherwise assign the array elements one by one as in #Readfidy's answer.