It's dangerous to code alone! Take this.

Can Structs Have Methods?

I just got this question in Discord:

I’m debating whether to make a class or a struct. I know I’m going to need a few methods for what I’m doing. If I have methods, is it okay to put them in the struct? Should I put the methods in a separate static helper class instead? Should the whole thing just be a class?

From a syntax standpoint, structs allow methods. So the answer to the (unasked) question, “Can structs have methods?” is a clear “Yes.”

Of course, the real question isn’t “Can I?” but “Should I?”

My reaction, when I saw this question, was to go look at some examples from the Base Class Library. So I went to MSDN and found some common structs to look at.

Take System.Guid, for example. The first thing to note is that Guid has methods. Lots of them, in fact. These fall into a few categories:

  • Equality: Guid wants to ensure that two identifiers with the same data are considered equal, so it overrides Equals and GetHashCode, and also provides a definition for the == and != operators.
  • Static Factory Methods: The static methods Parse, ParseExact, TryParse, TryParseExact, and NewGuid are all methods for creating a new GUID from data in another format. Arguably, static methods are in a different category than what this question was asking for, but it is useful to see that data conversion methods are extremely common for structs.
  • Conversion to Other Types: The methods ToByeArray, TryFormat, TryWriteBytes, and even ToString are all methods intended to convert a GUID to other formats.
  • Comparisons: The last method, CompareTo, is used to order GUIDs.

This struct makes heavy use of methods, but it is interesting to note the categories they fall in. These seem to land in two buckets:

  1. Converting between different data representations (the factory and conversion methods).
  2. Answering basic questions about the data (equality and comparisons).

Let’s look at another one at random: System.Range. Range is used by the compiler to support range-based access to a collection, such as numbers[1..5]. It is composed of two System.Index values, each of which is a pairing of an int and bool, where the int represents the raw number and the bool represents if the index is from the front or back of the array.

Range’s methods seem to fit the same pattern as Guid:

  • Data Access: Technically, the Start and End properties are methods that retrieve the raw data from in the struct. In fairness, while these are methods, they are also meant to look like field access. So you might (reasonably) count these as data, not methods with behavior.
  • Static Factory Methods: EndAt and StartAt both create new Range values that only have one end (contrasted with the construtor, which expects both start and end indices).
  • Equality: GetHashCode and Equals make an appearance here as well.
  • Conversion to Other Types: The ToString method gives you a text representation of the range.
  • The Interesting One: The last method Range has is GetOffsetAndLength, and is the most interesting of Range’s methods.

GetOffsetAndLength is the method with the most “behavior” of them all. It isn’t just simple data access, nor is it about conversion. But it still fits into Category #2 above: Answering basic questions about the data.

We could keep looking at structs in the Base Class Library, but we’ll stop here for now.

I think what we’ve seen gives us some great rules for when it is okay to put methods into a struct:

  1. Converting between different data representations (constructors, factory methods, Parse, ToWhatever, etc.).
  2. Answering questions about the data (“Are these two things equal?”, “How do I order these?”, and even calculations that use the data.)
  3. Data access (usually in the form of properties).

If your methods go beyond these–if the focus is on behavior and not data–then perhaps you want a class after all.