C# & .NET (C# 14, .NET 10 LTS) Module 6 – Classes, objects and encapsulation
Classes, objects and constructors
Write a C# class with fields, constructors and methods, chain constructors, use object initializers and required members, and refuse bad data at creation.
What you will learn
- Define a class with fields, a constructor and methods, and create objects of it with new
- Predict the order in which field initializers, chained constructors and object initializers run
- Use object initializers and required members, and fix error CS9035
- Validate arguments in the constructor so that every object keeps its class invariant
Before you start
What you’ll learn · Before you start
What you will learn
- Define a class with fields, a constructor and methods, and create objects of it with new
- Predict the order in which field initializers, chained constructors and object initializers run
- Use object initializers and required members, and fix error CS9035
- Validate arguments in the constructor so that every object keeps its class invariant
Before you start
On this page
ravi.Withdraw(1000m) on an account that holds 250 rupees leaves a balance of -750.00, and nothing in the class
says no. A class bundles an object’s data with the only code allowed to change it, and its constructor decides what
every new object starts with, so a class is where a balance below zero can be made impossible.
You will write a class with fields, constructors and methods, chain one constructor to another, set the rest with object initializers and required members, and make a constructor refuse bad arguments before the object exists.
A class describes objects, and new creates each one
account.cs declares one class, BankAccount, and creates two accounts from it. A class is a type you write. Its
fields hold the data of each object, its constructor: A member that runs once when new creates an object. It has the name of its class and no return type, and it gives the new object's fields their starting values. runs when an object is created, and
its methods are what an object can do. new BankAccount("Asha Rao", 1500m) creates an object, also called an
instance, runs the constructor on it and hands back a reference to it:
// One class, two objects: each object gets its own copy of the fields, and each method works on the object it is
// called on.
var asha = new BankAccount("Asha Rao", 1500m);
var ravi = new BankAccount("Ravi Iyer", 0m);
Console.WriteLine(ravi.Describe());
asha.Deposit(500m);
asha.Withdraw(300m);
ravi.Deposit(250m);
Console.WriteLine(asha.Describe());
Console.WriteLine(ravi.Describe());
// Nothing in the class stops this withdrawal.
ravi.Withdraw(1000m);
Console.WriteLine(ravi.Describe());
class BankAccount
{
// Fields: the data that every BankAccount object carries. Only code in this class can use them.
private readonly string _owner;
private decimal _balance;
private int _transactions;
// The constructor has the class's name and no return type. It runs once, when new creates the object.
public BankAccount(string owner, decimal opening)
{
_owner = owner;
_balance = opening;
}
// Methods: what an account can do. Each one works on the fields of the account it is called on.
public void Deposit(decimal amount)
{
_balance += amount;
_transactions++;
}
public void Withdraw(decimal amount)
{
_balance -= amount;
_transactions++;
}
public string Describe() => $"{_owner}: balance {_balance:F2}, transactions {_transactions}";
} Output
Ravi Iyer: balance 0.00, transactions 0 Asha Rao: balance 1700.00, transactions 2 Ravi Iyer: balance 250.00, transactions 1 Ravi Iyer: balance -750.00, transactions 2
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run account.cs
Each object has its own copy of the three fields, and each method call works on the object named before the dot. Step through the calls and watch which object changes:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The first object. new BankAccount("Asha Rao", 1500m) creates an object and runs the constructor, which stores the owner and the opening balance. Nothing sets _transactions, so it starts at 0, the default for an int field.
- The second object. A second new makes a second object from the same class, with fields of its own: Ravi Iyer, a balance of 0.00 and 0 transactions, as the first line of output shows.
- Calls on asha. asha.Deposit(500m) and asha.Withdraw(300m) work on the fields of the object that asha refers to. Its balance becomes 1700.00 after 2 transactions; the other object does not change.
- A call on ravi. ravi.Deposit(250m) changes only the second object: 250.00 after 1 transaction. Each method call has one object to work on, the one before the dot.
- Nothing says no. ravi.Withdraw(1000m) takes 1000 from 250 and leaves -750.00. The class let it happen, because no method checks anything. A later section of this lesson adds the checks.
The first line of output shows _transactions at 0, although no code set it. A field that nothing assigns holds the
default value of its type: 0 for numbers, false for bool and null for a reference. The fields are private, so
only code inside BankAccount can read or change them, and code outside has to go through the methods. That is what
makes the last line worth fixing: Withdraw is the only way to lower a balance, so one check in it would protect
every account. It has no check yet, and ravi ended at -750.00.
Coming from Java
A C# class looks like a Java class: fields, constructors and methods, with objects created by new. C# code reads
data through properties such as course.Title rather than getTitle() methods, as the next lesson shows. C# has
finalizers, once called destructors, but the garbage collector decides when they run and most classes never need
one; files and connections are closed with using and IDisposable, in the module on exceptions and resources.
Checkpoint
Check: Classes, objects and their own fields
A constructor runs once, before any code can use the object
A constructor has the name of its class and no return type, not even void. It runs exactly once for each object,
when new creates it, and a class may have several constructors with different parameters. A class that declares no
constructor gets a public parameterless one from the compiler. Declare any constructor yourself and that one is gone:
for a class whose only constructor is BankAccount(string owner, decimal opening), new BankAccount() no longer
compiles.
Several constructors usually share most of their work. Instead of copying it, one constructor calls another with
: this(...) after its parameter list. order.cs gives its Ticket class two constructors and prints a numbered
line at each step of new Ticket("4519203761", "Asha Rao") { Coach = "B2" }. Step is a static helper that prints
those lines; static members are two lessons on.
// Prints each step of creating one ticket, in the order C# runs them.
var ticket = new Ticket("4519203761", "Asha Rao") { Coach = "B2" };
Console.WriteLine($"5. new has finished: {ticket.Describe()}");
class Ticket
{
private static int s_step;
private readonly string _status = Step("field initializer sets _status", "booked");
private string _coach = "not assigned";
public Ticket(string pnr, string passenger, int farePaise)
{
Step($"Ticket(pnr, passenger, farePaise) runs its body, fare {farePaise}");
Pnr = pnr;
Passenger = passenger;
FarePaise = farePaise;
}
// This constructor chains to the one above: that one runs first, then this body.
public Ticket(string pnr, string passenger)
: this(pnr, passenger, 0)
{
Step("Ticket(pnr, passenger) runs its body");
}
public string Pnr { get; }
public string Passenger { get; }
public int FarePaise { get; }
public string Coach
{
get => _coach;
set
{
Step($"object initializer sets Coach = {value}");
_coach = value;
}
}
public string Describe() => $"{Pnr} {Passenger}, coach {Coach}, {_status}";
// Prints a numbered line and hands back the value it was given.
private static T Step<T>(string what, T value)
{
Console.WriteLine($"{++s_step}. {what}");
return value;
}
private static void Step(string what) => Step(what, 0);
} Output
1. field initializer sets _status 2. Ticket(pnr, passenger, farePaise) runs its body, fare 0 3. Ticket(pnr, passenger) runs its body 4. object initializer sets Coach = B2 5. new has finished: 4519203761 Asha Rao, coach B2, booked
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run order.cs
Line 2 came before line 3: the constructor named by this(...) ran its whole body before the body of the
constructor that new called. Line 1 shows that the field initializers ran before both bodies, and only once,
although two constructors ran. That gives a rule for writing them: put every check and assignment in the constructor
with the most parameters, and let the shorter ones chain to it with the values they supply, as Ticket(pnr, passenger) supplies a fare of 0.
Copying the checks into each constructor
Two constructors that each check the passenger name drift apart the first time someone fixes one of them, and
tickets created through the other keep the bug. Put the checks in one constructor and chain the others to it with
: this(...), so that every way of creating a ticket runs the same checks.
Checkpoint
Check: Constructors and chaining with this(...)
Object initializers and required members set the rest
Line 4 of order.cs came from the braces after the constructor call. An
object initializer: The braces after a constructor call, as in new Ticket(pnr, name) { Coach = "B2" }. They assign fields and properties after every constructor has finished. such as { Coach = "B2" } assigns fields and properties that
the caller can reach, and it runs after every constructor has finished, so a caller can set what no constructor
takes. Here is the whole order, step by step:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Empty fields. Before any of your code runs, every field of the new object holds its default: 0 for numbers, null for references, false for bools.
- Field initializers. The initializers written on the field declarations run next, in the order they are written. order.cs printed line 1: the field initializer sets _status to "booked".
- this(...) first. new called Ticket(pnr, passenger), which chains with : this(pnr, passenger, 0). The constructor it chains to runs its body first, so line 2 is the three-parameter body.
- Then the caller's body. Only when the chained constructor has returned does the body of Ticket(pnr, passenger) run: line 3. The field initializers did not run a second time.
- The object initializer. The braces after the constructor call run last, after every constructor has finished: line 4 is the Coach setter, called with "B2".
- Ready. new hands the finished object to the variable ticket, and line 5 describes it: coach B2, booked. No code saw the object before this step.
A property declared with { get; init; } may be set by an object initializer or a constructor and never again; the
next lesson covers init. An initializer is optional, though: nothing makes a caller write one. The required
modifier, added in C# 11, does. Every expression that creates the object must set each
required member: A field or property marked required (C# 11). Every expression that creates the object must set it in an object initializer, or the build fails with CS9035. in its object initializer. required.cs creates three passengers:
using System.Diagnostics.CodeAnalysis;
// Three ways to create a Passenger: an object initializer that sets the required members, a constructor that
// promises to set them, and an initializer that sets an optional member as well.
var asha = new Passenger { Name = "Asha Rao", Age = 34 };
var ravi = new Passenger("Ravi Iyer", 61);
var meena = new Passenger { Name = "Meena Das", Age = 8, Berth = "lower" };
foreach (var p in new[] { asha, ravi, meena })
{
Console.WriteLine($"{p.Name}, {p.Age}, berth: {p.Berth}");
}
class Passenger
{
public Passenger()
{
}
// SetsRequiredMembers tells the compiler that this constructor sets every required member.
[SetsRequiredMembers]
public Passenger(string name, int age)
{
Name = name;
Age = age;
}
public required string Name { get; init; }
public required int Age { get; init; }
public string Berth { get; init; } = "any";
} Output
Asha Rao, 34, berth: any Ravi Iyer, 61, berth: any Meena Das, 8, berth: lower
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run required.cs
Asha and Meena set Name and Age in their initializers, and Meena set the optional Berth as well. Ravi was
created through a constructor marked [SetsRequiredMembers], which tells the compiler that this constructor sets
every required member, so his new needed no initializer. The compiler takes the attribute on trust: Microsoft’s
reference warns that it does not check that such a constructor really sets them. Leave the required members out, or
assign an init-only property after the object exists, and the build stops:
// Two mistakes the compiler stops: a Passenger without its required members, and a change to an init-only property.
var upper = new Passenger { Berth = "upper" };
var ravi = new Passenger { Name = "Ravi Iyer", Age = 61 };
ravi.Name = "R. Iyer";
class Passenger
{
public required string Name { get; init; }
public required int Age { get; init; }
public string Berth { get; init; } = "any";
} from session import run
# passengers creates one Passenger without its two required members and changes the init-only Name of another after
# it was created. The project is built once; the compiler stops at both mistakes.
run("""
cd passengers
!dotnet build -v q
""") Output
$ cd passengers
$ dotnet build -v q
passengers/Program.cs(2,17): error CS9035: Required member 'Passenger.Name' must be set in the object initializer or attribute constructor. [passengers/App.csproj]
passengers/Program.cs(2,17): error CS9035: Required member 'Passenger.Age' must be set in the object initializer or attribute constructor. [passengers/App.csproj]
passengers/Program.cs(4,1): error CS8852: Init-only property or indexer 'Passenger.Name' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor. [passengers/App.csproj]
Build FAILED.
passengers/Program.cs(2,17): error CS9035: Required member 'Passenger.Name' must be set in the object initializer or attribute constructor. [passengers/App.csproj]
passengers/Program.cs(2,17): error CS9035: Required member 'Passenger.Age' must be set in the object initializer or attribute constructor. [passengers/App.csproj]
passengers/Program.cs(4,1): error CS8852: Init-only property or indexer 'Passenger.Name' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor. [passengers/App.csproj]
0 Warning(s)
3 Error(s)
Time Elapsed …
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 required_session.py
CS9035 names each required member that the initializer left out, Name and Age. CS8852 refuses
ravi.Name = "R. Iyer";, because an init-only property cannot change once the object exists. Both are compile
errors, so no code that compiles can create a Passenger without saying who the passenger is.
Key idea
Field initializers run first, then the chained constructor, then the constructor that new called, and the object
initializer last; required makes the compiler insist that the initializer sets the members no constructor sets.
Checkpoint
Check: Object initializers and required members
Checks in the constructor keep every object valid
An account’s balance should never be negative, and its owner should never be blank. A rule that must hold for every
object of a class at every moment is a class invariant: A rule about an object's state that holds for every object of a class whenever other code can see it, such as a balance that is never negative. Constructors make it true and methods keep it true.. The constructor is the first
place to enforce it, because it runs before any other code can see the object. validate.cs starts its constructor
and each method with guard clause: A check at the start of a method that returns or throws at once when an input is bad, so the code after it can assume the input is good and stays unindented. More in “if, switch statements and switch expressions” that throw when an argument is wrong:
// Every attempt either creates a valid account or throws before any code can use the object.
Try("new BankAccount(\" \", 100m)", () => new BankAccount(" ", 100m));
Try("new BankAccount(\"Ravi Iyer\", -500m)", () => new BankAccount("Ravi Iyer", -500m));
var asha = new BankAccount("Asha Rao", 1500m);
Console.WriteLine($"created {asha.Describe()}");
Try("asha.Withdraw(0m)", () => asha.Withdraw(0m));
Try("asha.Withdraw(2000m)", () => asha.Withdraw(2000m));
Try("asha.Withdraw(400m)", () => asha.Withdraw(400m));
Console.WriteLine($"now {asha.Describe()}");
static void Try(string code, Action action)
{
Console.WriteLine(code);
try
{
action();
Console.WriteLine(" ok");
}
catch (ArgumentException e)
{
Console.WriteLine($" {e.GetType().Name}: {e.Message.ReplaceLineEndings(" ")}");
}
catch (InvalidOperationException e)
{
Console.WriteLine($" {e.GetType().Name}: {e.Message}");
}
}
class BankAccount
{
private readonly string _owner;
private decimal _balance;
public BankAccount(string owner, decimal opening)
{
// Guard clauses: check every argument before storing anything.
ArgumentException.ThrowIfNullOrWhiteSpace(owner);
ArgumentOutOfRangeException.ThrowIfNegative(opening);
_owner = owner.Trim();
_balance = opening;
}
public void Deposit(decimal amount)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
_balance += amount;
}
public void Withdraw(decimal amount)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
if (amount > _balance)
{
throw new InvalidOperationException($"cannot withdraw {amount:F2}: the balance is {_balance:F2}");
}
_balance -= amount;
}
public string Describe() => $"{_owner}: balance {_balance:F2}";
} Output
new BankAccount(" ", 100m)
ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'owner')
new BankAccount("Ravi Iyer", -500m)
ArgumentOutOfRangeException: opening ('-500') must be a non-negative value. (Parameter 'opening') Actual value was -500.
created Asha Rao: balance 1500.00
asha.Withdraw(0m)
ArgumentOutOfRangeException: amount ('0') must be a non-negative and non-zero value. (Parameter 'amount') Actual value was 0.
asha.Withdraw(2000m)
InvalidOperationException: cannot withdraw 2000.00: the balance is 1500.00
asha.Withdraw(400m)
ok
now Asha Rao: balance 1100.00
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run validate.cs
Step through the three constructor calls and the withdrawal that asked for too much:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- A blank owner. new BankAccount(" ", 100m): ThrowIfNullOrWhiteSpace finds only spaces and throws ArgumentException. The constructor stops at its first line, and new never hands back an object.
- A negative balance. new BankAccount("Ravi Iyer", -500m): the owner passes, and ThrowIfNegative throws ArgumentOutOfRangeException. Its message names the parameter, opening, and the value, -500.
- A valid account. new BankAccount("Asha Rao", 1500m) passes both checks, so the constructor stores the fields and new hands back an account with a balance of 1500.00.
- Methods check too. Withdraw(2000m) asks for more than the balance, so it throws InvalidOperationException before it subtracts anything. The balance stays 1500.00, and Withdraw(400m) then leaves 1100.00.
A constructor that throws never finishes, so new never hands an object to anyone, and every account that does
exist has passed both checks. The same goes for the methods: Withdraw(2000m) threw before it subtracted anything,
so the balance stayed 1500.00, and Withdraw(400m) then left 1100.00.
Checks in a Validate method that callers must remember
var account = new BankAccount(owner, opening); account.Validate(); leaves an invalid account in memory until
someone remembers the second line, and one caller always forgets. Run the checks inside the constructor, where no
path that creates an object can skip them.
The checks come from helper methods added in .NET 8, which build the message for you. The run shows that each message names the parameter, and that the two range checks name the value they rejected as well:
| Helper | Throws when the argument is | Exception |
|---|---|---|
ArgumentException.ThrowIfNullOrWhiteSpace(owner) |
null, empty or only white space |
ArgumentNullException for null, else ArgumentException |
ArgumentOutOfRangeException.ThrowIfNegative(opening) |
below 0 | ArgumentOutOfRangeException |
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount) |
0 or below | ArgumentOutOfRangeException |
A rule about the state of the object rather than one argument, such as a withdrawal larger than the balance, throws
InvalidOperationException instead, as Withdraw does.
Key idea
Check every argument at the top of the constructor, before anything is stored: an object whose constructor throws is never handed to anyone, so every object that exists has passed the checks.
Checkpoint
Check: Class invariants checked in the constructor
Practise: a train ticket that refuses bad data
ticket.cs checks a TrainTicket class that you complete. Its constructor takes a PNR of 10 digits, the passenger’s
name, the journey date and the fare in paise, and Cancel returns the refund in paise and marks the ticket
cancelled. The refund rule written at the top of the file is made up for this exercise. The class you start from
only throws, so all 8 checks fail:
// Practice: write the TrainTicket constructor and Cancel, then run this file until all eight checks pass.
// The refund rule is made up for this exercise and is no railway's real rule: cancelled 2 or more days before the
// journey, 75 % of the fare comes back; 1 day before, 50 %; on the day of the journey or later, nothing.
var journey = new DateOnly(2026, 12, 20);
(string Check, Func<string> Run, string Expected)[] checks =
[
("a valid ticket keeps its PNR", () => Make("4519203761", "Asha Rao", 84_500).Pnr, "4519203761"),
("a PNR of 5 digits", () => Make("45192", "Asha Rao", 84_500).Pnr, "throws ArgumentException"),
("a blank passenger name", () => Make("4519203761", " ", 84_500).Pnr, "throws ArgumentException"),
("a fare of 0 paise", () => Make("4519203761", "Asha Rao", 0).Pnr, "throws ArgumentOutOfRangeException"),
("cancelled 5 days before", () => Valid().Cancel(new DateOnly(2026, 12, 15)).ToString(), "63375"),
("cancelled 1 day before", () => Valid().Cancel(new DateOnly(2026, 12, 19)).ToString(), "42250"),
("cancelled on the journey day", () => Valid().Cancel(journey).ToString(), "0"),
("cancelled twice", CancelTwice, "throws InvalidOperationException"),
];
int passed = 0;
foreach (var (check, run, expected) in checks)
{
string got;
try
{
got = run();
}
catch (Exception e)
{
got = $"throws {e.GetType().Name}";
}
bool ok = got == expected;
if (ok)
{
passed++;
}
Console.WriteLine(ok ? $"{check}: ok" : $"{check}: FAIL, {got} instead of {expected}");
}
Console.WriteLine($"{passed} of {checks.Length} checks pass");
return passed == checks.Length ? 0 : 1;
TrainTicket Make(string pnr, string passenger, long farePaise) => new(pnr, passenger, journey, farePaise);
TrainTicket Valid() => Make("4519203761", "Asha Rao", 84_500);
string CancelTwice()
{
var ticket = Valid();
ticket.Cancel(new DateOnly(2026, 12, 15));
return ticket.Cancel(new DateOnly(2026, 12, 16)).ToString();
}
class TrainTicket
{
public TrainTicket(string pnr, string passenger, DateOnly journey, long farePaise)
{
throw new NotImplementedException();
}
public string Pnr { get; }
public string Passenger { get; }
public DateOnly Journey { get; }
public long FarePaise { get; }
public bool Cancelled { get; private set; }
// Returns the refund in paise and marks the ticket cancelled.
public long Cancel(DateOnly today) => throw new NotImplementedException();
} Output (exit status 1)
a valid ticket keeps its PNR: FAIL, throws NotImplementedException instead of 4519203761 a PNR of 5 digits: FAIL, throws NotImplementedException instead of throws ArgumentException a blank passenger name: FAIL, throws NotImplementedException instead of throws ArgumentException a fare of 0 paise: FAIL, throws NotImplementedException instead of throws ArgumentOutOfRangeException cancelled 5 days before: FAIL, throws NotImplementedException instead of 63375 cancelled 1 day before: FAIL, throws NotImplementedException instead of 42250 cancelled on the journey day: FAIL, throws NotImplementedException instead of 0 cancelled twice: FAIL, throws NotImplementedException instead of throws InvalidOperationException 0 of 8 checks pass
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run ticket.cs
Write the constructor and Cancel
Copy ticket.cs to your computer. In the constructor, throw ArgumentException unless the PNR has 10 chars that are
all digits (pnr.All(char.IsAsciiDigit)), reject a blank name with ThrowIfNullOrWhiteSpace and a fare of 0 or
less with ThrowIfNegativeOrZero, then store the four values. In Cancel, throw InvalidOperationException when
the ticket is already cancelled, and work out the days left with Journey.DayNumber - today.DayNumber. Run
dotnet run ticket.cs until it prints 8 of 8 checks pass.
Then test yourself on objects, constructors, initializers and invariants. The questions start where your answers so far are weakest, and every choice explains why it is right or what it misses.
Practice that adapts to you
Practise this lesson
9 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What is the difference between a class and an object in C#?
A class is a type you declare: it describes the fields every object of it carries, the constructors that create one
and the methods it offers. An object is one instance of that class, created at run time by new, with its own copy
of the instance fields. One BankAccount class can give a thousand accounts, each with its own owner and balance,
and a method called on one account works on that account’s fields only. A class is a reference type, so a variable
of type BankAccount holds a reference to an object, not the object itself, and two variables can refer to one
account. The class exists once in the compiled program, while objects come and go as the program runs.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
Orders reach your database with an empty customer id and a quantity of -3. The Order class has a public parameterless constructor and a public setter on every property. How would you redesign the way an Order is created so that an invalid one cannot exist, and what would you still let callers change?
-
Fresher to mid
A Shipment class has five constructors, each with its own copy of the same checks and defaults. A fix to the weight check reached two of them, and shipments created through the other three still accept a weight of 0. How do you restructure the constructors, and how do you show that nothing else changed?
Key takeaways
- A class declares the fields every object carries, the constructors that create one and the methods it offers, and
newcreates an object with its own copy of the instance fields, each starting at its type’s default. - A constructor has the class’s name and no return type; once a class declares one, the compiler stops supplying the parameterless constructor.
- Chain constructors with
: this(...)so that the checks live in one place: the chained constructor’s body runs first, and field initializers run once, before both. - Object initializers run after every constructor;
requiredmembers must be set in them, or the build stops with CS9035, andinitproperties cannot change afterwards. - Guard clauses at the top of the constructor keep a class invariant from the first moment, because an object whose constructor throws is never handed to anyone.
Terms in this lesson
- class invariant
- A rule about an object's state that holds for every object of a class whenever other code can see it, such as a balance that is never negative. Constructors make it true and methods keep it true.
- constructor
- A member that runs once when new creates an object. It has the name of its class and no return type, and it gives the new object's fields their starting values.
- object initializer
- The braces after a constructor call, as in new Ticket(pnr, name) { Coach = "B2" }. They assign fields and properties after every constructor has finished.
- required member
- A field or property marked required (C# 11). Every expression that creates the object must set it in an object initializer, or the build fails with CS9035.
References
- C# classes (Microsoft Learn)
- Constructors (C# programming guide) (Microsoft Learn)
- Instance constructors (Microsoft Learn)
- Using constructors (Microsoft Learn)
- Object and collection initializers (Microsoft Learn)
- required modifier (Microsoft Learn)
- The init keyword (Microsoft Learn)
- ArgumentOutOfRangeException.ThrowIfNegative method (Microsoft Learn)
- ArgumentException.ThrowIfNullOrWhiteSpace method (Microsoft Learn)
- Default values of C# types (Microsoft Learn)
- Finalizers (C# programming guide) (Microsoft Learn)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress