คืนค่าเวทย์มนตร์โยนข้อยกเว้นหรือคืนเท็จเมื่อล้มเหลว?


83

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

ฉันคิดว่ามีวิธีแก้ปัญหาสามประการที่เป็นไปได้สำหรับสถานการณ์ที่ไม่เป็นพิเศษเช่นนี้เพื่อระบุความล้มเหลวใน C # 4:

  • คืนค่าเวทย์มนตร์ที่ไม่มีความหมายเป็นอย่างอื่น (เช่นnullและ-1);
  • โยนข้อยกเว้น (เช่นKeyNotFoundException);
  • ส่งคืนfalseและระบุค่าส่งคืนจริงในoutพารามิเตอร์ (เช่นDictionary<,>.TryGetValue)

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

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


ในไลบรารีคลาสพื้นฐาน. NET Framework ใช้วิธีการทั้งสาม:

  • คืนค่าเวทย์มนตร์ที่ไม่มีความหมายเป็นอย่างอื่น:
  • โยนข้อยกเว้น:
  • ส่งคืนfalseและระบุค่าส่งคืนจริงในoutพารามิเตอร์:

โปรดทราบว่าตามที่Hashtableถูกสร้างขึ้นในเวลาที่ไม่มี generics ใน C # จะใช้objectและสามารถกลับnullเป็นค่าเวทย์มนตร์ แต่ด้วย generics ข้อยกเว้นจะใช้ในต้นและมันไม่ได้มีDictionary<,> TryGetValueเห็นได้ชัดว่าการเปลี่ยนแปลงข้อมูลเชิงลึก

เห็นได้ชัดว่าItem- TryGetValueและParse- TryParseคู่คือมีเหตุผลดังนั้นฉันคิดว่าการขว้างปาข้อยกเว้นสำหรับความล้มเหลวที่ไม่ได้โดดเด่นอยู่ใน C # 4 ไม่ได้ทำ อย่างไรก็ตามTry*วิธีการไม่ได้มีอยู่เสมอแม้จะDictionary<,>.Itemมีอยู่


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

6
ไม่ใช่ว่าฉันคิดว่าคำถามของคุณกว้างเกินไปมันไม่ใช่คำถามที่สร้างสรรค์ ไม่มีคำตอบที่ยอมรับตามที่ได้รับคำตอบแล้ว
George Stocker

2
@GeorgeStocker หากคำตอบตรงไปตรงมาและชัดเจนแล้วฉันจะไม่ถามคำถาม ความจริงที่ว่าผู้คนสามารถโต้แย้งได้ว่าทำไมตัวเลือกที่ให้นั้นเป็นที่ต้องการมากกว่าจากมุมมองใด ๆ (เช่นประสิทธิภาพการอ่านการใช้งานการบำรุงรักษาแนวทางการออกแบบหรือความสม่ำเสมอ) ทำให้มันไม่เป็นที่ยอมรับ คำถามทั้งหมดสามารถตอบได้ค่อนข้างเป็นส่วนตัว เห็นได้ชัดว่าคุณคาดหวังว่าคำถามจะมีคำตอบที่คล้ายกันมากที่สุดเพื่อให้เป็นคำถามที่ดี
Daniel AA Pelsmaeker

3
@Virtlink: George เป็นผู้ดูแลที่ได้รับการเลือกตั้งจากชุมชนซึ่งอาสาสมัครใช้เวลาอย่างมากในการช่วยดูแล Stack Overflow เขาระบุว่าทำไมเขาจึงปิดคำถามและคำถามที่พบบ่อยสำรองไว้
Eric J.

15
คำถามอยู่ที่นี่ไม่ใช่เพราะมันเป็นเรื่องส่วนตัว แต่เพราะมันเป็นแนวคิด Rule of thumb: หากคุณกำลังนั่งอยู่หน้ารหัส IDE ของคุณให้ถามใน Stack Overflow หากคุณกำลังยืนอยู่หน้าบอร์ดระดมสมองให้ถามที่นี่กับโปรแกรมเมอร์
Robert Harvey

คำตอบ:


56

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

  1. ค่าเวทย์มนตร์เป็นตัวเลือกที่ดีเมื่อมีเงื่อนไข "จนถึง" เช่นStreamReader.Readหรือเมื่อมีค่าในการใช้งานที่ง่ายซึ่งจะไม่มีคำตอบที่ถูกต้อง (-1 สำหรับIndexOf)
  2. โยนข้อยกเว้นเมื่อความหมายของฟังก์ชั่นคือผู้โทรมั่นใจว่ามันจะทำงาน ในกรณีนี้คีย์ที่ไม่มีอยู่หรือรูปแบบดับเบิลไม่ดีเป็นพิเศษอย่างแท้จริง
  3. ใช้พารามิเตอร์ out และส่งคืนบูลหากซีแมนทิกส์ใช้โพรบหากการดำเนินการเป็นไปได้หรือไม่

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

NaNกลับโดยMath.Sqrtเป็นกรณีพิเศษ - มันเป็นไปตามมาตรฐานจุดลอย


10
ไม่เห็นด้วยกับหมายเลข 1 ค่าเวทย์มนตร์ไม่เคยเป็นตัวเลือกที่ดีเพราะพวกเขาต้องการ coder ถัดไปที่จะรู้ถึงความสำคัญของค่าเวทย์มนตร์ พวกเขายังทำร้ายการอ่าน ฉันสามารถคิดได้ว่าไม่มีอินสแตนซ์ใดที่การใช้ค่าเวทย์มนตร์ดีกว่ารูปแบบลอง
0b101010

1
แต่แล้วEither<TLeft, TRight>Monad ล่ะ?
sara

2
@ 0b101010: ใช้เวลาค้นหาวิธีที่ streamreader.read สามารถกลับมาได้อย่างปลอดภัย -1 ...
jmoreno

33

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

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

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

ตัวเลือกอื่นคือการส่งคืน "ผลลัพธ์" วัตถุพิเศษ แต่ฉันพบว่ามันน่าเบื่อมากขึ้น:

interface IMyResult
{
    bool Success { get; }
    // Only access this if Success is true
    MyOtherClass Result { get; }
    // Only access this if Success is false
    string ErrorMessage { get; }
}

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

ในความเป็นจริงถ้าคุณเป็นประเภทนั้นคุณสามารถใช้Tuple<>คลาสใหม่ที่เปิดตัวใน. NET 4 โดยส่วนตัวแล้วฉันไม่ชอบความจริงที่ว่าความหมายของแต่ละฟิลด์นั้นไม่ชัดเจนเพราะฉันไม่สามารถให้Item1และItem2ชื่อที่มีประโยชน์


3
โดยส่วนตัวแล้วฉันมักจะใช้คอนเทนเนอร์ผลลัพธ์เช่นIMyResultสาเหตุของคุณมันเป็นไปได้ที่จะสื่อสารผลลัพธ์ที่ซับซ้อนกว่าเพียงแค่trueหรือfalseหรือค่าผลลัพธ์ Try*()มีประโยชน์สำหรับสิ่งที่ง่ายเช่นการแปลงสตริงเป็นอิน
this.myself

1
โพสต์ที่ดี สำหรับฉันฉันชอบโครงสร้างสำนวนที่คุณระบุไว้ด้านบนซึ่งตรงข้ามกับการแยกระหว่างค่า "return" และพารามิเตอร์ "out" ทำให้มันเรียบร้อยและเป็นระเบียบ
Ocean Airdrop

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

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

2
นี่เป็นเพียงการนำเสนอข้อยกเว้นที่ตรวจสอบใหม่ใน C # ในวิธีที่น่าเบื่อกว่าการตรวจสอบข้อยกเว้นใน Java
ฤดูหนาว

17

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

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

คำตอบสั้น ๆ

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

คำตอบที่ยาว

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

  • ตามกฎของหัวแม่มือคาดว่าการโยนข้อยกเว้นจะช้ากว่าผลตอบแทนปกติ 200 เท่า (ในความเป็นจริงมีความแปรปรวนอย่างมากในเรื่องนั้น)

  • ในฐานะที่เป็นกฎง่ายๆอีกข้อการขว้างข้อยกเว้นมักจะอนุญาตให้โค้ดที่สะอาดกว่าเมื่อเปรียบเทียบกับค่าเวทย์มนตร์ที่หยาบที่สุดเพราะคุณไม่ได้อาศัยโปรแกรมเมอร์ที่แปลรหัสข้อผิดพลาดเป็นรหัสข้อผิดพลาดอื่น ๆ จุดที่มีบริบทเพียงพอสำหรับการจัดการในลักษณะที่สอดคล้องและเพียงพอ (กรณีพิเศษ: nullมีแนวโน้มค่าโดยสารที่ดีกว่าค่าเวทอื่น ๆ เนื่องจากมีแนวโน้มที่จะแปลตัวเองเป็นNullReferenceExceptionกรณีที่บางส่วน แต่ไม่ใช่ข้อบกพร่องทุกประเภทโดยปกติ แต่ไม่เสมอไปค่อนข้างใกล้กับแหล่งที่มาของข้อบกพร่อง )

ดังนั้นบทเรียนคืออะไร

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

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

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

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

คุณแสดงรายการตัวอย่างที่ยอดเยี่ยมDictionary<,>.Itemซึ่งพูดอย่างหลวม ๆ เปลี่ยนจากการส่งคืนnullค่าเป็นการขว้างปาKeyNotFoundExceptionระหว่าง. NET 1.1 และ. NET NET 2.0 (เฉพาะในกรณีที่คุณยินดีพิจารณาHashtable.Itemให้เป็นผู้เบิกทางที่ไม่ใช้งานทั่วไป) เหตุผลของ "การเปลี่ยนแปลง" นี้ไม่ได้อยู่ที่นี่ไม่มีความสนใจ การเพิ่มประสิทธิภาพการปฏิบัติงานของประเภทค่า (ไม่มีการชกมวยอีกต่อไป) ทำให้ค่าเวทมนตร์ดั้งเดิม ( null) ไม่ใช่ตัวเลือก; outพารามิเตอร์จะนำส่วนเล็ก ๆ ของค่าประสิทธิภาพกลับมา การพิจารณาประสิทธิภาพหลังนี้เล็กน้อยมากเมื่อเทียบกับค่าใช้จ่ายในการขว้าง a KeyNotFoundExceptionแต่การออกแบบข้อยกเว้นยังคงเหนือกว่าที่นี่ ทำไม?

  • พารามิเตอร์ ref / out ต้องเสียค่าใช้จ่ายทุกครั้งไม่ใช่เฉพาะในกรณี "ล้มเหลว"
  • ใครก็ตามที่สนใจสามารถโทรหาContainsก่อนที่จะโทรไปที่ตัวจัดทำดัชนีและรูปแบบนี้จะอ่านอย่างเป็นธรรมชาติ หากผู้พัฒนาต้องการที่จะลืมการโทรContainsไม่มีปัญหาเรื่องประสิทธิภาพที่สามารถคืบคลานเข้ามาได้ KeyNotFoundExceptionดังพอที่จะสังเกตและแก้ไขได้

ฉันคิดว่า 200x กำลังมองโลกในแง่ดีเกี่ยวกับความสมบูรณ์แบบของข้อยกเว้น ... ดูblogs.msdn.com/b/cbrumme/archive/2003/10/01/51524.aspx ส่วน "ประสิทธิภาพและแนวโน้ม" ก่อนการแสดงความคิดเห็น
gbjbaanb

@gbjbaanb - อาจเป็นไปได้ บทความนั้นใช้อัตราความล้มเหลว 1% เพื่อหารือเกี่ยวกับหัวข้อที่ไม่ใช่ ballpark ที่แตกต่างอย่างสิ้นเชิง ความคิดของฉันเองและการวัดที่จำได้ไม่ชัดอาจมาจากบริบทของ C ++ ที่นำไปใช้กับตาราง (ดูหัวข้อ 5.4.1.2 ของรายงานนี้โดยที่ปัญหาหนึ่งคือข้อยกเว้นแรกของชนิดนี้น่าจะเริ่มต้นด้วยความผิดพลาดหน้า แต่ตัดค่าใช้จ่ายครั้งเดียว) แต่ฉันจะทำและโพสต์การทดสอบกับ. NET 4 และอาจปรับแต่งค่า ballpark นี้ฉันแล้วความเครียดแปรปรวน
Jirka Hanika

ค่าใช้จ่ายสำหรับพารามิเตอร์ ref / out นั้นสูงหรือไม่? งั้นเหรอ และเรียกก่อนที่จะเรียกร้องให้ทำดัชนีอาจก่อให้เกิดสภาพการแข่งขันที่ไม่ได้มีที่จะนำเสนอด้วยContains TryGetValue
Daniel AA Pelsmaeker

@gbjbaanb - ทดลองเสร็จแล้ว ฉันขี้เกียจและใช้โมโนบน Linux ข้อยกเว้นให้ฉัน ~ 563000 พ่นใน 3 วินาที ผลตอบแทนให้ฉัน ~ 10900000 ผลตอบแทนใน 3 วินาที นั่นคือ 1:20 ไม่ใช่แม้แต่ 1: 200 ฉันยังคงแนะนำให้คิด 1: 100+ สำหรับรหัสที่สมจริงกว่านี้ (ตัวแปรที่แตกต่างออกไปตามที่ฉันคาดการณ์ไว้มีค่าใช้จ่ายเล็กน้อย - จริง ๆ แล้วฉันสงสัยว่ากระวนกระวายใจอาจทำให้การโทรไปได้อย่างสมบูรณ์ในตัวอย่างที่เรียบง่ายของฉันถ้าไม่มีข้อยกเว้นโยนโดยไม่คำนึงถึงลายเซ็น)
Jirka Hanika

@Virtlink - เห็นด้วยกับความปลอดภัยของเธรดโดยทั่วไป แต่เนื่องจากคุณอ้างถึง. NET 4 โดยเฉพาะให้ใช้ConcurrentDictionaryสำหรับการเข้าถึงแบบมัลติเธรดและDictionaryสำหรับการเข้าถึงเธรดเดี่ยวเพื่อประสิทธิภาพที่ดีที่สุด นั่นคือไม่ใช้ `` ประกอบด้วย 'ไม่ทำให้เธรดรหัสปลอดภัยกับDictionaryคลาสนี้โดยเฉพาะ
Jirka Hanika

10

อะไรคือสิ่งที่ดีที่สุดที่ต้องทำในสถานการณ์ที่ไม่เป็นพิเศษเช่นนี้เพื่อบ่งบอกถึงความล้มเหลวและเพราะอะไร

คุณไม่ควรอนุญาตให้เกิดความล้มเหลว

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

กลยุทธ์สำหรับการนี้ก็คือKeyNotFoundsscenario ในเกือบทุก codebase ฉันได้ทำงานตั้งแต่ 3.0 บางอย่างเช่นวิธีการขยายนี้มีอยู่:

public static class DictionaryExtensions {
    public static V GetValue<K, V>(this IDictionary<K, V> arg, K key, Func<K,V> ifNotFound) {
        if (!arg.ContainsKey(key)) {
            return ifNotFound(key);
        }

        return arg[key];
    }
}

ไม่มีโหมดความล้มเหลวจริงสำหรับสิ่งนี้ ConcurrentDictionaryมีสิ่งที่คล้ายกันGetOrAddในตัว

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

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

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

หากค่าส่งคืนและพารามิเตอร์ไม่สำคัญดังนั้นข้อเสนอแนะของสก็อตต์วิทล็อคเกี่ยวกับวัตถุผลลัพธ์เป็นที่ต้องการ (เช่นMatchคลาสของ Regex )


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

ฉันว่ามักจะเพราะคนใช้ เช่นเดียวกับที่คุณพูดพวกเขาเป็นเพียงค่าตอบแทนหลาย ๆ
Telastyn

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

@virtlink โดยใช้วิธีการตรวจสอบที่แตกต่างกัน คุณจำเป็นต้องใช้มันanywaysเพื่อให้ข้อความที่เหมาะสมในการ UI ก่อนที่จะส่ง
Telastyn

1
มีรูปแบบที่ถูกต้องตามกฎหมายสำหรับพารามิเตอร์ out และนั่นคือฟังก์ชันที่มีโอเวอร์โหลดที่ส่งคืนชนิดที่แตกต่างกัน การแก้ปัญหาการโอเวอร์โหลดจะไม่ทำงานสำหรับประเภทการคืนสินค้า แต่จะใช้เพื่อหาพารามิเตอร์
Robert Harvey

2

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

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


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

1
นั่นเป็นความต้องการของผู้เชี่ยวชาญมากกว่าไม่ใช่กรณีทั่วไป
DeadMG

2
ดังนั้นคุณพูด แต่คุณใช้คำว่า "เสมอ" :)
Robert Harvey

@DeadMG เห็นด้วยกับ RobertHarvey แม้ว่าฉันคิดว่าคำตอบนั้นได้รับการโหวตมากเกินไปหากมีการแก้ไขเพื่อให้สะท้อนถึง "เวลาส่วนใหญ่" แล้วชี้ให้เห็นข้อยกเว้น (ไม่มีจุดประสงค์ในการเล่นสำนวน) กับกรณีทั่วไปเมื่อใช้ประสิทธิภาพสูง โทรเพื่อพิจารณาตัวเลือกอื่น ๆ
Gerald Davis

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

2

ดังที่คนอื่น ๆ ได้กล่าวไว้ว่าค่าเวทย์มนตร์ (รวมถึงค่าส่งคืนบูลีน) นั้นไม่ใช่วิธีการแก้ปัญหาที่ดียกเว้นว่าเป็นเครื่องหมาย "end-of-range" เหตุผล: ความหมายไม่ชัดเจนแม้ว่าคุณจะตรวจสอบวิธีการของวัตถุ คุณต้องอ่านเอกสารทั้งหมดของวัตถุทั้งหมดลงไปที่ "โอ้ใช่ถ้ามันคืนค่า -42 นั่นหมายถึง bla bla bla"

วิธีนี้อาจใช้เพื่อเหตุผลในอดีตหรือเพื่อเหตุผลด้านประสิทธิภาพ แต่ควรหลีกเลี่ยงเป็นอย่างอื่น

กรณีนี้มีสองกรณีทั่วไป: การตรวจสอบหรือการยกเว้น

ที่นี่กฎง่ายๆคือว่าโปรแกรมไม่ควรตอบสนองต่อข้อยกเว้นยกเว้นที่จะจัดการเมื่อโปรแกรม / ไม่ตั้งใจ / ละเมิดเงื่อนไขบางอย่าง ควรใช้การตรวจสอบเพื่อให้แน่ใจว่าสิ่งนี้จะไม่เกิดขึ้น ดังนั้นข้อยกเว้นอย่างใดอย่างหนึ่งหมายความว่าการตรวจสอบที่เกี่ยวข้องไม่ได้ดำเนินการล่วงหน้าหรือมีบางสิ่งที่ไม่คาดคิดเกิดขึ้น

ตัวอย่าง:

คุณต้องการสร้างไฟล์จากเส้นทางที่กำหนด

คุณควรใช้วัตถุไฟล์เพื่อประเมินล่วงหน้าว่าพา ธ นี้ถูกกฎหมายสำหรับการสร้างหรือเขียนไฟล์หรือไม่

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

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


0

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

public sealed class Bag<TValue>
{
    public Bag(TValue value, bool hasValue = true)
    {
        HasValue = hasValue;
        Value = value;
    }

    public static Bag<TValue> Empty
    {
        get { return new Bag<TValue>(default(TValue), false); }
    }

    public bool HasValue { get; private set; }
    public TValue Value { get; private set; }
}

ดังนั้นฉันสามารถเขียนรหัสต่อไปนี้

    public static Bag<XElement> GetXElement(this XElement element, string elementName)
    {
        try
        {
            XElement result = element.Element(elementName);
            return result == null
                       ? Bag<XElement>.Empty
                       : new Bag<XElement>(result);
        }
        catch (Exception)
        {
            return Bag<XElement>.Empty;
        }
    }

ดูเหมือนว่าเป็นโมฆะ แต่ไม่เพียง แต่สำหรับประเภทค่า

ตัวอย่างอื่น

    public static Bag<string> TryParseString(this XElement element, string attributeName)
    {
        Bag<string> attributeResult = GetString(element, attributeName);
        if (attributeResult.HasValue)
        {
            return new Bag<string>(attributeResult.Value);
        }
        return Bag<string>.Empty;
    }

    private static Bag<string> GetString(XElement element, string attributeName)
    {
        try
        {
            string result = element.GetAttribute(attributeName).Value;
            return new Bag<string>(result);
        }
        catch (Exception)
        {
            return Bag<string>.Empty;
        }
    }

3
try catchจะทำให้เกิดความเสียหายอย่างมากกับประสิทธิภาพการทำงานของคุณหากคุณโทรหาGetXElement()และล้มเหลวหลายครั้ง
Robert Harvey

บางครั้งมันไม่สำคัญ ลองดูการโทรแบบกระเป๋า ขอบคุณสำหรับการสังเกต

กระเป๋า <T> clas ของคุณเกือบจะเหมือนกับ System.Nullable <T> หรือที่รู้จักว่า "nullable object"
aeroson

ใช่เกือบจะpublic struct Nullable<T> where T : structแตกต่างหลักในข้อ จำกัด btw รุ่นล่าสุดอยู่ที่นี่github.com/Nelibur/Nelibur/blob/master/Source/Nelibur.Sword/…
GSerjo

0

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

    public static Lazy<TValue> GetValue<TValue, TKey>(
        this IDictionary<TKey, TValue> dictionary,
        TKey key)
    {
        TValue retVal;
        if (dictionary.TryGetValue(key, out retVal))
        {
            var retValRef = retVal;
            var lazy = new Lazy<TValue>(() => retValRef);
            retVal = lazy.Value;
            return lazy;
        }

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