Expression in With statement becomes Nothing in lambda expression in Task - vb.net

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.

Related

Access VBA tries to terminate return value of Function

I'm building an Access database using some VBA code.
I've made a very simple class and a function which creates an instance of that class. Executing this function yields an error 91: object variable or With block variable not set. After debugging I found out Access tries to terminate the newly created object, ending the terminate function results in the error.
My intention was to create a factory for MyObject in order to create an object and initialize it with given arguments. I've stripped all this away to find the problem and now I'm dumbfounded. Can someone point out what I'm doing wrong because I have no idea anymore.
The class I made, called MyObject:
Private Sub Class_Initialize()
'does literally nothing
End Sub
Private Sub Class_Terminate()
'does literally nothing
End Sub
The function I made to initialize the class, located in another module:
Public Function createMyObject(someArg As String) As MyObject
Set createMyObject = New MyObject
End Function
I obviously expected the function createMyObject to return an instance of MyObject, but it gives me an error 91. Debugging led me to the Class_Terminate Sub in MyObject where the error gets thrown at the "end sub" line.
Try with using a property:
Option Compare Database
Option Explicit
Private mTest As Variant
Private Sub Class_Initialize()
'does literally nothing
End Sub
Private Sub Class_Terminate()
'does literally nothing
End Sub
Public Property Get Test() As Variant
Test = mTest
End Property
Public Property Let Test(ByVal NewValue As Variant)
mTest = NewValue
End Property
and then:
Public Function CreateMyObject(SomeArg As String) As MyObject
Set CreateMyObject = New MyObject
CreateMyObject.Test = SomeArg
End Function
which you can call like this:
Set o = CreateMyObject("Joe")
Debug.Print o.Test

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

VBA Class with Static Methods

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?

Error. .. Main not accessible by program

I wrote a module and into which a Public Sub Main method. But, when I run the program. It gives " No accessible 'Main' method with an appropriate signature was found in 'abc'."
Could you please suggest possible solutions to the error.
Public Sub Main(ByVal cmdArgs As String)
Dim returnValue As Integer
If cmdArgs.Length > 0 Then
returnValue = Import_Start(cmdArgs, "f9880")
Console.WriteLine("Import end with an error " & returnValue)
Else
Console.WriteLine("parameter failure")
End If
End Sub
End Module
If you want to start your app from a Sub Main, the correct signature is:
Public Sub Main(args As String())
' or
Public Sub Main()
The command line args will be passed as a string array. Yours just declares it as String resulting in the compiler error. You also need to set the StartUp object to Sub Main in Project Properties, but that already seems to have been done.
If you do not want/need to use a module you can add it to a form (it is not clear if this is even a WinForms app) using:
Public Shared Sub Main(args As String())
' or
Public Shared Sub Main()

Calling Subroutines from lambda in vb.net

I find myself calling functions from lambdas frequently as the provided delegate does not match or does not have sufficient parameters. It is irritating that I cannot do lambda on subroutines. Each time I want to do this I have to wrap my subroutine in a function which returns nothing. Not pretty, but it works.
Is there another way of doing this that makes this smoother/prettier?
I have read that this whole lambda inadequacy will probably be fixed in VS2010/VB10 so my question is more out of curiosity.
A simple Example:
Public Class ProcessingClass
Public Delegate Sub ProcessData(ByVal index As Integer)
Public Function ProcessList(ByVal processData As ProcessData)
' for each in some list processData(index) or whatever'
End Function
End Class
Public Class Main
Private Sub ProcessingSub(ByVal index As Integer, _
ByRef result As Integer)
' (...) My custom processing '
End Sub
Private Function ProcessingFunction(ByVal index As Integer, _
ByRef result As Integer) As Object
ProcessingSub(index, result)
Return Nothing
End Function
Public Sub Main()
Dim processingClass As New ProcessingClass
Dim result As Integer
' The following throws a compiler error as '
' ProcessingSub does not produce a value'
processingClass.ProcessList( _
Function(index As Integer) ProcessingSub(index, result))
' The following is the workaround that'
' I find myself using too frequently.'
processingClass.ProcessList( _
Function(index As Integer) ProcessingFunction(index, result))
End Sub
End Class
If you find that you are doing it too often and generally with the same type of data, you can wrap the delegate in a class.
Create a base class that converts to the delegate:
Public MustInherit Class ProcessDataBase
Public Shared Widening Operator CType(operand As ProcessDataBase) as ProcessingClass.ProcessData
Return AddressOf operand.Process
End Sub
Protected MustOverride Sub Process(index As Integer)
End Class
Inherit from the class:
Public Class ProcessResult
Inherits ProcessDataBase
Public Result As Integer
Protected Overrides Sub Process(index as Integer)
' Your processing, result is modified.
End SUb
End Class
Use it:
Public Class Main()
Public Sub Main()
Dim processingClass As New ProcessingClass
Dim processor As New ProcessResult
processingClass.ProcessList(processor)
Dim result as integer=processor.Result
End Sub
End Class
It IS fixed in VB10, the VS10 Beta is available, if it's an option for you to use it. In VB10 you have lambdas without a return value, and inline subs/functions.
For now, maybe you could just forget lambdas and work with delegates instead? Something like:
processingClass.ProcessList(AddressOf ProcessingSub)