0

I want to create a dictionary of products where the id is key and the product parameters are custom class that contain the information about the products. It seems like I am storing the information correctly, but obviously I am not, because I can retrieve only the last record. What am I doing wrong?

Public Class ProductClass
    Public id As Integer
    Public name As String
    Public details As String
End Class

Sub Test
    Dim i As Integer
    Dim singleProduct As New ProductClass
    Dim testDb As New Dictionary(Of Integer, ProductClass)

    For i = 1 To 3
        singleProduct.id = i
        singleProduct.name = "Product " & i
        singleProduct.details = "blablabla"
        testDb.Add(singleProduct.id, singleProduct)
    Next i

    For Each dbKey As Integer In testDb.Keys
        Debug.Print(dbKey & " :: " & testDb(dbKey).name & vbCrLf)
    Next
End Sub

The debug output:

1 :: Product 3
2 :: Product 3
3 :: Product 3
0

2 Answers 2

3

The problem is that you are only creating one instance of singleProduct, outside of the loop. Inside the loop, you overwrite this instances properties on every pass, and store the same instance in the map every time.

That is because "ProductClass" is an object-type. Objects are not copied when you pass them to a function, but instead given a reference to that instance. "Integer" on the other hand is not an object, so the map-key get copied every time. Thus, you will end up of a map of 3 entries, each pointing to the same "singleProduct".

The solution is simple. Move the line where you create "singleProduct" inside the loop. That way, you will add 3 individual instances into the map. I don't know VB.net very will, but most languages have the concept of "constructor arguments", which would help you avoid such errors by forcing you to pass the properties directly to the instance-construction, instead of setting them manually on the existing instance. You should try to see if VB supports it and use it for those cases.

Sign up to request clarification or add additional context in comments.

2 Comments

There's no point in me writing an answer when you've provided the solution, I just wanted to suggest that the OP could use Dim singleProduct = New ProductClass With {.id = i, .name = $"Product {i}", .details = "blablabla"} if they don't want to add a constructor to the class.
Your reasoning is correct, but I would recommend to use the correct terms of art, "value type" and "reference type". Ultimately, everything in .NET inherits from Object so it may not be strictly correct to say that a value type is not an object.
2

One needs to create a new instance of the Product class for each iteration of the for loop.

Each programming language has it's own Naming Conventions which you may consider adopting (the word Class, isn't typically appended to the name of the class). Also, public properties are preferred over public fields. See Properties vs Fields – Why Does it Matter? and Auto-Implemented Properties (Visual Basic) for more information.

Public Class Product
    Public Property Id As Integer
    Public Property Name As String
    Public Property Description As String
End Class

Sometimes one would like to sort the products. For this we can implement IComparable. We'll sort by 'Id'.

Public Function CompareTo(other As Product) As Integer Implements IComparable(Of Product).CompareTo
    'sort by 'Id'
    If Id = other.Id Then
        Return 0
    ElseIf Id > other.Id Then
        Return 1
    Else
        Return -1
    End If
End Function

In this code example 'Id' is unique. However, what if 'Id' isn't unique by itself, but the combination of 'Id' and 'Name' is unique. Then, we'd first compare 'Id', then 'Name'.  

Public Class Product
    Implements IComparable(Of Product)
    Public Property Id As Integer
    Public Property Name As String
    Public Property Description As String

    Public Function CompareTo(other As Product) As Integer Implements IComparable(Of Product).CompareTo
        'sort by 'Id', then 'Name'
        If Id = other.Id Then
            If String.Compare(Name, other.Name, StringComparison.OrdinalIgnoreCase) = 0 Then
                Return 0
            ElseIf String.Compare(Name, other.Name, StringComparison.OrdinalIgnoreCase) > 0 Then
                Return 1
            Else
                Return -1
            End If
        ElseIf Id > other.Id Then
            Return 1
        Else
            Return -1
        End If
    End Function
End Class

For further study, also see IComparer.

One may wish to override the ToString() method, so that when ToString() is called, we get the desired output. When ToString() is called, we'll output both 'Id' and 'Name' - you'll notice that I've chosen to use Interpolated Strings

So, now our class will be:

Public Class Product
    Implements IComparable(Of Product)
    Public Property Id As Integer
    Public Property Name As String
    Public Property Description As String

    Public Function CompareTo(other As Product) As Integer Implements IComparable(Of Product).CompareTo
        'sort by 'Id'
        If Id = other.Id Then
            Return 0
        ElseIf Id > other.Id Then
            Return 1
        Else
            Return -1
        End If
    End Function

    Public Overrides Function ToString() As String
        'when ToString() is called, return both 'Id' and 'Name'
        Return $"{Id}: {Name}"
    End Function
End Class

Now, we'll create a Function that populates a SortedDictionary and returns it. Each iteration of the for loop must create a new instance of Product. For the product name I've appended a letter of the alphabet to the word Product based on the value of i. In the code below, I've used an initializer to set the property values. See How to: Declare an Object by Using an Object Initializer (Visual Basic) for more information.

Private Function CreateTestData() As SortedDictionary(Of Integer, Product)
    Dim products As SortedDictionary(Of Integer, Product) = New SortedDictionary(Of Integer, Product)()
    Dim alphabet As String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

    For i As Integer = 0 To 3

        'create new instance and set values
        Dim prod As Product = New Product() With {.Id = i, .Name = $"Product {alphabet(i)}"}

        'only add to dictionary if it doesn't already exist
        If Not products.ContainsKey(i) Then
            products.Add(i, prod)
        End If
    Next

    'for debugging, output the dictionary contents
    For Each kvp As KeyValuePair(Of Integer, Product) In products
        Debug.WriteLine(kvp.Value.ToString())
    Next

    Return products
End Function

Output:

0: Product A
1: Product B
2: Product C
3: Product D

Update:

When one creates a class, by default, one get's the empty (or default) constructor without having to specify it. If desired one can explicitly define it, by adding the following code to the class:

    Public Sub New()
    End Sub

As mentioned in a different answer, if desired, rather than using an initializer (with the empty constructor), one could use a custom constructor. However, one thing to note, is that once one defines a custom constructor the empty (or default) constructor is no longer available unless we explicitly define it. When explicitly defining constructors, one needs to define every constructor that one desires to have, including the empty (default) constructor.

In the code below, I've defined a constructor that takes parameters: 'id', 'name' and 'description' - I've chosen to make 'description' optional.

Product:

Public Class Product
    Implements IComparable(Of Product)
    Public Property Id As Integer
    Public Property Name As String
    Public Property Description As String

    Public Sub New(id As Integer, name As String, Optional description As String = Nothing)
        Me.Id = id
        Me.Name = name
        Me.Description = description
    End Sub
End Class

Usage 1:

Dim prod As Product = New Product(i, $"Product {alphabet(i)}")

Usage 2:

Dim prod As Product = New Product(i, $"Product {alphabet(i)}", "This is the product description")

Usage 3:

Dim prod As Product = New Product(i, Nothing)

Here's a version that contains both the empty (default) constructor, as well as, a custom constructor.

Product:

Public Class Product
    Implements IComparable(Of Product)
    Public Property Id As Integer
    Public Property Name As String
    Public Property Description As String

    Public Sub New()
    End Sub

    Public Sub New(id As Integer, name As String, Optional description As String = Nothing)
        Me.Id = id
        Me.Name = name
        Me.Description = description
    End Sub
End Class

Below, you'll notice that one can either use the custom constructor, or the initializer - which uses the empty (default) constructor.

Usage 1:

Dim prod As Product = New Product(i, $"Product {alphabet(i)}")

Usage 2:

Dim prod As Product = New Product(i, $"Product {alphabet(i)}", "This is the product description")

Usage 3:

Dim prod As Product = New Product(i, Nothing)

Usage 4:

Dim prod As Product = New Product() With {.Id = i, .Name = $"Product {alphabet(i)}"}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.