Kodokon kodokon.com

การจำลองข้อมูลของคุณ: คลาส, JSON และความไม่เปลี่ยนรูป

แปลง JSON ให้เป็นคลาส Dart ที่ไม่เปลี่ยนรูปด้วย fromJson, toJson, copyWith และการเปรียบเทียบค่าที่เชื่อถือได้

9 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

ในระดับนี้ ประเด็นที่แท้จริงไม่ใช่ไวยากรณ์ของคลาสอีกต่อไป แต่เป็นเส้นแบ่งระหว่างโลกภายนอกกับโค้ดของคุณ ตราบใดที่การตอบกลับจาก API ยังถูกส่งต่อไปมาในรูป Map<String, dynamic> การอ่านทุกฟิลด์ก็คือการเสี่ยงดวงตอนรันไทม์ กฎของมืออาชีพคือ แปลง JSON ให้เป็นอ็อบเจกต์ที่มีชนิด ทันทีที่คุณได้รับมัน แล้วส่งต่อเฉพาะอ็อบเจกต์เหล่านั้นไปมา เมื่อรวมสิ่งนี้เข้ากับความไม่เปลี่ยนรูป (ฟิลด์ final, คอนสตรักเตอร์ const) คุณจะกำจัดบั๊กได้ทั้งตระกูล นั่นคือ ไม่มีใครสามารถแก้ไขอ็อบเจกต์ที่ใช้ร่วมกันลับหลังคุณได้อีก และอินสแตนซ์ const สองตัวที่เหมือนกันจะถูกคอมไพเลอร์ทำให้เป็นตัวเดียวกัน

DART
class User {
  const User({
    required this.id,
    required this.name,
    this.email,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: (json['id'] as num).toInt(),
      name: json['name'] as String,
      email: json['email'] as String?,
    );
  }

  final int id;
  final String name;
  final String? email;
}
โมเดลที่ไม่เปลี่ยนรูปพร้อมการแยกวิเคราะห์ JSON เชิงป้องกัน

มีสามทางเลือกที่ควรอธิบายให้ชัด อย่างแรก (json['id'] as num).toInt() API จำนวนมากทำให้จำนวนเต็มเป็นอนุกรม (serialize) บางครั้งในรูป 3 บางครั้งในรูป 3.0 ขึ้นอยู่กับแบ็กเอนด์ การแคสต์ตรง ๆ เป็น int จะพังเมื่อเจอ double ต่อมา as String จะล้มเหลวทันทีพร้อม TypeError ที่ชัดเจนถ้าฟิลด์นั้นหายไป ซึ่งนั่นคือความตั้งใจ การล้มเหลวเร็วตั้งแต่ตอนแยกวิเคราะห์ ดีกว่าปล่อยให้ null แพร่กระจายไปแล้วไประเบิดเอาในอีกสามหน้าจอถัดไป และสุดท้าย as String? เป็นการระบุไว้ในชนิดของข้อมูลว่าฟิลด์นี้เป็นทางเลือกได้จริง ๆ

DART
class User {
  const User({required this.id, this.email});

  final int id;
  final String? email;

  Map<String, dynamic> toJson() => {
        'id': id,
        'email': email,
      };

  User copyWith({int? id, String? email}) {
    return User(
      id: id ?? this.id,
      email: email ?? this.email,
    );
  }
}
toJson และ copyWith บนโมเดลที่ลดเหลือเพียงสองฟิลด์

ความไม่เปลี่ยนรูปเรียกร้องการเปรียบเทียบด้วยค่า โดยค่าเริ่มต้น Dart เปรียบเทียบด้วยการอ้างอิง (reference) นั่นคือ User สองตัวที่เหมือนกันทุกฟิลด์จะถูกมองว่า "ต่างกัน" ซึ่งเป็นปัญหาที่เป็นรูปธรรมทันทีที่คุณเปรียบเทียบสถานะ ไม่ว่าจะในเทสต์ ใน select ของ Provider หรือในการค้นหาในรายการ จงนิยาม == และ hashCode ใหม่คู่กันเสมอ

DART
class Point {
  const Point(this.x, this.y);

  final int x;
  final int y;

  @override
  bool operator ==(Object other) =>
      other is Point && other.x == x && other.y == y;

  @override
  int get hashCode => Object.hash(x, y);
}
การเปรียบเทียบด้วยค่า: == และ hashCode มาเป็นคู่กัน

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. ทำไมจึงเขียน (json['id'] as num).toInt() แทนที่จะเป็น json['id'] as int?
    • เพราะ as int ช้ากว่า as num ตอนรันไทม์
    • เพราะแบ็กเอนด์อาจทำให้จำนวนเต็มเป็นอนุกรมในรูป 3.0 การแคสต์ตรง ๆ เป็น int จะล้มเหลวเมื่อเจอ double
    • เพราะ json['id'] เป็นสายอักขระเสมอ
  2. user.copyWith(email: null) ให้ผลลัพธ์อะไรเมื่อใช้การพัฒนา copyWith แบบดั้งเดิม?
    • ฟิลด์ email ถูกรีเซ็ตเป็น null ตามที่ร้องขอ
    • มีข้อยกเว้นถูกโยนขึ้นตอนรันไทม์
    • อ็อบเจกต์ยังคงค่า email ปัจจุบันไว้ เพราะ null ?? this.email ย้อนกลับไปใช้ค่าเดิม
    • โค้ดไม่คอมไพล์ในโหมดเข้มงวด
  3. ทำไมจึงต้องนิยาม == และ hashCode ใหม่บนโมเดลที่ไม่เปลี่ยนรูป?
    • เพราะ Dart เปรียบเทียบด้วยการอ้างอิงโดยค่าเริ่มต้น มิฉะนั้นอ็อบเจกต์สองตัวที่เหมือนกันทุกฟิลด์จะถูกมองว่า "ต่างกัน"
    • เพื่อเร่งความเร็วการสร้างอินสแตนซ์ const
    • มันเป็นข้อบังคับทันทีที่มีฟิลด์ถูกประกาศเป็น final