Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 6 – Classes, objects and encapsulation

Primary constructors for classes and structs

Declare C# 12 primary constructors on classes and structs, see which parameters the compiler keeps, and stop one value from being stored twice.

  • Intermediate
  • 20 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: Parameters on the type's first line, in scope in every member · Other constructors call this(...), and a struct keeps its zero default · The compiler keeps a parameter only when a member reads it. With a pass: 3 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Declare a primary constructor on a class or a struct, and chain other constructors to it with this(...)
  • Predict which primary constructor parameters the compiler keeps in hidden fields, and what a record does instead
  • Find and fix a value that a primary constructor stores twice, the problem behind warning CS9124
  • Prevent values from changing after creation with readonly copies and readonly structs
  • Choose between a primary constructor, an ordinary constructor and a record for a type's arguments and checks

Before you start

What you’ll learn · Before you start

What you will learn

  • Declare a primary constructor on a class or a struct, and chain other constructors to it with this(...)
  • Predict which primary constructor parameters the compiler keeps in hidden fields, and what a record does instead
  • Find and fix a value that a primary constructor stores twice, the problem behind warning CS9124
  • Prevent values from changing after creation with readonly copies and readonly structs
  • Choose between a primary constructor, an ordinary constructor and a record for a type's arguments and checks

Before you start

On this page

A reminder service that needs a clock and an outbox spends seven lines on two fields and a constructor that only copies its arguments into them. A primary constructor writes the same two parameters after the class name, and every member of the class can use them.

You will declare primary constructors for classes and structs, chain other constructors to them, predict which parameters the compiler keeps, and stop a value from being stored twice.

Parameters on the type’s first line, in scope in every member

service.cs writes one service twice. ReminderService(TimeProvider clock, Outbox outbox) declares a primary constructor: A parameter list written right after a class or struct name, as in class Fare(string route, int paise). Its parameters are in scope in the whole body of the type, and every other constructor must call it with this(...). Records have one too, and only theirs become properties., and ClassicReminderService does the same job with two fields and an ordinary constructor:

service.cs: one service, written two ways C# · service/service.cs
using System.Globalization;

// The same reminder service twice: with a primary constructor, then with a constructor and two fields.
// The clock always gives the same time, so every run prints the same lines.
var clock = new FixedClock(new DateTimeOffset(2026, 10, 17, 4, 0, 0, TimeSpan.Zero));

var primaryOutbox = new Outbox();
var primary = new ReminderService(clock, primaryOutbox);
primary.Remind("Asha", "coach B2 opens at 10:15");
primary.Remind("Ravi", "seat 12B is confirmed");
primaryOutbox.Print("primary constructor");

var classicOutbox = new Outbox();
var classic = new ClassicReminderService(clock, classicOutbox);
classic.Remind("Asha", "coach B2 opens at 10:15");
classic.Remind("Ravi", "seat 12B is confirmed");
classicOutbox.Print("classic constructor");

// clock and outbox are parameters of the whole class: every member below can use them.
sealed class ReminderService(TimeProvider clock, Outbox outbox)
{
    private static readonly TimeSpan India = TimeSpan.FromHours(5.5);

    public void Remind(string name, string text) => outbox.Add($"{Stamp()} {name}: {text}");

    private string Stamp() => clock.GetUtcNow().ToOffset(India).ToString("HH:mm", CultureInfo.InvariantCulture);
}

// The same class without a primary constructor: two fields, a constructor and two assignments.
sealed class ClassicReminderService
{
    private static readonly TimeSpan India = TimeSpan.FromHours(5.5);
    private readonly TimeProvider _clock;
    private readonly Outbox _outbox;

    public ClassicReminderService(TimeProvider clock, Outbox outbox)
    {
        _clock = clock;
        _outbox = outbox;
    }

    public void Remind(string name, string text) => _outbox.Add($"{Stamp()} {name}: {text}");

    private string Stamp() => _clock.GetUtcNow().ToOffset(India).ToString("HH:mm", CultureInfo.InvariantCulture);
}

// A clock that always gives the same time: a TimeProvider with a primary constructor of its own.
sealed class FixedClock(DateTimeOffset now) : TimeProvider
{
    public override DateTimeOffset GetUtcNow() => now;
}

sealed class Outbox
{
    private readonly List<string> _lines = [];

    public void Add(string line) => _lines.Add(line);

    public void Print(string title)
    {
        Console.WriteLine($"{title}: {_lines.Count} reminders");
        foreach (var line in _lines)
        {
            Console.WriteLine($"  {line}");
        }
    }
}

Output

primary constructor: 2 reminders
  09:30 Asha: coach B2 opens at 10:15
  09:30 Ravi: seat 12B is confirmed
classic constructor: 2 reminders
  09:30 Asha: coach B2 opens at 10:15
  09:30 Ravi: seat 12B is confirmed

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run service.cs

Both sent the same two reminders at 09:30, the time of the fixed clock. Inside ReminderService, Stamp reads clock and Remind reads outbox: the parameters are in scope in the whole body of the type. They stay parameters, not members, so outside code cannot write primary.clock, and inside the class this.clock does not compile. Primary constructors for classes and for structs arrived in C# 12; records had them since C# 9, and only a record turns its parameters into public properties. Primary constructors for classes suit types like this one, a service that keeps what it was given.

Checkpoint

Check: Parameters in scope in every member

  1. Question 1 of 2 In service.cs, which code can use the parameter clock of ReminderService(TimeProvider clock, Outbox outbox)?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Every member of ReminderService: its methods, properties and initializers

    The parameters of a primary constructor are in scope throughout the type's body, but they stay parameters: only the type's own code can use them.

  2. Question 2 of 2 After creating primary in service.cs, a teammate adds Console.WriteLine(primary.clock);. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It does not compile: clock is a parameter of the class, not a member that callers can read

    Outside code reaches a value only through a member such as public TimeProvider Clock => clock;. Records are the exception: their parameters become public properties.

Other constructors call this(…), and a struct keeps its zero default

journey.cs gives a struct and a class a primary constructor each, plus a second constructor that fills in a default:

journey.cs: constructors of a struct and a class C# · journey/journey.cs
// A struct and a class with primary constructors, the constructors that call them with this(...), and the
// constructors each type really has.
var trip = new Journey(from: 3, to: 9);
var shortTrip = new Journey(stations: 4);
var empty = new Journey();
var berth = new Berth("B2", 12);
var chained = new Berth(40);

Console.WriteLine($"new Journey(3, 9):  {trip}");
Console.WriteLine($"new Journey(4):     {shortTrip}");
Console.WriteLine($"new Journey():      {empty}");
Console.WriteLine($"new Berth(\"B2\", 12): {berth}");
Console.WriteLine($"new Berth(40):      {chained}");
ListConstructors(typeof(Journey));
ListConstructors(typeof(Berth));

static void ListConstructors(Type type)
{
    var signatures = type.GetConstructors()
        .OrderByDescending(c => c.GetParameters().Length)
        .Select(c => $"({string.Join(", ", c.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}"))})");
    Console.WriteLine($"{type.Name} declares {string.Join(" and ", signatures)}");
}

// A struct: new Journey() still works and gives every parameter its default value, 0.
struct Journey(int from, int to)
{
    // Any other constructor must call the primary constructor with this(...).
    public Journey(int stations)
        : this(1, 1 + stations)
    {
    }

    public int Stops => Math.Abs(to - from);

    public override string ToString() => $"station {from} to {to}, {Stops} stops";
}

// A class: once it has a primary constructor, it has no parameterless constructor.
sealed class Berth(string coach, int number)
{
    public Berth(int number)
        : this("S1", number)
    {
    }

    public override string ToString() => $"{coach}-{number}";
}

Output

new Journey(3, 9):  station 3 to 9, 6 stops
new Journey(4):     station 1 to 5, 4 stops
new Journey():      station 0 to 0, 0 stops
new Berth("B2", 12): B2-12
new Berth(40):      S1-40
Journey declares (Int32 from, Int32 to) and (Int32 stations)
Berth declares (String coach, Int32 number) and (Int32 number)

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run journey.cs

new Journey(4) ran this(1, 1 + 4) and became station 1 to 5. Every other constructor must call the primary constructor, directly or through another constructor, with : this(...), so that the parameters have values in every object; errors CS8862 in a class and CS8982 in a struct refuse a constructor without it. From there, structs and classes differ in one rule:

A class with a primary constructor A struct with a primary constructor
new T() with no arguments does not compile: the class has no parameterless constructor always works: unless the struct declares its own, every parameter is zero
Other constructors each calls this(...) each calls this(...)

Reflection listed two constructors for each type, yet new Journey() worked and gave station 0 to 0: every struct has a public parameterless constructor, and one the struct does not declare itself fills every field with zeros. A primary constructor does not take it away. Give a struct parameters whose zero values make sense.

Checkpoint

Check: this(...) and the struct default

  1. Question 1 of 2 Which second constructor compiles in class Berth(string coach, int number)?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: public Berth() : this("S1", 1) { }

    Every other constructor reaches the primary constructor, directly or through another constructor, so the parameters are assigned in every object.

  2. Question 2 of 2 struct Fare(int paise) declares Rupees => paise / 100m, and class Ticket(string pnr) has no other constructor. What do new Fare() and new Ticket() do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: new Fare() gives a fare of 0 rupees; new Ticket() does not compile

    Structs and classes differ in one rule: a struct can always be created with no arguments, and when it declares no parameterless constructor of its own, every parameter is then zero.

The compiler keeps a parameter only when a member reads it

capture.cs asks reflection for every field the compiler generated, for a class with three parameters and for a record with the same three:

capture.cs: the fields behind three parameters C# · capture/capture.cs
using System.Reflection;

// Which primary constructor parameters does the compiler keep? Reflection lists the fields it generated for a class
// and for a record with the same three parameters.
ShowFields(typeof(Fare));
ShowFields(typeof(FareRecord));

var fare = new Fare("NDLS-AGC", 450_00, 3);
Console.WriteLine(fare.Describe());

static void ShowFields(Type type)
{
    const BindingFlags all = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
    var fields = type.GetFields(all).Select(f => f.Name).Order(StringComparer.Ordinal);
    var properties = type.GetProperties().Select(p => p.Name).Order(StringComparer.Ordinal);
    Console.WriteLine(type.Name);
    Console.WriteLine($"  fields: {string.Join(", ", fields)}");
    Console.WriteLine($"  public properties: {string.Join(", ", properties)}");
}

class Fare(string route, int basePaise, int stops)
{
    // basePaise is read only by this initializer, so no copy of the parameter is kept.
    public int BasePaise { get; } = basePaise;

    // stops and route are read by members, so the compiler keeps each in a hidden field.
    public int TotalPaise => BasePaise + (stops * 25_00);

    public string Describe() => $"{route}: {TotalPaise / 100} rupees for {stops} stops";
}

// A record turns the same parameters into public properties.
record FareRecord(string Route, int BasePaise, int Stops);

Output

Fare
  fields: <BasePaise>k__BackingField, <route>P, <stops>P
  public properties: BasePaise, TotalPaise
FareRecord
  fields: <BasePaise>k__BackingField, <Route>k__BackingField, <Stops>k__BackingField
  public properties: BasePaise, Route, Stops
NDLS-AGC: 525 rupees for 3 stops

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run capture.cs

basePaise is read only by the initializer of BasePaise, which runs while the object is being created, so no copy of the parameter is kept: the property’s backing field holds the value. route and stops are read by members that run later, so the compiler keeps each in a hidden field, <route>P, a name no C# code can write. Such a captured parameter: A primary constructor parameter that a member of the type reads after construction. The compiler keeps it in a hidden private field for as long as the object lives; that field is not readonly unless the type is a readonly struct. lives as long as the object. The record kept the same values as three public properties instead. Step through where each parameter ends up:

Interactive · step by step Where the three parameters of Fare end up Step through new Fare("NDLS-AGC", 450_00, 3). The parameters stand on the left, the object's fields on the right.
Where the three parameters of Fare end up, step 5 of 5. Showing route, basePaise, stops, <route>P (hidden field, writable), BasePaise, <stops>P (hidden field, writable). In focus: <route>P, <stops>P.capturedcopied oncecapturedrouteread by Describe()basePaiseread by one initializerstopsread by TotalPaise<route>Phidden field, writableBasePaisethe property's field<stops>Phidden field, writable

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Three parameters. new Fare("NDLS-AGC", 450_00, 3) passes three values. Inside Fare they are parameters, in scope in every member, and none of them is a member that other code can read.
  2. Read once, kept once. basePaise is read only by the initializer of BasePaise, which runs while the object is being created. The value goes into the backing field of the property, and no copy of the parameter is kept.
  3. Read later, captured. Describe() runs long after the constructor has finished, so route has to outlive it. The compiler keeps it in a private field named <route>P, a name that no C# code can write.
  4. Captured too. TotalPaise reads stops every time it is called, so stops is captured as well. Reflection listed exactly these three fields, and Describe() printed 525 rupees for 3 stops.
  5. Hidden, and writable. The two captured fields are not readonly: a method of Fare could assign to route, and every later Describe() would show the new value. A record would have made all three values public properties instead.

Two rules follow. A parameter that a member captures and an initializer also copies into a property is stored twice, and warning CS9124 points at the copy. A method may assign to a captured parameter, because its field is readonly only in a readonly struct.

Key idea

A primary constructor parameter belongs to the whole type: the compiler stores it only when a member reads it, in a hidden field that is writable unless the type is a readonly struct, and never as a property unless the type is a record.

Checkpoint

Check: Which parameters the compiler keeps

  1. Question 1 of 2 class Seat(string coach, int number, int farePaise) declares public string Coach { get; } = coach;, public string Label() => $"{Coach}-{number}"; and public decimal Rupees => farePaise / 100m;. Which parameters does the compiler keep in hidden fields?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • farePaise
    • number

    A parameter is captured when a member reads it. A parameter read only by initializers needs no field of its own, because initializers run while the object is being created.

  2. Question 2 of 2 capture.cs listed the members of class Fare and of record FareRecord, built from the same three parameters. What did the record have that the class did not?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A public property for each parameter: Route, BasePaise and Stops

    Only records generate public properties from primary constructor parameters. In a class you declare the properties you want callers to see.

One value stored twice: CS9124 and CS9107 reproduced, with the fixes Pro: opens with a Pro, Premium or Ultimate pass

A parameter can be copied into a property and captured by a member as well, or handed to a base class and captured again. The run below does both, and the copies drift apart as soon as the object changes.

In this part:

  • CS9124 reproduced: a metro card whose summary forgets its payments
  • CS9107: a derived class that keeps the old name
  • One copy, read through the member that owns it

2 runnable examples (C#, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Captured parameters are writable: a quota that loses its limit, and the readonly fixes Pro: opens with a Pro, Premium or Ultimate pass

Nothing stops a method from assigning to a captured parameter, so the value a type was created with can be lost for good. The run below counts down a daily quota both ways, then the compiler stops the same mistake in a readonly struct.

In this part:

  • A quota that counts down its own parameter, reproduced
  • readonly struct and two rules the compiler enforces

3 runnable examples (C#, Python) · about 4 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Validation, and choosing between primary, ordinary and record constructors Pro: opens with a Pro, Premium or Ultimate pass

A primary constructor has no body, so every check of its arguments lives in an initializer or in a member. The run below checks a booking number three ways and finds the way that lets an invalid booking exist.

In this part:

  • Three places to check a PNR: one lets a bad booking exist
  • A decision you can test against your own type

1 runnable example (C#) · about 7 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Practise: platforms, fares and passengers

platform.cs checks three types you write. Platform and Fare become a class and a struct with primary constructors; Passenger keeps an ordinary constructor, because it trims the name and refuses bad values before it stores anything. The types you start from only throw, so all 8 checks fail:

platform.cs: eight checks of three types C# · practice/platform.cs
// Practice: write Platform and Fare with primary constructors, and keep Passenger's ordinary constructor, which
// checks and trims its arguments. Then run this file until all eight checks pass.
(string Check, Func<string> Run, string Expected)[] checks =
[
    ("the platform number", () => new Platform(4, "New Delhi").Number.ToString(), "4"),
    ("an announcement", () => new Platform(4, "New Delhi").Announce("12951"), "Platform 4, New Delhi: 12951"),
    ("the chained constructor", () => new Platform(7).Announce("12002"), "Platform 7, New Delhi: 12002"),
    ("a fare in rupees", () => new Fare(45_50).Rupees.ToString(System.Globalization.CultureInfo.InvariantCulture), "45.5"),
    ("new Fare()", () => new Fare().Rupees.ToString(System.Globalization.CultureInfo.InvariantCulture), "0"),
    ("a name trimmed", () => new Passenger("  Asha Rao ", 34).Name, "Asha Rao"),
    ("a blank name", () => new Passenger("   ", 34).Name, "throws ArgumentException"),
    ("an age of -1", () => new Passenger("Ravi Iyer", -1).Name, "throws ArgumentOutOfRangeException"),
];

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;

// Rewrite with a primary constructor (int number, string station); Platform(int number) uses "New Delhi".
class Platform
{
    public Platform(int number, string station) => throw new NotImplementedException();

    public Platform(int number) => throw new NotImplementedException();

    public int Number => throw new NotImplementedException();

    // "Platform 4, New Delhi: 12951"
    public string Announce(string train) => throw new NotImplementedException();
}

// Rewrite as a struct with a primary constructor (int paise); Rupees is paise / 100m.
struct Fare
{
    public Fare(int paise) => throw new NotImplementedException();

    public decimal Rupees => throw new NotImplementedException();
}

// Keep an ordinary constructor: it trims the name, refuses a blank one and an age outside 0 to 125.
class Passenger
{
    public Passenger(string name, int age) => throw new NotImplementedException();

    public string Name => throw new NotImplementedException();

    public int Age => throw new NotImplementedException();
}

Output (exit status 1)

the platform number: FAIL, throws NotImplementedException instead of 4
an announcement: FAIL, throws NotImplementedException instead of Platform 4, New Delhi: 12951
the chained constructor: FAIL, throws NotImplementedException instead of Platform 7, New Delhi: 12002
a fare in rupees: FAIL, throws NotImplementedException instead of 45.5
new Fare(): FAIL, throws NotImplementedException instead of 0
a name trimmed: FAIL, throws NotImplementedException instead of Asha Rao
a blank name: FAIL, throws NotImplementedException instead of throws ArgumentException
an age of -1: FAIL, throws NotImplementedException instead of throws ArgumentOutOfRangeException
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 platform.cs

Write the three types

Copy platform.cs. Declare class Platform(int number, string station) with Platform(int number) : this(number, "New Delhi"), and struct Fare(int paise) with Rupees => paise / 100m. In Passenger, call ArgumentException.ThrowIfNullOrWhiteSpace(name) and check the age before storing name.Trim(). Run dotnet run platform.cs until it prints 8 of 8 checks pass.

Then check yourself on primary constructors. The questions start with the idea you know least:

Practice that adapts to you

Practise this lesson

8 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 8 Where are the parameters of class ReminderService(TimeProvider clock, Outbox outbox) in scope?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: In the whole body of the class

    A primary constructor's parameters are in scope throughout the type's body and nowhere else.

  2. Question 2 of 8 Callers of ReminderService now need to read the clock it uses. What is the smallest change that keeps the class as it is?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: Add public TimeProvider Clock => clock;

    Declare the members you want callers to see; primary constructor parameters never become members of a class.

  3. Question 3 of 8 What must every other constructor of a type with a primary constructor do?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: Call the primary constructor with : this(...), directly or through another constructor

    All roads lead through the primary constructor, which is why its parameters can be used anywhere in the type.

  4. Question 4 of 8 For struct Journey(int from, int to) of journey.cs, how many stops does new Journey().Stops report?

    Type a number.

    Show the answer: Question 4 of 8

    Answer: 0 stops

    Journey declares no parameterless constructor, so new Journey() sets every field to zero: from and to are both 0 and Stops is 0, as the run printed: station 0 to 0, 0 stops.

  5. Question 5 of 8 A library changes class Berth, which had only a parameterless constructor, to class Berth(string coach, int number) with no other constructor. What breaks for the library's callers?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: Every new Berth() in their code stops compiling

    Adding a primary constructor removes a class's implicit parameterless constructor. Declare public Berth() : this("S1", 1) { } if callers still need it.

  6. Question 6 of 8 When does the compiler keep a primary constructor parameter in a hidden field?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: When a member, such as a method or a property getter, reads it

    Capture follows use: a parameter read only by initializers is not stored, and one read by a member is.

  7. Question 7 of 8 Which parameters of this class does the compiler capture?

    Read the code, then choose one answer.

    class Order(string id, int quantity, decimal price)
    {
        public int Quantity { get; } = quantity;
        public decimal Total { get; } = quantity * price;
        public override string ToString() => $"{id} x {Quantity}";
    }
    Show the answer: Question 7 of 8

    Answer: Only id

    Look for each parameter outside the initializers: only id is read there, in ToString(), so only id is captured.

  8. Question 8 of 8 Reflection on a class you did not write shows fields named <route>P and <stops>P. What can you conclude?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: The class has a primary constructor whose parameters route and stops are read by its members

    The names are an implementation detail no C# code can write, but they show which parameters members use.

Interview questions

Fresher · What does a primary constructor give a C# class, and what doesn’t it?

It declares the constructor’s parameters after the class name, as in class ReminderService(TimeProvider clock, Outbox outbox), and puts them in scope in every member, so the class needs no fields and no constructor just to keep its dependencies. It does not make them members or properties: callers cannot read them, and this.clock does not compile; only a record turns its parameters into public properties. The compiler stores a parameter only when a member reads it, in a hidden field that is not readonly. Every other constructor must call the primary one with this(...), and a class loses its parameterless constructor.

  1. Fresher to mid

    A MetroCard class takes a balance in its primary constructor, declares Balance { get; private set; } = balance; and builds Summary() from balance. After two payments, Summary() still shows the opening balance while Balance is right. What is happening, and how do you fix it and keep it fixed?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Your team wants to convert all 40 service and entity classes of a project to primary constructors in one pull request. Which classes would you convert, which would you leave alone, and what would you check after the change?

    Model answer, Pro. First step and follow-ups

  3. Fresher to mid

    A Booking(string pnr) class with a primary constructor checks the PNR inside the getter of its Pnr property. Logs show bookings created with malformed PNRs that fail much later, far from where they came in. Why does the check run late, and where should it run?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A primary constructor writes a type’s parameters after its name; they are in scope in every member but stay parameters, not members, and only a record makes them properties.
  • Every other constructor calls the primary one with this(...); a class loses its parameterless constructor, and a struct keeps one, which sets every parameter to zero unless the struct declares its own.
  • The compiler keeps a parameter only when a member reads it, in a hidden field that is writable outside a readonly struct; a parameter read only by initializers is not kept at all.

Terms in this lesson

captured parameter
A primary constructor parameter that a member of the type reads after construction. The compiler keeps it in a hidden private field for as long as the object lives; that field is not readonly unless the type is a readonly struct.
primary constructor
A parameter list written right after a class or struct name, as in class Fare(string route, int paise). Its parameters are in scope in the whole body of the type, and every other constructor must call it with this(...). Records have one too, and only theirs become properties.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.