เหตุใดฟังก์ชันจึงควรมีจุดออกเพียงจุดเดียว [ปิด]


98

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

ฉันคิดว่าสิ่งนี้เกี่ยวข้องกับ CS แต่คำถามนี้ถูกยิงที่ cstheory stackexchange



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

1
@finnw ม็อดฟาสซิสต์ได้ลบสองคำถามสุดท้ายเพื่อให้แน่ใจว่าพวกเขาจะต้องได้รับคำตอบอีกครั้งและอีกครั้งและอีกครั้ง
Maarten Bodewes

แม้ว่าจะมีคำว่า "โต้แย้ง" ในคำถาม แต่ฉันไม่คิดว่านี่เป็นคำถามที่อิงตามความคิดเห็น มันค่อนข้างเกี่ยวข้องกับการออกแบบที่ดี ฯลฯ ฉันไม่เห็นเหตุผลที่จะปิด แต่ w / e
Ungeheuer

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

คำตอบ:


109

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

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

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

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

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


1
การฝังลึกอาจถูกทำให้สับสนในsinge exitกระบวนทัศน์โดยการใช้go toคำพูดเล็กน้อย นอกจากนี้เรายังได้รับโอกาสในการประมวลผลภายหลังบางอย่างภายใต้Errorฉลากภายในของฟังก์ชันซึ่งเป็นไปไม่ได้ที่จะมีหลายreturns
Ant_222

2
มักจะมีทางออกที่ดีที่หลีกเลี่ยงความจำเป็นในการไป ฉันชอบที่จะ 'return (Fail (... ))' มากและใส่รหัสการล้างข้อมูลที่ใช้ร่วมกันลงในวิธี Fail สิ่งนี้อาจต้องผ่านคนในท้องถิ่นสองสามคนเพื่อให้หน่วยความจำได้รับการปลดปล่อย ฯลฯ แต่ถ้าคุณไม่ได้ใช้งานโค้ดที่สำคัญซึ่งมักจะเป็นโซลูชันที่สะอาดกว่า goto IMO นอกจากนี้ยังอนุญาตให้ใช้หลายวิธีในการแชร์โค้ดล้างข้อมูลที่คล้ายกัน
Jason Williams

มีแนวทางที่ดีที่สุดตามเกณฑ์วัตถุประสงค์ แต่เราสามารถยอมรับได้ว่ามีโรงเรียนแห่งความคิด (ถูกต้องและไม่ถูกต้อง) และขึ้นอยู่กับความชอบส่วนบุคคล (ความชอบหรือต่อต้านแนวทางที่ถูกต้อง)
Rick O'Shea

39

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

  if (! อาร์กิวเมนต์) // ตรวจสอบว่าไม่ใช่ค่าว่าง
    ส่งคืน ERR_NULL_ARGUMENT;
  ... ประมวลผลอาร์กิวเมนต์ที่ไม่ใช่ค่าว่าง
  ถ้า (ตกลง)
    กลับ 0;
  อื่น
    ส่งคืน ERR_NOT_OK;

ชัดเจนกว่า:

  int return_value;
  if (อาร์กิวเมนต์) // ไม่ใช่ null
  {
    .. ประมวลผลอาร์กิวเมนต์ที่ไม่ใช่ค่าว่าง
    .. กำหนดผลลัพธ์ให้เหมาะสม
  }
  อื่น
    ผลลัพธ์ = ERR_NULL_ARGUMENT;
  ผลตอบแทน;

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


ตัวอย่างแรกของคุณการจัดการokตัวแปรดูเหมือนว่าวิธีการคืนสินค้าเดียวสำหรับฉัน ยิ่งไปกว่านั้นบล็อก if-else สามารถเขียนreturn ok ? 0 : ERR_NOT_OK;
ซ้ำได้ที่

2
ตัวอย่างแรกมีreturnจุดเริ่มต้นก่อนโค้ดทั้งหมดซึ่งทำทุกอย่าง สำหรับการใช้ตัว?:ดำเนินการการเขียนเป็นบรรทัดแยกต่างหากทำให้ IDE หลาย ๆ ตัวสามารถแนบเบรกพอยต์ดีบักกับสถานการณ์ที่ไม่ตกลงได้ง่ายขึ้น BTW ที่จริงที่สำคัญในการ "ออกจากจุดเดียว" โกหกในการทำความเข้าใจว่าสิ่งที่สำคัญคือการที่สำหรับการโทรเฉพาะแต่ละฟังก์ชั่นปกติออกจากจุดนี้เป็นจุดทันทีหลังจากที่โทร โปรแกรมเมอร์ในปัจจุบันยอมรับว่าเป็นเช่นนั้น แต่สิ่งต่างๆไม่ได้เป็นเช่นนั้นเสมอไป ในบางกรณีรหัสที่หายากอาจต้องได้รับโดยไม่มีพื้นที่สแต็กนำไปสู่ฟังก์ชั่น ...
supercat

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

1
ตัวอย่างที่ชัดเจนที่เรียกกันติดปากนั้นชัดเจนและอ่านยากกว่ามาก จุดออกเพียงจุดเดียวอ่านง่ายกว่าดูแลรักษาง่ายกว่าและแก้จุดบกพร่องได้ง่ายกว่าเสมอ
GuidoG

8
@GuidoG: รูปแบบใดรูปแบบหนึ่งอาจอ่านได้มากกว่าขึ้นอยู่กับสิ่งที่ปรากฏในส่วนที่ละไว้ ใช้ "return x;" ทำให้ชัดเจนว่าถ้าถึงคำสั่งค่าตอบแทนจะเป็น x ใช้ "result = x;" ใบไม้จะเปิดโอกาสที่สิ่งอื่นอาจเปลี่ยนแปลงผลลัพธ์ก่อนที่จะส่งคืน ซึ่งจะมีประโยชน์หากจำเป็นต้องเปลี่ยนผลลัพธ์ในความเป็นจริง แต่โปรแกรมเมอร์ที่ตรวจสอบโค้ดจะต้องตรวจสอบเพื่อดูว่าผลลัพธ์อาจเปลี่ยนแปลงไปอย่างไรแม้ว่าคำตอบจะเป็น "ไม่ได้" ก็ตาม
supercat

15

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


นั่นไม่ใช่ปัญหาจริงๆกับ RAII
BigSandwich

14

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

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

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

- 2 เซนต์

โปรดดูเอกสารนี้สำหรับรายละเอียดเพิ่มเติม: NISTIR 5459


9
multiple returns in a function requires a much deeper analysisเฉพาะในกรณีที่ฟังก์ชั่นมีขนาดใหญ่อยู่แล้ว (> 1 หน้าจอ) มิฉะนั้นจะทำให้การวิเคราะห์ง่ายขึ้น
dss539

2
ผลตอบแทนที่หลากหลายไม่เคยทำให้การวิเคราะห์ง่ายขึ้นเพียงแค่ความ
คิดเห็นเท่านั้น

1
ลิงค์ตาย (404)
fusi

1
@fusi - พบใน archive.org และอัปเดตลิงก์ที่นี่
sscheider

4

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

  1. รหัสทางออกเดียวควรจะอ่านและแก้ไขข้อบกพร่องได้ง่ายกว่า (ฉันยอมรับว่าฉันไม่ได้คิดถึงเหตุผลนี้มากนัก แต่ได้รับสิ่งที่ง่ายกว่าอย่างมากในการอ่านและแก้ไขข้อบกพร่องคือรหัสป้อนครั้งเดียว)
  2. ลิงก์รหัสออกครั้งเดียวและส่งคืนได้อย่างหมดจดยิ่งขึ้น

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

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

โชคดี.


4

ฉันเคยเป็นผู้สนับสนุนรูปแบบการออกเดี่ยว เหตุผลของฉันส่วนใหญ่มาจากความเจ็บปวด ...

การออกทางเดียวนั้นง่ายกว่าในการแก้ไขข้อบกพร่อง

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

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

วิธีการออกเดี่ยวทำได้ง่ายกว่าในการดีบักเกอร์และง่ายต่อการแยกออกจากกันโดยไม่ทำลายตรรกะ


0

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

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

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


-7

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

ฉันอยากจะแนะนำให้อ่านบทเกี่ยวกับฟังก์ชันในหนังสือ Clean Code ของ Robert C.

โดยพื้นฐานแล้วคุณควรพยายามเขียนฟังก์ชันด้วยโค้ด 4 บรรทัดหรือน้อยกว่า

บันทึกบางส่วนจากบล็อกของ Mike Long :

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

30
4 บรรทัด? คุณเขียนโค้ดอะไรที่ทำให้คุณเรียบง่ายขนาดนั้น? ฉันสงสัยจริงๆว่าเคอร์เนลหรือ git ของ Linux ทำเช่นนั้น
ชินโซ

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

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

10
หนึ่งในคำตอบที่หายากเกี่ยวกับ SO ที่ฉันหวังว่าฉันจะลงคะแนนหลาย ๆ ครั้ง
Steven Rands

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