Lambda doesn't close over object in With statement [duplicate] - vb.net

Just thought I'd share this in case anyone else has run into this.
I did something similar today and it took me a while to figure out why this was causing a problem at runtime.
This code:
Public Class foo
Public bar As String = "blah"
End Class
Public Sub DoInline()
Dim o As New foo
Dim f As Func(Of String)
With o
f = Function() .bar
End With
Try
Console.WriteLine(f.DynamicInvoke())
Catch ex As Reflection.TargetInvocationException
Console.WriteLine(ex.InnerException.ToString)
End Try
End Sub
Throws a NullReferenceException. It seems as though the With is using the closure as its temp storage, and at the "End With", it sets the closure's variable to Nothing.
Here is that code in RedGate Reflector:
Public Shared Sub DoInline()
Dim o As New foo
Dim $VB$Closure_ClosureVariable_7A_6 As New _Closure$__1
$VB$Closure_ClosureVariable_7A_6.$VB$Local_VB$t_ref$L0 = o
Dim f As Func(Of String) = New Func(Of String)(AddressOf $VB$Closure_ClosureVariable_7A_6._Lambda$__1)
$VB$Closure_ClosureVariable_7A_6.$VB$Local_VB$t_ref$L0 = Nothing
Try
Console.WriteLine(RuntimeHelpers.GetObjectValue(f.DynamicInvoke(New Object(0 - 1) {})))
Catch exception1 As TargetInvocationException
ProjectData.SetProjectError(exception1)
Console.WriteLine(exception1.InnerException.ToString)
ProjectData.ClearProjectError
End Try
End Sub
Notice the
$VB$Closure_ClosureVariable_7A_6.$VB$Local_VB$t_ref$L0 = Nothing
Only "question" I can really ask is; is this a bug or a strange design decision that for some reason I'm not seeing.
I'm pretty much just going to avoid using "With" from now on.

This behavior is "By Design" and results from an often misunderstood detail of the With statement.
The With statement actually takes an expression as an argument and not a direct reference (even though it's one of the most common use cases). Section 10.3 of the language spec guarantees that the expression passed into a With block is evaluated only once and is available for the execution of the With statement.
This is implemented by using a temporary. So when executing a .Member expressio inside a With statement you are not accessing the original value but a temporary which points to the original value. It allows for other fun scenarios such as the following.
Dim o as New Foo
o.bar = "some value"
With o
o = Nothing
Console.WriteLine(.bar) ' Prints "some value"
End With
This works because inside the With statement you are not operating on o but rather a temporary pointing to the original expression. This temporary is only guaranteed to be alive for the lifetime of the With statement and is hence Nothingd out at the end.
In your sample the closure correctly captures the temporary value. Hence when it's executed after the With statement completes the temporary is Nothing and the code fails appropriately.

There's really only one bug that I see, the compiler should generate an error for this. Shouldn't be hard to implement. You can report it at connect.microsoft.com

Related

I am using a method with a stored procedure, but it's always returning false

I am using bool method with Visual Studio 2015 and SQL Server 2005.
When I am passing correct details and click loginButton, the code always returns false from the stored procedure.
This is my stored procedure in SQL Server 2005:
ALTER PROCEDURE [dbo].[UserCheckLoginDetails]
(#IsLoginIdCorrect BIT OUTPUT,
#IsPasswordCorrect BIT OUTPUT,
#LoginID NVARCHAR(200),
#Password NVARCHAR(20)
)
AS
BEGIN
SET #IsLoginIdCorrect = 0
SET #IsPasswordCorrect = 0
IF EXISTS (SELECT * FROM UserInfo
WHERE loginid = #LoginID AND password = #Password)
BEGIN
SET #IsLoginIdCorrect = 1
SET #IsPasswordCorrect = 1
END
ELSE
IF EXISTS (SELECT * FROM UserInfo WHERE loginid = #LoginID)
BEGIN
SET #IsLoginIdCorrect = 1
END
END
This is my method returning True or False:
Private Sub GetIsUserLoginCorrect(IsLoginIdCorrect As Boolean, IsPasswordCorrect As Boolean)
Using Conn As New SqlConnection(_SqlCon)
Using cmd As New SqlCommand("UserCheckLoginDetails", Conn)
cmd.CommandType = CommandType.StoredProcedure
Conn.Open()
'OutPut Parameters
cmd.Parameters.Add("#IsLoginIdCorrect", SqlDbType.Bit).Direction = ParameterDirection.Output
cmd.Parameters.Add("#IsPasswordCorrect", SqlDbType.Bit).Direction = ParameterDirection.Output
'InPut Parameters
cmd.Parameters.AddWithValue("#LoginID", LoginIDTextBox.Text)
cmd.Parameters.AddWithValue("#Password", PasswordTextBox.Text)
cmd.ExecuteNonQuery()
' Assign Parameters
IsLoginIdCorrect = Convert.ToBoolean(cmd.Parameters("#IsLoginIdCorrect").Value)
IsPasswordCorrect = Convert.ToBoolean(cmd.Parameters("#IsPasswordCorrect").Value)
End Using
End Using
End Sub
This is the Login button click event handler, even when I provide the correct values, it still always returns false:
Private Sub LoginButton_Click(sender As Object, e As EventArgs) Handles LoginButton.Click
Try
Dim IsLoginIdCorrect, IsPasswordCorrect As Boolean
GetIsUserLoginCorrect(IsLoginIdCorrect, IsPasswordCorrect)
If IsLoginIdCorrect And IsPasswordCorrect = True Then
Me.Hide()
' User Information
DeshBoard.MainUserIdLabel.Text = Me.MainUserIdLabel.Text
DeshBoard.UserNameLabel.Text = Me.UserNameLabel.Text
DeshBoard.UserLoginIdLabel.Text = Me.UserLoginIdLabel.Text
DeshBoard.UserLevelLabel.Text = Me.UserLevelLabel.Text
'Organanization Information
DeshBoard.MainOrgIDLabel.Text = Me.MainOrgIDLabel.Text
DeshBoard.OrgNameLabel.Text = Me.OrgNameLabel.Text
DeshBoard.OrgTelLabel.Text = Me.OrgTelLabel.Text
DeshBoard.OrgEmailLabel.Text = Me.OrgEmailLabel.Text
DeshBoard.OrgAddressLabel.Text = Me.OrgAddressLabel.Text
DeshBoard.Show()
Else
If IsLoginIdCorrect = False Then
MessageBox.Show("Login ID is not correct...!!", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
LoginIDTextBox.Clear()
PasswordTextBox.Clear()
LoginIDTextBox.Focus()
Else
MessageBox.Show("Password ID is not correct...!!", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
PasswordTextBox.Clear()
PasswordTextBox.Focus()
End If
End If
Catch ex As ApplicationException
MessageBox.Show("Error: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
End Sub
Thank you very much.
You need to add ByRef to both arguments in Sub GetIsUserLoginCorrect().
To demonstrate, try the following with and without ByRef.
Private Sub ChangeBoolean(ByRef TorF As Boolean)
TorF = True
End Sub
Private Sub OPCode2()
Dim TorF As Boolean
ChangeBoolean(TorF)
Debug.Print(TorF.ToString) ' Result False without ByRef in ChangeBoolean
'When ByRef is added result is True
End Sub
First off, a method can refer to either a sub or a function. A sub is a method that performs an action. A function is a method that calculates or retrieves one or more values.
A sub should not be called Getxxx, because its primary purpose should not be returning a value.
A function should be used to return values. Since you are trying to retrieve multiple values, if you were using 2017 I would suggest returning a named tuple with your two values, since you aren’t I would create an object that has the values and return that.
On a totally different note, you really can’t tell the difference between right user wrong password and wrong user right password and wrong user wrong password - so you shouldn’t tell someone you can. You just say login unsuccessful login, or invalid username/password combination.
There's a lot wrong with your code.
Firstly, why are you using so much of SQL code ? Correct me if i am wrong : You are trying to build a log in system. So much of SQL code or even the stored procedure is worthless here. You can simply write the SQL statements in your code by using the SqlCommand class. Even though you are using the ALTER PROCEDURE statement, i can surely say that things can be simplified here.
You are also using the Me keyword. It's not C# where the use of this(same as Me in VB.Net) becomes compulsory. I assume it's a Windows Forms Application and if that's so, then using Me keyword to access it's child elements wouldn't result in any different if it's not used at all.
The next worth mentioning issues is your Name Conventions. Most or should i say all of your variables have the same name. For example : IsLoginIdCorrect - used both as a parameter of a method and also a variable inside a method.
The next issues is in these two lies :
Dim IsLoginIdCorrect, IsPasswordCorrect As Boolean
GetIsUserLoginCorrect(IsLoginIdCorrect, IsPasswordCorrect)
You are passing the boolean variables before they have been assigned any value. You are lucky it's not C# or this wouldn't even compile. Passing the boolean variables without assigning any value will, by default, set their values to False. So, literally, you are always passing the same value in which case, the outcome will always be the same.
The next issue is in your If statement inside your LoginButton_Click method aka LoginButton's click event handler's method :
If IsLoginIdCorrect And IsPasswordCorrect = True Then
The if statements, if described in simple words, means : If IsLoginIdCorrect and IsPasswordCorrect are true, then proceed.... So, in this case, IsPasswordCorrect = True doesn't affect much. However, this is not the best practice too. You should better follow the following coding rule while using If statements:
If (IsLoginIdCorrect = True AndAlso IsPasswordCorrect = True) Then
AndAlso operators evaluates each side just like the And operator. The difference is that it would return False if the left side(IsLoginIdCorrect, in this case) returns False.
The next issues is the usage of ApplicationException. I don't understand why, in this era, you are using that class! This class is usually used to derive from and create exceptions. You can simply use Exception instead of ApplicationException.
Your Try-Catch block seems not useful as well. All of your codes inside the LoginButton_Click are in If conditions and they perform very basic operation. It is unlikely to ever throw any exception at all.
Your logics, for most part, are illogical(sorry to put it this way). In your GetIsUserLoginCorrect method, you are setting IsLoginIdCorrect and IsPasswordCorrect to either true or false but it wouldn't matter because they are parameters of the method itself. So even if you set their values, they will be reset when you call the method again. The reason why ByRef (according to Mary's answer) works is because ByRef, in short, means that you are pointing to original variable that you passed(not it's copy).
And finally, the solution you are looking for....
Even though i see you have marked Mary's answer as the answer, i would like to help you out a bit as-well.
Firstly, get rid of the stored procedure if possible and also if you are not using it anywhere else. I see you are using the If Exist condition inside your SQL queries. This is actually a nice move because according to performance reports, checking if data exists in a database/table using IF EXISTS yields the fastest output. So bravo for that one. But if you follow my advice and want to ditch the stored procedure, then you need to get rid of the IF EXISTS statement as well. Rather, you can simply use the SELECT statement itself, use the ExecuteScalar method of the SqlCommand class, convert it's value to Integer and check if the value of the Integer is 1 or not.
Example :
Dim cmd = New SqlCommand("SELECT COUNT(*) FROM UserInfo
WHERE loginid = #LoginID AND password = #Password")
Dim Exists = Convert.ToInt32(cmd.ExecuteScalar)
If Exists = 1 Then
''Code if data exists
End if
Note that i have used Convert.ToInt32 here. This will prevent null-reference exception as when ExecuteScalar returns Null, it will be converted to 0 integer value.
Also, why are you using GetIsUserLoginCorrect as method ? You can simply use it as a function and return required values. As you are returning multiple values, you can easily use the Tuple type as your function's type:
Private Function GetIsUserLoginCorrect(IsLoginIdCorrect As Boolean, IsPasswordCorrect As Boolean) As Tuple(of Boolean, Boolean)
....
....
return Tuple.Create(IsLoginIdCorrect, IsPasswordCorrect)
End Sub
Usage
Dim IsLoginCorrect = GetIsUserLoginCorrect(first_boolean_variable,second_boolean_variable).Item1
Dim IsPasswordCorrect = GetIsUserLoginCorrect(first_boolean_variable,second_boolean_variable).Item2
One last thing. As you are showing DeshBoard form after hiding the main form, make sure to call MainForm.Close on the Dashboard form's Closing/Closed event. This will ensure the application's exit(unless you have other plans for the main form, of course).
Hope this answer helps you.

ByRef Local Variable using Linq

I'm having trouble with selecting only part of a collection and passing it by reference.
So I have a custom class EntityCollection which is , who guessed, a collection of entities. I have to send these entities over HTTPSOAP to a webservice.
Sadly my collection is really big, let's say 10000000 entities, which throws me an HTTP error telling me that my request contains too much data.
The method I am sending it to takes a Reference of the collection so it can further complete the missing information that is autogenerated upon creation of an entity.
My initial solution:
For i As Integer = 0 To ecCreate.Count - 1 Step batchsize
Dim batch As EntityCollection = ecCreate.ToList().GetRange(i, Math.Min(batchsize, ecCreate.Count - i)).ToEntityCollection()
Q.Log.Write(SysEnums.LogLevelEnum.LogInformation, "SYNC KLA", "Creating " & String.Join(", ", batch.Select(Of String)(Function(e) e("nr_inca")).ToArray()))
Client.CreateMultiple(batch)
Next
ecCreate being an EntityCollection.
What I forgot was that using ToList() and ToEntityCollection() (which I wrote) it creates a new instance...
At least ToEntityCollection() does, idk about LINQ's ToList()...
<Extension()>
Public Function ToEntityCollection(ByVal source As IEnumerable(Of Entity)) As EntityCollection
Dim ec As New EntityCollection()
'ec.EntityTypeName = source.FirstOrDefault.EntityTypeName
For Each Entity In source
ec.Add(Entity)
Next
Return ec
End Function
Now, I don't imagine my problem would be solved if I change ByVal to ByRef in ToEntityCollection(), does it?
So how would I actually pass just a part of the collection byref to that function?
Thanks
EDIT after comments:
#Tim Schmelter it is for a nightly sync operation, having multiple selects on the database is more time intensive then storing the full dataset.
#Craig Are you saying that if i just leave it as an IEnumerable it will actually work? After all i call ToArray() in the createmultiple batch anyway so that wouldn't be too much of a problem to leave out...
#NetMage you're right i forgot to put in a key part of the code, here it is:
Public Class EntityCollection
Implements IList(Of Entity)
'...
Public Sub Add(item As Entity) Implements ICollection(Of Entity).Add
If IsNothing(EntityTypeName) Then
EntityTypeName = item.EntityTypeName
End If
If EntityTypeName IsNot Nothing AndAlso item.EntityTypeName IsNot Nothing AndAlso item.EntityTypeName <> EntityTypeName Then
Throw New Exception("EntityCollection can only be of one type!")
End If
Me.intList.Add(item)
End Sub
I Think that also explains the List thing... (BTW vb or c# don't matter i can do both :p)
BUT: You got me thinking properly:
Public Sub CreateMultiple(ByRef EntityCollection As EntityCollection)
'... do stuff to EC
Try
Dim ar = EntityCollection.ToArray()
Binding.CreateMultiple(ar) 'is also byref(webservice code)
EntityCollection.Collection = ar 'reset property, see below
Catch ex As SoapException
Raise(GetCurrentMethod(), ex)
End Try
End Sub
And the evil part( at least i think it is) :
Friend Property Collection As Object
Get
Return Me.intList
End Get
Set(value As Object)
Me.Clear()
For Each e As Object In value
Me.Add(New Entity(e))
Next
End Set
End Property
Now, i would still think this would work, since in my test if i don't use Linq or ToEntityCollection the byref stuff works perfectly fine. It is just when i do the batch thing, then it doesn't... I was guessing it could maybe have to do with me storing it in a local variable?
Thanks already for your time!
Anton
The problem was that i was replacing the references of Entity in my local batch, instead of in my big collection... I solved it by replacing the part of the collection that i sent as a batch with the batch itself, since ToList() and ToEntityCollection both create a new object with the same reference values...
Thanks for putting me in the correct direction guys!

Variable declaration with Nothing check

Very (very) often we need to write stuff like
Dim Data = GetSomeData()
If Data IsNot Nothing Then
Data.DoSomething()
Else
...
End If
Maybe I am asking in vain but I am seriously hoping that VB.Net has some construct like:
IfExists Data = GetSomeData() Then
Data.DoSomething()
Else
...
End IfExists
In my dreams it does two important things:
No extra line for Nothing check
Variable A is not visible outside of the block and thus can't be used later on by mistake (just like "Using" or "With")
Is there anything similar to that that I haven't found yet?
Thanks!
EDIT:
Inspired by Bjørn-Roger Kringsjå's Answer I came up with something that would satisfy me (humbled by VB.Net's deficiencies):
<Extension()>
Public Sub IfExists(Of T)(This As T, DoIfNotNothing As Action(Of T), Optional DoIfNothing As Action = Nothing)
If This IsNot Nothing Then
DoIfNotNothing(This)
ElseIf DoIfNothing IsNot Nothing Then
DoIfNothing()
End If
End Sub
Then I can call it like this (with the false part being optional)
GetSomeData().IfExists(Sub(Data) Data.DoSomething())
or
GetSomeData().IfExists(Sub(Data) Data.DoSomething(), Sub() DoSomethingElse())
As stated by others and implied by me, it can't be done. Just like to share a 3'rd solution. This time we're going to use delegates.
No extra line for Nothing check
Variable A is not visible outside of the block and thus can't be used later on by mistake.
Implementation
Public Module Extensions
Public Sub IfExists(Of T)(testExpr As T, trueDlg As Action(Of T), falseDlg As Action)
If (Not testExpr Is Nothing) Then
trueDlg.DynamicInvoke(New Object(0) {testExpr})
Else
falseDlg.DynamicInvoke(New Object(-1) {})
End If
End Sub
End Module
Usage
IfExists(GetSomeData(),
Sub(A As Object)
'We have something (A)
End Sub,
Sub()
'We have nothing
End Sub
)
Shorter:
IfExists(GetSomeData(), Sub(A As Object)
'We have something (A)
End Sub, Sub()
'We have nothing
End Sub)
Or, the shortest version:
IfExists(GetSomeData(), Sub(A As Object) Debug.WriteLine(A.ToString()), Sub() Debug.WriteLine("Nothing"))
No, unfortunately there is nothing like that currently in VB.NET. The closest thing you could do to approximate that behavior would be to write a function like this:
Public Function Assign(ByRef target As Object, value As Object) As Boolean
target = value
Return (target IsNot Nothing)
End Function
Then you could use it like this:
Dim A As SomeType
If Assign(A, GetSomeData()) Then
' ...
Else
' ...
End If
But, as you pointed out, that doesn't really solve either of your stated problems. It's still an extra line of code, and the variable is still not scoped to only be accessible within the block where it was properly assigned.
The first point is not possible. There is no way to declare a variable and check it in a single statement.
The second point is sort of possible. Creating your own block scope is not supported by VB.NET, but you can achieve it by abusing other blocks. Insert your code within a single-use Loop While block:
Do
Dim A = GetSomeData()
If A IsNot Nothing Then
...
Else
...
End If
Loop While False
The Do block will be entered, with A declared local to that block, then the block will immediately exit at While False, and A will no longer be accessible.
A probably better way to simplify this is by restructuring the code into more, smaller methods so that the If ... Else is the entire method and there is no possibility of accidentally accessing an outdated variable later. When you do this, you can also exit early from the method in the simpler case instead of keeping both If and Else branches:
Dim A = GetSomeData()
If A Is Nothing Then
...
Exit Sub
End If
...
End Sub

Why to set an object to Nothing in the Finally block?

In this VB.NET code:
Dim o as SomeClass
Try
o = new SomeClass
'call some method on o here
Catch(...)
...
Finally
o = Nothing
End Try
Why is there a need to set o to Nothing? What if i don't set it to Nothing in the Finally block? What i think is that it is OK if you don't set it to Nothing because the object will be marked for GC.
If the object is unsafe to use out of the try catch should this be done. If this was a stream for example you'd see the stream closed and then set to nothing. It is not always the right thing to do but this code is seen a lot.
consider this code
Sub Main()
Dim o As String
Try
o = "Hello"
Console.Out.WriteLine("hi {0}", o)
Catch ex As Exception
' do something here
Finally
o = Nothing
End Try
' unable to do something here
End Sub
Allthough it is a silly example it does mean you cannot reference o outside now because it is no longer set to an instance of an object. That is why it is done by a lot of people. If you are in a function and a function ends at that point there is no need to set to Nothing as the object falls out of scope however lots of people will set stuff to Nothing out of habit I'd consider that incorrect and bad code design
It's because the object is not safe to use outside the the try.. catch.. finally block. It's not guaranteed it's in a consistent state, so it's set to Nothing to make it obvious t's not supposed to be used.

VB.Net - "With" and Closures don't mix

Just thought I'd share this in case anyone else has run into this.
I did something similar today and it took me a while to figure out why this was causing a problem at runtime.
This code:
Public Class foo
Public bar As String = "blah"
End Class
Public Sub DoInline()
Dim o As New foo
Dim f As Func(Of String)
With o
f = Function() .bar
End With
Try
Console.WriteLine(f.DynamicInvoke())
Catch ex As Reflection.TargetInvocationException
Console.WriteLine(ex.InnerException.ToString)
End Try
End Sub
Throws a NullReferenceException. It seems as though the With is using the closure as its temp storage, and at the "End With", it sets the closure's variable to Nothing.
Here is that code in RedGate Reflector:
Public Shared Sub DoInline()
Dim o As New foo
Dim $VB$Closure_ClosureVariable_7A_6 As New _Closure$__1
$VB$Closure_ClosureVariable_7A_6.$VB$Local_VB$t_ref$L0 = o
Dim f As Func(Of String) = New Func(Of String)(AddressOf $VB$Closure_ClosureVariable_7A_6._Lambda$__1)
$VB$Closure_ClosureVariable_7A_6.$VB$Local_VB$t_ref$L0 = Nothing
Try
Console.WriteLine(RuntimeHelpers.GetObjectValue(f.DynamicInvoke(New Object(0 - 1) {})))
Catch exception1 As TargetInvocationException
ProjectData.SetProjectError(exception1)
Console.WriteLine(exception1.InnerException.ToString)
ProjectData.ClearProjectError
End Try
End Sub
Notice the
$VB$Closure_ClosureVariable_7A_6.$VB$Local_VB$t_ref$L0 = Nothing
Only "question" I can really ask is; is this a bug or a strange design decision that for some reason I'm not seeing.
I'm pretty much just going to avoid using "With" from now on.
This behavior is "By Design" and results from an often misunderstood detail of the With statement.
The With statement actually takes an expression as an argument and not a direct reference (even though it's one of the most common use cases). Section 10.3 of the language spec guarantees that the expression passed into a With block is evaluated only once and is available for the execution of the With statement.
This is implemented by using a temporary. So when executing a .Member expressio inside a With statement you are not accessing the original value but a temporary which points to the original value. It allows for other fun scenarios such as the following.
Dim o as New Foo
o.bar = "some value"
With o
o = Nothing
Console.WriteLine(.bar) ' Prints "some value"
End With
This works because inside the With statement you are not operating on o but rather a temporary pointing to the original expression. This temporary is only guaranteed to be alive for the lifetime of the With statement and is hence Nothingd out at the end.
In your sample the closure correctly captures the temporary value. Hence when it's executed after the With statement completes the temporary is Nothing and the code fails appropriately.
There's really only one bug that I see, the compiler should generate an error for this. Shouldn't be hard to implement. You can report it at connect.microsoft.com