Spiral noteboook with notes, pen, keyboard on desk

When should you use a struct instead of a class?

Sam RuebyC# Leave a Comment

Deciding when to use a struct in C# can be tricky. In many ways they’re the same.  If you went your entire programming career only using classes you’d probably be fine. Structs have some benefits over classes, and knowing the differences between them can give provide you a useful tool.

Before I took this deep dive, two things came to my mind about structs:

  • Structs are value types
  • Structs live on the stack instead of the heap*

*This is not necessary true and I am surprised MSDN wrote this in their post without any additional explanation. Keep reading!

TL;DR;

MSDN tells you exactly when to use a struct:

  • It logically represents a single value, similar to primitive types (int, double, etc.).
  • It has an instance size under 16 bytes.
  • It is immutable.
  • It will not have to be boxed frequently.

In all other cases, you should define your types as classes.

Okay, sounds easy enough. But let’s dig deeper.

It logically represents a single value

Structs should be used to represent a single value because structs are value types, like a number. The number ‘5’ is an int, which is a value type, which makes sense because every 5 is a 5. It wouldn’t make sense to have an instance of 5; A single 5 is no-different than any other 5. This is important is because structs are value types which are copied by value. This means that, when you pass a struct as a parameter to the method, the contents of the entire struct is duplicated. Not understanding this fact can cause a lot of confusion if you’re passing a struct to a method and expecting that method to manipulate that struct. Without the method returning the manipulated struct, everything that changed in that struct will be lost. Unless you’re passing the struct by reference, but you probably wouldn’t be doing that unless you understood this about structs.

        private class Point3D {
            public int X { get; set; }
            public int Y { get; set; }
            public int Z { get; set; }

            public override string ToString() => $"X:{X}, Y:{Y}, Z:{Z}";
        }

        static void Main( string[] args ) {
            var mypoint = new Point3D { X = 10, Y = 20, Z = 30 };
            Console.WriteLine( mypoint );
            addThree( mypoint );
            Console.WriteLine( mypoint );
        }

        /// <summary>
        /// Adds 3 to all coordinates in the Point3D
        /// </summary>
        private static void addThree( Point3D point ) {
            point.X += 3;
            point.Y += 3;
            point.Z += 3;
        }

The output shows that adding 3 to each of the class's properties was not lost after executing the method

Here we can see, because Point3D is a class, when mypoint is passed to addThree, all three coordinates have been modified.

        private struct Point3D {
            public int X { get; set; }
            public int Y { get; set; }
            public int Z { get; set; }

            public override string ToString() => $"X:{X}, Y:{Y}, Z:{Z}";
        }

        static void Main( string[] args ) {
            var mypoint = new Point3D { X = 10, Y = 20, Z = 30 };
            Console.WriteLine( mypoint );
            addThree( mypoint );
            Console.WriteLine( mypoint );
        }

        /// <summary>
        /// Adds 3 to all coordinates in the Point3D
        /// </summary>
        private static void addThree( Point3D point ) {
            point.X += 3;
            point.Y += 3;
            point.Z += 3;
        }

The output shows that adding 3 to each of the structs's properties was lost after executing the method

Now because Point3D is a struct, the values of X,Y,Z are the same as they were before calling the method. This is because Point3D is now a value types where the entire contents of the struct are copied when passed to the method. ‘addThree’ manipulates its own copy of the struct, which is thrown out at the end of the method and the original mypoint remains unmodified.

        private struct Point3D {
            public int X { get; set; }
            public int Y { get; set; }
            public int Z { get; set; }

            public override string ToString() => $"X:{X}, Y:{Y}, Z:{Z}";
        }

        static void Main( string[] args ) {
            var mypoint = new Point3D { X = 10, Y = 20, Z = 30 };
            Console.WriteLine( mypoint );
            addThree( ref mypoint );
            Console.WriteLine( mypoint );
        }

        /// <summary>
        /// Adds 3 to all coordinates in the Point3D
        /// </summary>
        private static void addThree( ref Point3D point ) {
            point.X += 3;
            point.Y += 3;
            point.Z += 3;
        }

The output shows that adding 3 to each of the structs's properties was not lost after executing the method

In this example manipulating the struct’s properties were not lost. This is because the struct was passed by reference. Instead of the struct’s contents being copied before being passed to the method as in the pervious example, the struct’s reference was passed to the method just as if it were a class. This can give you the best of both worlds, however will make your software somewhat less maintainable because it’s easy not to include the ‘ref’ when declaring the parameters. The C# compiler will not warn you of this mistake and would be difficult to track down.

It has an instance size under 16 bytes.

Before I found this StackOverflow question I could only speculate why this would be recommended. This recommendation is for nothing more than performance. 16 bytes is thought to be a sweet-spot for performance because structs will be copied by value. Because of this, every property in the struct needs to be copied. As the size of your struct grows, this becomes more work.

This recommendation should be taken with a grain of salt and should not be a deal-breaker for choosing struct. You should never make a design decision based-on performance without measuring. Test both ways and choose what is most important to you.

It is immutable.

This is suggested because structs are copied by value. Why is that significant? It has been said that mutable structs are evil. Structs look just like classes when they’re passed around and nothing will warn you (except maybe ReSharper) when you attempt to modify a struct that wasn’t passed by referenced or returned at the end of the method. This allows for easy-to-make, hard-to-catch mistakes, making your code less maintainable. If your structs are immutable, this mistake can’t be made!

It will not have to be boxed frequently.

I haven’t found a clear explanation of why this is suggested. However if I had to guess I would say it’s because boxing and unboxing value types is inefficient. If that’s true, then this suggestion seems to be along the same lines as “it has an instance size of under 16 bytes.” If your design benefits from the value-type semantics of struct, that’s what you should choose unless you decide something else is more important.

Structs are always allocated on the stack instead of the heap*

*No they’re not

Structs can indeed be allocated on the heap instead of the stack. The stack and heap are actually an implementation detail of C#. Nothing in the spec guarantees where an object will be allocated.

Using structs instead of classes for performance reasons is a nano-optimization

Just as Eric Lippert said here. Using a struct instead of a class for the sole-purpose of performance isn’t a good reason— I would even recommend that MSDN remove their third bullet point for when to use structs. The size of your struct does not matter; use a struct if you want struct semantics.

Deal-breakers for structs

There are a few legitimate reasons to not use a struct as well. First, you cannot create a hierarchy of types using struct. The moment you need to utilize inheritance it’s over for struct [ Pro C# ]. Second, you cannot define a default constructor. This means that it’s possible to create values of your struct that may be in a state you consider invalid. If this is a deal breaker for you then you’ll have to use classes.

Sources