C# - Class vs Struct vs Record
Hero image credit: Photo by Christina Morillo
One of the basic concepts in C#, as with most object oriented languages, is that of the data type. Software development is all about manipulating data in one way or another. Data types are how you hold that data in your software application. A data type can represent a very simple piece of data, like a single number or a single character of text. Or it can be something more complex, like a sentence, or a set of coordinates like latitude and longitude, or a group of data that represents a person, like first name, last name, height, hair color, etc.
Primitive and Reference Types
Every data type in C# falls into 2 general groups: primitive and reference. The biggest difference between the two is how C# handles these objects (i.e. how it stores, manipulates, and retrieves these pieces of data), which is outside the scope of this discussion.
A primitive data type is used to represent a single, simple piece of data. In C#, you generally have the following primitive types:
- Integer types: These represent a single whole number that does not contain a decimal point
- Floating-Point types: These represent numbers that have decimal points
- Boolean: A true/false or yes/no kind of value
- Character: A single character of text
A reference type of data holds a more complex object. C# has a couple of built-in reference types:
- String: a collection of 0 or more characters that are taken together
- Dynamic: An object which isn’t defined at compile time and may represent different kinds of things while the application is running
- Object: The base type upon which all other reference types are constructed
The vast majority of what you will work with in C# is one of three data types: struct, class, and record. All three of these data types are derived from the Object type. Each one contains a group of data and functionality related to it. There are a number of other data types, like enum, but most of those are for specific and clear purposes, so we won’t go in to those here.
Struct
Generally, a struct is the simplest of the three data types we’re looking at, and is used to represent smaller groups of data that don’t have a lot of related functionality. The biggest “gotcha” with struct as opposed to the other two types is that structs are not passed around between functions. What this means is that if a struct is passed as a parameter into a function, the original instance of the struct is not passed in. Instead, by default, a copy is made and that copy is given to the function to work with. Any updates to the data in the copy of the struct do not get reflected back to the original instance.
It’s important to say “by default” here, because over the years, the differences between struct and class have largely been erased via various modifiers. You can indeed pass a reference to a struct when passing a struct to a function by using the “ref” parameter modifier. When you do that, it instead passes a reference to the original instance and, in that case, any value updates do get applied to the original, not a copy. Check the docs for more on how to do this. This can also lead to performance issues if you are passing a lot of structs around since each time it gets passed into another function, a new copy gets made.
A struct is declared like so:
public struct GeoCoordinates {
public GeoCoordinates (double latitude, double longitude){
Latitude = latitude;
Longitude = longitude;
}
public double Latitude {get;set;}
public double Longitude {get;set;}
}
A struct cannot have a parameterless constructor:
public struct GeoCoordinates {
public GeoCoordinates (){} // invalid
public double Latitude {get;set;}
public double Longitude {get;set;}
}
A struct by definition has a built-in parameterless constructor, which sets all parameters to their default values.
Field initializers are also not allowed in structs:
public struct GeoCoordinates {
public double Latitude = 0.0000; // invalid
public double Longitude {get;set;}
}
Finally, a struct has a strict size limit of 16 bytes of memory. It cannot be larger than that. So, a struct is not ideal for large, complex objects.
Class
For anything with any real complexity, or for any situation where you want to ensure that the default functionality is to update the values in the original instance of an object when it gets passed into a function, use a class.
A class is defined as follows:
public class GeoCoordinates {
public GeoCoordinates (double latitude, double longitude){
Latitude = latitude;
Longitude = longitude;
}
public double Latitude {get;set;}
public double Longitude {get;set;}
}
As you can see, it’s pretty much identical to how a struct is defined. But there are differences in what you can do, and in how C# handles instances of the object. For example, by default, a class is passed to a function by reference. That means the original object is being “pointed to” and any changes to the data in that object will update the original. It isn’t a copy. Again, I say by default, because just as you can pass a struct by reference, you can also pass a class by value, if you need to for some reason.
A class will also allow you to set a parameterless constructor. You can use field initializers. And a class has no real size limit aside from the limits of the system it’s running in.
A class will also let you inherit from another class:
public class GeoCoordinates {
public GeoCoordinates (double latitude, double longitude){
Latitude = latitude;
Longitude = longitude;
}
public double Latitude {get;set;}
public double Longitude {get;set;}
}
public class Address:GeoCoordinates {
public Address(){
Latitude = 0.000; // Even though it isn't defined in Address it can use the Latitude from the GeoCoordinates base class
Longitude = 0.000; // Even though it isn't defined in Address it can use the Longitude from the GeoCoordinates base class
}
public string AddressLine1 {get;set;}
public string AddressLine2 {get;set;}
public string City {get;set;}
public string State {get;set;}
public string Zip {get;set;}
}
While some languages allow for multiple inheritance, where a class can inherit the properties and functions of more than one parent class, C# only allows for single inheritance, where a class can inherit from only a single parent class.
Record
A record (or record class), is a special modifier for a class, which by default is immutable. What this means is that once it is initialized, the values of the properties in a record cannot be changed. There are a number of reasons why you might do this, but the two primary reasons are to protect the values from being changed when they shouldn’t be, and performance.
Again there’s that pesky “by default” phrase. A record can be set to be mutable, but the default out of the box functionality is to be immutable.
A record is declared as follows:
public class GeoCoordinates {
public GeoCoordinates (double latitude, double longitude){
Latitude = latitude;
Longitude = longitude;
}
public double Latitude {get;} // omit the setter
public double Longitude {get;} // omit the setter
}
This can also be simplified:
public class GeoCoordinates {
public GeoCoordinates (double Latitude, double Longitude){}
}
The properties will be implicit in this case.
A record can inherit from another record, but it cannot inherit from a class, and vice-versa. You can also create a record struct object, which is basically an immutable version of a struct rather than a class.
When To Use Each
As you can see, the lines between struct and class and record have gotten somewhat muddled and blurred over the years with each iteration of C#, to the point where there is an excessive degree of overlap between the three types. However, developers should generally stick to the original intent for each.
- If you need a small and simple object without a lot of functionality and which doesn’t get passed around a lot, use a struct. Especially if the data in the object is made up of only primitive data types.
- If you need a more complex object and you want to ensure that value updates are applied to the original object instance and not a copy, use a class
- If you need an immutable object of any structure, use a record