1

The reason I "require" or prefer using a Value type in my case rather than sticking with the direct reference type required, is because a struct is being used as a Dictionary's key, and this was already the case before I added a reference type field into the struct. At this point, the dictionary's keys are being passed around in many areas with a simple = reassignment with the intention of being a copy of all the struct's values with no reference shenanigans. This is particularly important in the case of being a Dictionary key, where the keys should be unique, but also should be equal/copied when passing them around to eventually re-query the dictionary using the copied value.

With that said, is there some merit to breaking down reference types used into their primitive values in order to avoid the struct being a reference type, or would it be more advisable to simply create some kind of deep-copy function and ensure we are using that for all reassignments?

Here is an example struct where I'm having this issue, and an example struct with my described solution:

// This struct contains a reference type property, so can't directly be copied with = reassignment,
// would have to create a deep-copy function for reassignment and only use that.
struct MyReferenceStruct
{
  private const int MY_REF_PROPERTY_EXPECTED_LENGTH = 8; // myRefProperty has an expected, consistent byte length

  int myValProperty; // value type - no reference issues
  byte[] myRefProperty; // byte[] is a reference type! We cannot copy this struct using = reassignment
}

// this struct only contains value types, so it can be easily copied with = reassignment
struct MyValueStruct
{
  // will still be used to validate the byte[] before converting it to a long
  private const int MY_REF_PROPERTY_EXPECTED_LENGTH = 8; // myRefProperty has an expected, consistent byte length

  int myValProperty; // value type - no reference issues

  // value type - no reference issues
  // myRefProperty is supposed to have a consistent byte length of 8 bytes, so we could instead
  // save the data as 8 individual byte properties, or more concisely, 1 long/Int64 (we will
  // make sure to have consistent endianness when making this conversion)
  long myValProperty2;
}

This is more of a discussion post where I already know of two working solutions:

  1. Keeping the struct as a reference-type and using a deep-copy function for reassignment
  2. Breaking down reference types into their value types in order to keep the struct as a value-type

I am curious to hear thoughts on what is the better approach in this situation. I personally prefer breaking down the reference types, so that I don't have to remember to use a deep copy function when reassigning/copying this struct, and won't have to hunt down all the existing copy-reassignments.

1 Answer 1

1

The byte array is a problem independent of whether the type is a struct or a class. Consider wrapping it into a ReadOnlyCollection Class to make it read-only.

It is advisable to make structs read-only anyway. See: Why are mutable structs “evil”?.

You can also make a class read-only. Such a class behaves much like a value type if you also override GetHashCode and Equals to reflect the value equality. A prominent example is System.String.

Consider also using a Record Class or Record Struct. They are just classes or structs, but the C# compiler adds a lot of boilerplate code to make them behave like read-only value types automatically.

Note that according to Custom copy semantics:

Any record class type has the copy constructor. A copy constructor is a constructor with a single parameter of the containing record type. It copies the state of its argument to a new record instance.

3
  • I will certainly look into this, particularly the record struct, but I'm not sure if it will be viable to switch to an immutable/read-only struct in my case. With my second proposed solution, the byte[] is no longer an issue in the struct, because it's no longer a reference type, it's been refactored into a long since it has a consistent length of 8 bytes. A simpler example would have been a class which only contains primitives, and replacing it with those primitives. I was mostly hoping to hear some discussion around the idea of breaking down a reference type into its primitives.
    – Matsyir
    Commented Jun 6, 2023 at 21:33
  • 1
    Still consider using an immutable record struct. You can then use the with keyword to create a copy with changed values. This also happens with when you manipulate strings. E.g. t = s; s = s + "x"; does not change the old s but creates a new string and assigns it to s. Through t you still see the old shorter string. Commented Jun 7, 2023 at 11:40
  • I do think this is the most robust approach, however I had some issues integrating this, so I'm probably going to stick with my 'break-down-the-types' solution in this case. My project is forced to be in C#9.0 (Unity) so I can't use Record structs, sadly. I attempted to make the struct and all its fields readonly, integrated my own copy-constructor, and did my best to replace all "copy re-assignments" of this struct to use the copy-constructor. Couldn't get it working properly. I probably failed to find/replace a new dictionary key assignment somewhere along the line (lots of usages...)
    – Matsyir
    Commented Jun 7, 2023 at 22:07

Not the answer you're looking for? Browse other questions tagged or ask your own question.