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
Related
Anyone else finding that their Terminate() method in Access isn't being called?
Here's my code for my cBusyPointer class with the comments removed for brevity:
Option Compare Database ' Use database order for string comparisons
Option Explicit ' Declare EVERYTHING!
Option Base 1 ' Arrays start at 1
Public Sub show()
DoCmd.hourGlass True
End Sub
Private Sub Class_Terminate()
DoCmd.hourGlass False
End Sub
Typical usage is:
Sub doTehThings()
Dim hourGlass As New cBusyPointer
hourGlass.show()
' Do all teh things
End Sub
In previous versions, this class would restore the hourglass whenever the object went out of scope and was destroyed.
I even tried to manually destroy my hourglass object:
Sub doTehThings()
Dim hourGlass As cBusyPointer
Set hourGlass = New cBusyPointer
hourGlass.show()
' Do all teh things
Set hourGlass = Nothing
End Sub
The only way to fix this is to add a hide() method and call that.
Has anyone else encountered this problem?
I cannot replicate the issue. The Terminate() method is called upon reaching the Set hourGlass = Nothing.
A couple of points:
Dim hourGlass As New cBusyPointer
This will create a new instance every time you call the hourGlass variable even to set it to Nothing. See the answer in the link below for additional info:
What's the difference between Dim As New vs Dim / Set
You should always use Dim/Set when working with objects.
hourGlass.show()
This does not even compile in VBA. Subs do not accept parentheses even when arguments are being expected, unless they are preceded with the Call keyword.
Lastly, the cleanest way to reference an object is to access it using the With statement which ensures the object is terminated when the End With statement is reached.
With New cBusyPointer
.show
End With
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!
I'm new to VB6, and trying to write some macro in use for CorelDraw.
I have a variable that need to be passed from Class module to Standard module, in my Class Module "SaveOptClass" I have a public variable called IsSaved and it's set on the class module:
Public IsSaved As Boolean
Public Sub SaveFile()
If <some triggers> Then
IsSaved = True
End If
In Standard module:
Sub DoSave()
Dim SaveClass As SaveOptClass
Set SaveClass = New SaveOptClass
If SaveClass.IsSaved = True Then
ActiveDocument.Save
Else
Form1.Show
End If
End Sub
Basically I'm trying to pass "IsSaved" boolean value from class module to standard. (If IsSaved is true, save the document or else display a form.)
I have tested that the boolean is True when I executed the code, but I can't get the state to pass to the other module.
Is there something I miss here? Thanks in advance.
As already answered by #shahkalpesh the problem is that you're not using a meaningful instance of SaveOptClass.
In my opinion the best way to design this kind of dependency is by mean of a parameter in the routine is using it and avoid as much as possible the use of global variables.
In your case this brings to this rewriting:
' in someOtherModule
Public Sub DoSave(saveOptObj as SaveOptClass)
If saveOptObj.IsSaved Then ' = True is unnecessary
ActiveDocument.Save
Else
Form1.Show
End If
End Sub
The client code could be:
private saveOptObj as SaveOptClass
Public Sub SaveFile()
If <some triggers> Then
saveOptObj.IsSaved = True
End If
' ....
someOtherModule.DoSave(saveOptObj)
' ...
Consider also, at this point, a renaming of DoSave, given that the actions taken suggest different semantics. In similar cases is preferable moving the If Else logic in the caller. Anyway, if you prefer to group actions with different semantics in the same routine you'd better use namings like DoSaveOr<SomethingElse>.
Okay, so I am working on a small scripting language using a VB Console Application.
I want the user to input "say('something')" and it calls the function I made named "say", is there a way to call the function and still use the following code:
Module Module1
Sub say(sayline)
Console.WriteLine(sayline)
End Sub
Sub Main()
Dim cmd As String
Console.WriteLine(">")
Do
Console.Write("")
cmd = Console.ReadLine()
If cmd IsNot Nothing Then cmd
Loop While cmd IsNot Nothing
End Sub
End Module
No, you cannot just call a method from user's string. You need to interpret the entered data.
First, you need to split your method name and arguments so that entered "say('something')" will transform to say and something. Remember that user can enter wrong data and you need to check if this call is correct - it's all about syntactic and lexical analysis. I hope you understand how to do this because it is pretty difficult.
Then, you need to check if you have a method called say. In case of plain and simple structure, switch construction will be enough. If your have such method, then pass something argument to this method. Else, output something like "unknown method".
If you wanted to call the method say upon typing the word say(something) and display the word something, then you can just have a certain condition that if the user types the word say within the input then call say method else, do whatever you want to do under else portion. Parse the input and omit the word say from the input and display it then.
You can have your code this way for example. (I just copied your code and added some codes to meet what you wanted... in my understanding)
Module Module1
Sub say(ByVal sayline)
Console.WriteLine(sayline)
End Sub
Sub Main()
Dim cmd As String
Do
Console.Write("> ")
cmd = Console.ReadLine()
Try
If cmd IsNot Nothing And cmd.Substring(0, 3).ToUpper().Equals("SAY") Then
say(parseInput(cmd))
End If
Catch ex As Exception
Console.WriteLine("message here")
End Try
Loop While cmd IsNot Nothing
End Sub
Function parseInput(ByVal cmd As String) As String
Dim input As String = ""
For index As Integer = 3 To cmd.Length - 1
If Char.IsLetter(cmd) Then
input += cmd.Substring(index, 1)
Else
input = input
End If
Next
Return input
End Function
End Module
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