Odd behaviour copying structs
I've created this sample now to demonstrate:
https://dotnetfiddle.net/waruzF
CC @hamarb123 @Aaron
This is certainly user error; I'm just not sure what I'm doing wrong in all honesty, sorry! :(
C# Online Compiler | .NET Fiddle
Test your C# code online with .NET Fiddle code editor.
36 Replies
This is also what tripped me up on structs vs classes being passed by reference vs by value.
you have boxed the struct
so you're effectively using it as if it was a class
Oh right, yeah, makes sense now - I always forget that boxing easily happens implicitly...
Any suggestions for how to best address this?
Thus far I've just created an explicit Clone() function on the interface that every subtype needs to implement, hence why I asked about compile-time reflection earlier
ideally just don't box - since if you're going to box anyway, there's not much point using a struct
if you're going to use a boxed struct, you can use the API I linked earlier to make a copy
I need to read up on boxing again, I don't think about it as much as I should
RuntimeHelpers.GetObjectValue
Does that work under NativeAOT?
probably - try it and see
Any suggestions to avoid boxing?
(oh hey I'm blue now. neat)
it's not that complicated to see when you're boxing - whenever you convert a struct or a generic of a struct type to
object
, ValueType
, an interface it implements, it boxes - or if you call a method that is from a base type that you didn't override like ToString()
, or a default interface method implementation that you didn't override, on a struct
depends on what you're trying to do - boxing can come up in many scenariosIn my case, I want to keep being able to interact with entities over a generic interface
do you need to be able to have multiple types of entities in the same collection?
Yeah, I just use simple structs implementing a generic interface in a sparse array as an entity system.
So being able to access them generically is sort of needed
it's not necessarily needed - you could have a collection for each type
if you do need them all in the 1 collection, then the easiest solution is probably to just use a class instead of boxing, since that way you make sure there's only the 1 allocation for the 1 thing & it makes it more clear that you're intentionally doing it by reference, otherwise there are more complex solutions available to avoid boxing:
- you could store all the data inline (assuming it's unmanaged, otherwise it will be more difficult), along with some sort of type id (& size, if size varies between types & you don't just want to pad) - this is the basic idea behind a lot of the solutions to avoid boxing for this
- instead of storing inline, you could store the data in another memory location if you wanted and manage that memory (you will still probably have to do some memory management for the inline approach, but probably not as much, but this has other benefits like making it more easy to tightly pack)
- instead of storing a type id & doing switch statement on it, you could store some sort of pointer/object, that you have 1 of per type, which exposes the interface in a way that explicitly takes the memory to operate on as a parameter, so that way you can still call methods on it fully generically, but with the benefit of no allocations
- etc.
Hm, yeah, seems I'll have to deal with it being passed by reference. I'll change over to a class then, it's not a big deal in my case
Thank you for informing me though, greatly appreciated :)
I've had a look now at this, however this seems to just be used to box the type. I'm still trying to create a copy of my object (now a class). Reading up on it online, it does seem that now the only way to actually do that sort of copy (without weird hacks like serializing & deserialising) is via a clone function, which returns to my original question about being able to source-generate it. Are you randomly aware of any info or resources about that? I've heard source generation can be quite difficult & tedious, so if it ends up being too complicated I may just stick to manually writing a clone function.
I said that one was for making a shallow copy of a boxed struct - if you want to do a shallow copy of a class, you can just call MemberwiseClone (as I also mentioned lol) - you can just expose this as a public API in a base class if you just want to write it once
Right, I should actually be fine with a shallow copy since I would expect my entity types to only have value types, so that might actually work - thanks! Sorry for stealing your time.
Looking into MemberwiseClone - seems I unfortunately can't just have a standard implementation in my interface that calls it :(
Unless there's some odd trick I don't know about.
you can put it in a base class - or if you really wanted, you could use UnsafeAccessor to call it, but there's really no good reason to do that here as there's an easy better solution
So I'd need to use a baseclass instead of an interface?
you don't have to use it instead, you could use it in addition to
instead would also work obviously
Hm, that seems to go a bit into multi-inheritance which I generally try to avoid; so instead may be better
using an interface in the way you're using it is really just a far more complex form of multiple inheritance than using a base class
Eh, is it? I just have a single interface that my entities have to implement for some basic engine callbacks
there's 2 reasons to use interfaces generally - one reason to use an interface is because you want complex "inheritance" graphs - if you want a simple graph, then using inheritance is effectively a simpler form of achieving that and it limits you to simple graphs - the other reason to use an interface is if you want to expose common functionality to a generic parameter, where T is the actual type implementing it, which you're not doing clearly as you're just boxing it
Hmmm, okay. I generally considered Interfaces as essentially just a "contract" that a type has to implement in order to qualify generically as that interface.
I didn't consider the interface to actually exist within itself; just to be a template to implement.
the difference between interfaces and base types is that you can implement an interface at any point, whereas base types explicitly limit you to only 1 base type (and hence force a simpler "inheritance" graph) - there are other differences too, such as base types being able to store fields if you need that - and base types generally being faster - but you generally select based on what functionality you will actually require - if you only ever plan to have entities that don't inherit from some other random class, then a base class is probably a better choice than an interface - if you will need an entity that inherits from some other type, you can either achieve that by using a wrapper or by making it an interface instead - if you were going to make them structs and never box them, then you'd obviously want to use an interface & pass them around by a constrained generic
if you have some other reason to pick one over the other, that's fine obviously - but I wouldn't choose interfaces over base types to avoid multiple inheritance, you'd actually do the exact opposite more or less
Hm, I see. I suppose I'll migrate over the a base class then (I'm not super familiar with them - I come from a procedural & data-oriented background with C++ primarily), I didn't know they actually prevented multi-inheritance.
I always thought classes were moreso about overriding base behaviour rather than having to conform to a template
well, you can only have 1 base class, unlike c++, so it disallows it by construction
whereas you can implement as many interfaces as you'd like
Oh I was talking about multi-inheritance in the sense of inheritance depth, not necessarily amount of interfaces implemented at once
but tbh, I wouldn't pick one based solely on whether it disallows multiple "inheritance" or not - I would pick whatever is most logical based on what features you need, based on the performance, etc., and then just not do multiple inheritance if you don't want it
lol - that's what multiple inheritance means :kekw:
you can still do that with interfaces anyway
I'm aware you can; I just avoid it :)
Higher performance is certainly preferred in my case; but actually the Nr. 1 priority is ease of implementation for someone creating a new entity type, as that's the code on the project that won't be written by me. I.e. minimizing boilerplate and all that
hence my interest in source generators, because right now there's still a bit of boilerplate here and there (such as registering a button with the editing tool to create a new entity of a new given entity type, and now also the clone function). In an ideal world, I'd be able to just create a new entity type that derives from some interface or baseclass and have the rest happen automatically, but that's sadly not possible without advanced reflection (which I can't due to NAOT) or source generation.
if anything, I'd argue that interfaces are more prone to it (with good reason generally) - e.g., look at
IList<T>
, it has 3 other interfaces that you need to do too.
with base classes, I find you generally will just write the base class inheriting from object with all the common functionality & abstract methods needed, and then write the derived classes directly from the base classhave you considered something like an EC or ECS were there isn't any/very little inheritance and mainly composition?
the concept of each entity having its own unique integer ID in your sample is especially reminiscent of an ECS
I have, and it's not suitable for this project due to the target demographic of programmer, additional overhead to tooling creation, etc etc
ECS just makes everything more complicated in this project where the total project complexity is not expected to be very large.
mmm
i see
In a larger project - ECS for the win, absolutely