Making it stick: Structs vs. Classes vs. Records in C# .NET - Understanding Key Differences
From Stickers to Structure: A Developer's journey through code structure and design
In the dimly lit Codevania Office, in the bustling town of Codeville, where algorithms buzzed like bees and keyboard strokes clicked like gentle rain, there lived a coder named Alex. Alex, like many of their fellow developers, had a deep obsession for the perfect office décor. Not just any plant, tree, lights, wall cheatsheets, curved 49-inch monitor, fancy mouse or keyboard would do; it had to be a symbol, a representation of their coding expertise.

And so, on a bright Monday morning, whilst on their break working remotely from home, Alex set out on a mission for two specific items: a unique sticker for their laptop and the perfect box for their desk.
The laptop sticker: A tale of Structs
Alex first found themselves at an old-fashioned little shop named “Struct's Stickers” The shopkeeper, a friendly soul with a passion for organisation, handed Alex a vibrant sticker book. “This,” she said with a smile, “is a Struct sticker, like a struct in .NET. Each sticker in this book is a value in itself. When you place a sticker somewhere, it's independent. There's no referencing back to this book. It's gone and no one else can have it or modify it.”
Alex was intrigued. “So, if I put a sticker on my laptop, it stays there as it is, no changes back here?” they asked.
“Exactly!” the shopkeeper replied. “It's simple, efficient, and self-contained. No overhead, no strings, just the value as it is. Just like a struct, it's about having a direct, value-type interaction.”

The desk box: A story of Classes
Next, Alex found themselves in a modern store called “Classy Boxes” The store manager, a tech-savvy individual, presented Alex with an interactive catalog of boxes. “This,” he explained, “is similar to a class in .NET. You see, each item here has a key. When you select a key, it doesn’t bring you the item directly. Instead, it references the item stored in our central repository, in our warehouse at the back.”
Alex nodded thoughtfully. “So, if the item changes in the repository, it changes for everyone who has the key?”
“That’s right,” the manager said. “It’s about shared references and connections. One update, and it's reflected wherever that key is used and for everyone sharing the key. For example, you might paint the box blue, but your partner who has a copy of the key, decides that the box looks better green. Next time you decide that you need the box, you will be surprised that the box is now green, and not blue.”
Alex's choice: A blend of both worlds
Returning to their desk, Alex placed the struct-like sticker book and the class-like catalogue side by side. The sticker book, a testament to independence and simplicity, and the boxes catalogue, a symbol of interconnectedness and shared states.
And so, Alex's desk became a little haven of .NET wisdom, a fine balance between the shared references of classes and the straightforward values of structs. A daily reminder that in coding, as in life, the beauty often lies in using the right approach for the right purpose.
Navigating Struct, Class, and Record in practice
In C#, a struct is like a sticker in a Sticker Book: simple and value-based; a class is like a Catalog with Keys: more complex, with reference-based items.
Why mastering these concepts
Understanding the concepts of classes, structs, and records is vital not only for writing excellent code, but also for interviewing at FAANG companies (Facebook, Amazon, Apple, Netflix, Google) and Microsoft, particularly for software engineering roles. Here's why:
Fundamentals of Object-Oriented Programming (OOP): Classes are a core component of OOP, a widely-used programming paradigm. A solid grasp of OOP principles is often expected in software engineering interviews.
Understanding data structures: Structs, often used for lightweight data structures, are part of the broader topic of data structures, which is a key area in technical interviews.
Language-specific knowledge: If you're interviewing for a position that specifically requires or prefers experience with .NET or languages like C#, demonstrating knowledge in these areas can be advantageous.
System design and performance considerations: Understanding when to use classes, structs, or records can play a significant role in designing your system or solve a problem with efficient and appropriate use of data structures, focusing on high-performance computing or optimisation.
Immutability and modern programming practices: With the advent of records in C#, awareness of immutable data types and their benefits in certain programming paradigms (like functional programming or concurrent systems) can be relevant.
These concepts are generally considered foundational for software engineering. Let's dive into each individual one next. For interview questions related to these concepts, go to Interview Questions in the Additional Insights section.
.NET: Understanding Structs and Classes - Part 1: Structs
Why use a Struct?
In .NET, a struct
(short for structure) is a value type that represents simple, small data structures. Unlike classes, structs are stored on the stack, which can make them more efficient for certain use cases. They are ideal when you need a lightweight object that doesn’t require the overhead of a class.
Consider the Sticker
struct:
struct Sticker
{
public string Image;
public string Description;
}
This Sticker
struct is a perfect example of when to use a struct in .NET. It's a simple object with two properties, Image
and Description
, and doesn't require the additional functionality that a class provides.
When to use a Struct?
Small and simple: Use structs for small data structures that have simple behaviour and don't need to inherit from other types or be a base for other types.
Value semantics: If you want your type to exhibit value-like behaviour, where each instance is independent and copying creates a completely separate instance, a struct is a good choice.
Performance considerations: For scenarios where performance is critical and you're dealing with a large number of instances, structs can be more efficient as they are allocated on the stack.
How to use a Struct in .NET
Using the Sticker
struct is straightforward:
Initialisation: You can create an instance of a struct without a
new
operator, but usingnew
ensures all fields are initialised.
Sticker mySticker = new Sticker { Image = "smiley.png", Description = "Smiley Face" };
Value type behaviour: Remember, structs are value types. Assigning a struct variable to another one copies the struct entirely, not just the reference.
Limitations: Structs don’t support inheritance and have some limitations compared to classes. They can implement interfaces but cannot derive from another struct or class.
In the next part, we'll delve into classes using the Catalog with Keys
example, exploring their more complex and interconnected nature in .NET.
.NET: Understanding Structs and Classes - Part 2: Classes
Why use a Class?
A class
in .NET is a reference type that can represent more complex data structures and behaviours than structs. Classes are stored on the heap, which allows for more flexibility and functionality, such as inheritance, polymorphism, and more sophisticated memory management.
Consider the Catalog
and Box
classes:
class Catalog
{
private Dictionary<string, Box> boxes = new Dictionary<string, Box>();
public void AddBox(string key, Box box)
{
boxes[key] = box;
}
public Box GetBox(string key)
{
return boxes[key];
}
}
class Box
{
public string Name;
public string Type;
}
These classes exemplify the more complex and interconnected nature of classes in .NET.
When to use a Class?
Complex structures: Use classes for more complex data structures that require additional functionality like inheritance or polymorphism.
Reference semantics: If you need reference-type behaviour, where different instances can refer to the same object, a class is the appropriate choice.
Extensibility: Classes are ideal when you need to extend the functionality, such as adding new methods or using inheritance.
How to use a Class in .NET
Using the Catalog
class involves several steps:
Initialisation: Classes must be instantiated using the
new
keyword.
Catalog myCatalog = new Catalog();
Reference type behaviour: Remember, classes are reference types. When you assign a class instance to another variable, both variables refer to the same object instance.
Methods and properties: Classes can have methods, properties, and events. This allows for encapsulation and interaction with the object's data.
Box myBox = new Box { Name = "Gadget", Type = "Electronics" };
myCatalog.AddBox("001", myBox);
Inheritance and polymorphism: Classes can inherit from other classes, allowing for code reuse and polymorphic behaviour.
The Catalog
and Box
classes demonstrate the advantages of using classes in .NET for complex, interconnected data structures. While structs are excellent for simple, value-type objects, classes provide the necessary infrastructure for more sophisticated and flexible programming needs.
In C# there is one more related concept which we will have a look at next: Records, a feature that was added in a more recent version of the language.
.NET: Understanding Structs, Classes, and Records - Part 3: Records
Additionally to Structs and Classes, it is worth talking about Records. Microsoft introduced records primarily to address specific needs in modern programming that weren't fully catered to by existing constructs like classes and structs.
Why use a Record?
A record in .NET is a reference type like a class, but it's primarily intended for immutable object scenarios. Introduced in C# 9.0, records provide a simpler syntax for creating data-centric types with value-based equality semantics. This makes them particularly suitable for data transfer objects (DTOs), read-only data models, and other scenarios where data immutability is critical.
When to use a Record?
Immutability: Records are ideal when working with data that shouldn't change after creation. This immutability ensures data consistency and thread safety.
Value-Based equality: Use records when you need to compare objects based on their values rather than their references.
Concise syntax: Records offer a more concise syntax for creating types that are primarily used to store data.
How to use a Record in .NET
Here's an example of a record in C#:
record Box(string Name, string Type);
record Box
{
public string Name { get; init; }
public string Type { get; init; }
}
Using a record involves:
Initialisation: Records can be instantiated similarly to classes, but with
init
properties allowing for immutable data.
var box = new Box { Name = "Gadget", Type = "Electronics"};
Value-Based equality: When you compare two record instances, the comparison is based on the values of their properties.
var anotherBox = new Box { Name = "Gadget", Type = "Electronics" };
bool areEqual = box == anotherBox; // true, because values are the same
Implementing equality is the primary distinction compared to a class. Unlike records, determining if two instances of a class are equal typically involves defining properties and handling equality manually. This often requires more code and careful consideration to ensure correctness. Please refer to Implementing equality in a Class in the Additional Insights section for an example of how this is achieved.
With-Expressions: Records support with-expressions which enable you to create a new record while changing some properties.
var updatedBox = box with { Type = "AI" };
Immutability: Once a record is created, its data cannot be changed, making it safe to share across different parts of an application without the risk of unintended modifications.
Records in .NET provide an efficient way to handle immutable data structures with value-based equality. They simplify the creation of data models and ensure data consistency, making them an essential tool for modern .NET development.
This three-part sections on structs, classes, and records in .NET aims to provide a clear understanding of these fundamental types. Each part offers insights into when and how to use these constructs, helping developers to make informed decisions in their .NET coding journey.
Conclusion
I hope that my little story will make it easier to remember what data structure to use and when. Just think about the sticker on your laptop or picture on the wall, versus the key to a shared box that contains the toys you share with your best friends. I hope this analogy will make it stick.
In .NET programming, the choice between struct, class, and record depends on your specific needs. Structs are your go-to for lightweight data storage, classes are ideal for more complex entities with behaviours, and records shine in scenarios requiring immutability and consistency. Through these examples, you can better understand when and why to use each construct, paving the way for becoming an even more awesome, agile coder and software engineer.
Until our next coding story, I am wishing you happy coding!
Additional Insights
Too much information can be overwhelming, particularly for those at the beginning of their coding journey. Recognising this, I aim to keep my articles concise yet informative, ensuring they cover the essentials.
The information provided so far aims to be informative enough. In this 'Additional Insights' section, I will further expand on the initial topics and ideas of the article.
Implementing equality in a Class
In C#, to determine equality for two objects of a class, you typically need to override the Equals
method and, for best practices, the GetHashCode
method as well. This allows you to define what it means for two instances of your class to be considered "equal." By default, classes in C# compare references, so two distinct instances are not equal even if their properties have the same values.
Here's how you could implement this for the Box
class:
class Box
{
public string Name { get; set; }
public string Type { get; set; }
public Box(string name, string type)
{
Name = name;
Type = type;
}
public override bool Equals(object obj)
{
// Check for null and compare run-time types.
if (obj == null || GetType() != obj.GetType())
{
return false;
}
Box other = (Box)obj;
return Name == other.Name && Type == other.Type;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, Type);
}
}
// Usage Example
var box1 = new Box("Gadget", "Electronics");
var box2 = new Box("Gadget", "Electronics");
bool areEqual = box1.Equals(box2); // This will be true
Key Points:
Equals method: It's overridden to compare the properties (
Name
andType
in this case) of the twoBox
objects. If the properties match, the two objects are considered equal.GetHashCode method: This is overridden to provide a hash code based on the same properties used for equality. This is important for objects used in collections like hash tables.
Reference equality: Without these overrides,
box1.Equals(box2)
would returnfalse
because, by default, equality for reference types is based on reference equality (i.e., whether both references point to the same object in memory).
Implementing these methods gives you control over how instances of your class are compared and deemed equal, which can be crucial for certain types of applications, especially those involving collections or comparisons of object instances.
Understanding Structs, Classes, and Records across different languages
Different languages implement these constructs in unique ways, reflecting their individual paradigms and design philosophies. I hope that this addition will show that is not difficult to learn a new language, once you know the basics. Let's delve into how various languages, including C++, Java, Swift, F#, Kotlin, JavaScript, TypeScript, Rust, Scala and Python handle them:
C++:
Struct: Similar to classes but default to public access. Used for defining custom data types.
Class: Fundamental to C++'s object-oriented programming, offering features like encapsulation and inheritance.
Record: No native record; classes and structs serve similar purposes.
Java:
Struct: Not present; classes are used instead.
Class: Essential for Java’s object-oriented approach.
Record: Standardized in Java 16, offering a succinct way to create immutable data-carrier classes.
Swift:
Struct: Common for custom data types, especially when no inheritance is needed.
Class: Supports object-oriented features like inheritance.
Record: Not available; similar functionality achieved through structs and classes.
F#:
Struct: Used similarly to C# for value types.
Class: Less common than in C#, but available for object-oriented programming.
Record: A core feature, providing simple, immutable data containers.
Kotlin:
Struct: Not available; uses classes and data classes instead.
Class: Integral to Kotlin, with modern object-oriented features.
Record: Data classes in Kotlin are akin to records, facilitating the creation of immutable data-holders.
JavaScript:
Struct: Not applicable; JavaScript uses objects and prototypes.
Class: Introduced in ES6, providing a more traditional object-oriented syntax over JavaScript's existing prototype-based inheritance.
Record: Not a native concept; objects and arrays are typically used for similar purposes.
TypeScript:
Struct: TypeScript does not have structs; it uses interfaces and classes.
Class: Similar to JavaScript but with added type safety and features like interfaces.
Record: TypeScript provides a
Record
utility type, offering a way to construct objects with a common value type.
Rust:
Struct: Central to Rust, used for creating custom data types with an emphasis on safety.
Class: Rust does not have classes; it uses structs and traits for similar functionalities.
Record: Not explicitly present; structs along with pattern matching provide similar capabilities.
Scala:
Struct: Scala doesn't have a direct equivalent of a struct; case classes are somewhat similar.
Class: Fundamental in Scala, supporting both object-oriented and functional programming paradigms.
Record: Scala 3 introduces record-like structures through case classes with the
derives
keyword.
Python:
Struct: Not available as a specific type; classes are used for similar purposes.
Class: A primary feature in Python, used extensively for various types of data modelling.
Record: Python uses named tuples and data classes which can be seen as equivalents to records.
This selection encompasses languages that are widely used in various domains of software development, ranging from system programming to web and mobile applications, and represent a mix of paradigms from object-oriented to functional programming.
This diversity helps in understanding how different languages approach similar concepts, providing valuable insights for developers who work across multiple programming ecosystems or are curious about language-specific implementations of structs, classes, and records.
Interview Questions
We have explored several concepts in this article. Remember, reading is one thing, but doing is another. Actively engaging with the material reinforces it in our long-term memory, aiding not just in coding but also in interview situations. I would recommend examining the sample interview questions below and revisiting them after a few days for better retention.
Basic questions
What is the difference between a class and a struct in .NET?
This question tests basic understanding of the key differences between classes and structs.
Can you explain what immutability is and how records in C# support it?
This is to understand the candidate's knowledge of immutability and the use of records.
How do you implement inheritance in C#? Can structs inherit from other structs or classes?
A question to assess understanding of inheritance in C# and the limitations related to structs.
What are the default access modifiers for classes and structs in C#?
Tests knowledge of C# specific syntax and defaults.
Intermediate questions
When would you choose to use a struct over a class in a .NET application?
This evaluates the candidate's ability to make decisions about when to use value types (structs) versus reference types (classes).
Can you give an example of a situation where a record type would be more suitable than a class or struct in C#?
Looks for understanding of records' use cases.
Explain the concept of a constructor in a class. How does it differ in a struct?
Tests deeper understanding of object Initialisation in both classes and structs.
What is the significance of the ‘readonly’ keyword in structs in C#?
Checks knowledge about enhancing immutability in structs.
Advanced questions
Discuss the memory allocation differences between classes and structs in .NET and how it impacts performance.
A more in-depth question about the underlying workings of .NET's memory management.
How does garbage collection work with classes and structs in .NET?
Tests understanding of .NET’s garbage collection mechanism and how it applies to different types.
Can you explain the concept of boxing and unboxing in .NET? How does it relate to structs and classes?
Evaluates understanding of an important aspect of .NET performance optimization.
In what scenarios might you use a record with a with-expression in C#?
Looks for knowledge of newer C# features and their practical applications.
Scenario based questions
Given a scenario where you need to represent a lightweight, frequently used data point (like a 2D coordinate), would you use a class or a struct? Explain your choice.
A practical application question to assess decision-making skills.
If you were designing an API response model for a high-traffic web service, would you use classes, structs, or records for the data models? Justify your answer.
Tests application of knowledge in a real-world scenario, focusing on performance and design considerations.
These questions can provide a comprehensive overview of a developer's understanding and application of classes, structs, and records in .NET and general programming contexts.