Fields and Properties
By the end of this lesson
Choose between a field and a property, and control access to each.
A field is a variable that lives inside an object. It is real storage: somewhere in memory there is a slot holding that value.
A property looks like a field from the outside and is not one. A property is a pair of small methods — one that runs when a value is read, one that runs when a value is written. The compiler turns product.UnitPrice = 250m into a method call. Understanding that single fact explains most of the surprising behaviour in this lesson.
public class Product
{
private decimal _unitPrice; // the field: actual storage
public decimal UnitPrice // the property: two methods
{
get
{
return _unitPrice;
}
set
{
if (value < 0m)
{
throw new ArgumentOutOfRangeException(nameof(value), "Price cannot be negative.");
}
_unitPrice = value;
}
}
}- _unitPrice is private, so only code inside Product can touch it. The leading underscore is a widely used convention for private fields; the compiler does not care.
- get runs when something reads UnitPrice. set runs when something writes to it.
- Inside set, the keyword value holds whatever was assigned. It is a parameter you never declare.
- The field that stores the value behind a property is called its backing field. Callers never see it.
- Because set is a method, it can refuse. A negative price now throws at the moment somebody tries it.
Most properties need no logic, and writing that much code for storage-and-return would be tedious. So C# lets you write { get; set; } and it generates both methods and a hidden backing field for you. That is an auto-property. It is the same thing as the version above, minus the validation.
The accessor combinations you will actually use:
- { get; set; }
- Readable and writable by anyone. Fine for data-carrying types, risky on types with rules to protect.
- { get; }
- Readable by anyone, assignable only inside the constructor. The value is fixed for the object's life.
- { get; private set; }
- Readable by anyone, changeable only by the class itself. This is the workhorse when a value changes over time but only through methods you control.
- { get; init; }
- Readable by anyone, assignable in the constructor or in an object initialiser at creation, then fixed. Useful when you want new Product { Name = "Desk lamp" } to stay available without leaving the value mutable forever.
- public decimal StockValue => UnitPrice * Quantity;
- A computed property. It has a get and nothing else, and it stores nothing — the expression runs on every read, so the answer cannot go stale.
public class StockItem
{
public string Sku { get; }
public string Name { get; init; } = "";
public decimal UnitPrice { get; private set; }
public int QuantityOnHand { get; private set; }
// Computed on every read, so it can never disagree with the two values above.
public decimal StockValue => UnitPrice * QuantityOnHand;
public StockItem(string sku, decimal unitPrice)
{
Sku = sku;
UnitPrice = unitPrice;
}
public void Receive(int quantity)
{
if (quantity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(quantity), "Received quantity must be positive.");
}
QuantityOnHand += quantity;
}
public void Reprice(decimal newPrice)
{
if (newPrice <= 0m)
{
throw new ArgumentOutOfRangeException(nameof(newPrice), "Price must be above zero.");
}
UnitPrice = newPrice;
}
}- Sku never changes, so it is get-only. QuantityOnHand and UnitPrice do change, but only through Receive and Reprice, which is what private set buys.
- StockValue has no storage. Nobody can set it to a number that contradicts the price and the quantity, because there is nothing to set.
- = "" on Name gives it a starting value so the compiler is satisfied it can never be null.
- Reading item.StockValue performs a multiplication each time. That is cheap here. If a computed property did expensive work, a method named CalculateStockValue() would set the caller's expectations better.
Field or property?
| Field | Property | |
|---|---|---|
| What it is | Storage | A get method, a set method, or both |
| Can add validation or logging later | No, not without changing its nature | Yes, inside the accessor |
| Can be declared in an interface | No | Yes |
| Seen by data binding, serialisers and most ORMs | Usually not by default | Yes |
| Can be used with ref or out | Yes | No |
| Typical visibility | private | public |
Summary
- A field is storage; a property is a get and set method pair over hidden storage
- An auto-property, { get; set; }, generates both accessors and a backing field for you
- Choose the accessor pair deliberately: get-only, private set and init each express a different lifetime
- A computed property stores nothing and therefore cannot disagree with the values it derives from
- Public surface is properties; internal storage is private fields, often readonly
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Try it yourself
Take the StockItem class and add a Dispatch(int quantity) method that reduces QuantityOnHand. It must refuse to dispatch more than is on hand.
Then try to write the same protection with a public int QuantityOnHand { get; set; } instead, and see what goes wrong.
Show solution
With private set the check lives in one method, so there is exactly one route by which the quantity can fall, and it cannot fall below zero.
With a public setter there is no route to enforce anything. You could put the check inside the setter, but the setter has no idea whether the new value represents a dispatch, a stock correction or a typo — it sees only a number. Methods named after what is happening can validate; a bare setter cannot.
public void Dispatch(int quantity)
{
if (quantity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(quantity), "Dispatch quantity must be positive.");
}
if (quantity > QuantityOnHand)
{
throw new InvalidOperationException(
$"Cannot dispatch {quantity} of {Sku}; only {QuantityOnHand} on hand.");
}
QuantityOnHand -= quantity;
}Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.