Control Flow, Functions and Cascades
Control flow in Dart will look familiar. What is worth your attention is how functions declare their parameters, because Dart gives you three kinds and the choice shows up in every call site you write afterwards.
void main() {
final scores = [88, 42, 95, 61];
for (final score in scores) { // final: the loop variable is fresh
if (score >= 90) {
print('$score: excellent');
} else if (score >= 60) {
print('$score: pass');
} else {
print('$score: resit');
}
}
// The C-style loop is still there when you need the index.
for (var i = 0; i < scores.length; i++) {
if (scores[i] == 42) continue;
if (scores[i] > 90) break;
}
var countdown = 3;
while (countdown > 0) {
print('t-minus $countdown');
countdown--;
}
}
switch, which is an expression too
A switch statement in Dart needs no break: cases do not fall through. Since
Dart 3 there is also a switch expression, which produces a value.
String describe(int code) {
switch (code) {
case 200:
return 'ok';
case 404:
return 'not found';
default:
return 'something else';
}
}
// The same thing as an expression: => instead of :, commas between cases.
String describeShort(int code) => switch (code) {
200 => 'ok',
404 => 'not found',
_ => 'something else',
};
void main() {
print(describe(404));
print(describeShort(404));
print(describeShort(500));
}
The expression form has a property the statement form does not: it must produce a value for every input, so the compiler can tell you when you have missed a case. Level 3 uses that to make illegal states unrepresentable.
Three kinds of parameter
// Required and positional: the order is the contract.
int add(int a, int b) => a + b;
// Optional and positional, in square brackets, with defaults.
String greet(String name, [String greeting = 'Hello', String punctuation = '!']) =>
'$greeting, $name$punctuation';
// Named, in braces. Optional unless marked required, and order-free.
String describe({required String name, int age = 0, String? title}) {
final prefix = title == null ? '' : '$title ';
return '$prefix$name, $age';
}
void main() {
print(add(2, 3));
print(greet('Ada'));
print(greet('Ada', 'Good morning'));
print(describe(name: 'Ada', age: 36));
print(describe(age: 36, name: 'Ada', title: 'Dr'));
}
The rule of thumb: positional when the meaning is obvious and there are one or
two of them, named for everything else. draw(rect, 10, 20, true, false) tells
a reader nothing; draw(rect, x: 10, y: 20, filled: true, dashed: false) does.
Functions are values
int applyTwice(int Function(int) f, int start) => f(f(start));
void main() {
int double(int n) => n * 2;
print(applyTwice(double, 3)); // a named function as a value
print(applyTwice((n) => n + 10, 3)); // an anonymous one
print(applyTwice((int n) { // the block form, when it needs lines
final doubled = n * 2;
return doubled + 1;
}, 3));
// A typedef names a function type, which helps when it appears repeatedly.
print(transform([1, 2, 3], (n) => n * n));
}
typedef Mapper = int Function(int value);
List<int> transform(List<int> values, Mapper f) => values.map(f).toList();
Closures capture variables, not values
A function written inside another sees that function's variables, and keeps them alive after it returns.
Function makeCounter() {
var count = 0; // one variable, captured by the closure
return () {
count++;
return count;
};
}
void main() {
final next = makeCounter();
print(next()); // 1
print(next()); // 2
final other = makeCounter(); // a separate call, a separate count
print(other()); // 1
// Each iteration of a for-in with `final` gets its own variable, so these
// closures capture three different values rather than all seeing the last.
final callbacks = <Function>[];
for (final n in [1, 2, 3]) {
callbacks.add(() => print(n));
}
for (final callback in callbacks) {
callback();
}
}
Cascades, for when you are configuring one object
.. calls a method and returns the receiver rather than the method's result,
so a chain of setup stays one expression.
class Query {
final List<String> parts = [];
void select(String what) => parts.add('SELECT $what');
void from(String table) => parts.add('FROM $table');
void where(String condition) => parts.add('WHERE $condition');
@override
String toString() => parts.join(' ');
}
void main() {
final query = Query()
..select('title')
..from('courses')
..where('published = 1');
print(query);
}
What to take away
switchdoes not fall through, and the expression form makes it produce a value.- Positional for one or two obvious arguments; named for everything else.
requiredmakes a named parameter compulsory without making it positional.- Functions are values, with types like
int Function(int). - A closure captures the variable;
for (final x in ...)gives each turn its own. ..returns the receiver, which turns setup into a single expression.