การจัดการข้อผิดพลาดในรหัส C


152

สิ่งใดที่คุณพิจารณาว่า "แนวปฏิบัติที่ดีที่สุด" เมื่อพูดถึงข้อผิดพลาดในการจัดการข้อผิดพลาดด้วยวิธีที่สอดคล้องกันในไลบรารี C

มีสองวิธีที่ฉันคิดถึง:

ส่งคืนรหัสข้อผิดพลาดเสมอ ฟังก์ชั่นทั่วไปจะมีลักษณะเช่นนี้:

MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);

วิธีการระบุข้อผิดพลาดตัวชี้:

int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);

เมื่อใช้วิธีแรกเป็นไปได้ที่จะเขียนโค้ดเช่นนี้ซึ่งมีการตรวจสอบข้อผิดพลาดในการเรียกใช้ฟังก์ชันโดยตรง:

int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
  // Error handling
}

ซึ่งดูดีกว่ารหัสการจัดการข้อผิดพลาดที่นี่

MYAPIError error;
int size;
size = getObjectSize(h, &error);
if(error != MYAPI_SUCCESS) {
    // Error handling
}

อย่างไรก็ตามฉันคิดว่าการใช้ค่าส่งคืนสำหรับการส่งคืนข้อมูลทำให้โค้ดอ่านง่ายขึ้นเห็นได้ชัดว่ามีบางสิ่งเขียนไปยังตัวแปรขนาดในตัวอย่างที่สอง

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

แก้ไข: แนวคิดเฉพาะ C ++ ในเรื่องนี้ก็น่าสนใจที่จะได้ยินตราบใดที่พวกเขาไม่เกี่ยวข้องกับข้อยกเว้นเนื่องจากมันไม่ใช่ตัวเลือกสำหรับฉันในขณะนี้ ...


ฉันเพิ่งเรียนรู้ C เป็นเวลาประมาณสองสัปดาห์ แต่ความรู้สึกที่ฉันได้รับก็คือพารามิเตอร์ OUT นั้นเป็นค่าส่งคืน defacto สำหรับฟังก์ชั่นส่วนใหญ่เพราะมันช่วยหลีกเลี่ยงค่าใช้จ่ายที่จะได้รับคืนตามโครงสร้างและค่า mitigates ความจำเป็นในการจัดสรรคืนหน่วยความจำเนื่องจากตัวแปรส่วนใหญ่อยู่บนสแต็ก ดังนั้นเนื่องจากฉันไม่ได้ใช้ "คืนค่า" สำหรับมูลค่าที่แท้จริงของฟังก์ชันฉันจึงสามารถใช้เพื่อแก้ไขข้อผิดพลาดส่วนใหญ่ได้ตลอดเวลา
Joel Roberts

คำตอบ:


74

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

  • จัดเก็บสถานะข้อผิดพลาดที่เป็นไปได้ทั้งหมดใน enum ที่พิมพ์ไว้หนึ่งอันและใช้ใน lib ของคุณ อย่าเพียงแค่ส่งคืน ints หรือยิ่งแย่กว่านั้นให้ผสม int หรือการแจกแจงที่แตกต่างกับรหัสส่งคืน

  • จัดเตรียมฟังก์ชันที่แปลงข้อผิดพลาดเป็นสิ่งที่มนุษย์สามารถอ่านได้ สามารถทำได้ง่าย ข้อผิดพลาด enum, const char * out

  • ฉันรู้ว่าความคิดนี้ทำให้การใช้มัลติเธรดใช้งานได้ยาก แต่มันจะดีถ้าโปรแกรมเมอร์แอปพลิเคชันสามารถตั้งค่าการเรียกกลับข้อผิดพลาดทั่วโลกได้ ด้วยวิธีนี้พวกเขาจะสามารถใส่เบรกพอยต์ในการติดต่อกลับในระหว่างการประชุมบั๊ก

หวังว่ามันจะช่วย


5
ทำไมคุณถึงพูดว่า "ความคิดนี้ทำให้การใช้งานหลายเธรดค่อนข้างยาก" มัลติเธรดส่วนใดที่ทำให้ยากขึ้น คุณสามารถยกตัวอย่างรวดเร็ว
SayeedHussain

1
@crypticcoder กล่าวง่ายๆว่าการเรียกกลับข้อผิดพลาดทั่วโลกสามารถเรียกใช้ในบริบทของเธรด หากคุณเพิ่งพิมพ์ข้อผิดพลาดคุณจะไม่ประสบปัญหาใด ๆ หากคุณพยายามที่จะแก้ไขปัญหาคุณจะต้องค้นหาว่าการเรียกเธรดใดที่ทำให้เกิดข้อผิดพลาดและนั่นทำให้สิ่งต่าง ๆ ยาก
Nils Pipenbrinck

9
ถ้าคุณต้องการสื่อสารรายละเอียดเพิ่มเติมของข้อผิดพลาดล่ะ เช่นคุณมีข้อผิดพลาดในการแยกวิเคราะห์และต้องการให้หมายเลขบรรทัดและคอลัมน์ของข้อผิดพลาดทางไวยากรณ์และวิธีการพิมพ์ทั้งหมดอย่างสวยงาม
panzi

1
@panzi แล้วคุณต้องกลับ struct (หรือใช้ตัวชี้ out ถ้า struct มีขนาดใหญ่มาก) และมีฟังก์ชันในการจัดรูปแบบ struct เป็นสตริง
อนุรักษ์นิยม Sendon

ฉันสาธิตกระสุน 2 อันแรกของคุณในรหัสที่นี่: stackoverflow.com/questions/385975/error-handling-in-c-code/…
Gabriel Staples

92

ฉันใช้ทั้งสองวิธีและพวกเขาก็ใช้ได้ดีสำหรับฉัน ไม่ว่าอันไหนที่ฉันใช้ฉันพยายามใช้หลักการนี้เสมอ:

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

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


1
มีตัวอย่างของการยืนยันใน C หรือไม่? (ฉันเขียวมากถึง C)
thomthom

มันง่ายเหมือนassert(X)ที่ X เป็นคำสั่ง C ที่ถูกต้องที่คุณต้องการให้เป็นจริง ดู stackoverflow.com/q/1571340/10396
AShelly

14
ฮึไม่เคยใช้คำยืนยันในรหัสห้องสมุด ! นอกจากนี้อย่าผสมสไตล์การจัดการข้อผิดพลาดต่าง ๆ ไว้ในรหัสชิ้นเดียวเดียวเหมือนที่คนอื่นทำ…
mirabilos

10
ฉันเห็นด้วยอย่างแน่นอนเกี่ยวกับการไม่ผสมสไตล์ ฉันอยากรู้เกี่ยวกับเหตุผลของคุณในการยืนยัน หากเอกสารของฟังก์ชั่นของฉันบอกว่า "อาร์กิวเมนต์ X ต้องไม่เป็น NULL" หรือ "Y ต้องเป็นสมาชิกของ enum นี้" มากกว่าสิ่งที่ผิดปกติassert(X!=NULL);หรือassert(Y<enumtype_MAX);? ดูคำตอบนี้สำหรับโปรแกรมเมอร์และคำถามที่ลิงก์ไปเพื่อดูรายละเอียดเพิ่มเติมว่าทำไมฉันจึงคิดว่านี่เป็นวิธีที่ถูกต้อง
AShelly

8
@Ashelly ปัญหาเกี่ยวกับการยืนยันว่าพวกเขามักจะไม่ได้อยู่ในรุ่นที่วางจำหน่าย
Calmarius

29

มีชุดสไลด์ที่ดีจาก CERT ของ CMU พร้อมคำแนะนำว่าควรใช้เทคนิคการจัดการข้อผิดพลาด C (และ C ++) ทั่วไปเมื่อใด หนึ่งในสไลด์ที่ดีที่สุดคือแผนผังการตัดสินใจ:

เกิดข้อผิดพลาดในการจัดการแผนผังการตัดสินใจ

ส่วนตัวฉันจะเปลี่ยนสองสิ่งเกี่ยวกับผังงานนี้

อันดับแรกฉันจะชี้แจงว่าบางครั้งวัตถุควรใช้ค่าส่งคืนเพื่อระบุข้อผิดพลาด หากฟังก์ชั่นดึงข้อมูลจากวัตถุเพียงอย่างเดียว แต่ไม่ได้กลายพันธุ์ของวัตถุความสมบูรณ์ของวัตถุนั้นจะไม่ตกอยู่ในอันตรายและการระบุข้อผิดพลาดโดยใช้ค่าส่งคืนนั้นเหมาะสมกว่า

ประการที่สองก็ไม่เสมอเหมาะสมที่จะใช้ข้อยกเว้นใน C ++ ข้อยกเว้นนั้นดีเพราะพวกเขาสามารถลดจำนวนซอร์สโค้ดที่อุทิศให้กับการจัดการข้อผิดพลาดพวกเขาส่วนใหญ่จะไม่ส่งผลกระทบต่อลายเซ็นของฟังก์ชั่น ในทางตรงกันข้ามข้อยกเว้นอาจไม่ใช่ตัวเลือกที่เหมาะสมด้วยเหตุผลบางประการ:

  1. ข้อยกเว้น C ++ มีความหมายที่เฉพาะเจาะจงมาก หากคุณไม่ต้องการซีแมนทิกส์เหล่านั้นข้อยกเว้น C ++ เป็นตัวเลือกที่ไม่ดี ข้อยกเว้นจะต้องได้รับการจัดการทันทีหลังจากที่ถูกโยนทิ้งและการออกแบบสนับสนุนกรณีที่เกิดข้อผิดพลาดจะต้องผ่อนคลาย callstack สองสามระดับ

  2. ฟังก์ชั่น C ++ ที่โยนข้อยกเว้นไม่สามารถห่อได้ในภายหลังเพื่อไม่โยนข้อยกเว้นอย่างน้อยก็ไม่ได้โดยไม่ต้องจ่ายค่าใช้จ่ายทั้งหมดยกเว้น ฟังก์ชั่นที่ส่งคืนรหัสข้อผิดพลาดสามารถห่อเพื่อโยนข้อยกเว้น C ++ ทำให้พวกเขามีความยืดหยุ่นมากขึ้น C ++ newได้รับสิทธิ์นี้โดยการให้ตัวแปรที่ไม่ใช่การขว้างปา

  3. ข้อยกเว้น C ++ นั้นมีราคาค่อนข้างแพง แต่ข้อเสียนี้ส่วนใหญ่จะเป็นข้อผิดพลาดสำหรับโปรแกรมที่ใช้ข้อยกเว้นที่สมเหตุสมผล โปรแกรมไม่ควรโยนข้อยกเว้นบน codepath ที่ประสิทธิภาพเป็นกังวล ไม่สำคัญว่าโปรแกรมของคุณจะรายงานข้อผิดพลาดและออกเร็วเพียงใด

  4. บางครั้งข้อยกเว้น C ++ ไม่สามารถใช้ได้ ไม่ว่าพวกเขาจะไม่สามารถใช้งานได้จริงใน C ++


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


5
คุณอาจทราบว่ามาตรฐานการเข้ารหัส C ++ ของ Google ยังคงบอกว่าเราไม่ได้ใช้ข้อยกเว้น C ++
Jonathan Leffler

19

ฉันใช้วิธีแรกเมื่อใดก็ตามที่ฉันสร้างห้องสมุด มีข้อดีหลายประการในการใช้ typedef'ed enum เป็นรหัสส่งคืน

  • หากฟังก์ชันส่งคืนเอาต์พุตที่ซับซ้อนมากขึ้นเช่นอาร์เรย์และมีความยาวคุณไม่จำเป็นต้องสร้างโครงสร้างโดยพลการเพื่อส่งกลับ

    rc = func(..., int **return_array, size_t *array_length);
  • จะช่วยให้การจัดการข้อผิดพลาดที่เรียบง่ายมาตรฐาน

    if ((rc = func(...)) != API_SUCCESS) {
       /* Error Handling */
    }
  • จะช่วยให้การจัดการข้อผิดพลาดง่าย ๆ ในฟังก์ชั่นห้องสมุด

    /* Check for valid arguments */
    if (NULL == return_array || NULL == array_length)
        return API_INVALID_ARGS;
  • การใช้ typedef'ed enum ยังช่วยให้ชื่อ enum สามารถมองเห็นได้ในโปรแกรมดีบั๊ก วิธีนี้ช่วยให้การดีบักง่ายขึ้นโดยไม่จำเป็นต้องปรึกษาไฟล์ส่วนหัวอย่างต่อเนื่อง การมีฟังก์ชั่นในการแปล enum นี้เป็นสตริงก็มีประโยชน์เช่นกัน

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


9

ใช้setjmp setjmp

http://en.wikipedia.org/wiki/Setjmp.h

http://aszt.inf.elte.hu/~gsd/halado_cpp/ch02s03.html

http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

#include <setjmp.h>
#include <stdio.h>

jmp_buf x;

void f()
{
    longjmp(x,5); // throw 5;
}

int main()
{
    // output of this program is 5.

    int i = 0;

    if ( (i = setjmp(x)) == 0 )// try{
    {
        f();
    } // } --> end of try{
    else // catch(i){
    {
        switch( i )
        {
        case  1:
        case  2:
        default: fprintf( stdout, "error code = %d\n", i); break;
        }
    } // } --> end of catch(i){
    return 0;
}

#include <stdio.h>
#include <setjmp.h>

#define TRY do{ jmp_buf ex_buf__; if( !setjmp(ex_buf__) ){
#define CATCH } else {
#define ETRY } }while(0)
#define THROW longjmp(ex_buf__, 1)

int
main(int argc, char** argv)
{
   TRY
   {
      printf("In Try Statement\n");
      THROW;
      printf("I do not appear\n");
   }
   CATCH
   {
      printf("Got Exception!\n");
   }
   ETRY;

   return 0;
}

2
บล็อกที่สองของโค้ดอ้างอิงจากโค้ดรุ่นก่อนหน้าที่หน้าของFrancesco Niditoอ้างอิงที่ด้านบนของคำตอบ ETRYรหัสได้รับการแก้ไขตั้งแต่คำตอบนี้ถูกเขียนขึ้น
Jonathan Leffler

2
Setjmp เป็นกลยุทธ์การจัดการข้อผิดพลาดที่น่ากลัว มันแพง, เกิดข้อผิดพลาดได้ง่าย (w / nonvolatile เปลี่ยนแปลง locals ไม่รักษาค่าการเปลี่ยนแปลงและทั้งหมด) และการรั่วไหลของทรัพยากรถ้าคุณจัดสรรใด ๆ ในระหว่างการเรียก setjmp และ longjmp คุณควรจะสามารถทำเช่น 30 ผลตอบแทนและการตรวจสอบ int-val ก่อนที่คุณจะชดใช้ค่าใช้จ่ายของ sigjmp / longjmp callstacks ส่วนใหญ่ไม่ได้ลึกขนาดนั้นโดยเฉพาะถ้าคุณไม่หนักในการเรียกซ้ำ (และถ้าคุณทำคุณมีปัญหาที่สมบูรณ์แบบนอกเหนือจากค่าใช้จ่ายของผลตอบแทน + เช็ค)
PSkocik

1
หากคุณใช้หน่วยความจำ malloc'ed แล้วโยนหน่วยความจำจะรั่วไหลตลอดไป นอกจากนี้ยังsetjmpมีราคาแพงแม้ว่าจะไม่มีข้อผิดพลาดเกิดขึ้นมันจะกินซีพียูสักเล็กน้อยและพื้นที่สแต็ก เมื่อใช้ gcc สำหรับ Windows คุณสามารถเลือกระหว่างวิธีการจัดการข้อยกเว้นที่แตกต่างกันสำหรับ C ++ ซึ่งเป็นหนึ่งในนั้นsetjmpและมันทำให้โค้ดของคุณช้าลงถึง 30% ในทางปฏิบัติ
Mecki

7

ฉันชอบวิธีก่อนหน้านี้เป็นการส่วนตัว (ส่งกลับตัวบ่งชี้ข้อผิดพลาด)

ในกรณีที่จำเป็นผลลัพธ์ของการส่งคืนควรระบุว่ามีข้อผิดพลาดเกิดขึ้นพร้อมฟังก์ชันอื่นที่ใช้เพื่อค้นหาข้อผิดพลาดที่แน่นอน

ในตัวอย่าง getSize () ของคุณฉันจะพิจารณาว่าขนาดต้องเป็นศูนย์หรือเป็นบวกเสมอดังนั้นการส่งคืนผลลัพธ์เชิงลบจึงสามารถระบุข้อผิดพลาดได้เหมือนกับการเรียกใช้ระบบ UNIX

ฉันไม่สามารถนึกถึงห้องสมุดใด ๆ ที่ฉันใช้ซึ่งเป็นวิธีการหลังโดยมีวัตถุข้อผิดพลาดส่งผ่านเป็นตัวชี้ stdioฯลฯ ทั้งหมดไปด้วยค่าตอบแทน


1
สำหรับบันทึกหนึ่งห้องสมุดที่ฉันเคยเห็นใช้วิธีหลังคือ Maya programming API มันเป็นห้องสมุด c ++ มากกว่า C แม้ว่า มันค่อนข้างไม่สอดคล้องกันในการจัดการข้อผิดพลาดและบางครั้งข้อผิดพลาดจะถูกส่งเป็นค่าตอบแทนและเวลาอื่น ๆ ที่ส่งผ่านผลลัพธ์เป็นการอ้างอิง
Laserallan

1
อย่าลืม strtod โอเคอาร์กิวเมนต์สุดท้ายไม่เพียง แต่บ่งบอกข้อผิดพลาดเท่านั้น แต่ยังทำเช่นนั้นด้วย
quinmars

7

เมื่อฉันเขียนโปรแกรมในระหว่างการเริ่มต้นฉันมักจะแยกเธรดออกเพื่อจัดการข้อผิดพลาดและเริ่มต้นโครงสร้างพิเศษสำหรับข้อผิดพลาดรวมถึงการล็อก จากนั้นเมื่อฉันตรวจพบข้อผิดพลาดผ่านค่าส่งคืนฉันป้อนข้อมูลจากข้อยกเว้นลงในโครงสร้างและส่ง SIGIO ไปยังเธรดการจัดการข้อยกเว้นจากนั้นดูว่าฉันไม่สามารถดำเนินการต่อได้ หากฉันไม่สามารถทำได้ฉันจะส่ง SIGURG ไปยังเธรดข้อยกเว้นซึ่งจะหยุดโปรแกรมอย่างนุ่มนวล


7

การส่งคืนรหัสข้อผิดพลาดเป็นวิธีการปกติสำหรับการจัดการข้อผิดพลาดใน C.

แต่เมื่อเร็ว ๆ นี้เราได้ทดลองด้วยตัวชี้ข้อผิดพลาดขาออกเช่นกัน

มันมีข้อดีกว่าวิธีมูลค่าส่งคืน:

  • คุณสามารถใช้ค่าส่งคืนเพื่อวัตถุประสงค์ที่มีความหมายมากขึ้น

  • ต้องเขียนพารามิเตอร์ข้อผิดพลาดเตือนให้คุณจัดการข้อผิดพลาดหรือเผยแพร่ (คุณไม่เคยลืมตรวจสอบค่าส่งคืนของfcloseใช่ไหม?)

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

  • โดยการตั้งค่าเบรกพอยต์ข้อมูลในตัวแปรข้อผิดพลาดคุณสามารถตรวจสอบข้อผิดพลาดที่เกิดขึ้นก่อน โดยการตั้งค่าเบรกพอยต์แบบมีเงื่อนไขคุณสามารถตรวจจับข้อผิดพลาดเฉพาะเช่นกัน

  • มันทำให้ง่ายขึ้นโดยอัตโนมัติตรวจสอบว่าคุณจัดการข้อผิดพลาดทั้งหมด การประชุมรหัสอาจบังคับให้คุณเรียกตัวชี้ข้อผิดพลาดของคุณerrและจะต้องเป็นอาร์กิวเมนต์สุดท้าย ดังนั้นสคริปต์ที่สามารถจับคู่สตริงแล้วตรวจสอบถ้ามันตามมาด้วยerr); if (*errที่จริงแล้วในทางปฏิบัติเราสร้างมาโครที่เรียกว่าCER(check err return) และCEG(check err goto) ดังนั้นคุณไม่จำเป็นต้องพิมพ์ออกมาเสมอเมื่อเราต้องการกลับมาผิดพลาดและสามารถลดความยุ่งเหยิงที่มองเห็นได้

ฟังก์ชันบางอย่างในรหัสของเรามีพารามิเตอร์ขาออกนี้ สิ่งที่พารามิเตอร์ขาออกนี้จะใช้สำหรับกรณีที่คุณมักจะโยนข้อยกเว้น


6

ฉันเคยเขียนโปรแกรม C มาหลายครั้งแล้วในอดีต และฉันก็เสียใจจริง ๆ ค่าส่งคืนรหัสข้อผิดพลาด แต่มีข้อผิดพลาดที่เป็นไปได้หลายประการ:

  • หมายเลขข้อผิดพลาดที่ซ้ำกันสามารถแก้ไขได้ด้วยไฟล์ข้อผิดพลาดส่วนกลาง
  • ลืมที่จะตรวจสอบรหัสข้อผิดพลาดนี้ควรแก้ไขด้วย cluebat และการดีบักนานชั่วโมง แต่ในที่สุดคุณจะได้เรียนรู้ (หรือคุณจะรู้ว่าคนอื่นจะทำการแก้ไขข้อบกพร่อง)

2
ปัญหาที่สองสามารถแก้ไขได้โดยระดับการเตือนผู้รวบรวมที่เหมาะสมกลไกการตรวจสอบรหัสที่เหมาะสมและโดยเครื่องมือวิเคราะห์รหัสแบบคงที่
Ilya

1
คุณยังสามารถทำงานได้โดยใช้หลักการ: หากเรียกใช้ฟังก์ชัน API และไม่ได้ตรวจสอบค่าส่งคืนแสดงว่ามีข้อผิดพลาด
Jonathan Leffler

6

วิธี UNIX คล้ายกับข้อเสนอแนะที่สองของคุณมากที่สุด ส่งคืนผลลัพธ์หรือค่าเดียว "ผิดพลาด" ตัวอย่างเช่น open จะส่งคืน file descriptor เมื่อสำเร็จหรือ -1 เมื่อล้มเหลว ในความล้มเหลวมันยังกำหนดerrnoเป็นจำนวนเต็มโลกภายนอกเพื่อบ่งชี้ซึ่งความล้มเหลวที่เกิดขึ้น

สำหรับสิ่งที่คุ้มค่าโกโก้ก็ใช้วิธีการที่คล้ายกัน จำนวนเมธอดส่งคืน BOOL และรับNSError **พารามิเตอร์เพื่อให้เกิดความล้มเหลวในการตั้งค่าข้อผิดพลาดและส่งคืน NO จากนั้นการจัดการข้อผิดพลาดดูเหมือนว่า:

NSError *error = nil;
if ([myThing doThingError: &error] == NO)
{
  // error handling
}

สิ่งใดอยู่ระหว่างตัวเลือกสองรายการของคุณ :-)


5

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


ขอบคุณสำหรับการให้ฉันรู้ว่า. มันน่าสนใจที่จะเห็น
Laserallan

5

นี่เป็นวิธีที่ฉันคิดว่าน่าสนใจในขณะที่ต้องมีวินัย

สิ่งนี้จะถือว่าตัวแปรชนิดตัวจัดการเป็นอินสแตนซ์ที่ใช้งานฟังก์ชัน API ทั้งหมด

แนวคิดก็คือว่าโครงสร้างที่อยู่เบื้องหลังที่จับเก็บข้อผิดพลาดก่อนหน้านี้เป็นโครงสร้างที่มีข้อมูลที่จำเป็น (รหัสข้อความ ... ) และผู้ใช้จะได้รับฟังก์ชั่นที่ส่งกลับตัวชี้ tp ข้อผิดพลาดวัตถุนี้ การดำเนินการแต่ละครั้งจะอัปเดตวัตถุแหลมเพื่อให้ผู้ใช้สามารถตรวจสอบสถานะได้โดยไม่ต้องเรียกใช้ฟังก์ชัน ซึ่งแตกต่างจากรูปแบบ errno รหัสข้อผิดพลาดไม่ใช่แบบทั่วโลกซึ่งทำให้วิธีการเธรดปลอดภัยตราบใดที่มีการใช้แต่ละหมายเลขอ้างอิงอย่างถูกต้อง

ตัวอย่าง:

MyHandle * h = MyApiCreateHandle();

/* first call checks for pointer nullity, since we cannot retrieve error code
   on a NULL pointer */
if (h == NULL)
     return 0; 

/* from here h is a valid handle */

/* get a pointer to the error struct that will be updated with each call */
MyApiError * err = MyApiGetError(h);


MyApiFileDescriptor * fd = MyApiOpenFile("/path/to/file.ext");

/* we want to know what can go wrong */
if (err->code != MyApi_ERROR_OK) {
    fprintf(stderr, "(%d) %s\n", err->code, err->message);
    MyApiDestroy(h);
    return 0;
}

MyApiRecord record;

/* here the API could refuse to execute the operation if the previous one
   yielded an error, and eventually close the file descriptor itself if
   the error is not recoverable */
MyApiReadFileRecord(h, &record, sizeof(record));

/* we want to know what can go wrong, here using a macro checking for failure */
if (MyApi_FAILED(err)) {
    fprintf(stderr, "(%d) %s\n", err->code, err->message);
    MyApiDestroy(h);
    return 0;
}

4

วิธีแรกดีกว่า IMHO:

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

4

ฉันชอบวิธีแก้ปัญหาแรก:

int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
  // Error handling
}

ฉันจะปรับเปลี่ยนเล็กน้อยเพื่อ:

int size;
MYAPIError rc;

rc = getObjectSize(h, &size)
if ( rc != MYAPI_SUCCESS) {
  // Error handling
}

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

และถ้าเราพูดถึงการจัดการข้อผิดพลาดแล้วฉันจะแนะนำ goto Error;เป็นรหัสการจัดการข้อผิดพลาดเว้นแต่undoฟังก์ชั่นบางอย่างสามารถเรียกว่าการจัดการข้อผิดพลาดได้อย่างถูกต้อง


3

สิ่งที่คุณสามารถทำได้แทนที่จะส่งคืนข้อผิดพลาดของคุณและห้ามไม่ให้คุณส่งคืนข้อมูลด้วยฟังก์ชันของคุณกำลังใช้wrapperสำหรับประเภทที่ส่งคืน:

typedef struct {
    enum {SUCCESS, ERROR} status;
    union {
        int errCode;
        MyType value;
    } ret;
} MyTypeWrapper;

จากนั้นในฟังก์ชั่นที่เรียกว่า:

MyTypeWrapper MYAPIFunction(MYAPIHandle h) {
    MyTypeWrapper wrapper;
    // [...]
    // If there is an error somewhere:
    wrapper.status = ERROR;
    wrapper.ret.errCode = MY_ERROR_CODE;

    // Everything went well:
    wrapper.status = SUCCESS;
    wrapper.ret.value = myProcessedData;
    return wrapper;
} 

โปรดทราบว่าด้วยวิธีการต่อไปนี้ wrapper จะมีขนาดของ MyType บวกหนึ่งไบต์ (บนคอมไพเลอร์ส่วนใหญ่) ซึ่งค่อนข้างมีกำไร และคุณจะไม่ต้องกดอาร์กิวเมนต์อีกครั้งบนสแต็กเมื่อคุณเรียกใช้ฟังก์ชันของคุณ ( returnedSizeหรือreturnedErrorทั้งสองวิธีที่คุณนำเสนอ)


3

นี่เป็นโปรแกรมง่าย ๆ ที่แสดงให้เห็นถึง 2 กระสุนแรกของคำตอบของNils Pipenbrinck ที่นี่ที่นี่

กระสุน 2 นัดแรกของเขาคือ:

  • จัดเก็บสถานะข้อผิดพลาดที่เป็นไปได้ทั้งหมดใน enum ที่พิมพ์ไว้หนึ่งอันและใช้ใน lib ของคุณ อย่าเพียงแค่ส่งคืน ints หรือยิ่งแย่กว่านั้นให้ผสม int หรือการแจกแจงที่แตกต่างกับรหัสส่งคืน

  • จัดเตรียมฟังก์ชันที่แปลงข้อผิดพลาดเป็นสิ่งที่มนุษย์สามารถอ่านได้ สามารถทำได้ง่าย ข้อผิดพลาด enum, const char * out

mymoduleถือว่าคุณได้เขียนโมดูลที่มีชื่อว่า อันดับแรกใน mymodule.h คุณกำหนดรหัสข้อผิดพลาดที่ใช้ enum และคุณเขียนสตริงข้อผิดพลาดที่สอดคล้องกับรหัสเหล่านี้ ที่นี่ฉันใช้อาร์เรย์ของสตริง C ( char *) ซึ่งจะทำงานได้ดีถ้ารหัสข้อผิดพลาดตาม enum แรกของคุณมีค่า 0 และคุณไม่ได้จัดการกับตัวเลขหลังจากนั้น หากคุณใช้หมายเลขรหัสข้อผิดพลาดกับช่องว่างหรือค่าเริ่มต้นอื่น ๆ คุณจะต้องเปลี่ยนจากการใช้อาร์เรย์ C-string ที่แมป (ดังที่ฉันทำด้านล่าง) เป็นการใช้ฟังก์ชันที่ใช้คำสั่ง switch หรือหาก / อื่นถ้าคำสั่ง เพื่อแมปจากรหัสข้อผิดพลาด enum กับสตริง C ที่พิมพ์ได้ ทางเลือกเป็นของคุณ

mymodule.h

/// @brief Error codes for library "mymodule"
typedef enum mymodule_error_e
{
    /// No error
    MYMODULE_ERROR_OK = 0,
    
    /// Invalid arguments (ex: NULL pointer where a valid pointer is required)
    MYMODULE_ERROR_INVARG,

    /// Out of memory (RAM)
    MYMODULE_ERROR_NOMEM,

    /// Make up your error codes as you see fit
    MYMODULE_ERROR_MYERROR, 

    // etc etc
    
    /// Total # of errors in this list (NOT AN ACTUAL ERROR CODE);
    /// NOTE: that for this to work, it assumes your first error code is value 0 and you let it naturally 
    /// increment from there, as is done above, without explicitly altering any error values above
    MYMODULE_ERROR_COUNT,
} mymodule_error_t;

// Array of strings to map enum error types to printable strings
// - see important NOTE above!
const char* const MYMODULE_ERROR_STRS[] = 
{
    "MYMODULE_ERROR_OK",
    "MYMODULE_ERROR_INVARG",
    "MYMODULE_ERROR_NOMEM",
    "MYMODULE_ERROR_MYERROR",
};

// To get a printable error string
const char* mymodule_error_str(mymodule_error_t err);

// Other functions in mymodule
mymodule_error_t mymodule_func1(void);
mymodule_error_t mymodule_func2(void);
mymodule_error_t mymodule_func3(void);

mymodule.c มีฟังก์ชั่นการแมปของฉันเพื่อแม็พจากรหัสข้อผิดพลาด enum ไปยังสตริง C ที่พิมพ์ได้:

mymodule.c

#include <stdio.h>

/// @brief      Function to get a printable string from an enum error type
/// @param[in]  err     a valid error code for this module
/// @return     A printable C string corresponding to the error code input above, or NULL if an invalid error code
///             was passed in
const char* mymodule_error_str(mymodule_error_t err)
{
    const char* err_str = NULL;

    // Ensure error codes are within the valid array index range
    if (err >= MYMODULE_ERROR_COUNT)
    {
        goto done;
    }

    err_str = MYMODULE_ERROR_STRS[err];

done:
    return err_str;
}

// Let's just make some empty dummy functions to return some errors; fill these in as appropriate for your 
// library module

mymodule_error_t mymodule_func1(void)
{
    return MYMODULE_ERROR_OK;
}

mymodule_error_t mymodule_func2(void)
{
    return MYMODULE_ERROR_INVARG;
}

mymodule_error_t mymodule_func3(void)
{
    return MYMODULE_ERROR_MYERROR;
}

main.c มีโปรแกรมทดสอบเพื่อสาธิตการเรียกใช้ฟังก์ชันและพิมพ์รหัสข้อผิดพลาดจากพวกเขา:

main.c

#include <stdio.h>

int main()
{
    printf("Demonstration of enum-based error codes in C (or C++)\n");

    printf("err code from mymodule_func1() = %s\n", mymodule_error_str(mymodule_func1()));
    printf("err code from mymodule_func2() = %s\n", mymodule_error_str(mymodule_func2()));
    printf("err code from mymodule_func3() = %s\n", mymodule_error_str(mymodule_func3()));

    return 0;
}

เอาท์พุท:

การสาธิตรหัสข้อผิดพลาดที่อิง Enum ในรหัสข้อผิดพลาด C (หรือ C ++)
จาก mymodule_func1 () = MYMODULE_ERROR_OK
รหัสข้อผิดพลาดจาก mymodule_func2 () =
รหัสผิดพลาดของ MYMODULE_ERROR_INVARG

อ้างอิง:

คุณสามารถเรียกใช้รหัสนี้ด้วยตัวคุณเองที่นี่: https://onlinegdb.com/ByEbKLupS


2

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

ฉันกลับรหัสข้อผิดพลาดเป็นจำนวนเต็มลบโดยno_errorเป็นศูนย์ แต่มันทำให้คุณมีข้อผิดพลาดดังต่อไปนี้

if (MyFunc())
 DoSomething();

ทางเลือกคือมีความล้มเหลวที่ส่งคืนเป็นศูนย์เสมอและใช้ฟังก์ชัน LastError () เพื่อให้รายละเอียดของข้อผิดพลาดที่เกิดขึ้นจริง


2

ฉันพบคำถามหลายครั้งและต้องการช่วยตอบคำถามที่ครอบคลุมมากขึ้น ฉันคิดว่าวิธีที่ดีที่สุดในการคิดเกี่ยวกับสิ่งนี้คือวิธีคืนข้อผิดพลาดไปยังผู้โทรและอะไรคุณส่งคืน

อย่างไร

มี 3 วิธีในการส่งคืนข้อมูลจากฟังก์ชัน:

  1. ส่งคืนค่า
  2. ออกอาร์กิวเมนต์
  3. Out of Band, ที่มีไม่ใช่ goto (setjmp / longjmp), ไฟล์หรือตัวแปรที่กำหนดขอบเขตแบบโกลบอล, ระบบไฟล์ ฯลฯ

ส่งคืนค่า

คุณสามารถส่งคืนค่าได้เพียงวัตถุเดียวอย่างไรก็ตามมันอาจจะซับซ้อนโดยพลการ นี่คือตัวอย่างของฟังก์ชันการส่งคืนข้อผิดพลาด:

  enum error hold_my_beer();

ข้อดีอย่างหนึ่งของค่าตอบแทนคืออนุญาตให้มีการผูกมัดการเรียกสำหรับการจัดการข้อผิดพลาดที่รบกวนน้อย:

  !hold_my_beer() &&
  !hold_my_cigarette() &&
  !hold_my_pants() ||
  abort();

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

ออกอาร์กิวเมนต์

คุณสามารถส่งคืนมากกว่าผ่านวัตถุมากกว่าหนึ่งรายการผ่านการขัดแย้ง แต่แนวปฏิบัติที่ดีที่สุดแนะนำให้รักษาจำนวนอาร์กิวเมนต์ให้ต่ำ (พูดว่า <= 4):

void look_ma(enum error *e, char *what_broke);

enum error e;
look_ma(e);
if(e == FURNITURE) {
  reorder(what_broke);
} else if(e == SELF) {
  tell_doctor(what_broke);
}

ออกจากวงดนตรี

ด้วย setjmp () คุณกำหนดสถานที่และวิธีที่คุณต้องการจัดการกับค่า int และคุณโอนการควบคุมไปยังตำแหน่งนั้นผ่านทาง longjmp () ดูการใช้งานจริงของ setjmp และ longjmp ใน CC

อะไร

  1. ตัวบ่งชี้
  2. รหัส
  3. วัตถุ
  4. โทรกลับ

ตัวบ่งชี้

ตัวบ่งชี้ข้อผิดพลาดเพียงบอกคุณว่ามีปัญหา แต่ไม่มีอะไรเกี่ยวกับลักษณะของปัญหาที่กล่าวถึง:

struct foo *f = foo_init();
if(!f) {
  /// handle the absence of foo
}

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

รหัส

รหัสข้อผิดพลาดบอกผู้โทรเกี่ยวกับลักษณะของปัญหาและอาจอนุญาตการตอบสนองที่เหมาะสม (จากด้านบน) มันสามารถเป็นค่าตอบแทนหรือตัวอย่างเช่น look_ma () ด้านบนอาร์กิวเมนต์ข้อผิดพลาด

วัตถุ

ด้วยวัตถุข้อผิดพลาดผู้โทรสามารถทราบเกี่ยวกับปัญหาที่ซับซ้อนโดยพลการ ตัวอย่างเช่นรหัสข้อผิดพลาดและข้อความที่มนุษย์สามารถอ่านได้ นอกจากนี้ยังสามารถแจ้งให้ผู้โทรทราบว่ามีหลายสิ่งที่ผิดพลาดหรือเกิดข้อผิดพลาดต่อรายการเมื่อประมวลผลการรวบรวม:

struct collection friends;
enum error *e = malloc(c.size * sizeof(enum error));
...
ask_for_favor(friends, reason);
for(int i = 0; i < c.size; i++) {
   if(reason[i] == NOT_FOUND) find(friends[i]);
}

แทนที่จะจัดสรรอาเรย์ข้อผิดพลาดล่วงหน้าคุณยังสามารถจัดสรรได้อีกครั้งแบบไดนามิกตามที่ต้องการแน่นอน

โทรกลับ

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

 struct foo {
    ...
    void (error_handler)(char *);
 };

 void default_error_handler(char *message) { 
    assert(f);
    printf("%s", message);
 }

 void foo_set_error_handler(struct foo *f, void (*eh)(char *)) {
    assert(f);
    f->error_handler = eh;
 }

 struct foo *foo_init() {
    struct foo *f = malloc(sizeof(struct foo));
    foo_set_error_handler(f, default_error_handler);
    return f;
 }


 struct foo *f = foo_init();
 foo_something();

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

อย่างไรก็ตามมีการผกผันของการควบคุม รหัสการโทรไม่ทราบว่ามีการเรียกกลับหรือไม่ ด้วยเหตุนี้จึงอาจเหมาะสมที่จะใช้ตัวบ่งชี้


1

แก้ไข: หากคุณต้องการเข้าถึงข้อผิดพลาดล่าสุดเท่านั้นและคุณไม่สามารถใช้งานได้ในสภาพแวดล้อมแบบมัลติเธรด

คุณสามารถคืนค่าเป็น true / false (หรือ #define บางชนิดถ้าคุณทำงานใน C และไม่สนับสนุนตัวแปรบูล) และมีบัฟเฟอร์ข้อผิดพลาดส่วนกลางที่จะเก็บข้อผิดพลาดล่าสุด:

int getObjectSize(MYAPIHandle h, int* returnedSize);
MYAPI_ERROR LastError;
MYAPI_ERROR* getLastError() {return LastError;};
#define FUNC_SUCCESS 1
#define FUNC_FAIL 0

if(getObjectSize(h, &size) != FUNC_SUCCESS ) {
    MYAPI_ERROR* error = getLastError();
    // error handling
}

แน่นอน แต่ไม่ใช่ C มันอาจมีให้โดยระบบปฏิบัติการหรือไม่หากคุณกำลังทำงานกับระบบปฏิบัติการแบบเรียลไทม์เช่นคุณไม่ได้มีมัน
Ilya

1

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


1

ฉันชอบการจัดการข้อผิดพลาดใน C โดยใช้เทคนิคต่อไปนี้:

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

   // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

   // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

   // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

   // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

   // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

   // good? return list or else return NULL
    return (good ? list : NULL);
}

ที่มา: http://blog.staila.com/?p=114


1
เทคนิคที่ดี ผมพบว่าแม้ neater กับgoto's แทนซ้ำif' s อ้างอิง: หนึ่ง , สอง
Ant_222

0

นอกจากคำตอบที่ยอดเยี่ยมอื่น ๆ ฉันขอแนะนำให้คุณลองแยกการตั้งค่าสถานะข้อผิดพลาดและรหัสข้อผิดพลาดเพื่อบันทึกหนึ่งบรรทัดในการโทรแต่ละครั้งเช่น:

if( !doit(a, b, c, &errcode) )
{   (* handle *)
    (* thine  *)
    (* error  *)
}

เมื่อคุณมีการตรวจสอบข้อผิดพลาดจำนวนมากการทำให้เข้าใจง่ายเล็กน้อยนี้จะช่วยได้มาก

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