อะไรคือความแตกต่างระหว่างฟังก์ชันและคลาสในการสร้างวิดเจ็ตที่ใช้ซ้ำได้?


129

ฉันได้รู้ว่ามันเป็นไปได้ที่จะสร้างเครื่องมือที่ใช้ฟังก์ชั่นธรรมดาแทน subclassing StatelessWidget ตัวอย่างจะเป็นดังนี้:

Widget function({ String title, VoidCallback callback }) {
  return GestureDetector(
    onTap: callback,
    child: // some widget
  );
}

นี้เป็นที่น่าสนใจเพราะมันต้องไกลรหัสน้อยกว่าระดับเต็มเป่า ตัวอย่าง:

class SomeWidget extends StatelessWidget {
  final VoidCallback callback;
  final String title;

  const SomeWidget({Key key, this.callback, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
      return GestureDetector(
        onTap: callback,
        child: // some widget
      );
  }
}

ฉันจึงสงสัยว่า: มีความแตกต่างนอกเหนือจากไวยากรณ์ระหว่างฟังก์ชันและคลาสในการสร้างวิดเจ็ตหรือไม่? และเป็นแนวทางปฏิบัติที่ดีในการใช้ฟังก์ชันหรือไม่?


ฉันพบว่าชุดข้อความนี้มีประโยชน์มากสำหรับการทำความเข้าใจปัญหา reddit.com/r/FlutterDev/comments/avhvco/…
RocketR

คำตอบ:


181

TL; DR: ชอบใช้คลาสมากกว่าฟังก์ชันเพื่อสร้างวิดเจ็ตทรีที่ใช้ซ้ำได้


แก้ไข : เพื่อชดเชยความเข้าใจผิด: นี่ไม่ได้เกี่ยวกับฟังก์ชั่นที่ทำให้เกิดปัญหา แต่คลาสกำลังแก้ปัญหาบางอย่าง

Flutter จะไม่มีStatelessWidgetหากฟังก์ชันสามารถทำสิ่งเดียวกันได้

ในทำนองเดียวกันส่วนใหญ่มุ่งเป้าไปที่วิดเจ็ตสาธารณะซึ่งสร้างขึ้นเพื่อนำกลับมาใช้ใหม่ ไม่สำคัญเท่าสำหรับฟังก์ชันส่วนตัวที่สร้างขึ้นเพื่อใช้งานเพียงครั้งเดียว - แม้ว่าการตระหนักถึงพฤติกรรมนี้ก็ยังดีอยู่


มีข้อแตกต่างที่สำคัญระหว่างการใช้ฟังก์ชันแทนคลาสนั่นคือ: เฟรมเวิร์กไม่รู้จักฟังก์ชัน แต่สามารถมองเห็นคลาสได้

พิจารณาฟังก์ชัน "วิดเจ็ต" ต่อไปนี้:

Widget functionWidget({ Widget child}) {
  return Container(child: child);
}

ใช้วิธีนี้:

functionWidget(
  child: functionWidget(),
);

และเทียบเท่าระดับ:

class ClassWidget extends StatelessWidget {
  final Widget child;

  const ClassWidget({Key key, this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      child: child,
    );
  }
}

ใช้แบบนั้น:

new ClassWidget(
  child: new ClassWidget(),
);

บนกระดาษดูเหมือนว่าทั้งสองจะทำสิ่งเดียวกันทุกContainerประการ: สร้าง 2 โดยที่อีกอันหนึ่งซ้อนกัน แต่ในความเป็นจริงนั้นแตกต่างกันเล็กน้อย

ในกรณีของฟังก์ชันแผนผังวิดเจ็ตที่สร้างขึ้นจะมีลักษณะดังนี้:

Container
  Container

ในขณะที่มีคลาสต้นไม้วิดเจ็ตคือ:

ClassWidget
  Container
    ClassWidget
      Container

สิ่งนี้มีความสำคัญเนื่องจากจะเปลี่ยนวิธีการทำงานของเฟรมเวิร์กเมื่ออัปเดตวิดเจ็ต

ทำไมถึงสำคัญ

การใช้ฟังก์ชันเพื่อแยกโครงสร้างวิดเจ็ตของคุณออกเป็นหลาย ๆ วิดเจ็ตคุณจะพบกับจุดบกพร่องและพลาดการเพิ่มประสิทธิภาพบางอย่าง

ไม่มีการรับประกันว่าคุณจะมีข้อบกพร่องจากการใช้ฟังก์ชัน แต่ด้วยการใช้คลาสคุณจะรับประกันได้ว่าจะไม่ประสบปัญหาเหล่านี้

นี่คือตัวอย่างแบบโต้ตอบบางส่วนบน Dartpad ที่คุณสามารถดำเนินการเองเพื่อทำความเข้าใจปัญหาได้ดีขึ้น:

  • https://dartpad.dev/1870e726d7e04699bc8f9d78ba71da35
    ตัวอย่างนี้แสดงให้เห็นว่าการแยกแอปออกเป็นฟังก์ชันต่างๆโดยไม่ได้ตั้งใจคุณอาจทำสิ่งต่างๆเช่นAnimatedSwitcher

  • https://dartpad.dev/a869b21a2ebd2466b876a5997c9cf3f1
    ตัวอย่างนี้แสดงให้เห็นว่าคลาสอนุญาตให้สร้างแผนผังวิดเจ็ตขึ้นมาใหม่ได้อย่างไรโดยปรับปรุงการแสดง

  • https://dartpad.dev/06842ae9e4b82fad917acb88da108eee
    ตัวอย่างนี้แสดงให้เห็นว่าคุณใช้ฟังก์ชัน BuildContext ในทางที่ผิดและพบข้อบกพร่องเมื่อใช้ InheritedWidgets (เช่น Theme หรือผู้ให้บริการ) อย่างไร

สรุป

นี่คือรายการความแตกต่างระหว่างการใช้ฟังก์ชันและคลาส:

  1. ชั้นเรียน:
  • อนุญาตให้เพิ่มประสิทธิภาพการทำงาน (ตัวสร้าง const สร้างใหม่ละเอียดมากขึ้น)
  • ตรวจสอบให้แน่ใจว่าการสลับไปมาระหว่างสองรูปแบบที่แตกต่างกันทำให้เกิดการกำจัดทรัพยากรอย่างถูกต้อง (ฟังก์ชันอาจใช้สถานะก่อนหน้าบางส่วนซ้ำ)
  • ตรวจสอบให้แน่ใจว่าการรีโหลดแบบร้อนทำงานได้อย่างถูกต้อง (การใช้ฟังก์ชันอาจทำให้การรีโหลดร้อนสำหรับshowDialogs& สิ่งที่คล้ายกัน)
  • รวมอยู่ในตัวตรวจสอบวิดเจ็ต
    • เราเห็นClassWidgetในวิดเจ็ตทรีที่แสดงโดย devtool ซึ่งช่วยให้เข้าใจสิ่งที่อยู่บนหน้าจอ
    • เราสามารถแทนที่debugFillPropertiesเพื่อพิมพ์ว่าพารามิเตอร์ที่ส่งผ่านไปยังวิดเจ็ตคืออะไร
  • ข้อความแสดงข้อผิดพลาดที่ดีกว่า
    หากมีข้อยกเว้นเกิดขึ้น (เช่น ProviderNotFound) เฟรมเวิร์กจะให้ชื่อของวิดเจ็ตที่กำลังสร้างอยู่ หากคุณแยกโครงสร้างวิดเจ็ตของคุณเฉพาะในฟังก์ชัน + Builderข้อผิดพลาดของคุณจะไม่มีชื่อที่เป็นประโยชน์
  • สามารถกำหนดคีย์
  • สามารถใช้บริบท API
  1. ฟังก์ชั่น:
  • มีรหัสน้อยกว่า (ซึ่งสามารถแก้ไขได้โดยใช้functional_widgetรุ่นรหัส)

โดยรวมแล้วถือว่าเป็นแนวทางปฏิบัติที่ไม่ดีในการใช้ฟังก์ชันเหนือคลาสเพื่อนำวิดเจ็ตกลับมาใช้ใหม่เนื่องจากเหตุผลเหล่านี้
คุณทำได้แต่มันอาจกัดคุณได้ในอนาคต


ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Samuel Liew

11

ฉันได้ทำการค้นคว้าเกี่ยวกับปัญหานี้ในช่วง 2 วันที่ผ่านมา ฉันได้ข้อสรุปดังต่อไปนี้: เป็นเรื่องปกติที่จะแบ่งส่วนต่างๆของแอปออกเป็นฟังก์ชัน มันเหมาะอย่างยิ่งที่ฟังก์ชันเหล่านั้นจะส่งคืน a StatelessWidgetดังนั้นการเพิ่มประสิทธิภาพสามารถทำได้เช่นการสร้างStatelessWidget constดังนั้นจึงไม่สร้างใหม่หากไม่จำเป็นต้องทำ ตัวอย่างเช่นโค้ดส่วนนี้ใช้ได้อย่างสมบูรณ์แบบ:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      ++_counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
            const MyWidgetClass(key: const Key('const')),
            MyWidgetClass(key: Key('non-const')),
            _buildSomeWidgets(_counter),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  Widget _buildSomeWidgets(int val) {
    print('${DateTime.now()} Rebuild _buildSomeWidgets');
    return const MyWidgetClass(key: Key('function'));

    // This is bad, because it would rebuild this every time
    // return Container(
    //   child: Text("hi"),
    // );
  }
}

class MyWidgetClass extends StatelessWidget {
  const MyWidgetClass({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('${DateTime.now()} Rebuild MyWidgetClass $key');

    return Container(
      child: Text("hi"),
    );
  }
}

การใช้ฟังก์ชั่นนั้นดีอย่างสมบูรณ์แบบเนื่องจากส่งกลับไฟล์const StatelessWidget. กรุณาแก้ไขฉันถ้าฉันผิด


ใครช่วยอธิบายได้ไหมว่าทำไมสิ่งที่ฉันพูดถึงไม่ถูกต้อง? ฉันหมายความว่าฉันคิดว่ามันผิดเนื่องจากการโหวตลดลง
Sergiu Iacob

ฉันเห็นด้วยกับคุณจริงๆ ฉันตั้งใจจะเขียนรายละเอียดของความแตกต่างที่ละเอียดกว่านี้ แต่ก็ยังไม่เข้าใจ อย่าลังเลที่จะสรุปข้อโต้แย้งของคุณเพราะฉันคิดว่าสิ่งสำคัญคือต้องเข้าใจข้อดีข้อเสียของวิดเจ็ต v วิธีการ
TheIT

@SergiuIacob ใช้constหน้าชั้นคนไร้สัญชาติทุกกรณีได้ไหม หรือว่าต้องมีบางกรณี? ถ้าใช่มีอะไรบ้าง?
บ่าย

1
@aytunch ฉันไม่คิดว่าคุณจะใช้ได้constทุกที่ ตัวอย่างเช่นหากคุณมีStatelessWidgetคลาสที่ส่งคืนค่าTextที่มีค่าของตัวแปรและตัวแปรนั้นเปลี่ยนแปลงไปที่ใดที่หนึ่งเกินกว่าที่คุณStatelessWidgetควรจะสร้างขึ้นใหม่ดังนั้นจึงสามารถแสดงค่าที่แตกต่างกันได้ดังนั้นจึงไม่สามารถเป็นconstได้ ฉันคิดว่าวิธีที่ปลอดภัยในการวางไว้คือไม่ว่าคุณจะทำได้ใช้ทุกที่constถ้าทำได้อย่างปลอดภัย
Sergiu Iacob

3
ฉันกำลังชั่งใจว่าจะตอบคำถามนี้ด้วยตัวเองหรือไม่ คำตอบที่ได้รับการยอมรับนั้นผิดธรรมดา แต่Rémiได้พยายามอย่างมากในการช่วยเหลือชุมชนที่กระพือปีกดังนั้นผู้คนอาจไม่กลั่นกรองคำตอบของเขามากเท่าของคนอื่น ซึ่งอาจเห็นได้ชัดจากการโหวตเพิ่มคะแนนทั้งหมด ผู้คนต้องการเพียง "แหล่งเดียวของความจริง" :-)
DarkNeuron

5

มีความแตกต่างอย่างมากระหว่างฟังก์ชันอะไรและคลาสทำอะไร


ให้ฉันอธิบายตั้งแต่เริ่มต้น (เกี่ยวกับความจำเป็นเท่านั้น)

  • ประวัติการเขียนโปรแกรมเราทุกคนรู้ว่าเริ่มต้นด้วยคำสั่งพื้นฐานแบบตรง (เช่น -: Assembly)

  • ถัดไปการเขียนโปรแกรมแบบมีโครงสร้างมาพร้อมกับการควบคุมโฟลว์ (เช่น -: if, switch, while, เป็นต้น) กระบวนทัศน์นี้ช่วยให้โปรแกรมเมอร์สามารถควบคุมการไหลของโปรแกรมได้อย่างมีประสิทธิภาพและยังลดจำนวนบรรทัดโค้ดด้วยการวนซ้ำ

  • การเขียนโปรแกรมขั้นตอนถัดไปมาและกลุ่มคำสั่งใดเป็นโพรซีเดอร์ (funcions) สิ่งนี้ให้ประโยชน์หลักสองประการสำหรับโปรแกรมเมอร์

    1. คำสั่งกลุ่ม (การดำเนินการ) แยกเป็นบล็อก

    2. สามารถนำบล็อกเหล่านี้กลับมาใช้ใหม่ได้ (ฟังก์ชัน)

แต่เหนือทุกกระบวนทัศน์ไม่ได้ให้คำตอบสำหรับการจัดการแอปพลิเคชัน การเขียนโปรแกรมขั้นตอนสามารถใช้ได้เฉพาะกับแอปพลิเคชันขนาดเล็กเท่านั้น ที่ไม่สามารถใช้พัฒนาเว็บแอพพลิเคชั่นขนาดใหญ่ (เช่น -: banking, google, youtube, facebook, stackoverflow ฯลฯ ) ไม่สามารถสร้าง frameworks เช่น android sdk, flutter sdk และอื่น ๆ อีกมากมาย ......

ดังนั้นวิศวกรจึงทำการวิจัยเพิ่มเติมเพื่อจัดการโปรแกรมด้วยวิธีที่เหมาะสม

  • ในที่สุดObject Oriented Programmingมาพร้อมกับโซลูชันทั้งหมดสำหรับการจัดการแอปพลิเคชันทุกขนาด (ตั้งแต่สวัสดีชาวโลกไปจนถึงผู้คนกว่าล้านล้านคนโดยใช้การสร้างระบบเช่น Google, amazon และ 90% ของแอปพลิเคชันในปัจจุบัน)

  • ใน oop แอปพลิเคชันทั้งหมดสร้างขึ้นรอบ ๆ วัตถุหมายความว่าแอปพลิเคชันคือชุดของวัตถุเหล่านี้

ดังนั้นวัตถุจึงเป็นสิ่งปลูกสร้างพื้นฐานสำหรับแอปพลิเคชันใด ๆ

class (object at runtime) จัดกลุ่มข้อมูลและฟังก์ชันที่เกี่ยวข้องกับตัวแปรเหล่านั้น (data) ดังนั้นวัตถุจึงประกอบด้วยข้อมูลและการดำเนินการที่เกี่ยวข้อง

[ต่อไปนี้ฉันจะไม่อธิบายเกี่ยวกับ oop]


👉👉👉ตกลงตอนนี้มาหากรอบกระพือ

-Dart รองรับทั้งขั้นตอนและขั้นตอน แต่เฟรมเวิร์ก Flutter สร้างขึ้นอย่างสมบูรณ์โดยใช้คลาส (oop) (เนื่องจากเฟรมเวิร์กที่จัดการได้ขนาดใหญ่ไม่สามารถสร้างโดยใช้ขั้นตอน)

ที่นี่ฉันจะสร้างรายการเหตุผลที่พวกเขาใช้คลาสแทนฟังก์ชั่นในการสร้างวิดเจ็ต👇👇👇


1 - เวลาส่วนใหญ่ build method (วิดเจ็ตลูก) หมายเลขการโทรของฟังก์ชันซิงโครนัสและอะซิงโครนัส

เช่น:

  • เพื่อดาวน์โหลดภาพเครือข่าย
  • รับข้อมูลจากผู้ใช้ ฯลฯ

ดังนั้นวิธีการสร้างจึงจำเป็นต้องเก็บไว้ในวิดเจ็ตคลาสที่แยกจากกัน (เนื่องจากเมธอดอื่น ๆ ทั้งหมดเรียกโดย build () วิธีการสามารถเก็บไว้ในคลาสเดียว)


2 - การใช้คลาสวิดเจ็ตคุณสามารถสร้างจำนวนคลาสอื่นได้โดยไม่ต้องเขียนโค้ดซ้ำแล้วซ้ำอีก (** Use Of Inheritance ** (ขยาย))

และยังใช้การสืบทอด (ขยาย) และความหลากหลาย (แทนที่) คุณสามารถสร้างคลาสที่กำหนดเองได้ (ตัวอย่างด้านล่างในนั้นฉันจะปรับแต่ง (Override) ภาพเคลื่อนไหวโดยการขยาย MaterialPageRoute (เนื่องจากการเปลี่ยนแปลงเริ่มต้นฉันต้องการปรับแต่ง)

class MyCustomRoute<T> extends MaterialPageRoute<T> {
  MyCustomRoute({ WidgetBuilder builder, RouteSettings settings })
      : super(builder: builder, settings: settings);

  @override                                      //Customize transition
  Widget buildTransitions(BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
      Widget child) {
    if (settings.isInitialRoute)
      return child;
    // Fades between routes. (If you don't want any animation, 
    // just return child.)
    return new FadeTransition(opacity: animation, child: child);
  }
}

3 - ฟังก์ชันไม่สามารถเพิ่มเงื่อนไขให้กับพารามิเตอร์ได้ แต่การใช้ตัวสร้างวิดเจ็ตคลาสคุณสามารถทำได้

ด้านล่าง Code example👇 (คุณลักษณะนี้ถูกใช้อย่างมากโดยวิดเจ็ตเฟรมเวิร์ค)

const Scaffold({
    Key key,
    this.bottomNavigationBar,
    this.bottomSheet,
    this.backgroundColor,
    this.resizeToAvoidBottomPadding,
    this.resizeToAvoidBottomInset,
    this.primary = true,
    this.drawerDragStartBehavior = DragStartBehavior.start,
    this.extendBody = false,
    this.extendBodyBehindAppBar = false,
    this.drawerScrimColor,
    this.drawerEdgeDragWidth,
  }) : assert(primary != null),
       assert(extendBody != null),
       assert(extendBodyBehindAppBar != null),
       assert(drawerDragStartBehavior != null),
       super(key: key);

4 - ฟังก์ชั่นไม่สามารถใช้ const และวิดเจ็ตคลาสสามารถใช้ const สำหรับตัวสร้างได้ (ที่มีผลต่อประสิทธิภาพของเธรดหลัก)


5 - คุณสามารถสร้างวิดเจ็ตอิสระจำนวนเท่าใดก็ได้โดยใช้คลาสเดียวกัน (อินสแตนซ์ของคลาส / อ็อบเจ็กต์) แต่ฟังก์ชันไม่สามารถสร้างวิดเจ็ตอิสระ (อินสแตนซ์) ได้ แต่สามารถใช้ซ้ำได้

[แต่ละอินสแตนซ์มีตัวแปรอินสแตนซ์ของตัวเองและเป็นอิสระจากวิดเจ็ตอื่น ๆ (อ็อบเจ็กต์) แต่ตัวแปรโลคัลของฟังก์ชันขึ้นอยู่กับการเรียกใช้ฟังก์ชันแต่ละครั้ง * (ซึ่งหมายความว่าเมื่อคุณเปลี่ยนค่าของตัวแปรโลคัลจะมีผลกับส่วนอื่น ๆ ทั้งหมดของ แอปพลิเคชันที่ใช้ฟังก์ชันนี้)]


มีข้อดีหลายอย่างในคลาสมากกว่าฟังก์ชั่น .. (ด้านบนเป็นกรณีการใช้งานเพียงเล็กน้อยเท่านั้น)


🤯ความคิดสุดท้ายของฉัน

ดังนั้นอย่าใช้ฟังก์ชั่นเป็นส่วนประกอบพื้นฐานของแอปพลิเคชันของคุณใช้สำหรับการดำเนินการเท่านั้น มิฉะนั้นจะทำให้เกิดปัญหา unhandable มากเมื่อใบสมัครของคุณได้รับการปรับขนาดได้

  • ใช้ฟังก์ชันสำหรับทำงานส่วนเล็ก ๆ
  • ใช้คลาสเป็นส่วนประกอบพื้นฐานของแอปพลิเคชัน (การจัดการแอปพลิเคชัน)

📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍 📍📍📍📍📍📍📍

คุณไม่สามารถวัดคุณภาพของโปรแกรมตามจำนวนข้อความ (หรือบรรทัด) ที่ใช้โดยมัน

📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍📍 📍📍📍📍📍📍📍

ขอบคุณที่อ่าน


ยินดีต้อนรับสู่ Stackoverflow! ฉันไม่แน่ใจจริงๆว่าคุณพยายามจะแสดงอะไรกับคำตอบของคุณ คุณสามารถใช้ฟังก์ชันได้ดีในการสร้างวิดเจ็ต shrinkHelper() { return const SizedBox.shrink(); }เหมือนกับการใช้const SizedBox.shrink()อินไลน์ในแผนผังวิดเจ็ตของคุณและโดยการใช้ฟังก์ชันตัวช่วยคุณสามารถ จำกัด จำนวนการซ้อนในที่เดียวได้
DarkNeuron

@DarkNeuron ขอบคุณสำหรับการแบ่งปัน ฉันจะพยายามใช้ฟังก์ชันตัวช่วย
TDM

2

เมื่อคุณเรียกวิดเจ็ต Flutter ตรวจสอบให้แน่ใจว่าคุณใช้คีย์เวิร์ด const ตัวอย่างเช่นconst MyListWidget();


9
ฉันขอทราบว่าสิ่งนี้ตอบคำถาม OP ได้อย่างไร
CopsOnRoad

2
ดูเหมือนว่าฉันตอบว่าฉันผิดมาตรา ฉันพยายามตอบคำถามของแดเนียลว่ายังคงมีการเรียกวิธีการสร้างวิดเจ็ตไร้สถานะที่ถูกรีแฟคเตอร์ การเพิ่มconstคีย์เวิร์ดเมื่อเรียกวิดเจ็ตไร้สถานะที่ถูกรีแฟคเตอร์ควรเรียกเพียงครั้งเดียว
user4761410

1
ตกลง. เข้าใจแล้ว ผู้คนอาจลงคะแนนคำตอบนี้เนื่องจากไม่มีส่วนเกี่ยวข้องกับคำถาม OP ดังนั้นคุณควรลบมัน ยังไงก็ได้เป็นของคุณ
CopsOnRoad
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.