การเรียกซ้ำเป็นคุณลักษณะในตัวของมันเองหรือไม่?


116

... หรือเป็นเพียงการปฏิบัติธรรม?

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

ฉันถามที่นี่เพราะฉันสงสัยว่ามีใครบางคนมีคำตอบที่ชัดเจน

ตัวอย่างเช่นความแตกต่างระหว่างสองวิธีต่อไปนี้คืออะไร:

public static void a() {
    return a();
    }

public static void b() {
    return a();
    }

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

ท้ายที่สุดมันขึ้นอยู่กับว่าโดยการเรียนรู้return a()จากbสิ่งนั้นเราเรียนรู้return a()จากสิ่งaนั้นด้วยหรือไม่ เรา?


24
การอภิปรายที่ยอดเยี่ยม ฉันสงสัยว่าคุณอธิบายแบบนี้กับศาสตราจารย์ของคุณ ถ้าคุณเป็นเช่นนั้นฉันคิดว่าเขาควรให้เครดิตที่คุณเสียไป
Michael Yaworski

57
การเรียกซ้ำไม่ได้เป็นแนวคิดเฉพาะสำหรับวิทยาศาสตร์คอมพิวเตอร์ ฟังก์ชัน Fibonacci ตัวดำเนินการแฟกทอเรียลและสิ่งอื่น ๆ อีกมากมายจากคณิตศาสตร์ (และอาจเป็นฟิลด์อื่น ๆ ) (หรืออย่างน้อยก็สามารถ) แสดงแบบวนซ้ำได้ ศาสตราจารย์ต้องการให้คุณลืมสิ่งเหล่านี้เช่นกันหรือไม่?
Theodoros Chatzigiannakis

34
ศาสตราจารย์ควรให้เครดิตพิเศษแก่เขาแทนเพื่อหาวิธีแก้ปัญหาที่สวยงามหรือพูดนอกกรอบ

11
งานมอบหมายคืออะไร? นี่เป็นปัญหาที่ฉันมักสงสัยเมื่อคุณส่งงานเขียนโปรแกรมสิ่งที่ถูกทำเครื่องหมายความสามารถในการแก้ปัญหาหรือความสามารถในการใช้สิ่งที่คุณได้เรียนรู้ สองคนนี้ไม่จำเป็นต้องเหมือนกัน
Leon

35
FWIW การแจ้งให้ป้อนข้อมูลจนกว่าจะถูกต้องไม่ใช่สถานที่ที่ดีในการใช้การเรียกซ้ำมันง่ายเกินไปที่จะล้นสแต็ก a() { do { good = prompt(); } while (!good); }สำหรับกรณีนี้โดยเฉพาะอย่างยิ่งมันจะดีกว่าที่จะใช้สิ่งที่ต้องการ
Kevin

คำตอบ:


113

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

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

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

กองซ้อนล้น

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

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

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

การอ่าน

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

private int getInput() {
    int input;
    do {
        input = promptForInput();
    } while (!inputIsValid(input))
    return input;
}

กับ

private int getInput() {
    int input = promptForInput();
    if(inputIsValid(input)) {
        return input;
    }
    return getInput();
}

ใช่ทั้งสองอย่างนี้ใช้งานได้และใช่ทั้งคู่เข้าใจง่าย แต่วิธีการทั้งสองจะอธิบายเป็นภาษาอังกฤษอย่างไร? ฉันคิดว่ามันจะเป็นดังนี้:

ฉันจะแจ้งให้ป้อนข้อมูลจนกว่าอินพุตจะถูกต้องแล้วจึงส่งคืน

กับ

ฉันจะแจ้งให้ป้อนข้อมูลจากนั้นหากอินพุตถูกต้องฉันจะส่งคืนมิฉะนั้นฉันจะได้รับอินพุตและส่งคืนผลลัพธ์ของสิ่งนั้นแทน

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

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


8
คุณช่วยเพิ่มคำยืนยันว่าการเรียกซ้ำไม่ใช่คุณลักษณะได้หรือไม่? ฉันได้โต้แย้งในคำตอบของฉันว่ามันเป็นเพราะคอมไพเลอร์บางตัวไม่จำเป็นต้องรองรับ
Harry Johnston

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

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

6
ฉันคิดว่าคุณซ้อนสำรับด้วยถ้อยคำของคุณ คุณได้อธิบายถึงเวอร์ชันที่ทำซ้ำในหน้าที่และในทางกลับกัน ฉันคิดว่าคำอธิบายทีละบรรทัดที่ยุติธรรมกว่าของทั้งสองอย่างน่าจะเป็น“ ฉันจะแจ้งให้ป้อนข้อมูล หากอินพุตไม่ถูกต้องฉันจะทำซ้ำพร้อมต์จนกว่าจะได้รับข้อมูลที่ถูกต้อง แล้วฉันจะคืนให้” เทียบกับ“ ฉันจะแจ้งให้ป้อนข้อมูล หากอินพุตถูกต้องฉันจะส่งคืน มิฉะนั้นฉันจะส่งคืนผลลัพธ์ของการทำมากกว่านี้” (ลูก ๆ ของฉันเข้าใจแนวคิดการทำงานของสิ่งที่ต้องทำเมื่อพวกเขาอยู่ในช่วงก่อนวัยเรียนดังนั้นฉันคิดว่ามันเป็นบทสรุปภาษาอังกฤษที่ถูกต้องตามกฎหมายของแนวคิดการเรียกซ้ำที่นี่)
pjs

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

48

เพื่อตอบคำถามตามตัวอักษรแทนที่จะเป็นคำถามเชิงอภิมาน: การเรียกซ้ำเป็นคุณลักษณะในแง่ที่ไม่ใช่คอมไพเลอร์และ / หรือภาษาทั้งหมดที่จำเป็นต้องอนุญาต ในทางปฏิบัติคาดว่าจะมีคอมไพเลอร์สมัยใหม่ (ธรรมดา) ทั้งหมด - และแน่นอนว่าคอมไพเลอร์ Java ทั้งหมด! - แต่มันไม่เป็นความจริงในระดับสากล

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

สำหรับคอมไพเลอร์ดังกล่าวเมื่อคุณเรียกใช้ฟังก์ชันเช่นนี้

a();

จะดำเนินการเป็น

move the address of label 1 to variable return_from_a
jump to label function_a
label 1

และคำจำกัดความของ a ()

function a()
{
   var1 = 5;
   return;
}

ดำเนินการเป็น

label function_a
move 5 to variable var1
jump to the address stored in variable return_from_a

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

สำหรับคอมไพเลอร์ที่ฉันใช้จริง (ฉันคิดว่าในช่วงปลายยุค 70 หรือต้นทศวรรษที่ 80) โดยไม่มีการสนับสนุนการเรียกซ้ำปัญหานั้นละเอียดอ่อนกว่านั้นเล็กน้อย: ที่อยู่ที่ส่งคืนจะถูกเก็บไว้ในสแต็กเช่นเดียวกับในคอมไพเลอร์สมัยใหม่ แต่ตัวแปรในพื้นที่ไม่ทำงาน 'T (ในทางทฤษฎีนี่ควรหมายความว่าการเรียกซ้ำเป็นไปได้สำหรับฟังก์ชันที่ไม่มีตัวแปรโลคัลที่ไม่คงที่ แต่ฉันจำไม่ได้ว่าคอมไพลเลอร์สนับสนุนสิ่งนั้นอย่างชัดเจนหรือไม่อาจต้องการตัวแปรโลคัลโดยนัยด้วยเหตุผลบางประการ)

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


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

7
"ตัวอย่างที่สร้างขึ้น" ของคุณไม่ใช่สิ่งที่สร้างขึ้นทั้งหมด: มาตรฐาน Fortran 77 ไม่อนุญาตให้ฟังก์ชันเรียกตัวเองแบบวนซ้ำซึ่งเป็นเหตุผลที่คุณอธิบาย (ผมเชื่อว่าอยู่เพื่อข้ามไปเมื่อฟังก์ชั่นได้ทำถูกเก็บไว้ในตอนท้ายของรหัสที่ฟังก์ชั่นของตัวเองหรือสิ่งที่เทียบเท่ากับการจัดเรียงนี้.) ดูที่นี่สำหรับบิตเกี่ยวกับว่า
alexis

5
ภาษา Shader หรือภาษา GPGPU (เช่น GLSL, Cg, OpenCL C) ไม่รองรับการเรียกซ้ำตามตัวอย่าง นอกจากนี้อาร์กิวเมนต์ "ไม่ใช่ทุกภาษาที่รองรับ" นั้นใช้ได้อย่างแน่นอน การเรียกซ้ำถือว่าเทียบเท่ากับสแต็ก (ไม่จำเป็นต้องเป็นสแต็ก แต่จำเป็นต้องมีวิธีการจัดเก็บที่อยู่ที่ส่งคืนและเฟรมฟังก์ชันอย่างใดอย่างหนึ่ง )
Damon

คอมไพเลอร์ Fortran ที่ฉันทำงานในช่วงต้นปี 1970 ไม่มี call stack รูทีนย่อยหรือฟังก์ชันแต่ละรายการมีพื้นที่หน่วยความจำแบบคงที่สำหรับที่อยู่ส่งคืนพารามิเตอร์และตัวแปรของตัวเอง
Patricia Shanahan

2
แม้แต่ Turbo Pascal บางเวอร์ชันก็ปิดการใช้งานการเรียกซ้ำตามค่าเริ่มต้นและคุณต้องตั้งค่าคำสั่งคอมไพเลอร์เพื่อเปิดใช้งาน
dan04

17

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


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

3
"จะปล่อยให้อันไหนอ่านง่ายกว่าตามความชอบส่วนตัว" ตกลง การเรียกซ้ำไม่ใช่คุณลักษณะของ Java เหล่านี้คือ
ไมค์

2
@Vality: กำจัดหางโทร? JVM บางตัวอาจทำได้ แต่โปรดทราบว่าจำเป็นต้องรักษา stack trace สำหรับข้อยกเว้น หากอนุญาตให้มีการกำจัด tail call การติดตามสแต็กที่สร้างขึ้นอย่างไร้เดียงสาอาจไม่ถูกต้องดังนั้น JVM บางส่วนจึงไม่ทำ TCE ด้วยเหตุผลนั้น
icktoofay

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

7
+1 ดูว่าใน Ubuntu เมื่อเร็ว ๆ นี้หน้าจอการเข้าสู่ระบบเสียเมื่อผู้ใช้กดปุ่ม Enter อย่างต่อเนื่องเกิดขึ้นกับ XBox
Sebastian

13

การหักคะแนนเพราะ "เราไม่ครอบคลุมการเรียกซ้ำในชั้นเรียน" นั้นแย่มาก หากคุณได้เรียนรู้วิธีการเรียกฟังก์ชัน A ซึ่งเรียกฟังก์ชัน B ซึ่งเรียกฟังก์ชัน C ซึ่งจะส่งกลับไปที่ B ซึ่งจะส่งกลับไปยัง A ซึ่งส่งกลับไปยังผู้เรียกและครูไม่ได้บอกคุณอย่างชัดเจนว่าสิ่งเหล่านี้จะต้องเป็นฟังก์ชันที่แตกต่างกัน (ซึ่ง จะเป็นเช่นนั้นใน FORTRAN เวอร์ชันเก่าเป็นต้น) ไม่มีเหตุผลที่ A, B และ C ไม่สามารถเป็นฟังก์ชันเดียวกันได้ทั้งหมด

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


10

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

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

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

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

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

public bool foo() 
{
  validInput = GetInput();
  while(!validInput)
  {
    MessageBox.Show("Wrong Input, please try again!");
    validInput = GetInput();
  }
  return hasWon(x, y, piece);
}

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


จุดประสงค์ของเมธอดคือการตรวจสอบความถูกต้องของอินพุตจากนั้นเรียกและส่งคืนผลลัพธ์ของวิธีอื่น (นั่นคือสาเหตุที่ส่งคืนตัวมันเอง) เพื่อให้เฉพาะเจาะจงจะตรวจสอบว่าการย้ายในเกม Tic-Tac-Toe นั้นถูกต้องหรือไม่จากนั้นจะกลับมาhasWon(x, y, piece)(เพื่อตรวจสอบเฉพาะแถวและคอลัมน์ที่ได้รับผลกระทบ)

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

1
Yonatan Nir: เมื่อไหร่ที่การเรียกซ้ำเป็นการฝึกที่ไม่ดี? บางที JVM อาจจะระเบิดเพราะ Hotspot VM ไม่สามารถปรับให้เหมาะสมได้เนื่องจากความปลอดภัยของรหัสไบต์และสิ่งต่างๆก็เป็นข้อโต้แย้งที่ดี รหัสของคุณแตกต่างจากที่ใช้วิธีการอื่นอย่างไร

1
การเรียกซ้ำไม่ใช่วิธีปฏิบัติที่ไม่ดีเสมอไป แต่หากสามารถหลีกเลี่ยงและรักษารหัสให้สะอาดและง่ายต่อการบำรุงรักษาก็ควรหลีกเลี่ยง ใน Java, C และ Python การเรียกซ้ำค่อนข้างแพงเมื่อเทียบกับการทำซ้ำ (โดยทั่วไป) เนื่องจากต้องมีการจัดสรรสแต็กเฟรมใหม่ ในคอมไพเลอร์ C บางตัวเราสามารถใช้แฟล็กคอมไพเลอร์เพื่อกำจัดค่าใช้จ่ายนี้ซึ่งจะเปลี่ยนการเรียกซ้ำบางประเภท (จริงๆแล้วการเรียกหางบางประเภท) เป็นการกระโดดแทนการเรียกฟังก์ชัน
Yonatan Nir

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

6

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

ข้อได้เปรียบหลักของการเรียกซ้ำคืออัลกอริทึมแบบเรียกซ้ำมักจะเขียนได้ง่ายและรวดเร็วกว่ามาก

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

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


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

1
หากสแต็กล้นเป็นปัญหาคุณควรใช้ภาษา / รันไทม์ที่รองรับการเพิ่มประสิทธิภาพการโทรหางเช่น. NET 4.0 หรือภาษาโปรแกรมที่ใช้งานได้
Sebastian

การเรียกซ้ำทั้งหมดไม่ใช่การเรียกหาง
Warren Dew

6

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

  • การสนับสนุนฟังก์ชันชั้นหนึ่งทำให้การเรียกซ้ำเป็นไปได้ภายใต้สมมติฐานที่น้อยมาก ดูตัวอย่างการเขียนลูปใน Unlambdaหรือนิพจน์ Python ที่ป้านนี้ไม่มีการอ้างอิงตนเองลูปหรือการกำหนด:

    >>> map((lambda x: lambda f: x(lambda g: f(lambda v: g(g)(v))))(
    ...   lambda c: c(c))(lambda R: lambda n: 1 if n < 2 else n * R(n - 1)),
    ...   xrange(10))
    [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]
    
  • ภาษา / คอมไพเลอร์ที่ใช้การโยงล่าช้าหรือที่กำหนดการประกาศไปข้างหน้าทำให้การเรียกซ้ำทำได้ ตัวอย่างเช่นในขณะหลามช่วยให้ด้านล่างรหัสที่เป็นทางเลือกการออกแบบ (ผูกพันดึก) ไม่จำเป็นสำหรับการเป็นทัวริงสมบูรณ์ของระบบ ฟังก์ชันเรียกซ้ำร่วมกันมักขึ้นอยู่กับการสนับสนุนสำหรับการประกาศไปข้างหน้า

    factorial = lambda n: 1 if n < 2 else n * factorial(n-1)
    
  • ภาษาที่พิมพ์แบบคงที่ซึ่งอนุญาตให้มีการกำหนดประเภทซ้ำมีส่วนช่วยในการเปิดใช้งานการเรียกซ้ำ ดูนี้การดำเนินงานของ Y Combinator ใน Go หากไม่มีประเภทที่กำหนดแบบวนซ้ำก็ยังสามารถใช้การเรียกซ้ำใน Go ได้ แต่ฉันเชื่อว่า Y combinator โดยเฉพาะจะเป็นไปไม่ได้


1
สิ่งนี้ทำให้หัวของฉันระเบิดโดยเฉพาะ Unlambda +1
John Powell

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

5

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

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

การมีลูปแบบไม่วนซ้ำเพื่อทำสิ่งนี้คือวิธีที่จะไป


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

4
@fay แต่ถ้าอินพุตไม่ถูกต้องหลายครั้งเกินไปคุณจะได้รับ StackOverflowError การวนซ้ำนั้นดูสง่างามกว่า แต่ในสายตาของฉันมักจะเป็นปัญหามากกว่าการวนซ้ำปกติ (เนื่องจากการซ้อนกัน)
Michael Yaworski

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

1
@fay while(true)เป็นวงวนที่ไม่มีที่สิ้นสุด เว้นแต่คุณจะมีbreakคำสั่งฉันไม่เห็นประเด็นในนั้นเว้นแต่คุณจะพยายามทำให้โปรแกรมของคุณพังฮ่า ๆ ประเด็นของฉันคือถ้าคุณเรียกใช้เมธอดเดียวกัน (นั่นคือการเรียกซ้ำ) บางครั้งมันจะทำให้คุณมีStackOverflowErrorแต่ถ้าคุณใช้ a whileหรือforวนซ้ำมันจะไม่ ปัญหาไม่ได้เกิดขึ้นกับการวนซ้ำปกติ บางทีฉันอาจเข้าใจคุณผิด แต่คำตอบของฉันคือไม่
Michael Yaworski

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

3

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

การเรียกซ้ำเป็นคุณลักษณะ

กล่าวอย่างชัดเจน Java สนับสนุนโดยปริยายเนื่องจากอนุญาตให้เมธอด (ซึ่งโดยพื้นฐานแล้วเป็นฟังก์ชันพิเศษ) มี "ความรู้" ในตัวมันเองและวิธีการอื่น ๆ ที่เขียนคลาสที่เป็นของมัน พิจารณาภาษาที่ไม่ใช่กรณีนี้: คุณจะสามารถเขียนเนื้อหาของวิธีการaนั้นได้ แต่คุณจะไม่สามารถรวมการเรียกเข้าไปaในนั้นได้ ทางออกเดียวคือใช้การวนซ้ำเพื่อให้ได้ผลลัพธ์เดียวกัน ในภาษาดังกล่าวคุณจะต้องสร้างความแตกต่างระหว่างฟังก์ชันที่ตระหนักถึงการมีอยู่ของตัวเอง (โดยใช้โทเค็นไวยากรณ์เฉพาะ) และผู้ที่ไม่ทำ! จริงๆแล้วกลุ่มภาษาทั้งหมดสร้างความแตกต่างนั้น (ดูLispและlambdas ) เพื่อเรียกตัวเองซ้ำ ๆ (อีกครั้งด้วยไวยากรณ์เฉพาะ)ตัวอย่างเช่นตระกูล ML ) ที่น่าสนใจ Perl ยังอนุญาตให้ใช้ฟังก์ชันที่ไม่ระบุชื่อ (เรียกว่า

ไม่มีการเรียกซ้ำ?

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

การเรียกซ้ำเป็นการฝึก

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

  • ไวยากรณ์ยังไม่เป็นมิตรกับการเรียกซ้ำมากนัก
  • คอมไพเลอร์อาจไม่สามารถตรวจพบการปฏิบัตินั้นและปรับให้เหมาะสมได้

บรรทัดล่างสุด

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

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