I would like to store references to a bunch of structs in a collection. The general scaffolding looks like this:
Structure myStructType
Dim prop1 as String
Dim prop2 as int
End Structure
Dim myList as new List(Of myStructType)()
'Wrongness below
Dim myStruct as new myStructType()
myStruct.prop1 = "struct1"
myStruct.prop2 = 1
myList.Add(myStruct)
myStruct = new myStructType()
mystruct.prop1 = "number two"
mystruct.prop2 = 2
myList.Add(myStruct)
now this doesn't work, because it's referencing the same memory. What I would really want is the 'pass reference by value' behaviour that is also used for reference types, so that I can easily keep producing more of them.
Is there any way to fix this other than to make the structs into classes? Is this actually a proper way to use structs, or do I have it all wrong?
This code does the same thing whether it is a struct or a class because you are invoking new myStructType() for each object. That being said, be aware that later retrieving and modifiying those myStructType objects behave differently. If it is derrived froma structure then you are copying the data on a retrieve, leaving the original untouched in the list. If it is derrived from a class then you are getting a reference to that object and changes made using that reference change the instance in the list.
I still wonder what you are trying to accomplish (or avoid) by using structures instead of classes?
Related
Maybe superfluous, but some intro...
I am rewriting an add-in for my CAD-application (using VB.NET).
This add-in reads, via an API, a bunch of metadata from a file, presents it in a Form. This data can then be (partially) changed and written back to the file.
This metadata is accessible in a consistent way, however the data type is not the same everywhere (String, Currency, Date, Boolean, Long and IPictureDisp).
Currently I have a much too complex class with several arrays. I thought it might be smarter to create a structure. The problem is the varying data type.
Is it possible to define a structure with a member with varying datat type, or am I forced to define a different structure for each data type?
You have a few options...
1: Use Object
Nice and simple, every data type inherits from Object - so if your struct contains a property of type Object, you can put pretty much any data type in there
From the docs:
The Object data type can point to data of any data type, including any object instance your application recognizes. Use Object when you do not know at compile time what data type the variable might point to.
However, this does mean that you will get next to no help from the compiler when you are trying to write code using this property. You will also probably have to cast any time you need to do anything type-specific
2: Generic Types
This will not fit situations where you are not sure of the type. You can create a generic struct using the Of syntax.
You'd create it as so:
Structure MyStructure(Of T)
'our changing type
Dim MyCustomData As T
'...alongside regular types
Dim Name As String
Dim OtherThing As Integer
End Structure
and then when you need to create the structure, you'd simply pass the type in and assign the value
Dim struct As New MyStructure(Of Integer)
struct.MyCustomData = 123
Dim struct2 As New MyStructure(Of String)
struct2.MyCustomData = "a"
I understand arrays. I know some java from 15 years ago, and I know about classes, objects, instances, variables, static variables and constructors. Not so familiar with these things in VB.
I don't understand object collections..
Suppose I draw a listbox, and name it lstbox1
I see that I can say lstbox1.items.item(0) or lstbox1.items(0)
The fact that I can say listbox1.items(0) puzzles me a little bit. If an object collection is not an array, then it's clearly not an object either.
This link https://msdn.microsoft.com/en-us/library/yb7y698k(v=vs.90).aspx says Collection is an object.
But then what is items(0) items is not a class so that can't be calling a constructor... and items is not a method, it's a property that is an instance of object collection, so I can't see how the (0) works.. I know what it refers to the first object, the element with index 0, but I don't understand how that can work. I know blah(0) would work if blah was an array. And I am sure lstbox1.items is not an array of object collections it's just 1 object collection.
Is it a data structure like an Array, that has its own syntax.. for example one can say dim blah as Integer() or dim blah() as Integer and thus define it without even stating the class Array. Is ObjectCollection a bit like that? It does seem to allow (index) after an instance of it.
VB has a concept called Default Properties. In the case of an ItemCollection type (and a number of other types, as well), the Item property is the default property for the collection. This allows you to use the shorthand from the question.
It's basically just a bit of syntactical sugar. When you say, lstbox1.items(0), it's just shorthand for lstbox1.items.item(0).
Also, don't mistake the various collection types for simple arrays. They will have similar syntax, but every collection type has it's own quirks and use cases, and it's generally worth your time to look at the documentation for the specific type you're working with. Don't assume something is an array, just because you can access the items by index.
Think about this just as a "language feature". This is what really called indexer property. It is implemented as default property in vb.net. In c# implementation is different. The data structure behind it could be anything you want - array, list, dictionary, hashtable. The fact is - it lets you access something by supplying a parameter without calling property syntax. myParentObject(1) instead of myParentObject.GetChildObject(1)
In VB, default property must be indexed.
listbox1 . Items . item(0) --> listbox1 - main object that has property Items , which is a collection. This collection has property item, which is default property or indexer. Item is a property exposing single object from underlying collection.
I have a base class, DtaRow, that has an internal array of Strings containing data. I have dozens of subclasses of DtaRow, like UnitRow and AccountRow, who's only purpose is to provide Properties to retrieve the values, so you can do aUnit.Name instead of aUnit.pFields(3).
I also have a DtaTable object that contains a Friend pRows As New Dictionary(Of Integer, DtaRow). I don't generally insert DtaRows into the DtaTable, I insert the subclasses like UnitRows and AccountRows. Any given table has only one type in it.
Over in the main part of the app I have an accessor:
Public Readonly Property Units() As IEnumerable
Get
Return Tables(5).pRows.Values 'oh oh oh oh table 5, table 5...
End Get
End Property
This, obviously, returns a list of DtaRows, not UnitRows, which means I can't do MyDB.Units(5).Name, which is the ultimate goal.
The obvious solution is to Dim ret As New UnitRow() and DirectCast everything into it, but then I'm building thousands of new arrays all the time. Uggg. Alternately I could put DirectCast everywhere I pull out a value, also uggg.
I see there is a method called Array.ConvertAll that looks like it might be what I want. But maybe that just does the loop for me and doesn't really save anything? And if this is what I want, I don't really understand how to use DirectCast in it.
Hopefully I'm just missing some other bit of API that does what I want, but failing that, what's the best solution here? I suspect I need...
to make a widening conversion in each DtaRow subclass?
or something in DtaTable that does the same?
You can use ConvertAll to convert an array into a different type.
Dim arr(2) As A
Dim arr2() As B
arr(0) = New B
arr(1) = New B
arr(2) = New B
arr2 = Array.ConvertAll(arr, Function(o) DirectCast(o, B))
Class A
End Class
Class B
Inherits A
End Class
In your case, I think it would look like this
Return Array.ConvertAll(Tables(5).pRows.Values, Function(o) DirectCast(o, UnitRow))
Note that this will create a new array each time.
You can cast the objects into a list(Of String) based on the field you want.
Return Tables(5).pRows.Values.Cast(Of DtaRow).Select(Function(r) r.name).ToList
YES! I went non-linear. This only works because of OOP...
My ultimate goal was to return objects from the collection as a particular type, because I knew I put that type in there in the first place. Sure, I could get the value out of the collection and CType it, but that's fugly - although in C# I would have been perfectly happy because the syntax is nicer.
So wait... the method that retrieves the row from the collection is in the collection class, not the various subclasses of DtaRow. So here is what I did...
Public ReadOnly Property Units() As IEnumerable
Get
Return Tables(dbTblUnits).pRow.Values
End Get
End Property
Public ReadOnly Property Units(ByVal K as Integer) As UnitRow
Get
Return DirectCast(Tables(dbTblUnits)(K), UnitRow)
End Get
End Property
Public ReadOnly Property Units(ByVal K as String) As UnitRow
Get
Return DirectCast(Tables(dbTblUnits).Rows(K), UnitRow)
End Get
End Property
Why does this solve the problem? Well normally if one does...
Dim U as UnitRow = MyDB.Units(K)
It would call the first method (which is all I had originally) which would return the .Values from the Dictionary, and then the Default Property would be called to return .Item(K). But because of the way the method dispatcher works, if I provide a more specific version that more closely matches the parameters, it will call that. So I provide overrides that are peers to the subclasses that do the cast.
Now this isn't perfect, because if I just call Units to get the entire list, when I pull rows out of it I'll still have to cast them. But people expect that, so this is perfectly acceptable in this case. Better yet, when I open this DLL in VBA, only the first of these methods is visible, which returns the entire collection, which means that Units(k) will call the Default Property on the DtaTable, returning a DtaRow, but that's fine in VBA.
OOP to the rescue!
How can I get an item from a list as a new copy/instance, so I can use and change it later without changing the original object in the list?
Public Class entry
Public val As String
' ... other fields ...
End Class
Dim MyList As New List(Of entry)
Dim newitem As New entry
newitem.val = "first"
MyList.Add(newitem)
Now if I try to get an item from this list and change it to something else, it changes the original item in the list as well (it is used as a reference not as a new instance).
Dim newitem2 As New entry
newitem2 = MyList.Item(0)
newitem2.val = "something else"
So now the MyList.item(0).val contains "something else", yet I wanted only the newitem2 to contain that new value for the given field and retain other values from the object in the list.
Is there a way to do this without reassigning all fields one by one?
If entry is defined as a reference type (Class), then your only option is to explicitly create a new instance that has the same values as the originals. For example:
Public Partial Class Entry
Public Function Clone() As Entry
Return New Entry() With { .val = Me.val, … }
End Function
End Class
(The .NET Framework Class Library defined a type ICloneable from early on for exactly this purpose. The type never really caught on for certain reasons.)
Be aware that you might have to do this recursively, that is, if your class contains fields that are of a reference type, you'll have to clone the objects stored in these fields as well.
Then, instead of doing this:
Dim newitem2 As New entry ' (Btw.: `New` is superfluous here, since you are
newitem2 = MyList.Item(0) ' going to throw away the created instance here.)
Do this:
Dim newitem2 As Entry = MyList.Item(0).Clone()
One alternative is to use value types (i.e. declare your item type as Structure). Value types are automatically copied when passed around. However, there are lots of caveats to observe, among them:
Do not do this if your type contains many fields. (Why? Many fields usually means that the type will occupy more bytes in memory, which makes frequent copying quite expensive if the objects get too large.)
Value types should be immutable types. (Why? See e.g. Why are mutable structs evil?)
These are just two guidelines. You can find more infornation about this topic here:
When should I use a struct instead of a class?
Choosing Between Class and Struct
Let's say I got a list called
myFirstList
And then I want to create a copy of that list so I can do some tweaks of my own. So I do this:
mySecondList = myFirstList
mySecondList.doTweaks
But I noticed that the tweaks also affect the myFirstList object! I only want the tweaks to affect the second one...
And afterwards I will want to completely delete mySecondList, so I do mySecondList = Nothing and I'm good, right?
Adam Rackis, I don't like your "Of course it does", because it is not at all obvious.
If you have a string variable that you assign to another string variabe, you do not change them both when making changes to one of them. They do not point to the same physical piece of memory, so why is it obvious that classes do?
Also, the thing is not even consistent. In the following case, you will have all elements in the array pointing at the same object (they all end up with the variable Number set to 10:
SourceObject = New SomeClass
For i = 1 To 10
SourceObject.Number = i
ObjectArray.Add = SourceObject
Next i
BUT, the following will give you 10 different instances:
For i = 1 To 10
SourceObject = New SomeClass
SourceObject.Number = i
ObjectArray.Add = SourceObject
Next i
Apparently the scope of the object makes a difference, so it is not at all obvious what happens.
Here is how you do it:
'copy one object to another via reflection properties
For Each p As System.Reflection.PropertyInfo In originalobject.GetType().GetProperties()
If p.CanRead Then
clone.GetType().GetProperty(p.Name).SetValue(clone, p.GetValue(OriginalObject, Nothing))
End If
Next
in some cases when the clone object got read-only properties you need to check that first.
For Each p As System.Reflection.PropertyInfo In originalobject.GetType().GetProperties()
If p.CanRead AndAlso clone.GetType().GetProperty(p.Name).CanWrite Then
clone.GetType().GetProperty(p.Name).SetValue(clone, p.GetValue(OriginalObject, Nothing))
End If
Next
Since you have not divulged the type of item that you are storing n your list, I assume it's something that's implementing IClonable (Otherwise, if you can, implement IClonable, or figure out a way to clone individual item in the list).
Try something like this
mySecondList = myFirstList.[Select](Function(i) i.Clone()).ToList()
But I noticed that the tweaks also
affect the myFirstList object! I only
want the tweaks to affect the second
one...
Of course it does. Both variables are pointing to the same object in memory. Anything you do to the one, happens to the other.
You're going to need to do either a deep clone, or a shallow one, depending on your requirements. This article should give you a better idea what you need to do
Expanding on Adam Rackies' answer I was able to implement the following code using VB.NET.
My goal was to copy a list of objects that served mainly as data transfer objects (i.e. database data). The first the class dtoNamedClass is defined and ShallowCopy method is added. A new variable named dtoNamedClassCloneVar is created and a LINQ select query is used to copy the object variable dtoNamedClassVar.
I was able to make changes to dtoNamedClassCloneVar without affecting dtoNamedClassVar.
Public Class dtoNamedClass
... Custom dto Property Definitions
Public Function ShallowCopy() As dtoNamedClass
Return DirectCast(Me.MemberwiseClone(), dtoNamedClass)
End Function
End Class
Dim dtoNamedClassVar As List(Of dtoNamedClass) = {get your database data}
Dim dtoNamedClassCloneVar =
(From d In Me.dtoNamedClass
Where {add clause if necessary}
Select d.ShallowCopy()).ToList
Here's an additional approach that some may prefer since System.Reflection can be slow.
You'll need to add the Newtonsoft.Json NuGet package to your solution, then:
Imports Newtonsoft.Json
And given a class type of MyClass, cloning can be as easy as:
Dim original as New MyClass
'populate properties of original...
Dim copy as New MyClass
copy = JsonConvert.DeserializeObject(Of MyClass)(JsonConvert.SerializeObject(original))
So the approach is to first use the JSON converter to serialize the original object, and than take that serialized data and deserialize it - specifying the class type - into the class instance copy.
The JSON converters are extremely powerful and flexible; you can do all sorts of custom property mappings and manipulations if you need something the basic approach above doesn't seem to address.
this works for me:
mySecondList = myFirstList.ToList
clone is the object you are attempting to clone to.
dim clone as new YourObjectType
You declare it like that.