Chapter 8: Classes and Objects
What you'll learn: how to declare classes with fields, a constructor and methods; why methods that change an object are marked modifier and called inside safe; constructors, static factories and visibility; inheritance with super and override; abstract classes; equality; dynamic class; and how objects release resources.
Structs (Chapter 6) bundle data and are copied when you assign them. A class bundles data and behaviour, and its instances are objects that are shared by reference: assigning an object to another variable, or passing it to a function, gives a second name for the same object. Use a class when something has an identity — an account, a session, a parser in the middle of its input — or when you want inheritance.
Defining a Class
@mode(host.scripted)
class Dog {
pub let name: string;
pub let breed: string;
pub var age: int;
new(name: string, breed: string, age: int) {
this.name = name;
this.breed = breed;
this.age = age;
}
pub func bark(): string {
return i"{$ this.name $} the {$ this.breed $} says: Woof!";
}
pub modifier func haveBirthday(): void {
this.age = this.age + 1;
}
}
let dog = Dog("Rex", "Labrador", 3);
println(dog.bark());
safe {
dog.haveBirthday();
}
println(i"{$ dog.name $} is now {$ dog.age $}");
println(dog);
Listing 8-1: A class with fields, a constructor and two methods
Rex the Labrador says: Woof!
Rex is now 4
Dog(name: "Rex", breed: "Labrador", age: 4)
The pieces:
- Fields are declared with
let(cannot change after construction) orvar(can change), followed by a type.pubmakes a field readable outside the class. new(…) { … }is the constructor. A class has at most one. You create an object by calling the class like a function:Dog("Rex", "Labrador", 3).- Methods are functions declared inside the class. Inside a method or the constructor,
thisis the object the method was called on, and fields are always reached through it:this.name. modifiermarks a method that assigns avarfield. Calling amodifiermethod on an object needs asafe { … }block (or asafe funccaller); the section "Methods That Change an Object" explains why.printlnof an object prints its class name and fields.
A class with no : Base clause extends Any, the root of every class (§1-8.3). You will see class Dog : Any in older code; it means the same thing.
Mistake: a field without let or var
If you come from TypeScript or Swift, you may write fields the way structs declare them:
@mode(host.scripted)
class Point {
x: int;
y: int;
}
main.lpscr:4:5: error[E0001]: E_BARE_FIELD: class field `x` must be declared `let` or `var`; write `var x: ...` (mutable) or `let x: ...` (immutable)
main.lpscr:5:5: error[E0001]: E_BARE_FIELD: class field `y` must be declared `let` or `var`; write `var y: ...` (mutable) or `let y: ...` (immutable)
Struct fields need no keyword; class fields always do (ruling FIELD-1). Choose let for a field that is fixed once the object exists, var for one that changes, and add pub if code outside the class reads it: pub let x: int;.
Mistake: leaving out this.
In Java or C#, a method can name a field directly. Loup 1.25 requires this.:
@mode(host.scripted)
class Dog {
pub let name: string;
new(name: string) { this.name = name; }
pub func bark(): string {
return i"{$ name $} says: Woof!";
}
}
println(Dog("Rex").bark());
main.lpscr:8:21: error[E0001]: undefined identifier `name`
The compiler looked for a local variable or parameter called name and found none. Write this.name. The same goes for calling another method of the same object: this.bark(), not bark().
Some examples in the Language Reference read fields and call methods without
this.(for instance in §1-8.3 and §1-11.1). The 1.25 compilers do not accept that; always writethis..
Mistake: declaring the receiver as a parameter
Python methods take self as their first parameter. In Loup the receiver is implicit, and this is a reserved word:
@mode(host.scripted)
class Dog {
pub let name: string;
new(name: string) { this.name = name; }
pub func greet(this: Dog): string {
return i"{$ this.name $} says hi";
}
}
main.lpscr:7:20: error[E0001]: `this` is a reserved word (§1-3.4) and cannot name a parameter
note: rename the parameter — e.g. `this_value`
Delete the parameter: pub func greet(): string { … }. this is available in every method without declaring it.
Objects Are Shared by Reference
A variable of a class type holds a reference to an object. Copying the variable copies the reference:
Struct (value): Class (reference):
let first = Reading(…) let savings = Account("Ada")
+---------------------+ +---------+ +----------------+
| first celsius=21.5 | | savings |---->| owner: "Ada" |
+---------------------+ +---------+ | balance: 10100 |
+----------------+
var second = first (copy) let sameAccount = savings (same object)
+---------------------+ +-------------+ ^
| second celsius=21.5 | | sameAccount |--------+
+---------------------+ +-------------+
A struct copy is independent of the original:
struct Reading {
sensor: string;
var celsius: float;
}
let first = Reading(sensor = "attic", celsius = 21.5);
var second = first;
second.celsius = 30.0;
println(first.celsius);
println(second.celsius);
21.5
30
An object passed to a function is the caller's object, so a change made by the function is visible to the caller:
class Account {
pub let owner: string;
pub var balance: int = 0;
new(owner: string) {
this.owner = owner;
}
pub modifier func deposit(cents: int): void {
this.balance = this.balance + cents;
}
}
safe func payInterest(edits acct: Account): void {
acct.deposit(acct.balance // 100);
}
let savings = Account("Ada");
safe {
savings.deposit(10000);
payInterest(edits: savings);
}
println(savings.balance);
let sameAccount = savings;
println(sameAccount == savings);
println(Account("Ada") == Account("Ada"));
Listing 8-2: A function changes the caller's object; == compares identity
10100
true
false
By default == on objects asks "is this the same object?", so two separately created accounts with the same contents are not equal. "Equality" below shows how to define your own ==.
Methods That Change an Object: modifier and safe
Because many references can point at one object, Loup makes mutation of an object visible in the source code at three places (§3-2.4.11, §3-2.4.12):
- A method that assigns a
varfield is declaredmodifier. - A call to a
modifiermethod — or a direct assignment to apub varfield from outside the class — happens inside asafe { … }block or inside asafe func. - A function that changes an object it receives declares the parameter
edits, and the caller writesedits:at the call site.
Listing 8-2 shows all three. The rules let a reader find every place an object can change, and they let the compiler check — inside safe — that no other reference is being used while the change happens.
Mistake: forgetting modifier
@mode(host.scripted)
class Counter {
pub var count: int = 0;
pub func bump(): void {
this.count = this.count + 1;
}
}
main.lpscr:7:9: error[E_MODIFIER_REQUIRED]: E_MODIFIER_REQUIRED: writing to `var` field 'count' of `self` in method 'bump' of class 'Counter' requires the method to be declared as `modifier`
Declare it pub modifier func bump(): void. (The message says self; it means the object the method was called on, which you write as this.)
Mistake: calling a modifier method outside safe
@mode(host.scripted)
class Counter {
pub var count: int = 0;
pub modifier func bump(): void {
this.count = this.count + 1;
}
}
let hits = Counter();
hits.bump();
println(hits.count);
main.lpscr:12:1: error[E_SAFE_CONTEXT_REQUIRED]: E_SAFE_CONTEXT_REQUIRED: `modifier` method 'bump' called on ARC receiver of type 'Counter' outside `safe {}` or `safe func` context — wrap the call in `safe { ... }` or mark the enclosing function as `safe func`
"ARC receiver" means an ordinary, reference-counted object. Wrap the call, or put the code in a safe func:
class Counter {
pub var count: int = 0;
pub modifier func bump(): void {
this.count = this.count + 1;
}
}
safe func countWords(words: list[string]): Counter {
let counter = Counter();
for (word in words) {
counter.bump();
}
return counter;
}
let hits = Counter();
safe {
hits.bump();
}
println(hits.count);
println(countWords(["to", "be", "or"]).count);
1
3
Assigning a pub var field from outside the class follows the same rule: hits.count = 0; on its own is E_SAFE_CONTEXT_REQUIRED (the message calls the field a "modifier method 'count'"), and safe { hits.count = 0; } compiles.
Mistake: changing an object through a plain parameter
A parameter without a verb is reads: the function may look at the object but not change it.
@mode(host.scripted)
class Counter {
pub var count: int = 0;
pub modifier func bump(): void {
this.count = this.count + 1;
}
}
func record(c: Counter): void {
safe {
c.bump();
}
}
main.lpscr:13:9: error[E_READS_MUTATION]: E_READS_MUTATION: parameter 'c' is a `reads` borrow (a parameter without a verb is read-only, §3-2.4.9) and cannot be mutated (`modifier` method `bump` called on it); declare it `edits` and pass `edits: …` to change the caller's value, or copy it into a local first
Declare the parameter edits, make the function a safe func, and acknowledge the verb at the call site, as payInterest does in Listing 8-2:
safe func record(edits c: Counter): void {
c.bump();
}
let hits = Counter();
safe {
record(edits: hits);
record(edits: hits);
}
println(hits.count);
2
The Language Reference says a function with an
editsparameter is implicitly asafe func(§3-2.4.12). The 1.25 compiler still reportsE_SAFE_CONTEXT_REQUIREDinside such a function unless it is declaredsafe func, so writesafe funcexplicitly. Chapter 19 covers the verbs in depth.
Constructors
Generated constructors
A class that declares no new gets one generated from its fields. Arguments are matched to fields by name, in any order, and a field with a default value may be left out (ruling IMPCTOR-1):
class RetryPolicy {
pub let attempts: int;
pub let delayMs: int = 250;
pub let backoff: float = 2.0;
}
let standard = RetryPolicy(attempts = 3);
let patient = RetryPolicy(backoff = 1.5, attempts = 10, delayMs = 1000);
println(standard);
println(patient);
RetryPolicy(attempts: 3, delayMs: 250, backoff: 2.0)
RetryPolicy(attempts: 10, delayMs: 1000, backoff: 1.5)
Leaving out a field that has no default is an error that names the field:
@mode(host.scripted)
class RetryPolicy {
pub let attempts: int;
pub let delayMs: int = 250;
}
let policy = RetryPolicy(delayMs = 100);
main.lpscr:8:14: error[E0001]: missing required field 'attempts' in 'RetryPolicy' constructor call
Write your own new when construction needs to do more than copy arguments into fields — validate them, compute a field, or open a resource.
Static factories instead of overloaded constructors
In Java or C# you might give a class two constructors. A Loup class has at most one new:
@mode(host.scripted)
class Endpoint {
pub let host: string;
pub let port: int;
new(host: string, port: int) {
this.host = host;
this.port = port;
}
new(host: string) {
this.host = host;
this.port = 80;
}
}
main.lpscr:12:5: error[E0001]: canonical name collision: 'main.lpscr::Endpoint::constructor' is already assigned to another callable
note: previously assigned to callable with HirId(5001420)
The message comes from deep inside the compiler (the Language Reference names this error E_DUPLICATE_CONSTRUCTOR), but the cause is the second new. Give the alternatives names as static methods — static factories (§1-8.4). A factory can also return a Result when construction can fail, which new cannot:
class Endpoint {
pub let host: string;
pub let port: int;
new(host: string, port: int) {
this.host = host;
this.port = port;
}
pub static func localhost(port: int = 8080): Endpoint {
return Endpoint("localhost", port);
}
pub static func checked(host: string, port: int): Result[Endpoint, string] {
if (port < 1 or port > 65535) {
return Err(i"port {$ port $} is out of range");
}
return Ok(Endpoint(host, port));
}
pub func address(): string {
return i"{$ this.host $}:{$ this.port $}";
}
}
println(Endpoint.localhost().address());
println(Endpoint.localhost(port = 3000).address());
for (port in [443, 70000]) {
match Endpoint.checked("api.example.com", port) {
Ok(endpoint) => println(endpoint.address()),
Err(message) => println(i"error: {$ message $}"),
}
}
Listing 8-3: Named factories, one of them fallible
localhost:8080
localhost:3000
api.example.com:443
error: port 70000 is out of range
Visibility
Loup has no private or protected keyword. Visibility works like this (§1-8.2, ruling FPRIV-1):
| Declaration | Without pub |
With pub |
|---|---|---|
| A class field | visible only inside the class and its subclasses | visible everywhere |
| A method, function, class or other module-level name | visible in the same file (module) | visible to every module that imports it |
So a field without pub is private to its class, even from other code in the same file:
@mode(host.scripted)
class Wallet {
pub let owner: string;
var cents: int = 0;
new(owner: string) {
this.owner = owner;
}
pub modifier func add(amount: int): void {
this.cents = this.cents + amount;
}
pub func balance(): int {
return this.cents;
}
}
let wallet = Wallet("Grace");
safe {
wallet.add(250);
}
println(wallet.cents);
main.lpscr:24:9: error[E0001]: member 'cents' of class 'Wallet' is private
Read it through the public method instead — println(wallet.balance()); prints 250. Keeping cents private means every change to it goes through add, which is the one place you would add validation later.
§1-8.2 of the Language Reference still describes all non-
pubmembers as module-private. Ruling FPRIV-1 made class fields private to the class, which is what both compilers enforce; methods stay module-private.
Static Members
A static field or method belongs to the class rather than to an object, and is reached through the class name:
class Ticket {
pub static var issued: int = 0;
pub let number: int;
new() {
Ticket.issued = Ticket.issued + 1;
this.number = Ticket.issued;
}
pub static func clampPriority(value: int): int {
if (value < 1) {
return 1;
}
if (value > 5) {
return 5;
}
return value;
}
}
let a = Ticket();
let b = Ticket();
println(i"tickets {$ a.number $} and {$ b.number $}; {$ Ticket.issued $} issued");
println(Ticket.clampPriority(9));
tickets 1 and 2; 2 issued
5
Static methods have no this.
Inheritance
A class may extend one other class and implement any number of interfaces (Chapter 9). The subclass inherits the parent's fields and methods:
class Animal {
pub let name: string;
new(name: string) {
this.name = name;
}
pub func speak(): string {
return i"{$ this.name $} makes a sound";
}
pub func describe(): string {
return i"I am {$ this.name $}";
}
}
class Cat : Animal {
pub let indoor: bool;
new(name: string, indoor: bool) {
super(name);
this.indoor = indoor;
}
pub override func speak(): string {
return i"{$ this.name $} says: Meow!";
}
}
class Kitten : Cat {
new(name: string) {
super(name, true);
}
pub override func speak(): string {
return super.speak() + " (quietly)";
}
}
let animals: list[Animal] = [Animal("Generic"), Cat("Whiskers", false), Kitten("Tiny")];
for (animal in animals) {
println(animal.speak());
}
println(Kitten("Bo").describe());
Listing 8-4: Single inheritance, super(...), override and super.method()
Generic makes a sound
Whiskers says: Meow!
Tiny says: Meow! (quietly)
I am Bo
class Cat : AnimalmakesCata subclass ofAnimal. ACatcan be used wherever anAnimalis expected.super(name)runs the parent's constructor. It must come before the constructor touchesthis.overridemarks a method that replaces one the parent already implements (§1-8.3.1).super.speak()calls the parent's version of the method, soKittenextendsCat's behaviour instead of repeating it.- The loop calls
speak()on values typedAnimal, and each object runs its own class's version. That is dynamic dispatch.
A generated constructor includes the parent's fields, first in the argument list: with class Animal { pub let name: string; } and class Cat : Animal { pub let indoor: bool = true; }, Cat(name = "Tom") works.
Mistake: overriding without override
@mode(host.scripted)
class Animal {
pub func speak(): string {
return "...";
}
}
class Cat : Animal {
pub func speak(): string {
return "Meow";
}
}
main.lpscr:10:5: error[E_OVERRIDE_REQUIRED]: method `speak` on class `Cat` shadows a parent method on `Animal` and must be marked `override` (E_OVERRIDE_REQUIRED)
The keyword guards against replacing a parent method by accident. Add it: pub override func speak(). The reverse mistake — override on a method the parent does not have — is E_OVERRIDE_NO_TARGET.
Mistake: forgetting super(...)
@mode(host.scripted)
class Animal {
pub let name: string;
new(name: string) { this.name = name; }
}
class Cat : Animal {
pub let indoor: bool;
new(name: string, indoor: bool) {
this.indoor = indoor;
}
}
main.lpscr:12:9: error[E_SUPER_NOT_CALLED]: E_SUPER_NOT_CALLED: constructor of `Cat` accesses `this` before calling `super(...)`; `Animal` has a non-default constructor (§1-8.4)
Animal's constructor needs a name, and only Cat's constructor can supply it. Call super(name); as its first statement.
final
final class Money { … } cannot be extended (E_FINAL_EXTEND), and a pub final func cannot be overridden (E_FINAL_OVERRIDE). Use final on classes and methods whose behaviour other code relies on.
Abstract Classes
An abstract class is a base class that leaves some methods for its subclasses to supply. The classic use is a template method: the base class fixes the outline and calls an abstract step:
abstract class Report {
pub let title: string;
new(title: string) {
this.title = title;
}
abstract func writeBody(): void;
pub func show(): void {
println(i"== {$ this.title $} ==");
this.writeBody();
println("== end ==");
}
}
class SalesReport : Report {
pub let orders: list[int];
new(orders: list[int]) {
super("Sales");
this.orders = orders;
}
pub override func writeBody(): void {
var total = 0;
for (cents in this.orders) {
total = total + cents;
}
println(i"{$ this.orders.length() $} orders, {$ total $} cents");
}
}
class InventoryReport : Report {
pub let items: dict[string, int];
new(items: dict[string, int]) {
super("Inventory");
this.items = items;
}
pub override func writeBody(): void {
for (name in this.items.keys()) {
println(i"{$ name $}: {$ this.items[name] $}");
}
}
}
let reports: list[Report] = [
SalesReport([1250, 899, 4000]),
InventoryReport({"bolts": 120, "nuts": 75}),
];
for (report in reports) {
report.show();
}
Listing 8-5: An abstract class with a template method
== Sales ==
3 orders, 6149 cents
== end ==
== Inventory ==
bolts: 120
nuts: 75
== end ==
The Language Reference makes override optional when implementing an abstract method; the 1.25 compiler requires it, so write it.
⚠ Planned. Abstract classes are only partly implemented in Loup 1.25. An abstract method that returns a value is rejected —
abstract func area(): float;reportserror[E0001]: method 'area' may not return a value on all code paths— and the compiler does not yet reportE_ABSTRACT_INSTANTIATION(creating an abstract class) orE_ABSTRACT_NOT_IMPLEMENTED(a subclass that leaves an abstract method out), both specified in §1-8.3. Until then, use abstract methods that returnvoid, and express a contract with return values as an interface (Chapter 9), which the compiler checks completely.
Equality
Objects compare by identity unless the class defines ==. To compare by contents, implement Equatable and write the operator:
class Point implements Equatable[Point] {
pub let x: float;
pub let y: float;
new(x: float, y: float) {
this.x = x;
this.y = y;
}
pub operator func ==(other: Point): bool {
return this.x == other.x and this.y == other.y;
}
}
let a = Point(1.0, 2.0);
let b = Point(1.0, 2.0);
println(a == b);
println(a != Point(0.0, 0.0));
true
true
!= is derived from ==.
Mistake: an operator without its interface
@mode(host.scripted)
class Point {
pub let x: float;
pub let y: float;
new(x: float, y: float) {
this.x = x;
this.y = y;
}
pub operator func ==(other: Point): bool {
return this.x == other.x and this.y == other.y;
}
}
main.lpscr:12:5: error[E_OPERATOR_NO_INTERFACE]: E_OPERATOR_NO_INTERFACE: operator func '==' on 'Point' requires `implements Equatable` on the enclosing type
Every operator belongs to an interface (== to Equatable, + to Add, and so on; Chapter 9), and the class must say it implements it.
Known gap. Collections do not use a class's
==yet. In 1.25,list.contains,dictkeys andsetelements compare objects by identity even when the class implementsEquatable(andHashable), so[Point(1.0, 2.0)].contains(Point(1.0, 2.0))isfalse. Use strings, numbers or structs as keys, or search with a loop and==.
Dynamic Classes
Most classes have a fixed set of fields. A dynamic class can also carry fields added at run time — useful for loosely structured data such as settings read from a file. Dynamic fields are reached with obj!{name}, never with a dot (§1-8.7):
dynamic class Settings {
pub let app: string;
new(app: string) {
this.app = app;
}
}
let settings = Settings("indexer");
settings!{timeout} = 5000;
settings!{"log-level"} = "debug";
let timeout: int = settings!{timeout};
println(timeout + 1);
println(settings!{"log-level"});
println(settings.hasField("retries"));
println(settings.fieldNames());
5001
debug
false
["timeout", "log-level"]
A name with characters that are not valid in an identifier goes in quotes: !{"log-level"}. Reading a field that was never set throws KeyError; check first with hasField. Assigning a dynamic read to a typed binding (let timeout: int = …) converts it from Any.
Mistake: reading a dynamic field with a dot
@mode(host.scripted)
dynamic class Settings {
pub let app: string;
new(app: string) { this.app = app; }
}
let settings = Settings("indexer");
settings!{timeout} = 5000;
println(settings.timeout);
main.lpscr:10:9: error[E0001]: type 'Settings' has no member 'timeout'
A dot only ever reaches declared members. The different spelling is deliberate: settings.app is checked by the compiler, settings!{timeout} is looked up at run time. Write settings!{timeout}.
Known gap. §1-8.7 says a typed read such as
let t: int = settings!{timeout}throwsTypeErrorwhen the stored value has another type. In 1.25 the conversion is not checked: a string stored undertimeoutis bound totunchanged.
Prefer a regular class or a struct whenever you know the fields in advance. In host.dual a dynamic class draws the warning W_ANY_IN_AOT_MODE, and in freestanding it is an error.
Releasing Resources
Class instances are reference-counted in every mode (§1-8.1.1): an object lives while at least one reference to it exists. Memory takes care of itself, but a file, a lock or a temporary directory should be released at a known point. Loup gives you two tools:
withand theCloseableinterface: a class with aclose()method thatimplements Closeablecan be used in awithblock, andclose()runs when the block ends, even if it ends with an exception.del x: ends the bindingxand runs the object'sfinalize()method, if it has one (§1-6.11, §1-8.9).
class Session implements Closeable {
pub let name: string;
new(name: string) {
this.name = name;
println(i"open {$ name $}");
}
pub func close(): void {
println(i"close {$ this.name $}");
}
}
class ScratchFile {
pub let path: string;
new(path: string) {
this.path = path;
writeText(path, "scratch data");
}
pub func finalize(): void {
println(i"finalize {$ this.path $}");
}
}
with (session = Session("db")) {
println(i"using {$ session.name $}");
}
let scratch = ScratchFile("scratch.tmp");
println(exists(scratch.path));
del scratch;
println("done");
Listing 8-6: with closes a Closeable; del runs finalize()
open db
using db
close db
true
finalize scratch.tmp
done
Known gap. Under the reference-counting contract,
finalize()also runs when the last reference goes away on its own — for example, when a function that created the object returns. The 1.25 interpreter runsfinalize()only fordel. Usewithordelwhenever cleanup matters.
Because objects are reference-counted, two objects that refer to each other keep each other alive. Break such cycles with a weak field; Chapter 19 covers weak, with and defer together.
Classes That Scripts Cannot Use
unique class
A unique class has instances that can never be shared, only moved. It belongs to the native and freestanding modes:
@mode(host.scripted)
unique class Token {
pub let id: int;
new(id: int) { this.id = id; }
}
main.lpscr:3:1: error[E_UNIQUE_CLASS_ARC_BINDING]: E_UNIQUE_CLASS_ARC_BINDING: `unique class 'Token'` is only valid in `@mode(native)` or `@mode(freestanding)`
(The message uses the retired name native; the mode is host.native.) In a script, use an ordinary class, or a unique let binding (Chapter 19). The Loup Native Book covers unique class.
let fields are not yet enforced
A let field is meant to be fixed after construction. Loup 1.25 does not enforce that yet (candidate ruling FIELD-2):
class Account {
pub let id: string;
new(id: string) {
this.id = id;
}
pub func rename(newId: string): void {
this.id = newId;
}
}
let account = Account("A-1");
account.rename("B-2");
println(account.id);
B-2
The method assigns a let field and is not even a modifier; the compiler accepts it. Treat let fields as fixed anyway — a future release will reject this code.
⚠ Planned. Annotations such as
@deprecated("use fetchJson")are specified to produce a warning wherever the marked declaration is used (§1-8.8). In 1.25 the compiler warnsUnknown annotation '@deprecated'at the declaration and nothing at uses. Reading annotations at run time belongs tocore.reflect(Chapter 14), which is not implemented.
Summary
- A class declares fields with
letorvar(pubto expose them), at most onenewconstructor, and methods that reach the object throughthis.. - Objects are shared by reference;
==compares identity unless the class implementsEquatableand definesoperator func ==. - A method that assigns a
varfield ismodifier; calling it, or assigning apub varfield from outside, needssafe { … }or asafe func; a function that changes an object it receives takes it asedits. - Without
new, the generated constructor takes the fields by name, parent fields included; defaulted fields may be left out. - Alternative constructors are
staticfactories, which may returnResult. - Non-
pubfields are private to the class and its subclasses; non-pubmethods are private to the file. - Inheritance is single:
super(args)in the constructor,overrideon replaced methods,super.m()to call the parent's version,finalto forbid extension. - Abstract classes work for
voidabstract methods today; use interfaces for contracts that return values. dynamic classadds run-time fields reached with!{name}.with+Closeableanddel+finalize()release resources at a known point.
Try It Yourself
- Write a
RateLimiterclass with apub varlist of request timestamps and amodifiermethodallow(now: int): boolthat permits at most three requests in any ten-second window. Show that two limiters do not share state. - Give
Endpoint(Listing 8-3) a factoryfromText(text: string): Result[Endpoint, string]that splits"host:port", and print both a good and a bad input. - Extend Listing 8-4 with a
Dog : Animalclass. Then removeoverridefrom itsspeak()and read the error. - Write a
TempDirclass that implementsCloseable, creates a file in its constructor and prints a message inclose(). Use it in awithblock that throws, catch the exception outside, and confirm thatclose()ran first.