Is this an incorrect way of iterating over a dictionary? - vb.net

Are there any problems with iterating over a dictionary in the following manner?
Dim dict As New Dictionary(Of String, Integer) From {{"One", 1}, {"Two", 2}, {"Three", 3}}
For i = 0 To dict.Count - 1
Dim Key = dict.Keys(i)
Dim Value = dict.Item(Key)
'Do more work
dict.Item(Key) = NewValue
Next
I have used it a lot without any problems. But I recently read that the best way to iterate over a dictionary was using a ForEach loop. This led me to question the method that I've used.
Update: Note I am not asking how to iterate over a dictionary, but rather if the method that I've used successfully in the past is wrong and if so why.

Are there any problems with iterating over a dictionary in the following manner?
Yes and no. Technically there's nothing inherently wrong with the way you're doing it as it does what you need it to do, BUT it requires unnecessary computations and is therefore slower than simply using a For Each loop and iterating the key/value-pairs.
Iterating keys, then fetching value
The Keys property is not a separate collection of keys, but is actually just a thin wrapper around the dictionary itself which contains an enumerator for enumerating the keys only. For this reason it also doesn't have an indexer that lets you access the key at a specific index like you are right now.
What's actually happening is that VB.NET is utilizing the extension method ElementAtOrDefault(), which works by stepping through the enumeration until the wanted index has been reached. This means that for every iteration of your main loop, ElementAtOrDefault() also performs a similar step-through iteration until it gets to the index you've specified. You now have two loops, resulting in an O(N * N) = O(N2) operation.
What's more, when you access the value via Item(Key) it has to calculate the hash of the key and determine the respective value to fetch. While this operation is close to O(1), it's still an unnecessary additional operation compared to what I'm talking about below.
Iterating key/value-pairs
The dictionary already has an internal list (array) holding the keys and their respective values, so when iterating the dictionary using a For Each loop all it does is fetch each pair and put them into a KeyValuePair. Since it is fetching directly by index this time (at a specific memory location) you only have one loop, thus the fetch operation is O(1), making your entire loop O(N * 1) = O(N).
Based on this we see that iterating the key/value-pairs is actually faster.
This kind of loop would look like (where kvp is a KeyValuePair(Of String, Integer)):
For Each kvp In dict
Dim Key = kvp.Key
Dim Value = kvp.Value
Next

See here:
https://www.dotnetperls.com/dictionary-vbnet
Keys. You can get a List of the Dictionary keys. Dictionary has a get accessor property with the identifier Keys. You can pass the Keys to a List constructor to obtain a List of the keys.
It cites an example similar to yours:
Module Module1
Sub Main()
' Put four keys and values in the Dictionary.
Dim dictionary As New Dictionary(Of String, Integer)
dictionary.Add("please", 12)
dictionary.Add("help", 11)
dictionary.Add("poor", 10)
dictionary.Add("people", -11)
' Put keys into List Of String.
Dim list As New List(Of String)(dictionary.Keys)
' Loop over each string.
Dim str As String
For Each str In list
' Print string and also Item(string), which is the value.
Console.WriteLine("{0}, {1}", str, dictionary.Item(str))
Next
End Sub
End Module

Related

i want to make a new variable that says shot1 shot2 shot3 so on and forth how do i do this?

Here is what i have tried so far , but no new shot variable is being declared
Module Module1
Dim shotlist As New List(Of Boolean)
Dim shono As Integer = 0
Dim shonos As String
Dim shotname As String
Dim fshot As Boolean
Dim shots As String
Sub Main()
For i As Integer = 0 To 1000
Dim ("shots" & i) as String = "shots" & i
fshot = Convert.ToBoolean(shots)
Next
End Sub
End Module
You can't do things this way. The variable names you see when you write a program are lost, turned into memory addresses by the compiler when it compiles your program. You cannot store any information using a variable's name - the name is just a reference for you, the programmer, during the time you write a program (design time)
Think of variables like buckets, with labels on the outside. Buckets only hold certain kinds of data inside
Dim shotname as String
This declares a bucket labelled shotname on the outside and it can hold a string inside. You can put a string inside it:
shotname = "Shot 1"
You can put any string you like inside this bucket, and thus anything you can reasonably represent as a string can also be put inside this bucket:
shotname = DateTime.Now.ToString()
This takes the current time and turns it into a string that looks like a date/time, and puts it in the bucket. The thing in the bucket is always a string, and lots of things (nearly anything actually) can be represented as a string, but we don't type all our buckets as strings because it isn't very useful - if you have two buckets that hold numbers for example, you can multiply them:
Dim a as Integer
a=2
Dim b as Integer
b=3
Dim c as Integer
c=a*b
But you can't multiply strings, even if they're strings trust look like numbers; strings would have to be converted to numbers first. Storing everything as a string and converting it to some other type before working on it then converting it back to a string to store it would be very wearisome
So that's all well and good and solves the problem of storing varying information in the computer memory and giving it a name that you can reference it by, but it means the developer has to know all the info that will ever be entered into the program. shotname is just one bucket storing one bit of info. Sure you could have
Dim shotname1 as String
Dim shotname2 as String
Dim shotname3 as String
But it would be quite tedious copy paste exercise to do this for a thousand shotname, and after you've done it you have to refer to all of them individually using their full name. There isn't a way to dynamically refer to bucket labels when you write a program, so this won't work:
For i = 0 To 1000
shotname&i = "This is shotname number " & i
Next i
Remember, shotname is lost when compiling, and i is too, so shotname&i just flat out cannot work. Both these things become, in essence, memory addresses that mean something to the compiler and you can't join two memory addresses together to get a third memory address that stores some data. They were only ever nice names for you to help you understand how to write the program, pick good names and and not get confused about what is what
Instead you need the mechanism that was invented to allow varying amounts of data not known at the design-time of the program - arrays
At their heart, arrays are what you're trying to do. You're trying to have load of buckets with a name you can vary and form the name programmatically.
Array naming is simple; an array has a name like any other variable (what I've been calling a bucket up to now - going to switch to calling them variables because everyone else does) and the name refers to the array as a whole, but the array is divided up into multiple slots all of which store the same type of data and have a unique index. Though the name of the array itself is fixed, the index can be varied; together they form a reference to a slot within the array and provide a mechanism for having names that can be generated dynamically
Dim shotnames(999) as String
This here is an array of 1000 strings, and it's called shotnames. It's good to use a plural name because it helps remind you that it's an array, holding multiple something. The 999 defines the last valid slot in the array - arrays start at 0, so with this one running 0 to 999 means there are 1000 entries in it
And critically, though the shotnames part of the variable name remains fixed and must be what you use to refer to the array itself(if you want to do something with the entire thing, like pass it to a function), referring to an individual element/slot in the array is done by tacking a number onto the end within brackets:
shotnames(587) = "The 588th shotname"
Keep in mind that "starts at 0 thing"
This 587 can come from anything that supplies a number; it doesn't have to be hard coded by you when you write the program:
For i = 0 to 999
shotnames(i) = "The " & (i+1) & "th shotname"
Next i
Or things that generate numbers:
shotnames(DateTime.Now.Minute) = "X"
If it's 12:59 when this code runs, then shotnames(59), the sixtieth slot in the array, will become filled with X
There are other kinds of varying storage; a list or a dictionary are commonly used examples, but they follow these same notions - you'll get a variable where part of the name is fixed and part of the name is an index you can vary.
At their heart they are just arrays too- if you looked inside a List you'd find an array with some extra code surrounding it that, when the array gets full, it makes a new array of twice the size and copies everything over. This way it provides "an array that expands" type functionality - something that arrays don't do natively.
Dictionaries are worth mentioning because they provide a way to index by other things than a number and can achieve storage of a great number of things not known at design time as a result:
Dim x as New Dictionary(Of String, Object)
This creates a dictionary indexed by a string that stores objects (I.e. anything - dates, strings, numbers, people..)
You could do like:
x("name") = "John"
x("age") = 32
You could even realize what you were trying to do earlier:
For i = 0 to 999
x("shotname"&i) = "The " & (i+1) & "th shotname"
Next i
(Though you'd probably do this in a Dictionary(Of string, string), ie a dictionary that is both indexed by string and stores strings.. and you probably wouldn't go to this level of effort to have a dictionary that stores x("shotname587") when it's simpler to declare an array shotname(587))
But the original premise remains true: your variable has a fixed part of the name (ie x) and a changeable part of the name (ie the string in the brackets) that is used as an index.
And there is no magic to the indexing by string either. If you opened up a dictionary and looked inside it, you'd find an array, together with some bits of code that take the string you passed in as an index, and turn it into a number like 587, and store the info you want in index 587. And there is a routine to deal with the case where two different strings both become 587 when converted, but other than that it's all just "if you want a variable where part of the name is changeable/formable programmatically rather than by the developer, it's an array"

How to access a hashtable's first value without knowing the key?

After reading a config file during installation, I am saving the web services url into a Hashtable to test the connectivity to those services.
Before going through all the values I saved, I want to test just the first value. The keys I am using are the whole xml node containing the service url so it's unknown to me.
I didn't know much about Hashtable first, so I tried accessing it using an index. Assuming ht is a populated Hashtable, I tried this:
Dim serviceUrl as String = ht(0).Value
Which obviously failed since there's no key equal to 0, and the serviceUrl is just a Nothing.
Then I tried to access first element using:
Dim firstEntry as DictionaryEntry = ht(ht.Keys(0).ToString())
' Also tried this:
' Dim firstEntry as DictionaryEntry = ht(ht.Keys(0))
In both cases I got an error:
System.InvalidCastException: Specified cast is not valid.
I ended up using a For Each and exiting the loop directly after first iteration.
For Each entry As DictionaryEntry In ht
Dim serviceUrl as String = entry.Value
'Use it and exit for.
Exit For
Next
Well, this looks really awful.
After some time debugging and looking around, I used an array to hold the keys values:
Dim arr as Object() = new Object(100){}
'Copy the keys to that array.
ht.Keys.CopyTo(arr,0)
'Now I can directly access first item from the Hashtable:
Dim serviceUrl as String = ht(arr(0))
I am not sure if this is the right approach.
Is there any direct/clean way to access first item from a Hashtable?
The Keys property is an ICollection, not an IList, so it can't be indexed. ICollection is basically just IEnumerable with a Count property, so you should treat is the same way as an IEnumerable. That means enumerating it to get the first item. You can use LINQ:
Dim firstKey = myHashtable.Keys.Cast(Of Object)().FirstOrDefault()
or you can go old-school:
Dim firstKey As Object
For Each key In myHashtable.Keys
firstKey = key
Exit For
Next
If the collection may be empty, you can use the Count property to test first.

Dictionary returning same value for every key only when value is also an enumerable or custom

i am a noob, so please take this question with that in mind. Here is very simple piece of code:
Sub main()
{
Dim m_Dictionary as new Dictionary(Of Integer, List(Of String))
Dim workingList as new List(Of String)
Dim workingKey as Integer
Dim keyStash as List(Of Integer)
Dim workingDict as new Dictionary(Of Integer, String)
For i=0 to 9
Do
workingKey = RandomInteger()
Loop While workingList.ContainsKey(workingKey)
For n=0 to 4
workingList.Add(RandomString())
Next
keyStash.Add(workingKey)
workingDict.Add(workingKey, workingList)
Next
' now I just want to play back the generators of random data
For each Key As Integer in keyStash
For each Entry as String in workingDict(Key).Value
Line(Entry)
Next
Next
Instead of everything playing back nicely as one might expect, I am left with a fully accurate stash of keys for the dictionary. However, the values for strings inside each list instance are ALL THE SAME FOR EVERY KEY. Those values are equal to the values in the last loop of random data generation. So instead of playing back 50 uniques entries, it writes out 9 times the last loop. I looked inside - everything looks good. Get this. All lists, collections, hash-tables, all of iterated types and also custom types demonstrate this behavior. I found the solution, but it does not explain anything. Can anyone help explaining this, please!??
The variable that keeps the strings generated by RandomString is created outside the loop. Inside that loop you add continuosly new strings to the same instance and add the same list instance to every new integer key. At the end of the loop every integer key added has its value pointing to the same reference of the list. Of course they are identical....
A first fix to your code could be
Dim m_Dictionary as new Dictionary(Of Integer, List(Of String))
Dim workingKey as Integer
For i=0 to 9
' Internal to the loop. so at each loop you get a new list
' to use for the specific key generated in the current loop
Dim workingList as new List(Of String)
Do
workingKey = RandomInteger()
Loop While m_Dictionary.ContainsKey(workingKey)
For n=0 to 4
workingList.Add(RandomString())
Next
m_Dictionary.Add(workingKey, workingList)
Next
For each k in m_Dictionary
For each Entry in k.Value
' Line(Entry)
Console.WriteLine("Key=" & k.Key & " Values = " & Entry)
Next
Next
Please, remember to use Option Strict ON, the current code treats quietly strings as if they were numbers, and this is not a good practice. Option Strict ON will force you to think twice when you work with different type of data.

Sorting 2 arrays side by side

Basicly I have a program that retrieves data and parses it, which is fine, so it starts from:
11:981.8 which equals to November 981.8 now what I have done is split the "November" and the "981.8" into 2 different arrays, with other similar data, now what I need to be able to do, is sort the array in either Ascending or Descending order, however keeping in tact, the November and 981.8 side by side in a list box.
My current code is:
Private Sub sortData(ByVal strYear As String, ByVal strSort As String)
lbDispData.Items.Clear()
Dim strData As String = My.Settings.usage2011
Dim arrRawData() As String
Dim arrMonth As New ArrayList
Dim arrKilo As New ArrayList
arrRawData = strData.Split("_")
For Each strUsage As String In arrRawData
Dim arrSmall As String()
arrSmall = strUsage.Split(":")
arrSmall(0) = MonthName(arrSmall(0))
arrMonth.Add(arrSmall(0))
arrKilo.Add(arrSmall(1))
Next
If strSort = 0 Then
'Sort in ascending order
ElseIf strSort = 1 Then
'Sort in descending order
End If
End Sub
Or If possible is there a better way?
EDIT: Just to mention, there are many more values, not just November and 981.8, there would be for example December and 128.1, January and 191.1, etc.
Don't use two arrays. Use one array or List, and each item in the array should be either an instance of a custom Class which would have Month and Kilo properties, or a Tuple instance.
Then you can sort by whichever property you want, and don't have to worry about related information staying together.
To create a Tuple for an item, you use Tuple.Create:
Dim myTuple = Tuple.Create(monthValue, kiloValue)
Then you can access values from the Tuple using Item1, Item2, etc.
Dim month = myTuple.Item1
Dim kilo = myTuple.Item2
There are essentially two ways to store this kind of data: array of structures, or structure of arrays. Your implementation, at present, is the latter. As you suspect, the former is typically preferred in languages which provide access to objects and user-defined types.
To solve this problem using an array-of-structures data representation, you will define your own type - for instance, you could call it MonthFloatPair or something more relevant to your application, or you could simply use a built-in type that suits your needs - and have a single array of type MonthFloatPair. To add a MonthFloatPair, you'd parse a string exactly as you are now, and then simply assign the month to one member of MonthFloatPair and the number (Kilo?) to another. Then all you would have to do is to write a method to provide an ordering comparison of MonthFloatPair objects (I believe this is possible in vb.net, at any rate), and then sort this array normally however you'd like.
To solve this problem using a structure-of-arrays data representation, you do exactly what you're doing now. To sort, sort normally as you would for the array you're sorting on, with one caveat: every time you change the position of an element of the array on which you're basing the sorting (e.g., by exchanging in O(n^2) sorts, or placing into a new array as in MergeSort), you need to move the corresponding (same index) element in the other array in the same manner.

Setting the Item property of a Collection in VBA

I'm surprised at how hard this has been to do but I imagine it's a quick fix so I will ask here (searched google and documentation but neither helped). I have some code that adds items to a collection using keys. When I come across a key that already exists in the collection, I simply want to set it by adding a number to the current value.
Here is the code:
If CollectionItemExists(aKey, aColl) Then 'If key already has a value
'add value to existing item
aColl(aKey).Item = aColl(aKey) + someValue
Else
'add a new item to the collection (aka a new key/value pair)
mwTable_ISO_DA.Add someValue, aKey
End If
The first time I add the key/value pair into the collection, I am adding an integer as the value. When I come across the key again, I try to add another integer to the value, but this doesn't work. I don't think the problem lies in any kind of object mis-match or something similar. The error message I currently get is
Runtime Error 424: Object Required
You can't edit values once they've been added to a collection. So this is not possible:
aColl.Item(aKey) = aColl.Item(aKey) + someValue
Instead, you can take the object out of the collection, edit its value, and add it back.
temp = aColl.Item(aKey)
aColl.Remove aKey
aColl.Add temp + someValue, aKey
This is a bit tedious, but place these three lines in a Sub and you're all set.
Collections are more friendly when they are used as containers for objects (as opposed to containers for "primitive" variables like integer, double, etc.). You can't change the object reference contained in the collection, but you can manipulate the object attached to that reference.
On a side note, I think you've misunderstood the syntax related to Item. You can't say: aColl(aKey).Item. The right syntax is aColl.Item(aKey), or, for short, aColl(aKey) since Item is the default method of the Collection object. However, I prefer to use the full, explicit form...
Dictionaries are more versatile and more time efficient than Collections. If you went this route you could run an simple Exists test on the Dictionary directly below, and then update the key value
Patrick Matthews has written an excellent article on dictionaries v collections
Sub Test()
Dim MyDict
Set MyDict = CreateObject("scripting.dictionary")
MyDict.Add "apples", 10
If MyDict.exists("apples") Then MyDict.Item("apples") = MyDict.Item("apples") + 20
MsgBox MyDict.Item("apples")
End Sub
I think you need to remove the existing key-value pair and then add the key to the collection again but with the new value