c# init vs set: Choosing the Right Property Accessor
c# init vs set: Understand the difference between C# init and set accessors, when to use each, and how init enables immutable object creation with object initializers.
c# init vs set requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you create a C# class, the choice between init and set accessors determines when a property can be assigned. The init accessor, introduced in C# 9, allows a property to be set only during object initialization, while set allows assignment at any time after the object is created. This difference directly affects how you design immutable objects and enforce data integrity.
Consider a simple Product class. With a traditional set accessor, you can modify properties anytime:
public class Product { public string Name { get; set; } public decimal Price { get; set; } }
This allows code to create a product and later change its Name or Price, which may be desirable for mutable entities. However, for value objects or DTOs where values should not change after creation, set introduces accidental modification risks.
The init accessor restricts assignment to the initialization phase:
public class Product { public string Name { get; init; } public decimal Price { get; init; } }
With init, you can still use object initializers:
var product = new Product { Name = "Coffee Maker", Price = 49.99m };
But after product is constructed, any attempt to assign product.Name produces a compile-time error. This enforces immutability without requiring a constructor with many parameters.
How Object Initializers Interact with init and set
Object initializers are a common way to construct objects concisely. They work with both init and set, but they behave differently regarding the object's lifecycle. With set, you can assign properties in the initializer and later reassign them. With init, the initializer is the last place an assignment is allowed.
The C# compiler treats init accessors specially: they can Only be called from an object initializer, a constructor, or from within the same type (e.g., in a with expression). The following code fails to compile:
var product = new Product { Name = "Espresso Machine" }; product.Name = "Drip Filter"; // CS8852: Init-only property can only be assigned in an object initializer
This compile-time enforcement prevents accidental modifications and makes the intent clear to other developers.
When to Use init Over set
The decision between init and set hinges on whether you need mutability after construction. Use init when:
- The property represents a value that logically belongs to the object's identity.
- You want to enable object initializers without exposing setters.
- You need to create immutable records or DTOs.
- You want to support
withexpressions to create modified copies.
Use set when:
- The property must be updated frequently (e.g., a status field).
- The object is an entity that changes over time.
- You are implementing a class that requires setters for data binding or serialization.
For example, a Customer entity with an Email that can change over time should use set. A Coordinates class representing a point should use init to keep the values fixed.
Defining init-Only Properties and Constructors
You can also assign init-only properties from within a constructor. This is useful when you want to validate parameters before assignment:
public class Product { public string Name { get; init; } public decimal Price { get; init; } public Product(string name, decimal price) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Name is required.", nameof(name)); if (price < 0) throw new ArgumentOutOfRangeException(nameof(price), "Price cannot be negative."); Name = name; Price = price; } }
In this case, the constructor sets the properties, and because they are init, they cannot be changed later. This pattern ensures that validation logic is centralized and invariants are maintained.
Unlike a set accessor, an init accessor can be used in a constructor, but not in arbitrary methods of the class. This means you cannot write a method that modifies a property, which is exactly the enforceability you want for immutable types.
Compatibility and Older C# Versions
init accessors are a C# 9 feature, which means they require .NET 5 or later, or a target framework that supports C# 9. If you are working with .NET Core 3.1 or .NET Framework, you cannot use init. In those contexts, you must use set or rely on constructor parameters with private setters.
Even with private setters, you can approximate immutability but lose the ability to use object initializers. A common workaround is to define a constructor and expose properties with { get; } only, but that forces you to list all parameters in the constructor, which becomes awkward for many properties.
For modern .NET projects, init is the preferred way to achieve immutability without sacrificing object initializer syntax.
Comparing init, set, and get-Only Properties
To clarify the differences, consider the following class:
public class Example { public string ReadWrite { get; set; } public string ReadOnly { get; } public string InitOnly { get; init; } public Example(string readOnly) { ReadOnly = readOnly; } }
ReadWritecan be assigned at any time.ReadOnlycan be assigned only in the constructor.InitOnlycan be assigned in the constructor or object initializer, but not after construction.
The following table summarizes the assignment capabilities:
| Accessor | Constructor | Object Initializer | After Construction |
|---|---|---|---|
set | Yes | Yes | Yes |
init | Yes | Yes | No |
get only | Yes | No | No |
How init Enables with Expressions and Records
The init accessor is a fundamental enabler for with expressions, which allow non-destructive mutation of objects. Records, introduced in C# 9, use init-only properties by default to provide value equality and copy semantics.
public record Product(string Name, decimal Price); var original = new Product("Coffee Maker", 49.99m); var updated = original with { Price = 59.99m };
The with expression creates a copy of original, applies the specified property changes, and returns a new Product instance. This is only possible because the properties are init-only; if they had set accessors, the copy would still be mutable, and the semantics would be less clear.
If you prefer a class over a record but still want with support, you can manually implement a With method that returns a new instance, using init-only properties to assign the modified values:
public class Product { public string Name { get; init; } public decimal Price { get; init; } public Product With(string? name = null, decimal? price = null) { return new Product { Name = name ?? Name, Price = price ?? Price }; } }
This pattern is most useful when you want to preserve the class semantics while gaining immutable updates.
Potential Pitfalls and How to Avoid Them
One common mistake is using init on collections. If you expose a mutable collection like List<T> with an init accessor, the collection itself can still be modified. The init only restricts the reference assignment, not the contents of the collection. To achieve truly immutable collections, use IReadOnlyCollection<T> or IReadOnlyList<T> in combination with init.
Another pitfall is serialization frameworks that rely on setters. Some serializers (e.g., older versions of Newtonsoft.Json or custom serializers) expect a set accessor to deserialize properties. While Newtonsoft.Json supports init-only properties from version 13.0.1, other serializers may not. Verify your serializer's capabilities before adopting init in a DTO layer.
Additionally, mixing init and set accessors in the same class can lead to confusion. If you have a class with some mutable and some immutable properties, document the intent clearly. A better approach is to separate concern into distinct types: one immutable value object and one mutable entity wrapper.
Performance and Memory Considerations
Regarding performance, the difference between init and set is negligible at runtime. The init accessor is a compile-time attribute; the compiled IL is similar to a set accessor, but with metadata annotations. There is no additional memory overhead or runtime cost. The primary performance benefit comes from immutability, which makes your code safer in multi-threaded scenarios by reducing the need for defensive copies.
When you use with expressions, you do allocate a new object, which has a minor cost. If you are performing thousands of such operations per second, measure the impact, but in typical application code, the overhead is negligible. The safety gains from immutable objects usually outweigh the cost.
Making the Right Choice for Your C# Code
The decision between init and set is a design choice that affects the API surface of your types. Use init by default for properties that represent values that should not change after creation. Use set only when mutability is a requirement. This approach leads to more predictable code and fewer bugs.
For modern C# codebases targeting .NET 5 or later, adopting init for value-like types is a straightforward way to enforce immutability. For older runtimes, you can still use private setters for the same effect, but you lose object initializer convenience.
Consider a User class. If Username and Email are immutable identifiers, use init. If LastLoginAt changes frequently, use set. By applying this rule consistently, you create a clear contract for how each property can be modified, making the code easier to reason about and maintain.
Ultimately, c# init vs set is not about which is better; it's about understanding the lifecycle of your properties and choosing the accessor that matches your data's intended mutability.