Dart Language Tour
Variable และ inference
หัวข้อที่มีชื่อว่า “Variable และ inference”Dart เป็น static typed พร้อม inference ที่แข็งแรง ใช้ var แล้วให้ Dart infer หรือจะ annotate เมื่อช่วยให้อ่านง่ายขึ้นก็ได้ final แปลว่า assign ครั้งเดียว ส่วน const คือค่าคงที่ตอน compile
var name = 'Ada'; // inferred Stringfinal age = 36; // assign-once intconst pi = 3.14159; // compile-time constantint? maybe; // nullable (null safety — next lesson)
// `final` vs `const`: final is runtime-fixed, const is compile-time-fixed.final now = DateTime.now(); // OK — computed at runtime// const now = DateTime.now(); // ERROR — not a compile-time constantconst สำคัญมากใน Flutter: ค่าที่เป็น const จะถูก canonicalize — object const ที่เหมือนกันคือ instance เดียวกัน — นี่แหละที่ทำให้ Flutter ข้ามการ rebuild widget ที่เป็น const ได้
Function และ named parameter
หัวข้อที่มีชื่อว่า “Function และ named parameter”Function ของ Dart รองรับทั้ง positional และ named parameter API ทั้งหมดของ Flutter สร้างบน named parameter — constructor ของทุก widget คือกำแพงของ named parameter
// Named parameters in {}, `required` marks the mandatory ones, others have defaults.Widget greeting({required String name, String prefix = 'Hello'}) { return Text('$prefix, $name');}
greeting(name: 'Ada'); // Hello, Adagreeting(name: 'Ada', prefix: 'Hi'); // Hi, Ada
// Arrow syntax for single-expression functionsint square(int x) => x * x;นี่คือเหตุผลที่การอ่าน code Flutter คือการอ่าน Widget(child: ..., padding: ..., color: ...) — named parameter ทำให้ constructor ใหญ่ ๆ อ่านง่ายและไม่ต้องสนใจลำดับ
Collection และ collection-if / for
หัวข้อที่มีชื่อว่า “Collection และ collection-if / for”List, set, map พร้อม collection-if และ collection-for — ใช้ตลอดเพื่อ build list ของ widget แบบมีเงื่อนไข
final nums = [1, 2, 3];final set = {1, 2, 3};final map = {'a': 1, 'b': 2};
// Spread, collection-if, collection-for — you'll use these inside widget children:final children = [ const Header(), ...items, // spread if (isLoggedIn) const Profile(), // conditional element for (final n in nums) Text('$n'), // generated elements];pattern if/for-ใน-list นี้เป็น idiomatic ของ Flutter — ใช้แทนการ “สร้าง list แล้วค่อย push” ที่คุณอาจใช้ในภาษาอื่น
Cascade operator
หัวข้อที่มีชื่อว่า “Cascade operator”cascade .. รัน operation หลายอันบน object เดียวกันโดยไม่ต้องเขียน reference ซ้ำ — พบบ่อยใน code แบบ configuration
final paint = Paint() ..color = const Color(0xFF027DFD) ..strokeWidth = 4 ..style = PaintingStyle.stroke;// Each `..` returns the same Paint, so this configures one object.Record: คืนค่าหลายตัวโดยไม่ต้องมี wrapper class
หัวข้อที่มีชื่อว่า “Record: คืนค่าหลายตัวโดยไม่ต้องมี wrapper class”Record (Dart 3.0 — “Records require a language version of at least 3.0”) คือ aggregate ที่ไม่มีชื่อ (anonymous), immutable และเป็น structurally-typed มันแก้ช่องว่างที่มีจริง: การคืนค่าที่เกี่ยวข้องกัน 2-3 ตัวแต่ก่อนต้องเลือกระหว่าง List/Map (ไม่มี type ชัดเจน) หรือ class wrapper ที่เขียนขึ้นมาเฉพาะกิจ (boilerplate) record ไม่ใช่ทั้งสองแบบ — มันคือ type จริงที่ compiler เช็คให้โดยไม่ต้องประกาศอะไรเลย
ลองรันจริง — บล็อกด้านล่างรัน function และค่าเดียวกับที่ test ด้านบนพิสูจน์ไว้ ผ่าน Dart runtime จริงของ DartPad ไม่ใช่ transcript ที่จำลองไว้ล่วงหน้า
Records: positional & named, destructuring, equality
Loading DartPad…
// Positional + named records, destructuring, equality, multiple-return use.
typedef Point = (double x, double y); // positional record type alias
(double, double) minMax(List<double> values) { var lo = values.first, hi = values.first; for (final v in values) { if (v < lo) lo = v; if (v > hi) hi = v; } return (lo, hi); // multiple return — no wrapper class needed}
({String name, int age}) parsePerson(Map<String, Object?> json) { return (name: json['name']! as String, age: json['age']! as int); // named record}import 'package:flutter_test/flutter_test.dart';import 'package:probe/lessons/dart-foundations__dart-language-tour/records_basics.dart';
void main() { test('positional record: destructuring a multiple return', () { final (low, high) = minMax([3, 1, 4, 1, 5, 9]); expect(low, 1); expect(high, 9); });
test('named record: field-shorthand destructuring', () { final person = parsePerson({'name': 'Ada', 'age': 36}); final (:name, :age) = person; expect(name, 'Ada'); expect(age, 36); });
test('records always compare structurally with ==, and const records are canonicalized', () { const a = (1, 2); const b = (1, 2); expect(a == b, isTrue); // == compares field-by-field, like a data class expect(identical(a, b), isTrue); // const records ARE canonicalized, like const objects
// A non-const record built at runtime is a fresh instance... int notConst() => 1; final c = (notConst(), 2); final d = (notConst(), 2); expect(identical(c, d), isFalse); // ...so identity differs... expect(c == d, isTrue); // ...but == still compares structurally, not by identity. });
test('a typedef names a record shape, unwrapped with positional getters', () { const Point p = (1.0, 2.0); expect(p.$1, 1.0); expect(p.$2, 2.0); });}✓ positional record: destructuring a multiple return✓ named record: field-shorthand destructuring✓ records always compare structurally with ==, and const records are canonicalized✓ a typedef names a record shape, unwrapped with positional getters
00:00 +4: All tests passed!ผลของการ canonicalize คือสิ่งที่ควรจำ: const (1, 2) กับ const (1, 2) อีกตัว identical กัน — เป็น object เดียวกันจริง ๆ เหมือน widget ที่เป็น const ทุกประการ ส่วน record ที่สร้างตอน runtime ((notConst(), 2)) จะเป็น instance ใหม่ทุกครั้ง แต่ == ยังเทียบแบบ structural เหมือนเดิม ทำให้ record เป็น key หรือ return value ที่ใช้งานได้ดีและต้นทุนการจองหน่วยความจำต่ำ
Pattern matching: destructure, match แล้วให้ analyzer พิสูจน์ว่าคุณครอบคลุมทุก case
หัวข้อที่มีชื่อว่า “Pattern matching: destructure, match แล้วให้ analyzer พิสูจน์ว่าคุณครอบคลุมทุก case”Pattern (Dart 3.0 — ต้องการ language-version เดียวกับ record) ขยาย destructuring ไปไกลกว่า final (a, b) = pair มาก pattern สามารถ match บน “รูปร่าง” ของค่า — runtime type, field, ความยาว — ไม่ใช่แค่ผูกชื่อเข้ากับมัน
ลองรันจริง — type Shape/Status และ case เดียวกับที่ test ด้านบนตรวจ รันผ่าน analyzer และ runtime จริงของ DartPad:
Patterns: switch expressions, object/list/map patterns, guards, if-case
Loading DartPad…
// Switch expression/statement, object/list/map patterns, guards, if-case.
sealed class Shape {}final class Circle extends Shape { Circle(this.radius); final double radius;}final class Square extends Shape { Square(this.side); final double side;}
double area(Shape shape) => switch (shape) { Circle(:final radius) => 3.141592653589793 * radius * radius, // object pattern Square(:final side) => side * side,};
String describe(Shape shape) => switch (shape) { Circle(radius: > 10) => 'big circle', // object pattern + relational guard Circle() => 'small circle', Square(side: var s) when s > 10 => 'big square ($s)', // `when` guard Square() => 'small square',};
String firstTwo(List<int> xs) => switch (xs) { [] => 'empty', [final a] => 'one: $a', [final a, final b, ...] => 'starts $a, $b', // list pattern + rest // No trailing wildcard needed: these three list patterns already cover // every possible List<int> length (0, 1, 2+) — the analyzer proves it by // flagging a `_` case added here as unreachable_switch_case.};
String describeUser(Map<String, Object?> json) { if (json case {'name': String name, 'age': int age}) { // map pattern via if-case return '$name ($age)'; } return 'unknown';}
enum Status { active, archived, deleted }
String statusLabel(Status s) { switch (s) { case Status.active: return 'Active'; case Status.archived: return 'Archived'; case Status.deleted: return 'Deleted'; }}import 'package:flutter_test/flutter_test.dart';import 'package:probe/lessons/dart-foundations__dart-language-tour/patterns_basics.dart';
void main() { test('switch expression with object patterns computes area per subtype', () { expect(area(Circle(2)), closeTo(12.566, 0.001)); expect(area(Square(3)), 9); });
test('object pattern with a relational guard vs a `when` guard', () { expect(describe(Circle(20)), 'big circle'); // radius: > 10 expect(describe(Circle(1)), 'small circle'); expect(describe(Square(20)), 'big square (20.0)'); // when s > 10 expect(describe(Square(1)), 'small square'); });
test('list patterns destructure by shape, with a rest element', () { expect(firstTwo([]), 'empty'); expect(firstTwo([7]), 'one: 7'); expect(firstTwo([1, 2, 3, 4]), 'starts 1, 2'); });
test('if-case with a map pattern extracts fields only on a structural match', () { expect(describeUser({'name': 'Ada', 'age': 36}), 'Ada (36)'); expect(describeUser({'name': 'Ada'}), 'unknown'); // missing `age` — no match });
test('switch statement over every enum value, no default needed', () { expect(statusLabel(Status.active), 'Active'); expect(statusLabel(Status.archived), 'Archived'); expect(statusLabel(Status.deleted), 'Deleted'); });}✓ switch expression with object patterns computes area per subtype✓ object pattern with a relational guard vs a `when` guard✓ list patterns destructure by shape, with a rest element✓ if-case with a map pattern extracts fields only on a structural match✓ switch statement over every enum value, no default needed
00:00 +5: All tests passed!Exhaustiveness: analyzer พิสูจน์ว่าคุณจัดการทุก case แล้ว
หัวข้อที่มีชื่อว่า “Exhaustiveness: analyzer พิสูจน์ว่าคุณจัดการทุก case แล้ว”ผลตอบแทนของ pattern matching บน sealed class/enum คือ exhaustiveness checking — การรับประกันตอน compile-time จริง ๆ ไม่ใช่แค่ convention บท bloc-and-patterns พิสูจน์เรื่องนี้ไปแล้วสำหรับ switch expression บน sealed hierarchy ด้วย diagnostic จริง non_exhaustive_switch_expression — ที่นี่คือสองกรณีที่บทนั้นยังไม่ครอบคลุม คือรูปแบบ statement บน sealed class และ enum
ลบ case Square2 ออกจาก switch statement บน sealed hierarchy:
// @expect-error real non_exhaustive_switch_statement diagnostic, captured belowsealed class Shape2 {}final class Circle2 extends Shape2 { Circle2(this.radius); final double radius; }final class Square2 extends Shape2 { Square2(this.side); final double side; }
String label(Shape2 shape) { switch (shape) { case Circle2(): return 'circle'; // missing Square2 case }}output จริงของ flutter analyze แบบไม่แก้ไข:
error • The type 'Shape2' isn't exhaustively matched by the switch cases since it doesn't match thepattern 'Square2()'. Try adding a default case or cases that match 'Square2()' •non_exhaustive_switch_statementลบ case สุดท้ายออกจาก switch expression บน enum — enum ได้การปฏิบัติแบบเดียวกันเป๊ะ เพราะทุก enum ใน Dart 3 ถือเป็น type ปิด (closed) ที่ตรวจ exhaustiveness ได้โดยปริยาย:
// @expect-error real non_exhaustive_switch_expression diagnostic, captured belowenum Status2 { active, archived, deleted }
String label(Status2 s) => switch (s) { Status2.active => 'Active', Status2.archived => 'Archived', // missing Status2.deleted};error • The type 'Status2' isn't exhaustively matched by the switch cases since it doesn't match thepattern 'Status2.deleted'. Try adding a wildcard pattern or cases that match 'Status2.deleted' •non_exhaustive_switch_expressionวิธีแก้ของทั้งสองกรณีคือตามที่ message บอกตรง ๆ: เพิ่ม case ที่ขาด (หรือ default:/_ => เป็น wildcard ซึ่งจะสละสิทธิ์การเช็ค exhaustiveness ต่อจากนี้) นี่คือเหตุผลที่ state union ที่ AI สร้างขึ้นมาสมัยนี้เลือกใช้ sealed class + switch มากกว่า abstract class + chain ของ is check แบบเดิม — มีแค่ combination sealed+switch เท่านั้นที่ได้ proof เรื่อง completeness จาก analyzer แบบนี้ ทั้งบน class hierarchy และ enum
Class modifier: ใครได้รับอนุญาตให้ extend หรือ implement class ของคุณ
หัวข้อที่มีชื่อว่า “Class modifier: ใครได้รับอนุญาตให้ extend หรือ implement class ของคุณ”Dart 3.0 เพิ่ม modifier ห้าตัว — sealed, final, base, interface, และ mixin class — ที่จำกัดว่า class จะถูกใช้นอก library ของมันเองได้อย่างไร (แต่ละตัวมี flutter analyze error จริงที่ต่างกันเมื่อถูกละเมิด) บทนี้ต้องการแค่ภาพรวมเพื่ออ่านตัวอย่าง exhaustiveness ข้างบน (sealed คือสิ่งที่ทำให้ Shape/Shape2 ตรวจ exhaustiveness ได้) บทถัดไป Classes & Mixins จะพิสูจน์การละเมิด cross-library ของแต่ละ modifier พร้อมวิธีแก้แบบละเอียด
Extension type: wrapper ที่ zero-cost ตรวจสอบตอน compile-time
หัวข้อที่มีชื่อว่า “Extension type: wrapper ที่ zero-cost ตรวจสอบตอน compile-time”Extension type (Dart 3.3, กุมภาพันธ์ 2024 — ยืนยันโดยหน้า language evolution ของ dart.dev: “Extension types are a new feature in Dart that allow zero-cost wrapping of an existing type”) ห่อ type ที่มีอยู่แล้วด้วย interface ที่ต่าง ออกและจำกัดกว่า แบบ static-only — ต่างจาก wrapper class จริง ๆ ตรงที่ไม่มี object ตอน runtime เลย dart.dev บอกตรง ๆ ว่า: “Because extension types are static-only and compiled away at run time, they are essentially zero cost.”
// Extension types (Dart 3.3) — a compile-time-only wrapper, zero runtime cost.
extension type UserId(String value) { bool get isValid => value.length == 8;}
// `implements` a supertype of the representation type (double implements// Comparable<num>) to inherit its members transparently, at zero cost.extension type Meters(double value) implements Comparable<num> { Meters operator +(Meters other) => Meters(value + other.value);}import 'package:flutter_test/flutter_test.dart';import 'package:probe/lessons/dart-foundations__dart-language-tour/extension_types.dart';
void main() { test('an extension type has zero runtime footprint: it IS the representation type', () { final id = UserId('a1b2c3d4'); // Erased at compile time — at runtime this is a plain String, not a // wrapper object. `is`/runtimeType see straight through it. expect(id is String, isTrue); expect((id as Object).runtimeType, String); expect(id.isValid, isTrue); });
test('members not declared on the extension type are a real compile-time error, not a runtime one', () { // See the lesson prose for the captured analyzer error for // `UserId(...).toUpperCase()` — undefined_method, because UserId does // not implement String, so its full member set isn't visible. final id = UserId('short'); expect(id.isValid, isFalse); });
test('implements a supertype of the representation type to inherit members for free', () { final total = Meters(1.5) + Meters(2.5); expect(total.value, 4.0); expect(total.compareTo(4), 0); // inherited from Comparable<num> via `implements` });}✓ an extension type has zero runtime footprint: it IS the representation type✓ members not declared on the extension type are a real compile-time error, not a runtime one✓ implements a supertype of the representation type to inherit members for free
00:00 +3: All tests passed!UserId ห่อ String ไว้ แต่ไม่ implement String — มันจึงเปิดให้ใช้แค่สิ่งที่ประกาศไว้ชัดเจน (isValid) เท่านั้น ไม่มีอะไรมากกว่านั้น การเรียก member จริงของ String บนมันคือ compile-time error จริง:
// @expect-error real undefined_method diagnostic, captured belowextension type UserId(String value) { bool get isValid => value.length == 8;}
void demo() { final id = UserId('a1b2c3d4'); print(id.toUpperCase()); // String's toUpperCase — NOT a member of UserId}error • The method 'toUpperCase' isn't defined for the type 'UserId'. Try correcting the name tothe name of an existing method, or defining a method named 'toUpperCase' • undefined_methodนี่คือประเด็นทั้งหมดของ extension type เทียบกับ typedef ธรรมดาหรือ extension on String: มันจำกัด interface แทนที่จะขยายมัน ในขณะที่ไม่มีต้นทุนตอน runtime เลย — id is String ยังคงเป็น true เพราะไม่มี wrapper object ที่จะเป็นอย่างอื่นนอกจาก String
late: การ initialize แบบเลื่อนออกไป
หัวข้อที่มีชื่อว่า “late: การ initialize แบบเลื่อนออกไป”late คือคำสัญญาว่า “ค่านี้จะถูก assign ก่อนที่ใครจะอ่านมัน” — บทถัดไป Null Safety จะพูดถึง late final และ definite-assignment analysis แบบละเอียด และ setState & the Lifecycle ได้พิสูจน์ต้นทุนจริงตอน runtime ของการผิดสัญญานั้นไปแล้ว (LateInitializationError) สำหรับ controller ที่ถูก initialize ใน initState
อ่านโค้ดที่ AI สร้างให้
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้างให้”Dart ที่ AI สร้างมักจะ default ไปที่ idiom ที่เก่าที่สุดที่มันรู้จักสำหรับงานหนึ่ง ๆ — เขียน class มี field แทนที่จะใช้ record สำหรับ multiple-return, chain ของ is/as check แทนที่จะใช้ switch pattern, Map<String, dynamic> แทนที่จะใช้ record ที่มี type หรือ model จริง ไม่ได้ผิดเป๊ะ ๆ — แค่มักจะล้าหลังหลายปีเทียบกับสิ่งที่ type system เช็คให้คุณได้ตอนนี้
// AI-generated: "return the min and max of a list"class MinMaxResult { MinMaxResult(this.min, this.max); final double min; final double max;}
MinMaxResult minMax(List<double> values) { var lo = values.first, hi = values.first; for (final v in values) { if (v < lo) lo = v; if (v > hi) hi = v; } return MinMaxResult(lo, hi);}ก่อนเชื่อ code แบบนี้ ให้เช็ค:
- class wrapper แบบใช้แล้วทิ้งสำหรับ multiple-return (ไม่มีหน้าที่อื่นนอกจากรวบรวมค่า 2-3 ตัว) ที่ record —
(double, double)หรือ({double min, double max})— แทนที่ได้โดยไม่ต้องประกาศอะไรเลย พิสูจน์แล้วข้างบนด้วยminMax switch(หรือ chainif/else if) บน type hierarchy ที่ไม่ใช่sealed— แปลว่า analyzer ไม่มีทางบอกคุณได้ว่าพลาด case ไป ถ้า hierarchy นั้นตั้งใจให้เป็นแบบปิด (ชุด variant ที่ตายตัวและรู้แน่นอน) ควรเป็นsealedและswitchควรเป็นswitchexpression/statement จริง (ไม่ใช่ischeck) เพื่อให้ได้ proof เรื่อง exhaustiveness แบบที่แสดงข้างบน- wrapper class (จองหน่วยความจำเพิ่ม) ที่ใช้แค่เพื่อ type-safety บน primitive (ID, จำนวนเงิน) ที่ extension type ให้ความปลอดภัยแบบเดียวกันได้โดยไม่มีต้นทุนเลย — พิสูจน์แล้วข้างบนว่า
UserId('...')เป็นStringธรรมดาจริง ๆ ตอน runtime
🐛 หา bug ในโค้ดนี้
sealed class PaymentResult {}final class Success extends PaymentResult {Success(this.transactionId);final String transactionId;}final class Declined extends PaymentResult {Declined(this.reason);final String reason;}final class NetworkError extends PaymentResult {NetworkError(this.message);final String message;}
String summarize(PaymentResult result) => switch (result) {Success(:final transactionId) => 'Paid: $transactionId',Declined(:final reason) => 'Declined: $reason',};เฉลย
โค้ดนี้ compile ไม่ผ่าน: PaymentResult เป็น sealed ที่มีสามsubtype (Success, Declined, NetworkError) แต่ switch expression ใน summarize() ครอบคลุมแค่สอง — error จริงจาก analyzer คือ non_exhaustive_switch_expression ซึ่งบอกตรง ๆ ว่าขาดอะไร: "doesn't match the pattern 'NetworkError()'" เป็น diagnostic เดียวกับที่พิสูจน์ไปแล้วก่อนหน้านี้ในบทนี้สำหรับ Shape2/Status2 วิธีแก้คือเพิ่ม case ที่ขาด (NetworkError(:final message) => 'Error: \$message') — และเพราะ hierarchy เป็น sealed compiler จะบังคับให้แก้แบบเดียวกันนี้ทุกที่ที่ PaymentResult ถูก switch บนมัน ไม่ใช่แค่ที่นี่
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ฟีเจอร์เด่นของ Dart 3 (record, pattern, class modifier, extension type) มีเป้าหมายเดียวกัน: ย้ายการเช็คที่เคยอยู่ใน convention หรือ code review ไปไว้ที่ type checker โดยแทบไม่มีต้นทุนตอน runtime เลย record คือ “tuple แต่ compiler เช็ครูปร่างให้” sealed+switch คือ “union แบบ enum แต่ compiler เช็คทุก branch ให้” extension type คือ “wrapper class แต่ compiler ลบมันทิ้งตอน compile” ทั้งหมดนี้ไม่ใช่ ความสามารถ ใหม่ที่ Dart ไม่มีก่อน 3.0 — คุณเขียน wrapper class หรือ chain is-check ได้เสมอ — มันคือความสามารถเดิมที่ขั้นตอนตรวจสอบด้วยมือที่ลืมง่ายถูกทำให้อัตโนมัติแทน ทุกครั้งที่ build
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ฟีเจอร์ | ประโยชน์ | ต้นทุน |
|---|---|---|
| Record แทน wrapper class | ไม่ต้องประกาศอะไรเลย, structural equality, canonicalize เมื่อเป็น const | ไม่มี type ที่มีชื่อให้แนบ behavior เพิ่มเข้าไป — ต้องเปลี่ยนไปใช้ class เมื่อ record ต้องการ method |
sealed + switch แทน is-chain | เช็ค exhaustiveness ได้จริง พิสูจน์แล้วข้างบน | ทุก subtype ใหม่บังคับให้คุณแก้ทุก switch ที่ใช้มัน — เป็นข้อดีเมื่อคุณต้องการแบบนั้น เป็นความยุ่งยากเมื่อไม่ต้องการ |
| Extension type แทน wrapper class | ไม่มีการจองหน่วยความจำตอน runtime พิสูจน์แล้วข้างบน (id is String เป็น true) | ไม่ใช่ subtype relationship จริงเว้นแต่ implements จะระบุ supertype จริงของ representation type — เผลอคิดว่ามี polymorphism มากกว่าที่มีจริงได้ง่าย |
when guard / relational pattern | สื่อ “case นี้ แต่แค่ถ้า…” แบบ inline ไม่ต้อง nested if | ใช้ guard เยอะเกินไปอาจซ่อน case ที่ exhaustiveness checker ควรจะบังคับให้คุณระบุชัดเจน |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “switch ที่ไม่มี
defaultต้องมีเสมอ ไม่งั้น compiler จะบ่น” พิสูจน์แล้วว่าผิดข้างบน:switchบนsealedclass หรือenumที่ครอบคลุมครบแล้วไม่ต้องมีdefault/wildcard เลย — การเพิ่มมันเข้าไปใน list pattern ที่ครบถ้วนแล้วของfirstTwoจะถูก flag เป็นunreachable_switch_caseซึ่งเป็น diagnostic จริงในทิศตรงข้าม - “Record ก็แค่ tuple class ก็ทำแบบเดียวกันได้เสมอ” จริงในหลักการ แต่ record ไม่ต้องประกาศอะไรเลยและได้ structural equality กับ
constcanonicalization มาฟรี — พิสูจน์แล้วข้างบนด้วยidentical()— ในขณะที่ class ที่เขียนเองต้อง override==/hashCode(บทถัดไป) เพื่อให้ได้ equality แบบเดียวกัน - “Extension type ก็แค่
typedef” พิสูจน์แล้วว่าผิดข้างบน:typedefเปิดให้ใช้ทุก member ของ type ที่อยู่ข้างใต้ ส่วนUserId(...).toUpperCase()คือundefined_methoderror จริงบน extension type ที่ไม่implements Stringเพราะมันจำกัด interface แทนที่จะ alias มัน - “Exhaustiveness checking ใช้ได้กับ
abstract classhierarchy ไหนก็ได้” ต้องเป็นsealedเท่านั้น — พิสูจน์ในbloc-and-patternsและที่นี่: มีแค่ hierarchy แบบปิด (sealed) หรือenumเท่านั้นที่ให้ analyzer นับ subtype/ค่าที่เป็นไปได้ทั้งหมดเพื่อเช็คได้