Here are some thoughts after reading the documentation, I am not intend to be original and any examples are dedicated to their original authors.
Records
Reference doc: Records | Dart
The documentation may be somewhat insufficient to explain the type annotation, I add more examples here:
(int, int, {int a, int b}) record1 = (2, a: 1, b: 2, 1);
(int, int, {int a, int b}) record2 = (1, 2, b: 2, a: 1);
(int, int, {int a, int b}) record3 = (1, b: 2, 2, a: 1);
print(record1 == record2); // false
print(record2 == record3); // true
- When you define your record, the order is not matter (but you still need to maintain the general order of positional fields), the compiler can match the data to the corresponding field once you define your record correctly.
- Say the second line,
(int, int, {int a, int b}) record2 = (1, 2, b: 2, a: 1)
, 1 is matched to the FIRST positional field, 2 is matched to the SECOND positional field, 2 is matched to named field b, 1 is matched to the named field a.
Pedagogically, I think the doc is somewhat too early in introducing how to access a record’s field. I think explaining how the type annotation works first. Then, it is easier to understand why accessing field with the following syntax:
var record = ('first', a: 2, b: true, 'last');
print(record.$1); // Prints 'first'
print(record.a); // Prints 2
print(record.b); // Prints true
print(record.$2); // Prints 'last'
It is clearer why need to use $1
and $2
to access the positional field instead of using $1
and $4
after you understand the inner working of type annotation.
I think records should be introduced in book1 near the end of the book, probably after introducing maps. And introducing the application of returning multiple values of a function immediately. Earlier chapters in function can make a note to refer to here.
Another application is parallelization of futures of different types, which can introduce in book2 of Future chapters.
Patterns
Official doc: Patterns | Dart
The documentation is long but should be clear to the reader. I just want to mention some syntax difference when between direct assignment and matching:
(Just copy from my own notes)
variable assignment
var record1 = (3, "two");
var (a, b) = record1; // correct
(var c, var d) = record1; // wrong
// wrong below
if (record1 case var (e, f)) {
// do something
}
// correct below
if (record1 case (var g, var h)) {
// do something
}
Explanation:
- For
case
pattern matching, the assignment keyword must go inside, while outside when using direct variable assignment. (Why?)
rest (...
) and wildcard (_
)
var [a, b, ..., c, d] = [1, 2, 3, 4, 5, 6, 7];
// Prints "1 2 6 7".
print('$a $b $c $d');
var [e, f, _, _, _, g, h] = [1, 2, 3, 4, 5, 6, 7];
print('$e $f $g $h');
var (a, b, ..., c, d) = (1, 2, 3, 4, 5, 6, 7);
// error
print('$a $b $c $d');
var (e, f, _, _, _, g, h) = (1, 2, 3, 4, 5, 6, 7);
print('$e $f $g $h');
Explanation:
- rest keyword acts as a placeholder for several elements and can be used in list only
- wildcard acts as a placeholder for single element only and can be used in records, maps, list etc…
- wildcard has another usage to use as default in
switch
statement.
In my opinion, general concept of pattern can be introduced in beginning chapter of book 2 (may be after string manipulation chapter). Then, reinforcing the concepts sparingly on later chapter. For example, introduce destructure data about record, then introduce destructure class instance data in the later class chapter.
There is one application of writing algebraic data type. It combines the exhaustiveness checking of switch and sealed class feature, this can be mentioned in a separate chapter after sealed class, may be named “switch expression” and mention this application altogether. If it is just too short, just add the guard clause of switch here.
By the way, the official doc of pattern has told us WHEN to use algebraic data type, but not WHY we need to use it. But the official doc of switch: Branches | Dart has told you WHY but not WHEN to use it.
class modifier
Official doc: Class modifiers | Dart
There is one important change in mixin: Dart 3.0 no longer allows classes to be used as mixins by default. Instead, you must explicitly opt-in to that behavior by declaring a mixin class
:
Other than that, the doc is really clear.
I think base, interface and final modifier can be introduced together:
extended | implemented | |
---|---|---|
base | Yes | No |
interface | No | Yes |
final | No | No |
Explanation:
- The restriction is applied to outside of the library only. Within the same file, it just doesn’t matter.
Then, introduce sealed and some switch examples in another chapter.
The official has explained the drawbacks of extends
and implements
here: Class modifiers for API maintainers | Dart
It gives an example for implements
and worth to see. (Adapt this example a little bit, it illustrate how to access a private method in another library.)
The example of extends
here (adapt from wiki directly):
In library a.dart, we define:
class Super {
var _counter = 0;
void inc1() {
_counter++;
print(_counter);
}
void inc2() {
_counter++;
print(_counter);
}
}
In library b.dart, we define:
class Sub extends Super {
@override
void inc2() {
inc1();
}
}
In main
of b.dart, we run:
var sub = Sub();
sub.inc2(); // 1 will be printed
It seems don’t have problem, however, if we change the implementation of inc1
of the parent class as follows:
void inc1() {
inc2();
}
an instance of Sub will cause an infinite recursion between itself and the method inc1() of the super-class and eventually cause a stack overflow.
Just re-write some chapters in book2 to incorporate the class modifier content.
Combining class modifier
I think the official doc hasn’t mention enough examples about this. I hope the author can give more examples about combining class modifier.