LEVIATHAN v962456e · 962456eee1

Declarations

readonly fields

Fix an instance field once, either in its initializer or in every constructor, and keep it read-only afterwards.

since 0.1.0-alpha.1linuxwindowswasm

Description

readonly is the construction-time counterpart of const. It applies to instance fields only: a field that is set once while the object is being built and cannot change afterwards. Use it for values that are only known at construction time, such as an injected dependency or a generated id.

class AuthController {
    private readonly IUserService userService;
    new AuthController(IUserService userService) this.userService = userService;
}

class Session {
    readonly string id = generateId();   // initializer form; may be computed at run time
}

A readonly field is written in exactly one place: its initializer, or each of the declaring class's constructors. After the object is built the field is read-only.

readonly in the constructor form and the initializer form

interface IUserService { string find(); }
class Real : IUserService { string find() => "ada"; }
class AuthController {
    private readonly IUserService userService;
    new AuthController(IUserService userService) this.userService = userService;
    string who() => userService.find();
}
int nextId = 100;
int makeId() { nextId += 1; return nextId; }
class Session {
    readonly int id = makeId();
    readonly string tag;
    new Session(string t) { tag = t; }
    new Session() { tag = "default"; }
}
console.writeln(AuthController(Real()).who());
Session a = Session("a");
Session b = Session();
console.writeln(a.id);
console.writeln(b.id);
console.writeln(a.tag);
console.writeln(b.tag);
ada
101
102
a
default

Rules

  • A readonly field must be written exactly once.
  • Initializer form (readonly T x = v;): the initializer is the one write. A constructor that also assigns x is a second write and a compile error. The initializer can be any expression.
  • Constructor form (readonly T x;): every constructor the class declares must assign x exactly once. A constructor that does not assign it is a compile error, and so is a class with no constructor and no initializer, since nothing could assign the field.
  • The constructor assignment must be one of the constructor body's own top-level statements. An assignment inside an if/else, even one that covers both branches, is not recognized, and the constructor is rejected as not assigning the field.
  • Outside the write window, a write is a compile error: from an ordinary method, from another class, after construction, or from a derived class that reaches the base's readonly field directly. A derived class sets a base's readonly field by calling the base constructor.
  • readonly is not part of the type and is not transitive: the field itself is fixed, but the object it refers to can still change.
  • A set accessor cannot be declared over a readonly field, and a field cannot be both const and readonly.
  • If two bases declare a field of the same name and type, one readonly and one not, the class that inherits both is ambiguous and a compile error. Resolve it with distinct or by restating the field in the derived class.

Examples

Each of these is rejected at compile time:

Violations of write-once

class A { readonly int x; new A() { x = 1; x = 2; } }          // assigned twice
class B { readonly int x = 1; new B() { x = 2; } }             // initializer plus constructor
class C { readonly int x; new C() { } }                        // never assigned
class D { readonly int x; }                                    // no constructor, no initializer
class E { readonly int x = 1; void reset() { x = 2; } }        // outside the write window
class F { readonly int x; new F(bool c) { if (c) { x = 1; } else { x = 2; } } }   // not a top-level statement
not run — a compile error on purpose

A readonly field sets a base class's value through the base constructor:

A derived class uses the base constructor

class Base {
    readonly int id;
    new Base(int i) { id = i; }
}
class Derived : Base {
    new Derived() { Base::Base(42); }
}
console.writeln(Derived().id);
42

See also

  • const declarations — Fix a local, field, global, parameter or loop binding after its initializer, with the exact rules for each kind of slot.
  • Constructors — Declare constructors with new, select among them by label and argument types, call base constructors, and construct classes that declare none.
  • Fields — Declare the data a class or struct holds, with optional initializers, and how a field without an initializer gets its default value.