07. Properties and Encapsulation
Exposing fields raw lets anything happen. C# properties look like fields on the outside but act like methods inside — perfect for natural encapsulation.
What you'll learn
- 1Understand auto-property (`{ get; set; }`) and how it works
- 2Use `init` setters for "set-once" properties
- 3Tell apart `readonly` fields and the `required` keyword (C# 11)
- 4Add value validation in a full property with a backing field
Overview
In the previous lecture we protected data with `private` fields + `public` methods (`GetBalance` etc.). C# has a dedicated member that does this cleanly: a **property**. From the outside it looks like a field, but inside it runs like a method.
Core Concepts
1) Auto-property
class Person
{
public string Name { get; set; } = ""; // bound to an auto-generated hidden field
public int Age { get; set; }
}The compiler generates a hidden field like `_name`/`_age` and writes `get`/`set` for you.
2) Read-only property (`get` only)
public string Name { get; } // settable only from a constructorCannot be changed after the object is built.
3) `init` setter (C# 9+)
public string Name { get; init; } = "";Settable only during object initialization (`new Person { Name = "A" }`) or inside a constructor; read-only afterwards. Great for **immutable objects**.
var p = new Person { Name = "Alice" };
// p.Name = "Bob"; // compile error4) `required` keyword (C# 11)
Marks "this property must be given a value when the object is created."
class User
{
public required string Email { get; init; }
}
var u = new User { Email = "a@b.com" }; // OK
// var u2 = new User(); // compile errorCombines the safety of constructor + `init` with the convenience of object initializer syntax.
5) `readonly` field
Locks a **field** (not a property) so it can be assigned only once.
class Circle
{
public readonly double Pi = 3.14159;
}Settable only in the constructor. Use for near-constants.
6) Full property — backing field + validation
Auto-properties can't validate. If you need validation, declare a backing field and put logic in `set`.
class Temperature
{
private double celsius;
public double Celsius
{
get => celsius;
set
{
if (value < -273.15)
throw new ArgumentException("below absolute zero");
celsius = value;
}
}
}Inside `set`, `value` is the reserved parameter holding "the new incoming value."
Examples
Example 1 — `AutoProperty`: simplest form
// Program.cs
using CodingNow.Lecture.Oop07;
var p = new Person();
p.Name = "Alice";
p.Age = 30;
Console.WriteLine($"{p.Name} / {p.Age}");// Person.cs
namespace CodingNow.Lecture.Oop07;
internal class Person
{
public string Name { get; set; } = "";
public int Age { get; set; }
}**Output**
Alice / 30**Note:** Looks like exposing fields, but leaves room to add logic in `set` later.
Example 2 — `GetInit`: object initializer + `init`
// Program.cs
using CodingNow.Lecture.Oop07;
var alice = new Person { Name = "Alice", Age = 30 };
Console.WriteLine($"{alice.Name} / {alice.Age}");
// alice.Name = "Bob"; // init means no later mutation (compile error)// Person.cs
namespace CodingNow.Lecture.Oop07;
internal class Person
{
public string Name { get; init; } = "";
public int Age { get; init; }
}**Output**
Alice / 30**Note:** Settable only inside the object initializer `new Person { ... }`, read-only afterwards.
Example 3 — `RequiredProp`: required property
// Program.cs
using CodingNow.Lecture.Oop07;
var u = new User { Email = "alice@example.com" };
Console.WriteLine(u.Email);
// var u2 = new User(); // missing Email → compile error// User.cs
namespace CodingNow.Lecture.Oop07;
internal class User
{
public required string Email { get; init; }
public string DisplayName { get; init; } = "Anonymous";
}**Output**
alice@example.com**Note:** A `required` property must be given a value when constructing. Safe even without a constructor.
Example 4 — `FullProperty`: with validation
// Program.cs
using CodingNow.Lecture.Oop07;
var t = new Temperature();
t.Celsius = 25;
Console.WriteLine($"{t.Celsius}°C");
try
{
t.Celsius = -500; // below absolute zero → throws
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}// Temperature.cs
namespace CodingNow.Lecture.Oop07;
internal class Temperature
{
private double celsius;
public double Celsius
{
get => celsius;
set
{
if (value < -273.15)
throw new ArgumentException("cannot go below absolute zero (-273.15°C).");
celsius = value;
}
}
}**Output**
25°C
Error: cannot go below absolute zero (-273.15°C).**Note:** From outside you just write `t.Celsius = 25;` (field-like), but a method runs inside. That's the essence of a property.
Full example code (src/)
src/AutoProperty/AutoProperty.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/AutoProperty/Person.cs
namespace CodingNow.Lecture.Oop07;
internal class Person
{
// auto-property: compiler generates the hidden field and get/set automatically.
public string Name { get; set; } = "";
public int Age { get; set; }
}
src/AutoProperty/Program.cs
using CodingNow.Lecture.Oop07;
var p = new Person();
p.Name = "Alice";
p.Age = 30;
Console.WriteLine($"{p.Name} / {p.Age}");
src/FullProperty/FullProperty.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/FullProperty/Program.cs
using CodingNow.Lecture.Oop07;
var t = new Temperature();
t.Celsius = 25;
Console.WriteLine($"{t.Celsius}°C");
try
{
t.Celsius = -500; // below absolute zero → exception
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
src/FullProperty/Temperature.cs
namespace CodingNow.Lecture.Oop07;
internal class Temperature
{
// backing field: actual data lives here
private double celsius;
public double Celsius
{
get => celsius;
set
{
// 'value' inside set is the reserved parameter for "the new incoming value"
if (value < -273.15)
throw new ArgumentException("cannot go below absolute zero (-273.15°C).");
celsius = value;
}
}
}
src/GetInit/GetInit.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/GetInit/Person.cs
namespace CodingNow.Lecture.Oop07;
internal class Person
{
// init: settable only during object initialization or constructor; read-only after.
public string Name { get; init; } = "";
public int Age { get; init; }
}
src/GetInit/Program.cs
using CodingNow.Lecture.Oop07;
// Settable only in the object initializer.
var alice = new Person { Name = "Alice", Age = 30 };
Console.WriteLine($"{alice.Name} / {alice.Age}");
// alice.Name = "Bob"; // init = no later mutation (uncommenting -> compile error)
src/RequiredProp/Program.cs
using CodingNow.Lecture.Oop07;
// A required property must be given a value at object creation.
var u = new User { Email = "alice@example.com" };
Console.WriteLine($"{u.Email} / {u.DisplayName}");
// var u2 = new User(); // Email missing → compile error
src/RequiredProp/RequiredProp.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/RequiredProp/User.cs
namespace CodingNow.Lecture.Oop07;
internal class User
{
// required: must be given a value at object creation (C# 11+)
public required string Email { get; init; }
// optional with a default
public string DisplayName { get; init; } = "Anonymous";
}
Common Mistakes
- Exposing fields raw like `public string Name;` — prefer a property whenever possible.
- Trying to reassign an `init` property from outside — only allowed at object initialization or inside the constructor.
- Forgetting to supply a `required` property during construction → compile error.
- Inside `set`, writing `Celsius = value;` (calling itself) — **infinite recursion** (StackOverflow). Always assign to the backing field (`celsius`).
- Trying to mutate a `readonly` field outside the constructor — compile error.
Summary
- A property is "a method that looks like a field." Keeps the external API simple while hiding internal logic.
- Use `init` for immutable data, `required` for mandatory input, and a full property + backing field when you need validation.
- Encapsulation is about preventing outsiders from poking at internal state arbitrarily.
Practice
**Practice - 07. Properties and Encapsulation**
Problem 1 — `Temperature` (absolute-zero validation)
- Project folder: `Homework01/`
- Key concepts: full property, backing field, validation in `set`
Requirements
- Add a `Celsius` property to a `Temperature` class.
- On set, throw `ArgumentException` if the value is below `-273.15`.
- Add a read-only `Fahrenheit` property. Formula: `°F = °C × 9/5 + 32`.
- In `Program.cs` set valid and invalid values and print.
Expected output
0°C = 32°F
100°C = 212°F
Error: cannot go below absolute zero (-273.15°C).Hints
- Backing field as `private double celsius;`.
- `Fahrenheit` as `get => celsius * 9 / 5 + 32;` (computed).
Problem 2 — `Product` (non-negative price validation)
- Project folder: `Homework02/`
- Key concepts: `required`, `init`, full property + validation
Requirements
- Create a `Product` class.
- Make `Name` a `required string ... { get; init; }`.
- Make `Price` a full property; throw `ArgumentException` on a negative value.
- Create with object initializer `new Product { Name = "...", Price = ... }`.
Expected output
Apple / 1500 won
Pear / 0 won
Error: price must be >= 0.Hints
- Backing field for `Price` is `private int price;`.
- No default needed for an initial 0 price (`int` defaults to 0).
Check your answer
Try it yourself, then compare against the [`answer/`](./answer/) folder.
Answer (answer/)
homework/answer/Homework01/Homework01.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework01/Program.cs
using CodingNow.Lecture.Oop07;
var t = new Temperature();
t.Celsius = 0;
Console.WriteLine($"{t.Celsius}°C = {t.Fahrenheit}°F");
t.Celsius = 100;
Console.WriteLine($"{t.Celsius}°C = {t.Fahrenheit}°F");
try
{
t.Celsius = -300;
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
homework/answer/Homework01/Temperature.cs
namespace CodingNow.Lecture.Oop07;
internal class Temperature
{
private double celsius;
public double Celsius
{
get => celsius;
set
{
if (value < -273.15)
throw new ArgumentException("cannot go below absolute zero (-273.15°C).");
celsius = value;
}
}
// read-only computed property
public double Fahrenheit => celsius * 9 / 5 + 32;
}
homework/answer/Homework02/Homework02.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework02/Product.cs
namespace CodingNow.Lecture.Oop07;
internal class Product
{
// required: must be supplied at construction
public required string Name { get; init; }
private int price;
public int Price
{
get => price;
set
{
if (value < 0)
throw new ArgumentException("price must be >= 0.");
price = value;
}
}
}
homework/answer/Homework02/Program.cs
using CodingNow.Lecture.Oop07;
var apple = new Product { Name = "Apple" };
apple.Price = 1500;
Console.WriteLine($"{apple.Name} / {apple.Price} won");
var pear = new Product { Name = "Pear" };
Console.WriteLine($"{pear.Name} / {pear.Price} won");
try
{
pear.Price = -100;
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
Try It Yourself
cd src/AutoProperty
dotnet run
cd ../GetInit
dotnet run
cd ../RequiredProp
dotnet run
cd ../FullProperty
dotnet runNext Lecture
[08_Inheritance](../08_%EC%83%81%EC%86%8D/) — Extend an existing class to build a new one.
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗