VBA Class with Static Methods - vba

VBA allows you to define class methods as static. Despite a lot of experimentation, I cannot see how these differ from regular class methods. I suspect there is no difference and that the engine ignores the static keyword in this context.
Here is a sample class that will compile (tested Excel 2010):
' Class1.
' 4 methods, 2 static & 2 not.
Public Sub Method1()
Debug.Print "foo"
End Sub
Public Static Sub Method2()
Debug.Print "bar"
End Sub
Public Function Method3() As String
Method3 = "foo"
End Function
Public Static Function Method4() As String
Method4 = "bar"
End Function
Tested like so:
Sub TestClass1()
Dim x As Class1
Set x = New Class1
x.Method1 ' prints foo.
x.Method2 ' prints bar.
x.Method3 ' returns foo.
x.Method4 ' returns bar.
End Sub
Some observations:
static methods are recalculated on each call.
static methods cannot be called without first instancing the class.
Am I missing something?

Related

How to hold a reference to a field in VB?

In VB, I have a class that does some standard validations. What I'd LIKE to do is to declare some variables, then create instances of a validator class that include pointers to the variables, and then at some later time execute the validators to test the values in the fields that are pointed to.
Something like this:
public class MyData
public property foo as string
public property bar as string
dim vfoo as validator
dim vbar as validator
public sub new()
vfoo=new validator(&foo) ' i.e. & operator like in C
vbar=new validator(&bar)
end sub
public sub validate()
vfoo.validate
vbar.validate
end sub
end class
public class validator
dim _field as string* ' i.e. * like in C
public sub new(field as string*)
_field=field
end sub
public sub validate
if string.isnullorempty(_field) then
throw SomeException
else if not SomeOtherTest(_field) then
throw SomeOtherException
end sub
The catch is that, to the best of my knowledge, there is nothing like C pointers in VB. Is there any reasonably easy way to do this?
At present I am passing in the field values at the time I call the validate() function, but this is not ideal because I would like to be able to create a List of validators specific to a given caller, and then loop through the List. But at the time I loop, how would I know which value from MyClass to pass in, unless I had a giant select statement keying off some "field code"? (And of course in real life, there are not just two fields like in this example, there are quite a few.)
Am I just having a brain freeze and there's an easy way to do this? Or can this not be done in VB because there are no such thing as pointers?
Like Java, VB doesn't make direct use of pointers (it compensates where it can with library/framework calls). In the context of a garbage-collected language, I can't imagine that this style of validation would work out well.
But for fun, maybe a lambda-based solution could suit?:
Public Class MyData
Public Property foo As String
Public Property bar As String
Dim vfoo As validator
Dim vbar As validator
Public Sub New()
vfoo = New validator(Function() foo)
vbar = New validator(Function() bar)
End Sub
Public Sub validate()
vfoo.validate()
vbar.validate()
End Sub
End Class
Public Class validator
ReadOnly _fieldFunc As Func(Of String)
Public Sub New(fieldFunc As Func(Of String))
_fieldFunc = fieldFunc
End Sub
Public Sub validate()
Dim _field = _fieldFunc()
If String.IsNullOrEmpty(_field) Then
Throw New Exception("NullOrEmpty")
ElseIf Not SomeOtherTest(_field) Then
Throw New Exception("SomeOtherTest")
End If
End Sub
Public Function SomeOtherTest(f As String) As Boolean
Return True
End Function
End Class

Can I make methods visible to some classes but not others

Quite simple; I've made a class with a method that's public since another class calls this method.
I would like the method only to show up on intellisense in the 2nd class which holds some special reference to the 1st one. Any other class or module should not be able to see the method.
Something along the lines of
Semi-Private (except for Class2) Sub ...
in the method of Class1, or in Class2
Can See Semi-Private methods of Class1
Instead of getting complicated with it, and using a roundabout approach, just use the tools meant for the job:
Class Module IFoo
Public Sub Bar()
' Interface methods are empty
End Sub
Class Module Foo
Private Type TFoo
Baz As String
End Type
Private this As TFoo
Implements IFoo
Private Sub Class_Initialize()
this.Baz = "I am a class that implements IFoo."
End Sub
Private Sub IFoo_Bar()
' Do Something
Debug.Print this.Baz
End Sub
Module Baz
Sub RunBaz()
Dim MyFoo As IFoo
' Note that this WILL NOT work. Nothing happens.
Set MyFoo = New IFoo
Debug.Print MyFoo.Bar
' Set MyFoo to be equal to a Foo (which implements IFoo)
Set MyFoo = New Foo
Debug.Print MyFoo.Baz
End Sub
This makes the methods only visible when the methods are being accessed through an interface which makes them public. Therefore, in order to use the methods in Foo we must first create an instance of Foo using an IFoo variable type.
Then, it is as simple as creating a new class which creates a Foo from an IFoo for its own use.
Class Module IImportantWorker
Public Sub DoSomethingImportant()
End Sub
Class Module ImportantWorker
Private Type TImportantWorker
Implementation As IFoo
End Type
Private this As TImportantWorker
Implements IImportantWorker
Private Sub Class_Initialize()
Set this.Implementation = New Foo
End Sub
Public Sub IImportantWorker_DoSomethingImportant()
this.Implementation.Bar
End Sub
You could get fancy from here and make a property of Foo that is exposed by IFoo that tells it whether or not it can work. This would have to be exposed through the IFoo interface (or, alternatively a separate interface so that the two interfaces must be used in conjunction).
Without locking the class (which I wouldnt recommend anyways, it seems foolish and pointless) Foo will still allow Bar if it is created as a IFoo. But if you just make Foo = New Foo then Foo will do nothing (or rather, expose nothing).
For additional resources, I highly recommend reading these excellent posts that go into greater depth about the processes:
Is VBA an OOP language, and does it support polymorphism?
https://rubberduckvba.wordpress.com/2016/06/16/oop-vba-pt-1-debunking-stuff/
https://rubberduckvba.wordpress.com/2016/07/05/oop-vba-pt-2-factories-and-cheap-hotels/
You may create the class that contains all properties and methods, name it BigClass:
Option Explicit
Dim i1 As Integer
Dim i2 As Integer
Dim i3 As Integer
Property Let var1(v As Integer)
i1 = v
End Property
Property Get var1() As Integer
var1 = i1
End Property
Property Let var2(v As Integer)
i2 = v
End Property
Property Get var2() As Integer
var2 = i2
End Property
Property Let var3(v As Integer)
i3 = v
End Property
Property Get var3() As Integer
var3 = i3
End Property
Then you can make classes that inherits BigClass methods, but not necessary all of them. For example, SmallClass1 that will use only var3 variable.
Option Explicit
Dim BigOne As BigClass
Private Sub Class_Initialize()
Set BigOne = New BigClass
End Sub
Private Sub Class_Terminate()
Set BigOne = Nothing
End Sub
Property Let var3(v As Integer)
BigOne.var3 = v
End Property
Property Get var3() As Integer
var3 = BigOne.var3
End Property
Then you can create SmallClass2 that use var2 and var1 but not var3. You get the point.
An clear example would be involved and lengthy and I have deposited one on my blog here VBA - Difference between Friend and Public with inter project references.
Essentially you use the Friend keyword but nothing happens until you split your code across workbooks which itself brings issues such as Instancing and factory functions. Example given should work though. Enjoy!

Expression in With statement becomes Nothing in lambda expression in Task

I have discovered that referencing a member variable in a lambda expression executed in a Task throws a NullReferenceException when accessing it using the With statement.
For example I expect the following code to print two lines on the console. The first one accesses the SomeString member via obj.SomeString while the second one uses the With statement and accesses the member via .SomeString. I expected both options to be equivalent but the second one throws an exception.
Class Foo
Public SomeString As String
End Class
Module Module1
Sub Main()
Dim obj As New Foo With {.SomeString = "Hello World"}
With obj
Task.Factory.StartNew(
Sub()
Console.WriteLine("1:" + obj.SomeString) ' works
Console.WriteLine("2:" + .SomeString) ' NullReferenceException here
End Sub)
End With
Console.ReadKey()
End Sub
End Module
When I move the Console.ReadKey() statement into the With statement, the code works.
I fixed the actual code by not using the With statement but I still don't know what concept I'm missing here. Why can I access members of the obj variable in the lambda expression but not the members of the With expression? It has not been garbage collected because I can still see it in the debugger when the exception is thrown. The expression seems to go out of scope (or something like that) but why doesn't the compiler just do what I expect and treats it the same as obj?
It is because of the voodoo that the VB compiler does to support the With block and lambda expressions. If you look at your code through a decompiler like Redgate's Reflector, your code gets converted into something like the code below except that I renamed the variables to ones supported by VB; they can be quite long and include characters that are invalid for VB variable names
<STAThread> _
Public Shared Sub Main()
Dim var1 As New GeneratedClass1
Dim foo As New Foo With {.SomeString = "Hello World"}
var1.objVar = foo
Dim var2 As New GeneratedClass1.GeneratedClass2 With {.var2 = var1, .theWithVariable = var1.objVar}
Task.Factory.StartNew(New Action(AddressOf var2._Lambda___1))
var2.theWithVariable = Nothing
Console.ReadKey()
End Sub
<CompilerGenerated> _
Friend Class GeneratedClass1
' Methods
<DebuggerNonUserCode> _
Public Sub New()
End Sub
<DebuggerNonUserCode> _
Public Sub New(ByVal other As GeneratedClass1)
If (Not other Is Nothing) Then
Me.objVar = other.objVar
End If
End Sub
' Fields
Public objVar As Foo
' Nested Types
<CompilerGenerated> _
Friend Class GeneratedClass2
' Methods
<DebuggerNonUserCode> _
Public Sub New()
End Sub
<DebuggerNonUserCode> _
Public Sub New(ByVal other As GeneratedClass2)
If (Not other Is Nothing) Then
Me.theWithVariable = other.theWithVariable
End If
End Sub
<CompilerGenerated> _
Public Sub _Lambda___1()
Console.WriteLine(("1:" & Me.var2.objVar.SomeString))
Console.WriteLine(("2:" & Me.theWithVariable.SomeString))
End Sub
' Fields
Public theWithVariable As Foo
Public var2 As GeneratedClass1
End Class
End Class
You can see that the compiler creates a class that holds a reference to the With variable and the method of the lambda expression. As soon as the With variable is out of scope it is set to Nothing and hence the null reference expression when task executes.

VBA: evaluation order

when VBA executes this line:
GetClass1().Test(GetParam())
the GetParam function is evaluated before the GetClass1() call.
What is a good way to change this behaviour?
the only thing I came up with is this workaround:
With GetClass1
.Test(GetParam())
End With
here's the full example code, so that you can easily test it:
Class1
Option Explicit
Public Function Test(ByVal sText As String) As String
Debug.Print "Class1.Text: " & sText
Test = "Class1.Text: " & sText
End Function
Module1
Option Explicit
Private Function GetClass1() As Class1
Set GetClass1 = New Class1
Debug.Print "GetClass1()"
End Function
Private Function GetParam() As String
GetParam = "Param"
Debug.Print "GetParam()"
End Function
Private Sub Test()
Debug.Print "Test=" + GetClass1().Test(GetParam())
With GetClass1
Debug.Print "TestWith=" + .Test(GetParam())
End With
End Sub
Output when you run Test()
GetParam()
GetClass1()
Class1.Text: Param
Test=Class1.Text: Param
GetClass1()
GetParam()
Class1.Text: Param
TestWith=Class1.Text: Param
The evaluation order here is ok i think. The calling order of nested functions is from the inner most one to the outer most one which can't be done differently because outer most function needs to know its arguments and this arguments are evalueted only after the inner function was executed.
In your code (the first way) the object of type Class1 is created after the function GetParam() was called and this is because the object is created at the moment when function GetClass1() is called. In the second way with With GetClass1 the object is created immedialtelly after With and the call stack looks differently indeed.
What you can do is to create another class say 'Wrap' and this class will be responsible for creation of instance of type Class1.
E.g. like this:
' Class module Wrap
Private m_class1 As Class1
Public Function GetClass1() As Class1
Set GetClass1 = m_class1
Debug.Print "GetClass1()"
End Function
Private Sub Class_Initialize()
Set m_class1 = New Class1
End Sub
' Module code
Private Function GetParam() As String
GetParam = "Param"
Debug.Print "GetParam()"
End Function
Private Sub Test()
Dim wp As Wrap
Set wp = New Wrap
Debug.Print "Test=" + wp.GetClass1().Test(GetParam())
Debug.Print "---------------------------------------"
With New Wrap
Debug.Print "TestWith=" + .GetClass1.Test(GetParam())
End With
End Sub
Here the instance of class Class1 is created exactly at the moment you call New for class Wrap. So Set wp = New Wrap executes and creates the instance and the same way works With New Wrap, it executes and creates the instance as well.
But do not do it like this:
Dim wp As New Wrap
... then you will have the same behaviour like you had when the function GetClass1() was part of the Module1, which is: 'the instance is not created until it is needed' so you do not have the control of the moment of creation. HTH

What happens if a base method calls an overridden method in VB?

Consider the following example:
Public Class ParentClass
Public Sub GenerateReport
Dim Col As Collection
Col = GetItemCollection()
End Sub
Public Overridable Function GetItemCollection() As Collection
GetItemCollection = New Collection
GetItemCollection.Add("1")
GetItemCollection.Add("2")
GetItemCollection.Add("3")
End Function
End Class
Public Class ExtendedClass
Inherits ParentClass
Public Overrides Function GetItemCollection() As Collection
GetItemCollection = New Collection
GetItemCollection.Add("A")
GetItemCollection.Add("B")
GetItemCollection.Add("C")
End Function
End Class
Public Sub Main()
Dim cls As New ExtendedClass
cls.GenerateReport()
End Sub
When Main() calls cls.GenerateReport(), is the variable Col going to be a collection of numbers or letters? I'm hoping that it will recognize that cls is an instance of ExtendedClass and call the overridden method and return the letters.
It will be a collection of letters as you did override the method. However, where did you declare the GetItemCollection? You still need an instance variable.