Ever found yourself wondering why C# uses { get; set; } instead of just making variables public? It’s one of those small syntax details that makes a big difference in how your code handles data and prevents unwanted changes. If you’ve faced confusion around controlling access to class variables, then you might want to learn about get and set.
In this guide, we’ll break down how these accessors work and why understanding them is key to writing safer, cleaner C# code.
Why Getters and Setters Matter?
When you build a class in C#, your goal isn’t just to store data but to keep that data consistent and protected. Imagine you’re writing a game where each player has health points. You could make a public variable like this:
public int Health;
At first glance, it looks simple enough. But what if some part of your code sets Health = -50 by mistake? That breaks the logic of your game instantly. This is the problem properties solve.
Getters and setters let you control how values are assigned and retrieved. They act like small checkpoints for your data. With them, you can decide what’s valid, what isn’t, and what should happen when a value changes.
Here’s a cleaner and safer version of that same example:
public class Player
{
private int health;
public int Health
{
get { return health; }
set
{
if (value >= 0)
health = value;
}
}
}
Now the player’s health can never go below zero. The get part provides the value when you need it, and the set part allows you to define what constitutes acceptable input. From the outside, it still looks like a normal variable, but inside, you’re in control.
This is the core idea behind getters and setters. They make your code safer, easier to debug, and more predictable as your project grows.
Manual vs. Auto-Implemented Properties
When C# first introduced properties, every property had to be written manually with a private variable behind it. This variable, known as a backing field, actually stores the data. The property acts as a gateway, deciding how that field is accessed or modified.
Let’s look at a simple example:
public class Student
{
private int marks;
public int Marks
{
get { return marks; }
set
{
if (value >= 0 && value <= 100)
marks = value;
}
}
}
Here, marks is the private field that stores the value. The property Marks controls how it is set. If someone tries to assign a value outside the range of 0 to 100, the setter simply ignores it.
This kind of property is called a manual property because you write the full get and set logic yourself. It’s useful when you want control. Maybe you want to validate input, trigger an update, or log changes whenever the value changes.
But if your property doesn’t need any special logic, you can let the compiler handle that work for you. This is where auto-implemented properties come in.
public class Student
{
public int Marks { get; set; }
}
This line performs the same function as the previous version, except that the private field is no longer visible. The compiler automatically creates it in the background. You can still read and assign values just like before.
You can also set a starting value right inside the property:
public int Marks { get; set; } = 50;
Controlling Access to Your Data
Not every piece of data in your program should be open for change. Some values should be visible to other parts of your code but protected from being modified. C# lets you fine-tune this control directly inside your property by changing how the get and set accessors behave.
Here’s a simple example:
public class User
{
public string Username { get; private set; }
public User(string name)
{
Username = name;
}
}
In this case, anyone can read the Username, but only the class itself can change it. This is done by marking the set accessor as private. It’s a neat way to protect important data from accidental changes outside the class.
Now, sometimes you want a value that’s set once and never changed again. That’s where read-only properties come in:
public class Product
{
public int Id { get; }
public Product(int id)
{
Id = id;
}
}
Here, Id can only be assigned when the object is created. After that, it’s fixed.
In newer versions of C#, there’s another useful option called init. It behaves like set, but it only works during object creation or initialization:
public class Profile
{
public string Email { get; init; }
}
That means you can write:
var profile = new Profile { Email = "dev@code.com" };
Once the object is created, the property becomes read-only. This helps keep your objects more stable and predictable, especially when you’re working with models that shouldn’t change after setup.
When to Add Logic to Getters or Setters
Most of the time, a property just stores a value. However, sometimes you need it to do something more when the value changes or when it’s accessed. That’s where adding logic inside your getter or setter makes sense.
Think of a property as a small method that runs automatically when you read or write data. This means you can use it to validate input, trigger updates, or even calculate a value on the fly.
For example, let’s say we have a simple Account class that tracks a user’s balance:
public class Account
{
private decimal balance;
public decimal Balance
{
get { return balance; }
set
{
if (value >= 0)
balance = value;
}
}
}
Here, the setter checks that no one can set a negative balance. Now consider a case where one value depends on another. Suppose you want to automatically update a reward level whenever the score changes:
public class Player
{
private int score;
private string level;
public int Score
{
get { return score; }
set
{
score = value;
UpdateLevel();
}
}
public string Level
{
get { return level; }
private set { level = value; }
}
private void UpdateLevel()
{
Level = score >= 100 ? "Gold" : "Silver";
}
}
Every time the Score changes, the UpdateLevel() method runs. This keeps related values in sync without requiring the rest of the code to handle it.
As a general rule:
- Add logic when a change affects other parts of your class.
- Add validation to prevent bad input from breaking your logic.
- Avoid placing heavy calculations inside getters, as they should remain quick and predictable.
A getter should return data without altering state unless you are using a lazy property, where the value is calculated the first time it’s requested. In most other cases, keep getters simple and clean.
Wrapping Up
When it comes to properties, the real skill lies in knowing how much control to apply. Many developers make the mistake of adding unnecessary logic or exposing too much access. The best properties are typically those that are simple, focused, and predictable.
A good rule to follow is this: keep your properties clean, but never careless. If a value needs protection or validation, give it a controlled setter. If it doesn’t, use an auto-property and move on. Overcomplicating a property makes code harder to maintain and test later.
Here are a few habits that help keep your property design solid:
- Keep logic lightweight: Complex calculations or multiple operations should not be included in accessors. Use methods for that.
- Limit access where possible: Use private set or init to prevent unwanted changes from other parts of the program.
- Stick to naming conventions: Use PascalCase for property names and _camelCase for private fields to keep your code easy to read.
- Prefer properties over public fields: They make your class easier to refactor and maintain as it grows.
- Validate external input: When values come from users, APIs, or files, a setter is the ideal place to validate and keep them in check.