Classes, Constructors and Value Objects
Dart's class syntax is compact in one specific way: the constructor can assign
its own fields. Point(this.x, this.y) is the whole thing - no body, no
repetition.
class Point {
final double x;
final double y;
Point(this.x, this.y);
double get magnitude => (x * x + y * y);
@override
String toString() => 'Point($x, $y)';
}
void main() {
final p = Point(3, 4);
print(p);
print(p.magnitude);
// p.x = 9; // final fields: this would not compile
}
new is optional and nobody writes it. A getter declared with get is called
without parentheses, which is the convention for anything that only reads.
Named constructors, initialiser lists and validation
A class can have as many constructors as it has meanings, each with a name. An
initialiser list (after the :) runs before the body and is where final fields
and validation go.
class Temperature {
final double celsius;
// The main constructor validates in the initialiser list, because assert and
// field initialisers both run before the body.
Temperature(this.celsius) : assert(celsius >= -273.15, 'below absolute zero');
// Named constructors read at the call site: Temperature.fahrenheit(98.6).
Temperature.fahrenheit(double f) : celsius = (f - 32) * 5 / 9;
Temperature.absoluteZero() : celsius = -273.15;
double get fahrenheit => celsius * 9 / 5 + 32;
@override
String toString() => '${celsius.toStringAsFixed(1)}C';
}
void main() {
print(Temperature(21));
print(Temperature.fahrenheit(98.6));
print(Temperature.absoluteZero().fahrenheit);
}
Factory constructors
A factory does not have to create a new instance. It can return a cached one,
an instance of a subclass, or throw - which makes it the natural home for
parsing.
class Currency {
final String code;
// A private constructor, so the only way in is through the factory.
const Currency._(this.code);
static final Map<String, Currency> _cache = {};
factory Currency(String code) {
final upper = code.toUpperCase();
if (upper.length != 3) {
throw FormatException('a currency code is three letters', code);
}
return _cache.putIfAbsent(upper, () => Currency._(upper));
}
@override
String toString() => code;
}
void main() {
final a = Currency('gbp');
final b = Currency('GBP');
print(identical(a, b)); // true: the factory returned the cached one
try {
Currency('pounds');
} on FormatException catch (e) {
print(e.message);
}
}
Equality is identity until you say otherwise
Two separately built objects with the same contents are not equal in Dart
unless the class says how to compare them. A value object overrides both ==
and hashCode - both, always, because collections use the pair.
class Money {
final int pence;
final String currency;
const Money(this.pence, this.currency);
@override
bool operator ==(Object other) =>
other is Money && other.pence == pence && other.currency == currency;
@override
int get hashCode => Object.hash(pence, currency);
Money operator +(Money other) {
if (other.currency != currency) {
throw ArgumentError('cannot add $currency to ${other.currency}');
}
return Money(pence + other.pence, currency);
}
@override
String toString() => '${(pence / 100).toStringAsFixed(2)} $currency';
}
void main() {
const a = Money(500, 'GBP');
const b = Money(500, 'GBP');
print(a == b); // true, because == was overridden
print([a, b].toSet().length); // 1, because hashCode agrees with ==
print(a + const Money(250, 'GBP'));
// const constructors also mean the two literals are one object.
print(identical(a, b));
}
If you override == and forget hashCode, sets and map keys quietly stop
working: two equal objects land in different buckets and both survive.
Inheritance, and preferring composition to it
abstract class Shape {
const Shape();
double get area; // no body: subclasses must supply one
String describe() => '${runtimeType} of area ${area.toStringAsFixed(2)}';
}
class Rectangle extends Shape {
final double width;
final double height;
const Rectangle(this.width, this.height);
@override
double get area => width * height;
}
class Circle extends Shape {
final double radius;
const Circle(this.radius);
@override
double get area => 3.141592653589793 * radius * radius;
@override
String describe() => 'Circle r=$radius, ${super.describe()}';
}
void main() {
const shapes = <Shape>[Rectangle(3, 4), Circle(1)];
for (final shape in shapes) {
print(shape.describe());
}
}
abstract means the class cannot be instantiated and may leave members without
bodies. @override is not required by the compiler, but the analyzer will tell
you when it is wrong - which is the case where you renamed a method and left a
near-miss behind.
Static members and constants
class Config {
static const int maxRetries = 3;
static int _instances = 0;
final String name;
Config(this.name) {
_instances++;
}
static int get instances => _instances;
}
void main() {
Config('a');
Config('b');
print(Config.maxRetries);
print(Config.instances);
}
What to take away
this.fieldin a constructor assigns it; the initialiser list runs first.- Named constructors give each way of building the object a readable name.
factorycan cache, return a subclass, or throw - it need not build anything.- Override
==andhashCodetogether, or sets and maps misbehave. - A
constconstructor lets identical literals share one object. abstractfor a contract with a partial implementation;@overrideto catch typos.