Back to Blog
C#

C# Class vs Struct vs Record: Choosing the Right Type

c# class vs struct vs record: Learn the differences between class, struct, and record in C#: value vs reference semantics, equality, and performance, to choose the rig...

C#StructsRecordsValue SemanticsReference TypesEquality
Diagram comparing class, struct, and record in C# showing value and reference semantics.

When you declare a type in C#, the choice between class, struct, and record affects how values are copied, compared, and stored. The decision is not purely stylistic: it changes runtime behavior, memory layout, and the semantics of equality. This article compares c# class vs struct vs record and gives concrete guidance for choosing the right one.

The Core Difference: Reference vs Value Semantics

A class is a reference type. When you assign a class instance to another variable, both variables point to the same object on the heap. Modifying the object through one variable is visible through the other. A struct is a value type. Assignment copies the entire value, so each variable holds an independent copy. A record is a reference type by default (record class), but you can also declare a record struct, which is a value type. The choice of reference vs value semantics is the most fundamental difference because it affects how data flows through your code.

Consider this example:

public class PointClass { public int X; public int Y; } public struct PointStruct { public int X; public int Y; } var c1 = new PointClass { X = 1, Y = 2 }; var c2 = c1; c2.X = 10; Console.WriteLine(c1.X); // 10 var s1 = new PointStruct { X = 1, Y = 2 }; var s2 = s1; s2.X = 10; Console.WriteLine(s1.X); // 1

The class variable c2 references the same object as c1, so changing c2.X also changes c1.X. The struct variable s2 is a copy, so changing it has no effect on s1. This behavior is central to deciding which type to use.

Syntax and Declaration Differences

The syntax for declaring each type is similar, but there are important nuances. A class and a struct use the same member syntax. A record uses a special syntax that provides value-based equality and a concise way to define immutable data.

public class PersonClass { public string Name { get; init; } public int Age { get; init; } } public struct PersonStruct { public string Name { get; init; } public int Age { get; init; } } public record PersonRecord(string Name, int Age); public record struct PersonRecordStruct(string Name, int Age);

The record declaration with positional parameters automatically generates properties, a constructor, and equality members. For a record struct, the same applies but with value-type semantics. You can also write a record with a more traditional property syntax if you need additional members.

Equality Behavior: Why Records Stand Out

Equality is where records differ most significantly from classes and structs. By default, a class uses reference equality: two instances are equal only if they point to the same object. A struct uses value equality, comparing each field. However, struct equality is implemented via ValueType.Equals and can be slow because it uses reflection for non-primitive fields. Records override equality to compare all public properties, and they also implement GetHashCode consistently.

var p1 = new PersonRecord("Alice", 30); var p2 = new PersonRecord("Alice", 30); Console.WriteLine(p1 == p2); // True var c1 = new PersonClass { Name = "Alice", Age = 30 }; var c2 = new PersonClass { Name = "Alice", Age = 30 }; Console.WriteLine(c1 == c2); // False

Records also support with expressions to create copies with modified properties, which is convenient for immutable data. This is not available for classes or structs without writing extra code.

When to Use a Class

Classes are the default choice for most application objects. Use a class when you need inheritance or polymorphism, when the object is large and copying would be expensive, or when you want to share a mutable object across parts of your code. Classes are also appropriate when the identity of the object matters more than its content, such as an entity in a domain model.

When to Use a Struct

Structs are best for small, immutable values that are frequently created and discarded. Because they are value types, they can be allocated on the stack or inline in arrays, reducing heap allocation and garbage collection pressure. However, copying a large struct is expensive, and passing it by value repeatedly can degrade performance. Use a struct for data that is conceptually a single value, such as a coordinate, a range, or a small identifier. Avoid mutable structs because they lead to subtle bugs when copies are modified unintentionally.

When to Use a Record

Records are ideal for data transfer objects, API responses, and other immutable data containers where value equality is useful. The built-in equality and with expressions make it easy to work with immutable data. Use a record class when you need reference semantics and inheritance. Use a record struct when you want value semantics with the concise record syntax, for example, for small, frequently used values that benefit from stack allocation.

Performance and Memory Considerations

The runtime cost of each type depends on how it is used. Classes allocate on the heap and are managed by the garbage collector. Creating many short-lived class instances increases GC pressure. Structs are often allocated inline, which can reduce allocation overhead, but passing them by value copies the entire structure. Large structs can cause more copying than the allocation they save. Records, whether class or struct, have similar performance characteristics to their underlying type, but the generated equality code is more efficient than the default struct equality because it compares fields directly.

A common performance mistake is using a large struct in a collection or passing it by value repeatedly. For example, a struct with many fields will be copied each time it is passed to a method, leading to memory traffic. In contrast, a class only copies the reference. Measure the actual impact in your application rather than assuming a struct is always faster.

Practical Decision Criteria

To choose between class, struct, and record, ask these questions:

  • Do you need inheritance or polymorphism? Use a class.
  • Is the data immutable and do you want value equality? Use a record.
  • Is the value small, frequently created, and conceptually a single value? Consider a struct.
  • Do you need to share a mutable object across components? Use a class.
  • Do you need value semantics with the concise record syntax? Use a record struct.

These criteria are not absolute. For example, a record class can be used when you want value equality but still need reference semantics for large data. A struct can be used for performance-sensitive code, but only if the size is small and copying is not a bottleneck.

Common Pitfalls and Edge Cases

One pitfall is mutable structs. If a struct has mutable properties, it is easy to accidentally modify a copy, leading to confusion. Prefer immutable structs with readonly members. Another issue is that records with mutable properties are allowed, but they break the value equality expectation because changing a property changes the hash code. Use init accessors to keep records immutable.

Another edge case is inheritance: records can inherit from other records, but record structs cannot inherit. If you need inheritance, use a record class. Also, note that the with expression works on records and record structs, but not on classes or structs. If you need that functionality, a record is the right choice.

Finally, consider the impact on API design. A public method that accepts a struct receives a copy, so changes to the parameter do not affect the caller's variable. A class parameter is a reference, so modifications inside the method are visible outside. This distinction is important when designing method contracts.

c# class vs struct vs record: Practical Usage and Code Examp | RYUSLOG DEV