... oder ist es nur eine Übung?
Ich frage dies aufgrund eines Streits mit meinem Professor: Ich habe die Anerkennung dafür verloren, dass ich eine Funktion rekursiv aufgerufen habe, weil wir die Rekursion im Unterricht nicht behandelt haben, und mein Argument ist, dass wir sie implizit durch Lernen return
und Methoden gelernt haben .
Ich frage hier, weil ich vermute, dass jemand eine endgültige Antwort hat.
Was ist beispielsweise der Unterschied zwischen den folgenden beiden Methoden:
public static void a() {
return a();
}
public static void b() {
return a();
}
Gibt es außer " a
für immer fortfahren" (im eigentlichen Programm wird es korrekt verwendet, um einen Benutzer erneut aufzufordern, wenn eine ungültige Eingabe erfolgt) einen grundlegenden Unterschied zwischen a
und b
? Wie werden sie für einen nicht optimierten Compiler unterschiedlich behandelt?
Letztlich kommt es durch das Lernen zu, ob nach unten return a()
aus , b
dass wir dafür auch gelernt return a()
aus a
. Haben wir?
a() { do { good = prompt(); } while (!good); }
.Antworten:
Um Ihre spezielle Frage zu beantworten: Nein, unter dem Gesichtspunkt des Lernens einer Sprache ist Rekursion keine Funktion. Wenn Ihr Professor Sie wirklich angedockt hat, weil Sie eine "Funktion" verwendet haben, die er noch nicht unterrichtet hat, war das falsch.
Wenn Sie zwischen den Zeilen lesen, besteht eine Möglichkeit darin, dass Sie durch die Verwendung der Rekursion vermieden haben, jemals eine Funktion zu verwenden, die als Lernergebnis für seinen Kurs gedacht war. Zum Beispiel haben Sie möglicherweise überhaupt keine Iteration verwendet, oder Sie haben nur
for
Schleifen verwendet, anstatt beidefor
und zu verwendenwhile
. Es ist üblich, dass eine Aufgabe darauf abzielt, Ihre Fähigkeit zu testen, bestimmte Dinge zu tun, und wenn Sie dies vermeiden, kann Ihr Professor Ihnen die für diese Funktion vorgesehenen Noten einfach nicht gewähren. Wenn dies jedoch wirklich die Ursache für Ihre verlorenen Noten war, sollte der Professor dies als eigene Lernerfahrung betrachten. Wenn der Nachweis bestimmter Lernergebnisse eines der Kriterien für eine Aufgabe ist, sollte dies den Studenten klar erklärt werden .Trotzdem stimme ich den meisten anderen Kommentaren und Antworten zu, dass Iteration hier eine bessere Wahl ist als Rekursion. Es gibt mehrere Gründe, und obwohl andere Menschen sie bis zu einem gewissen Grad berührt haben, bin ich mir nicht sicher, ob sie den Gedanken hinter ihnen vollständig erklärt haben.
Stapelüberläufe
Das offensichtlichere ist, dass Sie das Risiko eines Stapelüberlauffehlers haben. Realistisch gesehen ist es sehr unwahrscheinlich, dass die von Ihnen geschriebene Methode tatsächlich zu einer führt, da ein Benutzer viele Male falsche Eingaben machen müsste, um tatsächlich einen Stapelüberlauf auszulösen.
Beachten Sie jedoch, dass sich nicht nur die Methode selbst, sondern auch andere Methoden, die höher oder niedriger in der Aufrufkette sind, auf dem Stapel befinden. Aus diesem Grund ist es für jede Methode ziemlich unhöflich, den verfügbaren Stapelspeicherplatz gelegentlich zu verschlingen. Niemand möchte sich ständig um freien Speicherplatz sorgen müssen, wenn er Code schreibt, da das Risiko besteht, dass anderer Code unnötig viel davon verbraucht hat.
Dies ist Teil eines allgemeineren Prinzips im Software-Design, das als Abstraktion bezeichnet wird. Wenn Sie anrufen
DoThing()
, müssen Sie sich im Wesentlichen nur darum kümmern, dass die Sache erledigt ist. Sie sollten nicht über die Details der Implementierung zu kümmern, wie es gemacht wird . Die gierige Verwendung des Stapels verstößt jedoch gegen dieses Prinzip, da sich jedes Codebit Gedanken darüber machen muss, wie viel Stapel es sicher annehmen kann, dass es ihm durch Code an anderer Stelle in der Aufrufkette überlassen wurde.Lesbarkeit
Der andere Grund ist die Lesbarkeit. Das Ideal, das Code anstreben sollte, ist ein für Menschen lesbares Dokument, in dem jede Zeile einfach beschreibt, was es tut. Nehmen Sie diese beiden Ansätze:
gegen
Ja, diese beiden funktionieren, und ja, sie sind beide ziemlich leicht zu verstehen. Aber wie könnten die beiden Ansätze auf Englisch beschrieben werden? Ich denke, es wäre so etwas wie:
gegen
Vielleicht können Sie sich für letzteres etwas weniger klobige Formulierungen vorstellen, aber ich denke, Sie werden immer feststellen, dass die erste konzeptionell eine genauere Beschreibung dessen sein wird, was Sie tatsächlich versuchen zu tun. Dies bedeutet nicht, dass die Rekursion immer weniger lesbar ist. In Situationen, in denen es glänzt, wie z. B. beim Durchqueren von Bäumen, können Sie dieselbe Art der Nebeneinanderanalyse zwischen Rekursion und einem anderen Ansatz durchführen, und Sie werden mit ziemlicher Sicherheit feststellen, dass Rekursion Code liefert, der Zeile für Zeile klarer selbstbeschreibend ist.
Für sich genommen sind dies beide kleine Punkte. Es ist sehr unwahrscheinlich, dass dies jemals wirklich zu einem Stapelüberlauf führen würde, und der Gewinn an Lesbarkeit ist gering. Aber jedes Programm wird eine Sammlung vieler dieser kleinen Entscheidungen sein. Selbst wenn sie isoliert nicht viel ausmachen, ist es wichtig, die Prinzipien zu lernen, die dahinter stehen, um sie richtig zu machen.
quelle
Um die wörtliche Frage und nicht die Meta-Frage zu beantworten: Rekursion ist ein Merkmal in dem Sinne, dass nicht alle Compiler und / oder Sprachen dies unbedingt zulassen. In der Praxis wird es von allen (gewöhnlichen) modernen Compilern erwartet - und sicherlich von allen Java-Compilern! - aber es ist nicht allgemein wahr.
Als ein erfundenes Beispiel dafür, warum Rekursion möglicherweise nicht unterstützt wird, betrachten Sie einen Compiler, der die Rücksprungadresse für eine Funktion an einem statischen Ort speichert. Dies kann beispielsweise bei einem Compiler für einen Mikroprozessor ohne Stapel der Fall sein.
Für einen solchen Compiler, wenn Sie eine Funktion wie diese aufrufen
es ist implementiert als
und die Definition von a (),
wird implementiert als
Hoffentlich liegt das Problem auf, wenn Sie versuchen, einen
a()
solchen Compiler rekursiv aufzurufen . Der Compiler weiß nicht mehr, wie er vom äußeren Aufruf zurückkehren soll, da die Rücksprungadresse überschrieben wurde.Für den Compiler, den ich tatsächlich verwendet habe (Ende der 70er oder Anfang der 80er Jahre, glaube ich), ohne Unterstützung für die Rekursion, war das Problem etwas subtiler: Die Rücksprungadresse würde wie bei modernen Compilern auf dem Stapel gespeichert, lokale Variablen jedoch nicht 't. (Theoretisch sollte dies bedeuten, dass eine Rekursion für Funktionen ohne nicht statische lokale Variablen möglich war, aber ich kann mich nicht erinnern, ob der Compiler dies explizit unterstützt hat oder nicht. Möglicherweise wurden aus irgendeinem Grund implizite lokale Variablen benötigt.)
Mit Blick auf die Zukunft kann ich mir spezielle Szenarien vorstellen - vielleicht stark parallele Systeme -, in denen es vorteilhaft sein könnte, nicht für jeden Thread einen Stapel bereitzustellen, und in denen eine Rekursion daher nur zulässig ist, wenn der Compiler sie in eine Schleife umgestalten kann. (Natürlich waren die primitiven Compiler, die ich oben diskutiere, nicht in der Lage, komplizierte Aufgaben wie das Refactoring von Code auszuführen.)
quelle
Der Lehrer möchte wissen, ob Sie studiert haben oder nicht. Anscheinend haben Sie das Problem nicht so gelöst, wie er es Ihnen beigebracht hat ( der gute Weg ; Iteration), und denken daher, dass Sie es nicht getan haben. Ich bin alle für kreative Lösungen, aber in diesem Fall muss ich Ihrem Lehrer aus einem anderen Grund zustimmen:
Wenn der Benutzer zu oft ungültige Eingaben macht (dh indem Sie die Eingabetaste gedrückt halten), haben Sie eine Stapelüberlauf-Ausnahme und Ihre Lösung wird abstürzen. Darüber hinaus ist die iterative Lösung effizienter und einfacher zu warten. Ich denke, das ist der Grund, warum dein Lehrer dich hätte geben sollen.
quelle
Es ist schrecklich, Punkte abzuziehen, weil "wir die Rekursion im Unterricht nicht behandelt haben". Wenn Sie gelernt haben, wie man die Funktion A aufruft, die die Funktion B aufruft, die die Funktion C aufruft, die zu B zurückkehrt, die zu A zurückkehrt, die zum Anrufer zurückkehrt, und der Lehrer Ihnen nicht ausdrücklich gesagt hat, dass dies verschiedene Funktionen sein müssen (welche) Dies wäre beispielsweise in alten FORTRAN-Versionen der Fall. Es gibt keinen Grund, warum A, B und C nicht alle dieselbe Funktion haben können.
Auf der anderen Seite müssten wir den tatsächlichen Code sehen, um zu entscheiden, ob in Ihrem speziellen Fall die Verwendung von Rekursion wirklich das Richtige ist. Es gibt nicht viele Details, aber es klingt falsch.
quelle
In Bezug auf die spezifische Frage, die Sie gestellt haben, gibt es viele Gesichtspunkte, aber ich kann sagen, dass Rekursion vom Standpunkt des Lernens einer Sprache keine eigenständige Funktion ist. Wenn Ihr Professor Ihnen wirklich Punkte für die Verwendung eines "Features" angedockt hat, das er noch nicht gelehrt hat, war das falsch, aber wie gesagt, es gibt andere Gesichtspunkte, die hier berücksichtigt werden müssen, damit der Professor beim Abziehen von Punkten tatsächlich Recht hat.
Aus dem, was ich aus Ihrer Frage ableiten kann, ist die Verwendung einer rekursiven Funktion, um bei einem Eingabefehler nach einer Eingabe zu fragen, keine gute Praxis, da der Aufruf jeder rekursiven Funktion auf den Stapel übertragen wird. Da diese Rekursion durch Benutzereingaben gesteuert wird, ist es möglich, eine unendliche rekursive Funktion zu haben, was zu einem StackOverflow führt.
Es gibt keinen Unterschied zwischen diesen beiden Beispielen, die Sie in Ihrer Frage im Sinne ihrer Funktionsweise erwähnt haben (aber auf andere Weise unterscheiden). In beiden Fällen werden eine Absenderadresse und alle Methodeninformationen in den Stapel geladen. In einem Rekursionsfall ist die Rücksprungadresse einfach die Zeile direkt nach dem Methodenaufruf (natürlich ist es nicht genau das, was Sie im Code selbst sehen, sondern im Code, den der Compiler erstellt hat). In Java, C und Python ist die Rekursion im Vergleich zur Iteration (im Allgemeinen) ziemlich teuer, da ein neuer Stapelrahmen zugewiesen werden muss. Ganz zu schweigen davon, dass Sie eine Stapelüberlaufausnahme erhalten können, wenn die Eingabe nicht zu oft gültig ist.
Ich glaube, der Professor hat Punkte abgezogen, da Rekursion als eigenständiges Thema betrachtet wird und es unwahrscheinlich ist, dass jemand ohne Programmiererfahrung an Rekursion denkt. (Natürlich heißt das nicht, dass sie es nicht tun, aber es ist unwahrscheinlich).
IMHO, ich denke, der Professor hat Recht, indem er Ihnen die Punkte abzieht. Sie hätten den Validierungsteil leicht auf eine andere Methode übertragen und wie folgt verwenden können:
Wenn das, was Sie getan haben, tatsächlich auf diese Weise gelöst werden kann, war das, was Sie getan haben, eine schlechte Praxis und sollte vermieden werden.
quelle
hasWon(x, y, piece)
(um nur die betroffenen Zeilen und Spalten zu überprüfen).Vielleicht hat Ihr Professor es noch nicht gelehrt, aber es scheint, als wären Sie bereit, die Vor- und Nachteile der Rekursion zu lernen.
Der Hauptvorteil der Rekursion besteht darin, dass rekursive Algorithmen oft viel einfacher und schneller zu schreiben sind.
Der Hauptnachteil der Rekursion besteht darin, dass rekursive Algorithmen Stapelüberläufe verursachen können, da für jede Rekursionsstufe ein zusätzlicher Stapelrahmen zum Stapel hinzugefügt werden muss.
Bei Produktionscode, bei dem die Skalierung in der Produktion zu viel mehr Rekursionsstufen führen kann als bei den Komponententests des Programmierers, überwiegt der Nachteil normalerweise den Vorteil, und rekursiver Code wird häufig vermieden, wenn dies praktikabel ist.
quelle
In Bezug auf die spezifische Frage ist Rekursion ein Merkmal, ich neige dazu, ja zu sagen, aber nachdem ich die Frage neu interpretiert habe. Es gibt gängige Entwurfsoptionen für Sprachen und Compiler, die eine Rekursion ermöglichen, und es gibt Turing-vollständige Sprachen , die eine Rekursion überhaupt nicht zulassen . Mit anderen Worten, Rekursion ist eine Fähigkeit, die durch bestimmte Auswahlmöglichkeiten im Sprach- / Compiler-Design ermöglicht wird.
Die Unterstützung erstklassiger Funktionen ermöglicht eine Rekursion unter sehr geringen Annahmen. Ein Beispiel finden Sie unter Schreiben von Schleifen in Unlambda oder in diesem stumpfen Python-Ausdruck, der keine Selbstreferenzen, Schleifen oder Zuweisungen enthält:
Sprachen / Compiler, die eine späte Bindung verwenden oder Vorwärtsdeklarationen definieren , ermöglichen eine Rekursion. Während Python beispielsweise den folgenden Code zulässt, ist dies eine Entwurfsauswahl (späte Bindung) und keine Voraussetzung für ein Turing-Complete System. Gegenseitig rekursive Funktionen hängen häufig von der Unterstützung von Vorwärtsdeklarationen ab.
Statisch typisierte Sprachen , die rekursiv definierte Typen zulassen , tragen zur Aktivierung der Rekursion bei. Siehe diese Implementierung des Y-Kombinators in Go . Ohne rekursiv definierte Typen wäre es immer noch möglich, die Rekursion in Go zu verwenden, aber ich glaube, dass der Y-Kombinator spezifisch unmöglich wäre.
quelle
Nach dem, was ich aus Ihrer Frage ableiten kann, ist es keine gute Praxis, eine rekursive Funktion zu verwenden, um im Falle eines Eingabefehlers nach Eingaben zu fragen. Warum?
Weil jeder Aufruf rekursiver Funktionen auf den Stapel übertragen wird. Da diese Rekursion durch Benutzereingaben gesteuert wird, ist es möglich, eine unendliche rekursive Funktion zu haben, was zu einem StackOverflow führt :-P
Eine nicht rekursive Schleife zu haben, um dies zu tun, ist der richtige Weg.
quelle
while(true)
Aufrufen derselben Methode erzielt werden? Wenn ja, würde ich nicht sagen, dass dies einen Unterschied zwischen Rekursion unterstützt, gut zu wissen, wie es ist.while(true)
ist eine Endlosschleife. Wenn Sie keinebreak
Aussage haben, sehe ich keinen Sinn darin, es sei denn, Sie versuchen, Ihr Programm zum Absturz zu bringen lol. Mein Punkt ist, wenn Sie dieselbe Methode aufrufen (das ist Rekursion), erhalten Sie manchmal einen StackOverflowError , aber wenn Sie einewhile
oder-for
Schleife verwenden, wird dies nicht der Fall sein . Das Problem besteht bei einer regulären Schleife einfach nicht. Vielleicht habe ich dich missverstanden, aber meine Antwort an dich ist nein.Rekursion ist ein Programmierkonzept , ein Merkmal (wie Iteration) und eine Praxis . Wie Sie dem Link entnehmen können, gibt es einen großen Forschungsbereich, der sich diesem Thema widmet. Vielleicht müssen wir nicht so tief in das Thema eintauchen, um diese Punkte zu verstehen.
Rekursion als Feature
Im Klartext unterstützt Java dies implizit, da es einer Methode (die im Grunde eine spezielle Funktion ist) ermöglicht, "Wissen" über sich selbst und andere Methoden zu haben, aus denen die Klasse besteht, zu der sie gehört. Stellen Sie sich eine Sprache vor, in der dies nicht der Fall ist: Sie könnten den Hauptteil dieser Methode schreiben
a
, aber Sie könnten keinen Aufruf ana
sie aufnehmen. Die einzige Lösung wäre, Iteration zu verwenden, um das gleiche Ergebnis zu erhalten. In einer solchen Sprache müssten Sie zwischen Funktionen unterscheiden, die sich ihrer eigenen Existenz bewusst sind (indem Sie ein bestimmtes Syntax-Token verwenden), und solchen, die dies nicht tun! Tatsächlich macht eine ganze Gruppe von Sprachen diese Unterscheidung (siehe Lisp und Lambdas ), um sich rekursiv zu nennen (wiederum mit einer dedizierten Syntax). zum Beispiel die Familien ML ). Interessanterweise erlaubt Perl sogar anonyme Funktionen (sogenannte)keine Rekursion?
Für Sprachen, die nicht einmal die Möglichkeit einer Rekursion unterstützen, gibt es häufig eine andere Lösung in Form des Festkomma-Kombinators. Die Sprache muss jedoch weiterhin Funktionen als sogenannte erstklassige Objekte unterstützen (dh Objekte, die möglicherweise vorhanden sind) innerhalb der Sprache selbst manipuliert).
Rekursion als Praxis
Wenn diese Funktion in einer Sprache verfügbar ist, bedeutet dies nicht, dass sie idiomatisch ist. In Java 8 wurden Lambda-Ausdrücke aufgenommen, sodass es möglicherweise einfacher wird, einen funktionalen Ansatz für die Programmierung zu verwenden. Es gibt jedoch praktische Überlegungen:
Das Endergebnis
Glücklicherweise (oder genauer gesagt, um die Verwendung zu vereinfachen) lässt Java Methoden standardmäßig auf sich aufmerksam machen und unterstützt daher die Rekursion. Dies ist also kein wirklich praktisches Problem, aber es bleibt immer noch ein theoretisches, und das nehme ich an Ihr Lehrer wollte es speziell ansprechen. Außerdem könnte es angesichts der jüngsten Entwicklung der Sprache in Zukunft zu etwas Wichtigem werden.
quelle