ฉันจะตอบจากมุมมองของ C ++ ฉันค่อนข้างมั่นใจว่าแนวคิดหลักทั้งหมดสามารถถ่ายโอนไปยัง C # ได้
ดูเหมือนว่าสไตล์ที่คุณชื่นชอบคือ "มักจะโยนข้อยกเว้น":
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
throw Exception("negative side lengths");
}
return x * y;
}
นี่อาจเป็นปัญหาสำหรับรหัส C ++ เนื่องจากการจัดการข้อยกเว้นมีจำนวนมาก - ทำให้กรณีความล้มเหลวทำงานช้าและทำให้กรณีความล้มเหลวจัดสรรหน่วยความจำ ความหนักเบาของ EH คือเหตุผลหนึ่งที่คุณได้ยินคนพูดว่า "อย่าใช้ข้อยกเว้นสำหรับการควบคุมการไหล"
ดังนั้นบางไลบรารี (เช่น<filesystem>
) ใช้สิ่งที่ C ++ เรียกว่า "dual API" หรือสิ่งที่ C # เรียกTry-Parse
รูปแบบ (ขอบคุณPeterสำหรับคำแนะนำ!)
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
throw Exception("negative side lengths");
}
return x * y;
}
bool TryCalculateArea(int x, int y, int& result) {
if (x < 0 || y < 0) {
return false;
}
result = x * y;
return true;
}
int a1 = CalculateArea(x, y);
int a2;
if (TryCalculateArea(x, y, a2)) {
// use a2
}
คุณสามารถดูปัญหาเกี่ยวกับ "dual APIs" ได้ทันที: การทำสำเนารหัสจำนวนมากไม่มีคำแนะนำสำหรับผู้ใช้ว่า API คือ "สิทธิ์" ที่จะใช้และผู้ใช้จะต้องเลือกอย่างหนักระหว่างข้อความแสดงข้อผิดพลาดที่เป็นประโยชน์ ( CalculateArea
) และความเร็ว ( TryCalculateArea
) เนื่องจากรุ่นที่เร็วกว่านั้นใช้"negative side lengths"
ข้อยกเว้นที่เป็นประโยชน์ของเราและทำให้มันแบนลงในไร้ประโยชน์false
- "มีบางอย่างผิดปกติอย่าถามฉันว่าอะไรหรือที่ไหน" (บาง APIs คู่ใช้ชนิดข้อผิดพลาดที่แสดงออกมากขึ้นเช่นint errno
หรือ C ++ 's std::error_code
แต่ที่ยังคงไม่ได้บอกคุณที่ข้อผิดพลาดที่เกิดขึ้น - เพียงแค่ว่ามันไม่ได้เกิดขึ้นที่ใดที่หนึ่ง.)
หากคุณไม่สามารถตัดสินใจได้ว่าโค้ดของคุณควรทำงานอย่างไรคุณสามารถตัดสินใจเลือกผู้โทรได้!
template<class F>
int CalculateArea(int x, int y, F errorCallback) {
if (x < 0 || y < 0) {
return errorCallback(x, y, "negative side lengths");
}
return x * y;
}
int a1 = CalculateArea(x, y, [](auto...) { return 0; });
int a2 = CalculateArea(x, y, [](int, int, auto msg) { throw Exception(msg); });
int a3 = CalculateArea(x, y, [](int, int, auto) { return x * y; });
นี่คือสิ่งที่ผู้ร่วมงานของคุณกำลังทำอยู่ ยกเว้นว่าเขาแยกตัว "ข้อผิดพลาดจัดการ" ออกเป็นตัวแปรทั่วโลก:
std::function<int(const char *)> g_errorCallback;
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
return g_errorCallback("negative side lengths");
}
return x * y;
}
g_errorCallback = [](auto) { return 0; };
int a1 = CalculateArea(x, y);
g_errorCallback = [](const char *msg) { throw Exception(msg); };
int a2 = CalculateArea(x, y);
การย้ายพารามิเตอร์สำคัญจากพารามิเตอร์ฟังก์ชันที่ชัดเจนไปสู่สถานะโกลบอลนั้นเป็นความคิดที่ไม่ดี ฉันไม่แนะนำ (ความจริงที่ว่าไม่ใช่รัฐทั่วโลกในกรณีของคุณ แต่เพียงรัฐสมาชิกทั่วทั้งอินสแตนซ์ช่วยลดความเลวร้ายเล็กน้อย แต่ไม่มาก)
นอกจากนี้เพื่อนร่วมงานของคุณกำลัง จำกัด จำนวนพฤติกรรมการจัดการข้อผิดพลาดที่อาจเกิดขึ้นโดยไม่จำเป็น แทนที่จะอนุญาตแลมบ์ดาใด ๆ ที่จัดการข้อผิดพลาดเขาได้ตัดสินใจเพียงสองข้อ:
bool g_errorViaException;
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
return g_errorViaException ? throw Exception("negative side lengths") : 0;
}
return x * y;
}
g_errorViaException = false;
int a1 = CalculateArea(x, y);
g_errorViaException = true;
int a2 = CalculateArea(x, y);
นี่อาจเป็น "จุดด่าง" จากกลยุทธ์ที่เป็นไปได้เหล่านี้ คุณได้นำความยืดหยุ่นทั้งหมดออกไปจากผู้ใช้ปลายทางโดยบังคับให้พวกเขาใช้หนึ่งในสองข้อเรียกร้องการจัดการข้อผิดพลาดของคุณ และคุณมีปัญหาทั้งหมดเกี่ยวกับสถานะโกลบอลที่แชร์ และคุณยังคงจ่ายเงินสำหรับสาขาที่มีเงื่อนไขนั้นทุกที่
ในที่สุดโซลูชันทั่วไปใน C ++ (หรือภาษาใด ๆ ที่มีการคอมไพล์แบบมีเงื่อนไข) จะบังคับให้ผู้ใช้ทำการตัดสินใจสำหรับโปรแกรมทั้งหมดของพวกเขาทั่วโลกในเวลารวบรวมเพื่อให้ codepath ที่ไม่ได้ถ่ายสามารถปรับให้เหมาะสมทั้งหมด:
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
#ifdef NEXCEPTIONS
return 0;
#else
throw Exception("negative side lengths");
#endif
}
return x * y;
}
// Now these two function calls *must* have the same behavior,
// which is a nice property for a program to have.
// Improves understandability.
//
int a1 = CalculateArea(x, y);
int a2 = CalculateArea(x, y);
ตัวอย่างของสิ่งที่ทำงานด้วยวิธีนี้เป็นassert
แมโครใน C และ C ++ NDEBUG
ซึ่งเงื่อนไขในพฤติกรรมของแมโครพรีโพรเซสเซอร์