Can structs contain fields of reference types
Asked Answered
S

6

82

Can structs contain fields of reference types? And if they can is this a bad practice?

Sacco answered 3/6, 2009 at 16:19 Comment(1)
Thanks for all the answers. This was a purely hypothetical one for me so it seemed quicker to ask a question than test it myself. Plus i'd rather it was referenced in here somewhere for others. Gonna mark skeets as the answer as it gave that little bit of extra info.Sacco
P
117

Yes, they can. Is it a good idea? Well, that depends on the situation. Personally I rarely create my own structs in the first place... I would treat any new user-defined struct with a certain degree of scepticism. I'm not suggesting that it's always the wrong option, just that it needs more of a clear argument than a class.

It would be a bad idea for a struct to have a reference to a mutable object though... otherwise you can have two values which look independent but aren't:

MyValueType foo = ...;
MyValueType bar = foo; // Value type, hence copy...

foo.List.Add("x");
// Eek, bar's list has now changed too!

Mutable structs are evil. Immutable structs with references to mutable types are sneakily evil in different ways.

Paz answered 3/6, 2009 at 16:26 Comment(11)
why should a struct be immutable? please explian - i have mutable structs all over my code and they work fine.Nicholenicholl
Search for "mutable structs are evil" and you'll get lots of hits. They really are a very bad idea, and will surprise anyone working with them who isn't made explicitly aware of them. Also see the class library design guidelines.Paz
Upvoted for the great use of the word "Eeek" lol, and also (more importantly), for the good tipElwina
@JonSkeet was just wondering how will this type of struct be stored on the stack and how will it be garbage collected ?Losse
@Kayani: Structs aren't garbage collected. The struct would be stored (either on the stack or the heap, depending on the situation and the implementation) just as normal... The object that the struct's field value refers to would be eligible for garbage collection when you couldn't reach it, just like normal...Paz
@Kayani: If you need to start showing a separate struct declaration, I strongly suggest you ask a new question with appropriate details.Paz
@JonSkeet just to clarify you mean to say that if there is reference type member of a struct say with a name X. That X whatever the type maybe will be stored on the heap with just its reference stored on the stack with other Struct members and once the struct goes out of scope GC will collect the X type from heap when it runs ?Losse
@Kayani: Again, don't assume that structs are always stored on the stack. But yes, the struct can act as a GC root - but there could be other references to the object, of course. Now if that's not enough, please ask another question instead of using comments.Paz
This is an excellent answer showing that value types (structs here) do not always imply value semantics/value object. However since structs are copied, mutable structs might not be necessarily evil as long as they don’t have reference members. Otherwise you can deep copy them. And if you still want to shallow copy them for performance, you could use a copy-on-write mechanism.Dory
So am i doing it wrong with my struct wrapper. I use a struct to track paths i have traversed. So one struct might be From NodeA and To NodeB, another struct would be the same only the two nodes are swapped implying the reverse direction. But my nodes are classes so my struct holds 2 references. I'm then hashing this struct to my hash table. Should i use a class instead of a struct for this wrapper? It's kind've unclear what is the right option or what could be so wrong with my current option.Straightout
@WDUK: We can't really tell from that brief description. Using a struct sounds like it might well be okay - but without more context it's hard to say. You might want to post your code on codereview.stackexchange.com.Paz
N
21

Sure thing and it's not bad practice to do so.

struct Example {
  public readonly string Field1;
}

The readonly is not necessary but it is good practice to make struct's immutable.

Nitaniter answered 3/6, 2009 at 16:22 Comment(1)
"The readonly is not necessary" - I'd argue it is necessary for any public field, otherwise an unwitting consumer could overwrite it themselves.Lives
F
11

Yes, it is possible, and yes, it is usually a bad practice.

If you look at the .NET framework itself, you'll see virtually all structs contain primitive value types alone.

Fuqua answered 3/6, 2009 at 16:25 Comment(0)
S
7

The reason you cannot have mutable structs is because of the behavoir of reference types. Read this article: http://www.yoda.arachsys.com/csharp/parameters.html

When you have a struct that contains an Object (anything that isn't a primitive like int or double) and you copy an instance of the struct, the Object inside isn't "deep" copied, because it is simply a reference (pointer) to a memory location containing the actual class. So if you copy a mutable struct that contains class instances, the copy will be referencing the same instances as the original (Hence bar's list being changed above).

If you absolutely have to have the struct be mutable, make any class instances inside readonly, or - this is the bad practice - try to ensure that you never make a copy of the struct.

Shopper answered 11/2, 2011 at 14:3 Comment(0)
G
3

Yes they can.

It depends.

Many hold the stance that a struct should be immutable, and in this case, holding a reference to an object could mean it isn't.

But it depends on the situation.

Gerdagerdeen answered 3/6, 2009 at 16:21 Comment(0)
O
3

Since this got a downvote, I'm trying to rewrite a little to see if it can become clearer. This question is old; but good! I recently also came across a few links that elaborate on this.

The point I'm trying to add is that if you do declare reference fields, you have to be able to reason outside of your own block: when someone uses your struct. The specific point I added was really only about declaring a readonly field of a struct; but in that case, the fields you have in your struct can change their results; and it's hard to reason about.

I came across this link, where the programmer declared a class containing a readonly struct field. The field in his class is a struct --- it's a LinkedList<T>.Enumerator -- and it broke because the field is readonly --- his own class methods get a copy of the enumerator struct and the state is copied and not dynamic.

But, if you go ahead and fix his code by simply removing the readonly from the struct field (which works); and then however, if you then decide to make your own class a struct, now once again, a consumer of your struct cannot use it as a readonly field or else they in turn get bitten by the same problem. (And if this seems contrived because you won't have a readonly enumerator, you actually might if it supports Reset!)

So if it's not the clearest example, the point I'm making is that you can reason about your own implementation, but if you are a struct you need to also reason about consumers that copy your value and what they will get.

The example i found is linked below.

His class is not a struct, but does contain the m_Enumerator field (and the programmer supposedly knows that it is a struct).

It turns out that this class's methods get a copy of that value, and do not work. --- You can actually examine this block very carefully to understand that.

You can fix it by making the field not readonly --- which is already pointing at confusing. But you can also fix it by declaring the field as the interface type --- IEnumerator<int>.

But, if you do fix it by leaving the field declared as the struct and declare it not readonly, and then choose to define your class as a struct, then now if someone declares an instance of your struct as a readonly field in some class, again they lose!

E.G.:

public class Program
{
    private struct EnumeratorWrapper : IEnumerator<int>
    {
        // Fails always --- the local methods read the readonly struct and get a copy
        //private readonly LinkedList<int>.Enumerator m_Enumerator;

        // Fixes one: --- locally, methods no longer get a copy;
        // BUT if a consumer of THIS struct makes a readonly field, then again they will
        // always get a copy of THIS, AND this contains a copy of this struct field!
        private LinkedList<int>.Enumerator m_Enumerator;

        // Fixes both!!
        // Because this is not a value type, even a consumer of THIS struct making a
        // readonly copy, always reads the memory pointer and not a value
        //private IEnumerator<int> m_Enumerator;


        public EnumeratorWrapper(LinkedList<int> linkedList) 
            => m_Enumerator = linkedList.GetEnumerator();


        public int Current
            => m_Enumerator.Current;

        object System.Collections.IEnumerator.Current
            => Current;

        public bool MoveNext()
            => m_Enumerator.MoveNext();

        public void Reset()
            => ((System.Collections.IEnumerator) m_Enumerator).Reset();

        public void Dispose()
            => m_Enumerator.Dispose();
    }


    private readonly LinkedList<int> l = new LinkedList<int>();
    private readonly EnumeratorWrapper e;


    public Program()
    {
        for (int i = 0; i < 10; ++i) {
            l.AddLast(i);
        }
        e = new EnumeratorWrapper(l);
    }


    public static void Main()
    {
        Program p = new Program();

        // This works --- a local copy every time
        EnumeratorWrapper e = new EnumeratorWrapper(p.l);
        while (e.MoveNext()) {
            Console.WriteLine(e.Current);
        }

        // This fails if the struct cannot support living in a readonly field
        while (p.e.MoveNext()) {
            Console.WriteLine(p.e.Current);
        }
        Console.ReadKey();
    }
}

If you declare a struct with an interface field, you won't know what you have in there, but yet you can actually reason more about what you get when you simply reference it! This is very interesting; but only because the language allows so many freedoms with a struct: you need to start from something very simple; and add only what you can concretely reason about!

One more point is that the reference also says you should define the default values as reasonable; which is not possible with a reference field! If you inadvertently invoke the default constructor --- always possible with a struct --- then you get a null reference.

One last note also. Many folks defend mutable structs, and large mutable structs. But if you peer in, you usually discover that they are simply scoping those objects in a way that allows them to finitely reason about the behaviors, and the structs do not leak into scopes where those invariants could change.

... Too many people begin explaining structs as "Just like a class but ... x, y, z, 1, 2, alpha, beta disco". It must be explained as a couple of readonly values; period; except now that you know something, you can begin to reason about adding something!

The example I came accros is here:

https://www.red-gate.com/simple-talk/blogs/why-enumerator-structs-are-a-really-bad-idea/

Orbital answered 27/11, 2017 at 14:21 Comment(1)
My answer was posted for less than 3 minutes and it got a down vote! HOW?Orbital

© 2022 - 2024 — McMap. All rights reserved.