ข้อผิดพลาดในการพิจารณาการจัดการ


31

ปัญหา:

ตั้งแต่เวลานานฉันกังวลเกี่ยวกับexceptionsกลไกเพราะฉันรู้สึกว่ามันไม่ได้แก้ไขสิ่งที่ควร

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

พยายามกำหนดข้อผิดพลาดฉันจะเห็นด้วยกับ CppCoreGuidelines จาก Bjarne Stroustrup & Herb Sutter

ข้อผิดพลาดหมายความว่าฟังก์ชันไม่สามารถบรรลุวัตถุประสงค์ที่โฆษณาไว้

การเรียกร้อง: exceptionกลไกคือ semantic ภาษาสำหรับการจัดการข้อผิดพลาด

การเรียกร้อง: สำหรับฉันมี "ไม่มีข้อแก้ตัว" สำหรับฟังก์ชั่นที่ไม่ได้งาน: เพราะเรากำหนดเงื่อนไขล่วงหน้า / โพสต์ผิดดังนั้นฟังก์ชันไม่สามารถรับประกันผลลัพธ์ได้หรือกรณีพิเศษบางกรณีไม่ถือว่าสำคัญพอสำหรับการใช้เวลาในการพัฒนา ทางออก เมื่อพิจารณาแล้ว IMO ความแตกต่างระหว่างการจัดการรหัสปกติและรหัสข้อผิดพลาดคือ (ก่อนการติดตั้ง) บรรทัดที่เป็นอัตนัย

การเรียกร้อง: การใช้ข้อยกเว้นเพื่อระบุว่าเมื่อใดที่ไม่ได้รักษาเงื่อนไขก่อนหรือหลังการโพสต์ไว้เป็นอีกจุดประสงค์หนึ่งของexceptionกลไก ฉันไม่ได้กำหนดเป้าหมายการใช้งานของexceptionsที่นี่

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

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

สิ่งนี้มีสองผล:

  1. ตรวจพบข้อผิดพลาดที่เกิดขึ้นบ่อยครั้งในช่วงต้นของการพัฒนาและดีบั๊ก (ซึ่งดี)
  2. ข้อยกเว้นที่หายากไม่ได้รับการจัดการและทำให้ระบบล่ม (ด้วยข้อความบันทึกที่ดี) ที่หน้าแรกของผู้ใช้ บางครั้งมีรายงานข้อผิดพลาดหรือไม่

เมื่อพิจารณาแล้ว IMO วัตถุประสงค์หลักของกลไกข้อผิดพลาดควรเป็น:

  1. ทำให้มองเห็นได้ในรหัสที่บางกรณีไม่ได้รับการจัดการ
  2. สื่อสารปัญหา runtime ให้กับรหัสที่เกี่ยวข้อง (อย่างน้อยผู้โทร) เมื่อสถานการณ์นี้เกิดขึ้น
  3. จัดเตรียมกลไกการกู้คืน

ข้อบกพร่องหลักของexceptionความหมายในฐานะกลไกการจัดการข้อผิดพลาดคือ IMO: มันง่ายที่จะดูว่า a อยู่ที่ไหนthrowในซอร์สโค้ด แต่ไม่ชัดเจนที่จะทราบว่าฟังก์ชันเฉพาะสามารถโยนได้โดยดูที่การประกาศ นี่ทำให้เกิดปัญหาทั้งหมดที่ฉันแนะนำไว้ข้างต้น

ภาษาไม่ได้บังคับใช้และตรวจสอบรหัสข้อผิดพลาดอย่างเคร่งครัดเท่าที่มันทำเพื่อด้านอื่น ๆ ของภาษา (เช่นชนิดของตัวแปรที่แข็งแกร่ง)

ลองวิธีแก้ปัญหา

ในความตั้งใจที่จะปรับปรุงสิ่งนี้ฉันได้พัฒนาระบบการจัดการข้อผิดพลาดที่ง่ายมากซึ่งพยายามที่จะทำให้การจัดการข้อผิดพลาดในระดับความสำคัญในระดับเดียวกันมากกว่ารหัสปกติ

ความคิดคือ:

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

เห็นได้ชัดว่าการออกแบบเต็มรูปแบบพิจารณาทุกแง่มุมอย่างละเอียด (ประมาณ 10 หน้า) รวมถึงวิธีการใช้กับ OOP

ตัวอย่างของSuccessชั้นเรียน:

class Success
{
public:
    enum SuccessStatus
    {
        ok = 0,             // All is fine
        error = 1,          // Any error has been reached
        uninitialized = 2,  // Initialization is required
        finished = 3,       // This object already performed its task and is not useful anymore
        unimplemented = 4,  // This feature is not implemented already
    };

    Success(){}
    Success( const Success& v);
    virtual ~Success() = default;
    virtual Success& operator= (const Success& v);

    // Comparators
    virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
    virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}

    // Retrieve if the status is not "ok"
    virtual bool operator!() const { return status!=ok;}

    // Retrieve if the status is "ok"
    operator bool() const { return status==ok;}

    // Set a new status
    virtual Success& set( SuccessStatus status, std::string msg="");
    virtual void reset();

    virtual std::string toString() const{ return stateStr;}
    virtual SuccessStatus getStatus() const { return status; }
    virtual operator SuccessStatus() const { return status; }

private:
    std::string stateStr;
    SuccessStatus status = Success::ok;
};

การใช้งาน:

double mySqrt( Success& s, double v)
{
    double result = 0.0;
    if (!s) ; // do nothing
    else if (v<0.0) s.set(Error, "Square root require non-negative input.");
    else result = std::sqrt(v);
    return result;
}

Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;

ฉันใช้มันในหลาย ๆ รหัส (ของตัวเอง) และมันบังคับให้โปรแกรมเมอร์ (ฉัน) คิดเพิ่มเติมเกี่ยวกับกรณีพิเศษที่เป็นไปได้และวิธีแก้ปัญหา (ดี) อย่างไรก็ตามมันมีช่วงการเรียนรู้และไม่ได้รวมเข้ากับโค้ดที่ใช้งานได้ในขณะนี้

คำถาม

ฉันอยากจะเข้าใจความหมายของการใช้กระบวนทัศน์ในโครงการให้ดีขึ้น

  • หลักฐานของปัญหาถูกต้องหรือไม่? หรือฉันพลาดบางสิ่งที่เกี่ยวข้อง?
  • การแก้ปัญหาเป็นแนวคิดทางสถาปัตยกรรมที่ดีหรือไม่? หรือราคาสูงเกินไป?

แก้ไข:

การเปรียบเทียบระหว่างวิธีการ:

//Exceptions:

    // Incorrect
    File f = open("text.txt"); // Could throw but nothing tell it! Will crash
    save(f);

    // Correct
    File f;
    try
    {
        f = open("text.txt");
        save(f);
    }
    catch( ... )
    {
        // do something 
    }

//Error code (mixed):

    // Incorrect
    File f = open("text.txt"); //Nothing tell you it may fail! Will crash
    save(f);

    // Correct
    File f = open("text.txt");
    if (f) save(f);

//Error code (pure);

    // Incorrect
    File f;
    open(f, "text.txt"); //Easy to forget the return value! will crash
    save(f);

    //Correct
    File f;
    Error er = open(f, "text.txt");
    if (!er) save(f);

//Success mechanism:

    Success s;
    File f;
    open(s, "text.txt");
    save(s, f); //s cannot be avoided, will never crash.
    if (s) ... //optional. If you created s, you probably don't forget it.

25
โหวตขึ้นสำหรับ "คำถามนี้แสดงให้เห็นถึงความพยายามในการวิจัยมันมีประโยชน์และชัดเจน" ไม่ใช่เพราะฉันเห็นด้วย: ฉันคิดว่ามีบางความคิดที่เข้าใจผิด (รายละเอียดอาจตามมาด้วยคำตอบ)
Martin Ba

2
ฉันเข้าใจและเห็นด้วยอย่างยิ่ง! มันเป็นจุดประสงค์ของคำถามนี้เพื่อรับการวิพากษ์วิจารณ์ และคะแนนของคำถามเพื่อระบุคำถามที่ดี / ไม่ดีไม่ใช่ว่า OP นั้นถูกต้อง
Adrian Maire

2
ถ้าฉันเข้าใจถูกต้องหลัก ๆ ของคุณเกี่ยวกับข้อยกเว้นคือผู้คนสามารถเพิกเฉยได้ (ใน c ++) แทนที่จะจัดการพวกเขา อย่างไรก็ตามโครงสร้างความสำเร็จของคุณมีข้อบกพร่องเหมือนกันโดยการออกแบบ เช่นเดียวกับข้อยกเว้นพวกเขาจะไม่สนใจมัน ยิ่งแย่ไปกว่า: มันละเอียดมากขึ้นนำไปสู่การคืนซ้อนและคุณไม่สามารถ "จับ" ต้นน้ำได้
dagnelies

3
ทำไมไม่ใช้สิ่งที่เหมือนพระ พวกเขาทำให้ข้อผิดพลาดของคุณโดยนัย แต่พวกเขาจะไม่เงียบในระหว่างการทำงาน จริงๆแล้วสิ่งแรกที่ฉันคิดว่าเมื่อดูรหัสของคุณคือ "monads, nice" ดูที่พวกเขา
bash0r

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

คำตอบ:


32

การจัดการข้อผิดพลาดอาจเป็นส่วนที่ยากที่สุดของโปรแกรม

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

ใน C ข้อผิดพลาดในการส่งสัญญาณจะกระทำโดยรหัสส่งคืนซึ่งเป็น isomorphic สำหรับการแก้ปัญหาของคุณ

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

อย่างไรก็ตามปัญหาคือข้อยกเว้นมีปัญหาของตนเอง คือการควบคุมการไหลที่มองไม่เห็น / ซ่อนอยู่ นี่คือจุดประสงค์: ซ่อนกรณีข้อผิดพลาดเพื่อให้ตรรกะของรหัสไม่สับสนโดยการจัดการข้อผิดพลาด มันทำให้ "เส้นทางแห่งความสุข" ชัดเจนยิ่งขึ้น (และเร็ว!) ด้วยค่าใช้จ่ายในการสร้างเส้นทางที่ผิดพลาดที่ไม่อาจพิสูจน์ได้


ฉันคิดว่ามันน่าสนใจที่จะดูว่าภาษาอื่น ๆ เข้าหาปัญหานี้ได้อย่างไร

  • Java ตรวจสอบข้อยกเว้น (และไม่ได้ตรวจสอบ)
  • ใช้รหัสข้อผิดพลาด / ความตื่นตระหนก
  • สนิมใช้ประเภทผลรวม / ความตื่นตระหนก)
  • ภาษา FP โดยทั่วไป

C ++ เคยมีรูปแบบของข้อยกเว้นที่ถูกตรวจสอบบางอย่างคุณอาจสังเกตเห็นว่ามันเลิกใช้แล้วและทำให้ง่ายขึ้นไปสู่ระดับพื้นฐานnoexcept(<bool>)แทน: ทั้งฟังก์ชั่นถูกประกาศว่าจะโยนหรือไม่เคยประกาศ การตรวจสอบข้อยกเว้นนั้นค่อนข้างมีปัญหาในกรณีที่ไม่มีความสามารถในการขยายซึ่งสามารถทำให้การแมป / การทำรังที่น่าอึดอัดใจ และลำดับชั้นของข้อยกเว้นที่ซับซ้อน (หนึ่งในกรณีการใช้งานที่สำคัญของการสืบทอดเสมือนคือข้อยกเว้น ... )

ในทางตรงกันข้ามไปและสนิมใช้วิธีการที่:

  • ควรส่งสัญญาณข้อผิดพลาดเป็นวง
  • ควรใช้ข้อยกเว้นสำหรับสถานการณ์ที่ยอดเยี่ยมจริงๆ

หลังค่อนข้างชัดเจนในที่ (1) พวกเขาตั้งชื่อข้อยกเว้นความตื่นตระหนกและ (2) ไม่มีลำดับชั้นของประเภท / ประโยคที่ซับซ้อนที่นี่ ภาษาไม่ได้มีสิ่งอำนวยความสะดวกในการตรวจสอบเนื้อหาของ "ความตื่นตระหนก": ไม่มีลำดับชั้นชนิดไม่มีเนื้อหาที่ผู้ใช้กำหนดเองเพียง "โอ๊ะโอสิ่งต่าง ๆ ที่ผิดไปมากไม่มีการกู้คืนที่เป็นไปได้"

สิ่งนี้กระตุ้นให้ผู้ใช้ใช้การจัดการข้อผิดพลาดที่เหมาะสมได้อย่างมีประสิทธิภาพในขณะที่ยังเหลือวิธีง่ายๆในการประกันตัวในสถานการณ์พิเศษ (เช่น: "เดี๋ยวก่อนฉันยังไม่ได้ใช้งาน!")

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

... วิธีสนิมอย่างไรก็ตามส่วนใหญ่มีศูนย์กลางอยู่สองชนิด:

  • Optionซึ่งมีลักษณะคล้ายกับstd::optional,
  • Resultซึ่งเป็นตัวแปรสองตัวเลือกที่เป็นไปได้: Ok และ Err

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


ภาษา FP ก่อให้เกิดข้อผิดพลาดในโครงสร้างซึ่งสามารถแบ่งออกเป็นสามชั้น: - Functor - การใช้งาน / ทางเลือก - Monads / Alternative

ลองดูที่ประเภทของ Haskell Functor:

class Functor m where
  fmap :: (a -> b) -> m a -> m b

ประการแรกประเภทของ typeclasses ค่อนข้างคล้ายกัน แต่ไม่เท่ากับส่วนต่อประสาน ลายเซ็นฟังก์ชั่นของ Haskell ดูน่ากลัวในลุคแรก แต่เรามาถอดรหัสพวกมันกัน ฟังก์ชั่นใช้เวลาฟังก์ชั่นเป็นพารามิเตอร์แรกซึ่งค่อนข้างคล้ายกับfmap สิ่งต่อไปที่เป็นstd::function<a,b> m aคุณสามารถจินตนาการmเป็นสิ่งที่ชอบstd::vectorและเป็นสิ่งที่ชอบm a std::vector<a>แต่ความแตกต่างก็คือว่าไม่ได้บอกว่ามันจะต้องมีอย่างชัดเจนm a std:vectorดังนั้นมันอาจเป็นstd::optionเช่นกัน โดยบอกภาษาว่าเรามีอินสแตนซ์สำหรับFunctorประเภทของประเภทที่เฉพาะเจาะจงเช่นstd::vectorหรือstd::optionเราสามารถใช้ฟังก์ชั่นfmapสำหรับประเภทนั้น เช่นเดียวกับที่จะต้องทำเพื่อ typeclasses Applicative, AlternativeและMonadซึ่งช่วยให้คุณทำการคำนวณที่เป็นไปได้และไม่เป็นไปได้ Alternativeแนวคิดการกู้คืนข้อผิดพลาดในการดำเนินการ typeclass โดยที่คุณสามารถพูดอะไรเช่นa <|> bความหมายมันเป็นทั้งคำหรือระยะa bหากการคำนวณทั้งสองไม่ประสบความสำเร็จก็ยังคงเป็นข้อผิดพลาด

ลองดูที่Maybeประเภทของ Haskell

data Maybe a
  = Nothing
  | Just a

วิธีการนี้ว่าที่คุณคาดหวังว่าMaybe aคุณจะได้รับอย่างใดอย่างหนึ่งหรือNothing Just aเมื่อมองfmapจากด้านบนการใช้งานอาจมีลักษณะเช่นนี้

fmap f m = case m of
  Nothing -> Nothing
  Just a -> Just (f a)

case ... ofแสดงออกเรียกว่าจับคู่รูปแบบและมีลักษณะคล้ายกับสิ่งที่เป็นที่รู้จักกันในโลก OOP visitor patternเป็น ลองนึกภาพเส้นcase m ofเป็นm.apply(...)และจุดที่มีการเริ่มของชั้นการใช้ฟังก์ชั่นการจัดส่งที่ บรรทัดด้านล่างcase ... ofนิพจน์เป็นฟังก์ชันการแจกจ่ายที่เกี่ยวข้องซึ่งนำฟิลด์ของคลาสโดยตรงในขอบเขตตามชื่อ ในNothingสาขาที่เราสร้างNothingและในJust aสาขาที่เราตั้งชื่อค่าของเราเท่านั้นaและสร้างอีกJust ...กับการเปลี่ยนแปลงฟังก์ชั่นที่ใช้กับf aอ่านมันเป็น: new Just(f(a)).

ตอนนี้สามารถจัดการกับการคำนวณที่ผิดพลาดในขณะที่สรุปข้อผิดพลาดที่เกิดขึ้นจริงการตรวจสอบออกไป มีการใช้งานสำหรับอินเทอร์เฟซอื่นซึ่งทำให้การคำนวณแบบนี้มีประสิทธิภาพมาก ที่จริงแล้วMaybeเป็นแรงบันดาลใจให้กับ Rust's Option-Type


ฉันอยากจะแนะนำให้คุณปรับปรุงSuccessชั้นเรียนของคุณไปทางอื่นResultแทน Alexandrescu จริงที่นำเสนอบางสิ่งบางอย่างมันใกล้ชิดเรียกว่าexpected<T>ซึ่งข้อเสนอมาตรฐานที่ถูกสร้างขึ้น

ฉันจะใช้ชื่อ Rust และ API เพียงเพราะ ... เป็นเอกสารและใช้งานได้ แน่นอน Rust มี?โอเปอเรเตอร์ต่อท้ายที่ดีซึ่งจะทำให้โค้ดมีความหวานมากขึ้น ใน C ++ เราจะใช้TRYแมโครและนิพจน์คำสั่งของ GCC เพื่อเลียนแบบ

template <typename E>
struct Error {
    Error(E e): error(std::move(e)) {}

    E error;
};

template <typename E>
Error<E> error(E e) { return Error<E>(std::move(e)); }

template <typename T, typename E>
struct [[nodiscard]] Result {
    template <typename U>
    Result(U u): ok(true), data(std::move(u)), error() {}

    template <typename F>
    Result(Error<F> f): ok(false), data(), error(std::move(f.error)) {}

    template <typename U, typename F>
    Result(Result<U, F> other):
        ok(other.ok), data(std::move(other.data)),  error(std::move(other.error)) {}

    bool ok = false;
    T data;
    E error;
};

#define TRY(Expr_) \
    ({ auto result = (Expr_); \
       if (!result.ok) { return result; } \
       std::move(result.data); })

หมายเหตุ: นี่Resultเป็นตัวยึดตำแหน่ง unionการดำเนินการที่เหมาะสมจะใช้ห่อหุ้มและ อย่างไรก็ตามก็เพียงพอแล้วที่จะรับประเด็นนี้

ซึ่งทำให้ฉันสามารถเขียน ( ดูการกระทำ ):

Result<double, std::string> sqrt(double x) {
    if (x < 0) {
        return error("sqrt does not accept negative numbers");
    }
    return x;
}

Result<double, std::string> double_sqrt(double x) {
    auto y = TRY(sqrt(x));
    return sqrt(y);
}

ซึ่งฉันคิดว่าเรียบร้อยจริงๆ:

  • ไม่เหมือนกับการใช้รหัสข้อผิดพลาด (หรือSuccessคลาสของคุณ) การลืมตรวจสอบข้อผิดพลาดจะส่งผลให้เกิดข้อผิดพลาดรันไทม์1แทนที่จะเป็นพฤติกรรมแบบสุ่ม
  • แตกต่างจากการใช้ข้อยกเว้นมันชัดเจนที่ไซต์การโทรซึ่งฟังก์ชั่นสามารถล้มเหลวดังนั้นจึงไม่แปลกใจ
  • ด้วยมาตรฐาน C ++ - 2X เราอาจได้conceptsมาตรฐาน สิ่งนี้จะทำให้การเขียนโปรแกรมแบบนี้เป็นที่น่าพึงพอใจมากขึ้นเนื่องจากเราสามารถเลือกประเภทของข้อผิดพลาดได้ เช่นกับการใช้งานstd::vectorผลลัพธ์เราสามารถคำนวณวิธีแก้ปัญหาที่เป็นไปได้ทั้งหมดในครั้งเดียว หรือเราสามารถเลือกที่จะปรับปรุงการจัดการข้อผิดพลาดตามที่คุณเสนอ

1 ด้วยการResultใช้งานที่ถูกห่อหุ้มอย่างถูกต้อง;)


หมายเหตุ: ไม่เหมือนข้อยกเว้นน้ำหนักเบานี้Resultไม่มี backtraces ซึ่งทำให้การบันทึกมีประสิทธิภาพน้อยลง คุณอาจพบว่ามีประโยชน์อย่างน้อยบันทึกไฟล์ / หมายเลขบรรทัดที่สร้างข้อความแสดงข้อผิดพลาดและโดยทั่วไปจะเขียนข้อความแสดงข้อผิดพลาด สิ่งนี้สามารถทบต้นได้โดยการจับไฟล์ / บรรทัดทุกครั้งที่TRYมีการใช้แมโครโดยพื้นฐานแล้วการสร้าง backtrace ด้วยตนเองหรือการใช้รหัสเฉพาะแพลตฟอร์มและไลบรารีเช่นlibbacktraceเพื่อแสดงรายการสัญลักษณ์ใน callstack


มีข้อแม้ใหญ่ ๆ อยู่หนึ่งข้อว่าไลบรารี C ++ ที่มีอยู่และแม้กระทั่งstdจะขึ้นอยู่กับข้อยกเว้น มันจะเป็นการต่อสู้ที่ยากเย็นแสนเข็ญที่จะใช้สไตล์นี้เนื่องจาก API ของห้องสมุดของบุคคลที่สามจะต้องห่อด้วยอะแดปเตอร์ ...


3
มาโครนั้นดูผิดไปมาก ฉันจะสมมติว่า({...})เป็นนามสกุล gcc แต่ถึงอย่างนั้นมันไม่ควรจะเป็นif (!result.ok) return result;? เงื่อนไขของคุณปรากฏย้อนหลังและคุณทำสำเนาข้อผิดพลาดที่ไม่จำเป็น
Mooing Duck

@MooingDuck คำตอบที่อธิบายว่า({...})เป็น GCC ของการแสดงออกงบ
jamesdlin

1
@ bash0r เอกสารใหม่ล่าสุดนั้นดีกว่ามาก: scala-lang.org/api/2.12.2/scala/util/Try.htmlมันเป็นen.wikipedia.org/wiki/Tagged_union - scala-lang.org/api/2.12 2 / scala / util / Either.html
Reactormonk

1
ฉันขอแนะนำให้ใช้std::variantเพื่อดำเนินการResultถ้าคุณใช้ C ++ 17 นอกจากนี้เพื่อรับคำเตือนหากคุณไม่สนใจข้อผิดพลาดให้ใช้[[nodiscard]]
Justin

2
@ จัสติน: ไม่ว่าจะใช้std::variantหรือไม่เป็นเรื่องของรสนิยมที่ได้รับการแลกเปลี่ยนรอบการจัดการข้อยกเว้น [[nodiscard]]ย่อมเป็นชัยชนะที่บริสุทธิ์
Matthieu M.

46

อ้างสิทธิ์: กลไกการยกเว้นเป็น semantic ภาษาสำหรับการจัดการข้อผิดพลาด

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

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

ลองพิจารณา: ฉันพยายามสร้างไฟล์ อุปกรณ์เก็บข้อมูลเต็ม

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

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


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

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

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


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

ฉันไม่แน่ใจว่ารูปแบบ monad และการประเมินผลที่ขี้เกียจแปลได้ดีในภาษา C ++


1
ขอบคุณคำตอบของคุณมันเพิ่มความสว่างให้กับหัวข้อ ฉันเดาว่าผู้ใช้จะไม่เห็นด้วยกับand allowing my program to fail gracefully, and be re-runเมื่อเขาเพิ่งสูญเสียงาน 2 ชั่วโมง:
Adrian Maire

14
โซลูชันของคุณหมายความว่าทุกที่ที่คุณอาจสร้างไฟล์คุณต้องแจ้งให้ผู้ใช้แก้ไขสถานการณ์และลองอีกครั้ง จากนั้นทุกคนอื่น ๆสิ่งที่อาจผิดไปคุณยังจำเป็นต้องแก้ไขอย่างใดในประเทศ ด้วยข้อยกเว้นคุณเพิ่งstd::exceptionเข้าสู่ระดับที่สูงขึ้นของการดำเนินการทางตรรกะบอกผู้ใช้"X ล้มเหลวเนื่องจาก ex.what ()"และเสนอให้ลองดำเนินการทั้งหมดอีกครั้งเมื่อพร้อม
ไร้ประโยชน์

13
@AdrianMaire: "การอนุญาตให้ล้มเหลวอย่างสง่างามและถูกเรียกใช้ซ้ำ" สามารถใช้งานได้เช่นshowing the Save dialog again along with an error message and allowing the user to specify an alternative location to tryกัน นั่นคือการจัดการปัญหาที่ไม่สามารถทำได้จากรหัสที่ตรวจพบได้ว่าที่เก็บข้อมูลแรกเต็ม
Bart van Ingen Schenau

3
@ การประเมินผลที่ขี้เกียจไม่มีประโยชน์เกี่ยวข้องกับการใช้ Error monad ซึ่งพิสูจน์ได้จากภาษาการประเมินผลที่เข้มงวดเช่น Rust, OCaml และ F # ที่ทุกคนใช้มันอย่างหนัก
8bittree

1
@ IMO ที่ไม่มีประโยชน์สำหรับซอฟต์แวร์ที่มีคุณภาพจะทำให้รู้สึกว่า“ ทุกที่ที่คุณอาจสร้างไฟล์คุณต้องแจ้งให้ผู้ใช้แก้ไขสถานการณ์และลองใหม่อีกครั้ง” โปรแกรมเมอร์ช่วงแรกมักจะมีความยาวที่น่าทึ่งต่อการกู้คืนข้อผิดพลาดอย่างน้อยโปรแกรม TeX ของ Knuth เต็มไปด้วยพวกเขา และด้วยกรอบการทำงาน "การเขียนโปรแกรมรู้หนังสือ" ของเขาทำให้เขาพบวิธีในการจัดการข้อผิดพลาดในส่วนอื่นเพื่อให้รหัสยังคงสามารถอ่านได้และการกู้คืนข้อผิดพลาดจะถูกเขียนด้วยความระมัดระวังมากขึ้น (เพราะเมื่อคุณเขียนส่วน นั่นคือประเด็นและโปรแกรมเมอร์มักจะทำงานได้ดีขึ้น)
ShreevatsaR

15

ฉันอยากจะเข้าใจความหมายของการใช้กระบวนทัศน์ในโครงการให้ดีขึ้น

  • หลักฐานของปัญหาถูกต้องหรือไม่? หรือฉันพลาดบางสิ่งที่เกี่ยวข้อง?
  • การแก้ปัญหาเป็นแนวคิดทางสถาปัตยกรรมที่ดีหรือไม่? หรือราคาสูงเกินไป?

แนวทางของคุณนำปัญหาใหญ่มาสู่ซอร์สโค้ดของคุณ:

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

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

แต่เป็นเวลาหลายปีในฐานะนักพัฒนาทำให้ฉันเห็นปัญหาจากแนวทางที่แตกต่าง:

แนวทางแก้ไขปัญหาเหล่านี้ควรได้รับการติดต่อในระดับโอกาสทางเทคนิคหรือระดับทีม:

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

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

ที่อยู่ด้วยการตั้งค่าการทดสอบอัตโนมัติแยกข้อกำหนดของการทดสอบหน่วยและการใช้งาน (ให้บุคคลสองคนทำสิ่งนี้)

โปรแกรมเมอร์มีแนวโน้มที่จะไม่อ่านเอกสารอย่างละเอียด [... ] นอกจากนี้แม้ว่าพวกเขารู้ว่าพวกเขารู้ว่าพวกเขาไม่ได้จัดการพวกเขา

คุณจะไม่พูดเรื่องนี้ด้วยการเขียนรหัสเพิ่มเติม ฉันคิดว่าทางออกที่ดีที่สุดของคุณคือการตรวจสอบโค้ดอย่างพิถีพิถัน

โปรแกรมเมอร์มักจะไม่ได้รับข้อยกเว้นเร็วพอและเมื่อพวกเขาทำมันส่วนใหญ่จะเข้าสู่ระบบและโยนต่อไป (อ้างถึงจุดแรก)

การจัดการข้อผิดพลาดที่เหมาะสมนั้นยาก แต่น่าเบื่อน้อยกว่ายกเว้นเมื่อเทียบกับค่าส่งคืน (ไม่ว่าจะถูกส่งคืนหรือส่งผ่านเป็นอาร์กิวเมนต์ i / o)

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

ในการจัดการปัญหานี้จำเป็นต้องจัดสรรความสนใจมากขึ้นเพื่อระบุและทำงานในเงื่อนไขข้อผิดพลาด (การทดสอบเพิ่มเติมการทดสอบหน่วย / การรวมเข้าด้วยกัน ฯลฯ )


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

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

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

1
@AdrianMaire - ฉันคิดว่ามันยากกว่ามากที่จะลืมข้อยกเว้นว่าวิธีการของคุณที่จะลืมคำสั่ง if ... นอกจากนี้ - ประโยชน์หลักของข้อยกเว้นคือชั้นที่จัดการกับพวกเขา คุณอาจต้องการให้ฟองสบู่เกิดข้อผิดพลาดเพิ่มเติมเพื่อแสดงข้อความข้อผิดพลาดระดับแอปพลิเคชัน แต่จัดการกับสถานการณ์ที่คุณทราบในระดับต่ำกว่า หากคุณกำลังใช้ห้องสมุดของบุคคลที่สามหรือรหัสนักพัฒนาอื่น ๆ นี้เป็นจริงทางเลือกเท่านั้น ...
Milney

5
@ เอเดรียไม่มีข้อผิดพลาดคุณดูเหมือนจะผิดในสิ่งที่ฉันเขียนหรือพลาดช่วงครึ่งหลังของมัน จุดทั้งหมดของฉันไม่ใช่ว่าข้อยกเว้นจะเกิดขึ้นระหว่างการทดสอบ / การพัฒนาและผู้พัฒนาจะตระหนักว่าพวกเขาจำเป็นต้องจัดการพวกเขา ประเด็นก็คือผลที่ตามมาของข้อยกเว้นที่ไม่สามารถจัดการได้อย่างสมบูรณ์ในการผลิตนั้นจะดีกว่าผลที่ตามมาของรหัสข้อผิดพลาดที่ไม่ได้ตรวจสอบ หากคุณพลาดรหัสข้อผิดพลาดคุณจะได้รับและใช้ผลลัพธ์ที่ผิดต่อไป หากคุณพลาดข้อยกเว้นปัญหาการสมัครและไม่ได้ทำงานต่อไปคุณจะได้รับผลไม่ได้ผลที่ไม่ถูกต้อง (ต่อ)
Mr.Mindor
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.