Please have a look at the code below:
Public Class TestClass
Public TestProperty As Integer
End Class
Public Class Form1
Private Sub Form1_Load(ByVal sender As Object,
ByVal e As System.EventArgs) Handles Me.Load
Dim i As Integer
Dim j As Integer
For j = 0 To 2
For i = 0 To 10
Dim k As Integer
Dim tc As TestClass
tc = New TestClass
tc.TestProperty = tc.TestProperty + 1
k = k + 1
Next
Next
End Sub
End Class
There is a new object (called tc) created on every iteration of the FOR loop, so tc.TestProperty is always 1. Why is this not the case with variable k i.e. the value of k increments by one on every iteration? I realise this is probably to do with how value types and reference types are dealt with, but I wanted to check.
It's because when something is defined as block level it applies to the entire block level, regardless of loops. normally with control logic like an IF block statement the scope starts and ends and no code lines repeat.
Inside a loop structure the variable is defined inside that block, even though the Dim statement appears to be called multiple times it is not, it is not actually an executable statement (just a definition and reservation of a placeholder as mentioned above in one comment)
To cause it to behave in the same way as "tc" you also need to initialize it in a similar way. (the assignment to 0 would occur each loop, not the definition)
Dim k As Integer = 0
Alternately if you change how your dealing with tc it would behave the same way as k where it is in block scope the entire time inside the loop. In the below example tc is not redefined each loop either.
Dim tc as TestClass
if tc is nothing then tc = New TestClass
You would have to Dim k As Integer = 0 to keep it at 1.
This is because Dim k As Integer retains it's value, while Dim k As Integer = 0 "declares and initializes" it.
Specifically: "If you alter the value but then return to the Dim statement, your altered value is replaced by the value supplied in the Dim statement."
Actually, I don't know why it doesn't seem go out of scope. Maybe without the New keyword it's using the same block of memory.
As implied by the title of this question, you're querying the scope versus the lifetime of the variable.
The scope of the local variables k and tc is the inner For loop. The lifetime is the whole of the Sub.
If you adjusted the tc = New TestClass to If tc Is Nothing Then tc = New TestClass (and ignored the warning that causes), you should then see the tc.TestProperty increment too.
"Dim k As Integer" isn't actually translate into any code except "space reservation" (that is surely made at compile time). So the application does not pass on that sentence 10 times.
As a matter of proof, you can not put a trace bullet on that line of code !
On the other hand, your code create on each loop a fresh new object TestClass (holding a brand new variable "TestProperty) and assign it to the variable "tc". The previous object is lost and carbage collected anytime soon.
Related
I have a class module called MyClass, with a public integer in it:
Public i as Integer
When I try to use this variable in a For loop like so:
Dim MyInstance as MyClass: Set MyInstance = New MyClass
For MyInstance.i = 1 To 10
Debug.Print "Hello"
Next
I get the error: Variable required. Can't assign to this expression
I have consulted the help page but cannot see how it applies to my case. The relevant fragment is: "You tried to use a nonvariable as a loop counter in a For...Next construction. Use a variable as the counter." But i is a variable after all, and not a Let Property function or any other expression.
What is wrong with the code?
EDIT: I should point out that the reason I want my iterator to be part of the class is that I have multiple instances of the class, serving different purposes in my project, and there are multiple nested For loops for each instance of the class. Therefore it is worth having the iterators belong to their respective objects, say:
For Client.i = 1 To Client.Count
For Order.i = 1 To Order.Count
For Item.i = 1 To Item.Count
etc.
I have settled for the following workaround but am still not entirely satisfied with it:
For ciii = 1 To Client.Count
Client.i = ciii ' Client.i is later used in the code
For oiii = 1 To Order.Count
Order.i = oiii
For iiii = 1 To Item.Count
Item.i = iiii
You cannot use MyInstance.i as the increment counter but you can use it as the terminator; e.g. For i = 1 To MyInstance.i.
MyClass class
Option Explicit
Public pi As Long
Public Property Get i() As Long
i = pi
End Property
Public Property Let i(Value As Long)
pi = Value
End Property
test sub procedure in Module1
Sub test()
Dim MyInstance As MyClass, i As Long
Set MyInstance = New MyClass
MyInstance.i = 10
For i = 1 To MyInstance.i
Debug.Print "Hello"
Next
End Sub
If you want a publicly accessible loop variable stick it at the top of a standard module i.e. declare the Public i at the top of a standard module.
Note that this would mean you need to re-write your standard module code as, as per point two, you are treating i as if it is a property/method of the class.
So, standard module code would be:
Public i As Long
Sub ........
For i = 1 To 10
Debug.Print "Hello"
Next i
End Sub ......
If you want it to somehow be a property/method then you need to define Getters and Setters (potentially) in the class. And then re-write your module code accordingly. Especially if you are planning on looping using i, you will need an incrementor method in the class.
And yes, I have changed i to Long as there are no advantages, in this case I believe, of having it declared as Integer. A Long is a safer bet for avoiding potential overflow.
If you need a workaround so that you iterate through a property of the instance, you could create a method to increment it, change your loop to a Do While ... Loop and call that method before the loop call.
'Class Module
Option Explicit
Public i As Integer
Public Sub increment_i()
i = i + 1
End Sub
Private Sub Class_Initialize()
i = 0
End Sub
'Module
Sub loop_myclass()
Dim instance As MyClass: Set instance = New MyClass
Do While instance.i <= 10
'Instance property dependent code here
Debug.Print instance.i
instance.increment_i
Loop
End Sub
OK, I found the answer. There is a Microsoft help page on For…Next loop regarding VB, but I think it pertains to VBA as well.
It says:
If the scope of counter isn't local to the procedure, a compile-time
warning occurs.
So there's not much to discuss here, it's just the way MS wants it to be. Though I'd think that if the scope is greater than the procedure it shouldn't cause any problems, but apparently it does.
I call a subroutine MyPartsMatrix inside nested Parallel.For loops (vb.net). MyPartsMatrix requires a variable called "unfilled" that is passed ByRef because this value is modified inside MyPartsMatrix subroutine. I need to grab and store this value after the subroutine MyPartsMatrix executes.
The "unfilled" variable yields a different value when I run the parallel version of this code compared to a one that is non-parallel, using normal nested For...Next loops. I can't figure out why this is the case.
Is it thread safe to call another subroutine from inside the Parallel.For loop?
Is this variable "unfilled" thread safe?
Dim ConcurrentListofResults As ConcurrentQueue(Of FindBestResults)
ConcurrentListofResults = New ConcurrentQueue(Of FindBestResults)
Dim x = 5, y = 5
Parallel.For(0, x, Sub(oD)
Parallel.For(0, y, Sub(oT)
Dim unfilled As Integer = 0
MyPartsMatrix (oD, oT, unfilled)
'Create a FBS item to add to the concurrent list collection
Dim FBSResultsItem = New FindBestResults
FBSResultsItem.oD = oD
FBSResultsItem.oT = oT
FBSResultsItem.unfilled = unfilled
'Add this item to the Concurent collection
ConcurrentListofResults.Enqueue(FBSResultsItem)
End Sub)
End Sub)
'Get best result.
Dim bestResult As FindBestResults
For Each item As FindBestResults In ConcurrentListofResults
If item.unfilled < bestResult.unfilled Then
bestResult.oD = item.oD
bestResult.oT = item.oT
bestResult.unfilled = item.unfilled
End If
Next
Public Sub MyPartsMatrix (ByVal oD As Integer, ByVal oT As Integer, ByRef unfilled As Integer)
'....do stuff with the unfilled variable....
'unfilled is a counter that is incremented while we run through the PartsMatrix
unfilled = unfilled + 1
End Sub
If this is not thread safe, is there another way to write this so that the "unfilled" variable is thread safe or to make calling another subroutine thread safe?
Without the definition of MakeSchedule (which you've called MyMakePartsMatrix in another place) it's impossible to say whether it's thread safe.
To be threadsafe the sub needs to not modify anything other than unfilled. This will ensure that calling it multiple times with the same inputs will always yield the same outputs. I'd also recommend converting to a function as I find this much easier to understand whats happening.
On another note:
Your performance will be better if you don't nest parallel loops. You're currently waiting for your inner loop to finish before launching your second loop. If you're using larger x + y values then something similar to the code will work better.
Dim scenarios = From x2 In Enumerable.Range(0, x)
From y2 In Enumerable.Range(0, y)
Select New With {x2, y2}
Parallel.ForEach(scenarios, Sub(s)
End Sub)
I’m trying to understand the use of the NEW keyword when used with an object vs. a list of objects.
EXAMPLE A:
This prints out “Class value = 10” 10 times.
Public Class TestClass
Private _testInt As Integer
Property TestInt As Integer
Get
Return _testInt
End Get
Set(value As Integer)
_testInt = value
End Set
End Property
End Class
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim test As New List(Of TestClass)
Dim localTest = New TestClass
For i As Integer = 0 To 10
localTest.TestInt = i
test.Add(localTest)
Next
For i As Integer = 0 To 10
Console.WriteLine("Class value = " & test(i).TestInt.ToString)
Next
End Sub
EXAMPLE B:
If I move the declaration of localTest to inside the loop, it will print out “Class value = 1 to 10”.
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim test As New List(Of TestClass)
For i As Integer = 0 To 10
Dim localTest = New TestClass
localTest.TestInt = i
test.Add(localTest)
Next
For i As Integer = 0 To 10
Console.WriteLine("Class value = " & test(i).TestInt.ToString)
Next
End Sub
It seems to me that localTest is acting like an instance of an object while test acts like a list of pointers to an object.
Q1. Is localTest an instance of an object or a pointer to an object?
Q2. Is test multiple instances of objects or a list of pointers to objects? If it is not a list of pointers, why does example A print out “Class value = 10” 10 times?
Q3. Can you explain why the behavior is this way and what the New keyword means when working with objects vs. collections or lists of objects?
Any additional insight would be great. Thank you.
EDIT:
I've marked an answer as accepted below, primarily because it contained a link to a webpage that was really useful. On the webpage it says the following which helped me understand what was happening:
You can assign either a reference type or a value type to a variable
of the Object data type. An Object variable always holds a pointer to
the data, never the data itself. However, if you assign a value type
to an Object variable, it behaves as if it holds its own data.
TestClass is declared as a Class. That means it is a reference type. Since it is a reference type, that means that all variables of that type will act as pointers. If you want it to not act as a pointer, you need to declare it as a Structure (a value type).
In some other languages, the same type of value can be stored directly by the variable or as a pointer (e.g. int x; vs int *x; in C). In .NET languages, however, that decision is determined globally by the type rather than by the variable.
Lists of reference types are lists of pointers. Lists of value types store copies of the actual values.
Static variables in VBA are simple enough:
Public Sub foo()
Static i As Integer
i = i + 1
Debug.Print i
End Sub
outputs (when called multiple times):
1
2
3
...
The problem is, VBA does not support initializing a variable on the same line as the declaration (not counting using : to put two lines on one):
Public Sub foo()
Dim i As Integer = 5 'won't compile!
Dim j As Integer
j = 5 'we have to do this instead
End Sub
This clashes with static variables:
Public Sub foo()
Static i As Integer 'we can't put an initial value here...
i = 5 'so this is how we'd usually initialize it, but...
i = i + 1
Debug.Print i
End Sub
You can probably see what happens - The very first thing the variable does every time foo is called is set itself back to 5. Output:
6
6
6
...
How can you initialize a static variable in VBA to a value other than its default? Or is this just VBA dropping the ball?
One way to do this if you want to keep the static semantics and not switch to a global is to sniff the default value and then set the initial condition:
Static i As Integer
if (i = 0) then i = 5
Safer alternative would perhaps be
Static i As Variant
if isempty(i) then i = 5
Or
Public Sub foo(optional init as boolean = false)
Static i As Integer
if init then
i = 5
exit sub
endif
You could probably also create a class with a default property and use class_initialize but that's probably a bit over-fussy.
I had the same issue in VB6, where it's exactly the same, and I like the Microsoft recommendation most:
Sub initstatic ()
Static Dummy, V, I As Integer, S As String
' The code in the following if statement will be executed only once:
If IsEmpty(Dummy) Then
' Initialize the dummy variant, so that IsEmpty returns FALSE for
' subsequent calls.
Dummy = 0
' Initialize the other static variables.
V = "Great"
I = 7
S = "Visual Basic"
End If
Print "V="; V, "I="; I, "S="; S
' Static variables will retain their values over calls when changed.
V = 7
I = I + 7
S = Right$(S, 5)
End Sub
I solved it as follows using a static boolean to indicate if you are entering the function for the first time. This logic should work for other situation as well, i think
Private Sub Validate_Date(TB as MSForms.TextBox)
Static Previous_Value as Date
Static Not_First_Time as Boolean
if Not_First_Time = False Then
Previous_Value = Now
Not_First_Time = True
endif
if IsDate(TB.Value) = False then TB.Value = Previous_Value
Previous_Value = TB.Value
End sub
The use of a Boolean to flag something as already initialized functions correctly in normal use but has an unexpected side effect when using the debugger. bIsInitialized is NOT reset to False when the VBA projuect is re-compiled. When the initialization code (or the constants the code uses) is changed changed, the thing being initialized will not be re-initialized while using the debugger.
One work-around is to set a breakpoint at the statement "If (Not bIsInitialized) Then" and add bIsInitialized as a watch variable. When the breakpoint is reached the first time, Click on the value and change it to false, remove the breakpoint and use F5 to continue. There may be a better work-around that uses something that is reliably reset by recompiling the project, but since the documentation for VBA says that the Boolean would be re-initialed after going out of context and all code stopping, there's no way to know if that behavior was version dependent. Not reinitializing the block of memory associated with the routine appears to be a performance optimization.
Static bIsInitialized as Boolean
Static something_time_consuming_to_initialize
If (Not bIsInitialized) Then
initialize something_time_consuming_to_initialize
bIsInitialized = True
End If
I am about to admit defeat on this, I'm fairly new to VB and I am sure there is something quite basic I've managed to miss,
My problem with the following code is when the Button3_Click function is executed a "Value cannot be null. Parameter name: item" exception arises at run-time, if I forget to include the ".Name" on this line "ListBox2.Items.Add(test.Name)" then stuff still gets vomited out to the listbox so assuming there's something there,
any help?
Regards
Dan
Private Sub Button3_Click(sender As System.Object, e As System.EventArgs) Handles
Button3.Click
Dim test As comdevice
Dim usbcoms() As comdevice = FindComs()
For Each test In usbcoms
ListBox2.Items.Add(test.Name)
Next
End Sub
Private Function FindComs() As comdevice()
Dim USBClass As New System.Management.ManagementClass("Win32_PNPEntity")
Dim USBCollection As System.Management.ManagementObjectCollection =
USBClass.GetInstances()
Dim USB As System.Management.ManagementObject
Dim temp() As comdevice
Dim n As Integer
n = 0
For Each USB In USBCollection
If USB("Name").ToString().Contains("P") Then
n += 1
End If
Next USB
ReDim temp(n)
n = 0
For Each USB In USBCollection
If USB("Name").ToString().Contains("COM") Then
temp(n).Name = USB("Name").ToString()
temp(n).DeviceID = USB("DeviceID").ToString()
End If
Next
Return temp
End Function
Private Structure comdevice
Public Name As String ' This employee's given name.
Public DeviceID As String ' This employee's family name.
End Structure
Your problems lies in the FindComs method.
The first loop search the USBCollection for devices that contains the letter P and you count them.
In the second loop, after dimensioning the return array with the number of devices found you try to fill this array with devices that contains the string COM, of course there is no relation between devices with COM and P in their name. You end up with an array bigger than the effective number of devices with COM in their name.
When the array returns, you add every slot of the array, but you have slots with NULL values and thus the error.
You can fix the problem dimensioning the array only for the devices with COM in their names
For Each USB In USBCollection
If USB("Name").ToString().Contains("COM") Then
n += 1
End If
Next USB
ReDim temp(n)
Oded's comment is the most likely cause of this failure.
An easy test is to set the Name property to some default string, like "test" in your comdevice class.
That way, the Name property will never be null and you can see if it ever gets changed.