Rekursive Funktionen iterativ ausführen

    • VB.NET
    • .NET (FX) 4.0

      Rekursive Funktionen iterativ ausführen

      Im Bezug auf diesen Thread: Iterative und rekursive Algorithmen

      Ich hab mir mal überlegt, wie man rekursive Funktionen so ausführen kann, dass auch bei beliebig großen Rekursionstiefen der Stapel nicht beliebig groß wird. Das Problem ist nämlich, dass bei rekursiven Funktionen relativ schnell ein StackOverflow auftreten kann (und damit ist nicht die Website gemeint ;) ).

      Weil das hier etwas länger geworden ist, als ich anfangs erwartet habe, habe ich es etwas gegliedert:
      • Problemstellung
      • Lösungsansatz
      • Implementierung
      • Fibonacci
      • Ackermann
      • Yield in C#
      • Hausaufgabe
      • Anhang


      Problemstellung

      Die wohl bekannteste, rekursive Funktion ist die Fibonacci-Funktion, welche so definiert ist (wir gehen davon aus, dass n immer >= 0 ist):

      VB.NET-Quellcode

      1. Function RecursiveFibonacci(n As Integer) As Integer
      2. Select Case n
      3. Case 0
      4. Return 0
      5. Case 1
      6. Return 1
      7. Case Else
      8. Return RecursiveFibonacci(n - 1) + RecursiveFibonacci(n - 2)
      9. End Select
      10. End Function

      Hier ist die Anzahl an benötigten StackFrames im Regelfall n. Also RecursiveFibonacci(30) = 832040 und es werden 30 StackFrames benötigt.

      ErfinderDesRades hat in diesem Post gezeigt, wie die Fibonacci-Funktion iterativ implementiert werden kann.

      Mit der Ackermann-Funktion geht das aber nicht mehr ganz so einfach. Die ist nämlich so definiert (die Argumente sind ebenfalls >= 0):

      VB.NET-Quellcode

      1. Function RecursiveAckermann(m As Integer, n As Integer) As Integer
      2. If m = 0 Then
      3. Return n + 1
      4. End If
      5. If n = 0 Then
      6. Return RecursiveAckermann(m - 1, 1)
      7. End If
      8. Return RecursiveAckermann(m - 1, RecursiveAckermann(m, n - 1))
      9. End Function

      Also wenn m und n nicht 0 sind muss man die Funktion rekursiv aufrufen, nur um ein Argument zu bekommen, mit dem man die Funktion nochmal rekursiv aufruft. Die Anzahl an StackFrames explodiert förmlich:

      Quellcode

      1. Für n = 0, 1, 2, 3, 4, 5, 6
      2. Bei m = 0: Anzahl an StackFrames = 1, 1, 1, 1, 1, 1, 1
      3. Bei m = 1: Anzahl an StackFrames = 2, 3, 4, 5, 6, 7, 8
      4. Bei m = 2: Anzahl an StackFrames = 4, 6, 8, 10, 12, 14, 16
      5. Bei m = 3: Anzahl an StackFrames = 7, 15, 31, 63, 127, 255, 511
      6. Bei m = 4: Anzahl an StackFrames = 16, >8596, ?, ?, ?, ?, ?
      7. Bei m = 5: Anzahl an StackFrames = >8593, ?, ?, ?, ?, ?, ?

      RecursiveAckermann(4, 1) und RecursiveAckermann(5, 0) lösen bei mir nach ca. 8600 StackFrames eine StackOverflowException aus. Abgesehen davon, dass es eine Weile dauert, das alles zu berechnen: Wenn möglich möchte man StackOverflows verhindern.


      Lösungsansatz

      Ich habe da an folgendes Konzept gedacht:
      Es gibt eine Liste von auszuführenden Tasks.
      Ein Task repräsentiert einen Rekursions-Aufruf. Also ein Task stellt alle Informationen bereit, die die Funktion benötigt, wie z.B. Argumente.
      Ein Task kann außerdem "Wartend" oder "Abgeschlossen" sein. Wenn ein Task abgeschlossen wird, wird er aus der Liste von auszuführenden Tasks entfernt.
      Ein Task kann unter-Tasks haben, von denen er abhängt. Ein Task kann ausgeführt werden, wenn alle unter-Tasks abgeschlossen sind (also auch, wenn keine unter-Tasks vorhanden sind). Während ein Task ausgeführt wird, kann er unter-Tasks einreihen. Wenn unter-Tasks eingereiht werden, muss der aktuelle Task abgebrochen werden, damit zuerst die unter-Tasks abgeschlossen werden können.

      Als Beispiel soll Fibonacci(4) berechnet werden:
      Viele Bilder und Text

      Zu Beginn wird der allererste Task eingereiht. Fibonacci(4) soll berechnet werden.

      Dann beginnt der eigentliche Ablauf. Die Liste wird immer von rechts nach links durchlaufen. Da nur ein Task in der Liste ist, wird mit diesem begonnen. Da alle unter-Tasks abgeschlossen sind (bzw. in diesem Fall keine unter-Tasks vorhanden sind), wird der Task ausgeführt. Fibonacci(4) lässt sich aber nicht trivial berechnen. Stattdessen werden Fibonacci(3) und Fibonacci(2) eingereiht und der aktuelle Task abgebrochen.

      Dann wird mit dem nächsten Task weitergemacht. Da der aktuelle ganz links in der Liste ist, wird wieder rechts begonnen. Da Fibonacci(2) auch nicht trivial berechnet werden kann, werden wieder 2 weitere Tasks (Fibonacci(1) und Fibonacci(0)) eingereiht.

      Fibonacci(3) kann ebenfalls nicht trivial berechnet werden. Fibonacci(2) und Fibonacci(1) werden eingereiht.

      Der nächste Task kann nicht ausgeführt werden, weil er nicht abgeschlossene unter-Tasks hat, was logisch ist, denn er ist ja der allererste Task, der überhaupt eingereiht wurde.

      Also wieder ganz nach rechts. Dieser Task kann ausgeführt werden. Da Fibonacci(1) trivial ist, wird das Ergebnis direkt berechnet, ohne unter-Tasks einzureihen.

      Der nächste Task kann ebenfalls ausgeführt werden, aber es müssen wieder Tasks eingereiht werden (Fibonacci(1) und Fibonacci(0)).

      Die nächsten beiden Tasks sind wieder trivial.

      Der nächste Task wurde bereits einmal ausgeführt, ist aber noch nicht abgeschlossen. Da alle unter-Tasks abgeschlossen sind, wird er erneut ausgeführt und da die Ergebnisse von Fibonacci(1) und Fibonacci(0) jetzt berechnet sind, wird auch dieser Task abgeschlossen.

      Bei den nächsten beiden Tasks sind nicht alle unter-Tasks abgeschlossen, deswegen werden sie noch nicht ausgeführt.

      Und wieder nach rechts. Die nächsten Beiden Tasks sind wieder trivial.

      Und bei den nächsten beiden Tasks werden wieder Ergebnisse abgeschlossener Tasks zusammengerechnet.

      Und damit bleibt noch der letzte (bzw. erste) Task übrig. Da alle unter-Task abgeschlossen sind, kann auch dieser ausgeführt und schlussendlich abgeschlossen werden.

      Das Ergebnis des letzten Tasks ist dann das Ergebnis, das man ursprünglich haben wollte.


      Wie muss nun die Fibonacci-Funktion aussehen, um mit diesem System zu funktionieren?
      Pseudocode-mäßig würde das inetwa so aussehen:

      Quellcode

      1. 'Fibonacci-Funktion:
      2. 'Wenn das der erste Aufruf ist
      3. 'Wenn n = 0
      4. 'Ergebnis = 0
      5. 'Angeben, dass der Task abgeschlossen ist
      6. Return
      7. 'Wenn n = 1
      8. 'Ergebnis = 1
      9. 'Angeben, dass der Task abgeschlossen ist
      10. Return
      11. 'Ansonsten
      12. 'Task einreihen mit n - 1
      13. 'Task einreihen mit n - 2
      14. 'Angeben, dass der Task nicht abgeschlossen ist
      15. Return
      16. 'Ansonsten
      17. 'Ergebnis = (Ergebnis von Task mit n - 1) + (Ergebnis von Task mit n - 2)
      18. 'Angeben, dass der Task abgeschlossen ist
      19. Return

      Also es muss irgendwie festgestellt werden, ob die Funktion schon mal ausgeführt wurde. Und es muss irgendwie auf andere Tasks verweisen werden. Wie oben beschrieben muss ein Task alle Informationen bereitstellen, die für die Funktion notwendig sind. Darunter sind Argumente, Ergebnis und andere Dinge, die je nach Anwendungsfall unterschiedlich sein können.


      Implementierung

      Generica!

      Die Funktion kann dann diese Signatur haben:

      VB.NET-Quellcode

      1. Public Delegate Sub TaskMethod(Of TInput, TOutput, TState)(Context As TaskContext(Of TInput, TOutput, TState))

      Der Kontext mit den Informationen wird als Parameter mitgegeben. Man beachte, dass die Funktion nichts mehr zurückgibt, sondern das Ergebnis im Kontext ablegt.
      Der Kontext sieht so aus:

      VB.NET-Quellcode

      1. Public Class TaskContext(Of TInput, TOutput, TState)
      2. Public ReadOnly Property ReadyToExecute As Boolean
      3. Get
      4. Return _DependingOn.All(Function(i) i._IsFinished)
      5. End Get
      6. End Property
      7. Dim _IsFinished As Boolean
      8. Public ReadOnly Property IsFinished As Boolean
      9. Get
      10. Return _IsFinished
      11. End Get
      12. End Property
      13. Dim _Input As TInput
      14. Public ReadOnly Property Input As TInput
      15. Get
      16. Return _Input
      17. End Get
      18. End Property
      19. Dim _Result As TOutput = Nothing
      20. Public ReadOnly Property Result As TOutput
      21. Get
      22. Return _Result
      23. End Get
      24. End Property
      25. Public Property State As TState
      26. Dim Executor As TaskExecutor(Of TInput, TOutput, TState)
      27. Dim DependingOn As New List(Of TaskContext(Of TInput, TOutput, TState))
      28. Friend Sub New(NewExecutor As TaskExecutor(Of TInput, TOutput, TState), NewInput As TInput, NewState As TState)
      29. Executor = NewExecutor
      30. _Input = NewInput
      31. _State = NewState
      32. End Sub
      33. Public Function QueueTask(Input As TInput) As TaskContext(Of TInput, TOutput, TState)
      34. Dim Result = Executor.QueueTask(Input)
      35. DependingOn.Add(Result)
      36. Return Result
      37. End Function
      38. Public Sub Finish(Result As TOutput)
      39. _IsFinished = True
      40. _Result = Result
      41. End Sub
      42. End Class

      Alles ziemlich selbsterklärend, bis auf die TaskExecutor-Klasse, aber die kommt gleich.
      Es gibt eine Liste von Tasks, die abgeschlossen sein müssen, bevor dieser Task ausgeführt werden kann und eine Property, um genau das unkompliziert abzufragen. Dann noch Properties für Parameter, Ergebnis, Zustand und ob der Task abgeschlossen ist. Die QueueTask-Funktion reiht einen neuen Task ein und fügt diesen der oben genannten Liste hinzu. Die Finish-Methode stellt klar, dass der Task abgeschlossen ist. Hier wird auch das Ergebnis festgelegt.

      Im TaskExecutor ist dann die eigentliche Logik verpackt, die die einzelnen Tasks in der richtigen Reihenfolge ausführt.

      VB.NET-Quellcode

      1. Public Class TaskExecutor(Of TInput, TOutput, TState)
      2. Dim TaskMethod As TaskMethod(Of TInput, TOutput, TState)
      3. Dim InitialStateGenerator As Func(Of TState)
      4. Dim RunningTasks As New List(Of TaskContext(Of TInput, TOutput, TState))
      5. Public Sub New(NewTaskMethod As TaskMethod(Of TInput, TOutput, TState), NewInitialStateGenerator As Func(Of TState))
      6. TaskMethod = NewTaskMethod
      7. InitialStateGenerator = NewInitialStateGenerator
      8. End Sub
      9. Public Function Execute(Input As TInput) As TOutput
      10. Dim InitialTask = QueueTask(Input)
      11. Do
      12. For i = RunningTasks.Count - 1 To 0 Step -1
      13. Dim CurrentTask = RunningTasks(i)
      14. If CurrentTask.ReadyToExecute Then
      15. TaskMethod(CurrentTask)
      16. If CurrentTask.IsFinished Then
      17. RunningTasks.RemoveAt(i)
      18. End If
      19. End If
      20. Next
      21. Loop Until RunningTasks.Count = 0
      22. Return InitialTask.Result
      23. End Function
      24. Friend Function QueueTask(Input As TInput) As TaskContext(Of TInput, TOutput, TState)
      25. Dim Result As New TaskContext(Of TInput, TOutput, TState)(Me, Input, InitialStateGenerator())
      26. RunningTasks.Add(Result)
      27. Return Result
      28. End Function
      29. End Class

      TaskMethod ist ein Delegat auf die Funktion, die ausgeführt werden soll. RunningTasks ist die weiter oben beschriebene Liste von Tasks, die auszuführen ist.
      InitialStateGenerator ist ein Delegat auf eine Funktion, die eine Instanz von TState zurückgibt. Man kann nicht einfach eine TState-Instanz im Konstruktor angebe, die man bei allen Tasks verwendet. Denn die Tasks würden sich in die Quere kommen, weil sie alle die gleiche Instanz verwenden. Natürlich wäre es möglich, TState As New zu deklarieren, um über einen parameterlosen Konstruktor Instanzen erzeugen zu können, aber dadurch verliert man etwas Kontrolle über den Startzustand. Kann natürlich abgeändert werden.
      Die QueueTask-Funktion (die von der TaskContext.QueueTask-Funktion verwendet wird) erstellt einen neuen Task mit den angegebenen Argumenten, holt sich einen Anfangszustand vom InitialStateGenerator-Delegaten und reiht den Task ein.
      Und in der Execute-Funktion passiert die Magic. Ist auch nicht sonderlich kompliziert und ich denke, der Code ist ziemlich selbsterklärend. Nur ein Hinweise: Anders als die Darstellung mit den Bildchen vermittelt werden die abgeschlossenen Tasks aus der Liste etnfernt. Es war nur einfacher zu zeichnen (und auch für die Verständlichkeit besser, wenn die Tasks nicht ins Nirvana verschwinden.


      Fibonacci

      Um die Fibonacci-Funktion passend umzuschreiben braucht es jetzt noch die passende Zustands-Klasse:

      VB.NET-Quellcode

      1. Private Class FibonacciState
      2. Public IsInitialCall As Boolean = True
      3. Public RecursionTask1 As TaskContext(Of Integer, Integer, FibonacciState)
      4. Public RecursionTask2 As TaskContext(Of Integer, Integer, FibonacciState)
      5. End Class

      Und dann kann auch schon losgelegt werden:

      VB.NET-Quellcode

      1. Private Shared Sub NonRecursiveFibonacci(Context As TaskContext(Of Integer, Integer, FibonacciState))
      2. If Context.State.IsInitialCall Then
      3. Select Case Context.Input
      4. Case Is < 0
      5. 'Darf eigentlich nie passieren, wird aber zur Sicherheit nochmal geprüft, für den Fall, dass sich irgendwo ein Bug eingeschlichen hat.
      6. Throw New NopeException
      7. 'Triviale Fälle abfragen
      8. Case 0
      9. Context.Finish(0)
      10. Return
      11. Case 1
      12. Context.Finish(1)
      13. Return
      14. Case Else
      15. 'Rekursion einleiten. Durch das Setzen von IsInitialCall auf False wird beim nächsten Aufruf der Code weiter unten ausgeführt.
      16. Context.State.IsInitialCall = False
      17. 'Tasks einreihen und im Zustand einen Verweis darauf behalten.
      18. Context.State.RecursionTask1 = Context.QueueTask(Context.Input - 1)
      19. Context.State.RecursionTask2 = Context.QueueTask(Context.Input - 2)
      20. Return
      21. End Select
      22. Else
      23. 'Ergebnisse der Rekursionen addieren und als Ergebnis festlegen.
      24. Context.Finish(Context.State.RecursionTask1.Result + Context.State.RecursionTask2.Result)
      25. Return
      26. End If
      27. End Sub

      Und ein Aufruf:

      VB.NET-Quellcode

      1. Dim FibonacciTaskExecutor As New TaskExecutor(Of Integer, Integer, FibonacciState)(AddressOf NonRecursiveFibonacci, Function() New FibonacciState)
      2. For i = 0 To 10
      3. Console.WriteLine("Fibonacci({0}) = {1}", i, FibonacciTaskExecutor.Execute(i))
      4. Next



      Ackermann

      Und weils so schön war hier noch die Ackermann-Funktion nach dem Schema:

      VB.NET-Quellcode

      1. 'Ackermann benötigt 2 Parameter
      2. Private Structure AckermannInput
      3. Public m As Integer
      4. Public n As Integer
      5. End Structure
      6. '... und 3 Schritte
      7. Private Enum AckermannStateNumber As Integer
      8. Initial = 0
      9. Recurse1 = 1
      10. Recurse2 = 2
      11. End Enum
      12. Private Class AckermannState
      13. Public Number As AckermannStateNumber = AckermannStateNumber.Initial
      14. Public RecursionTask As TaskContext(Of AckermannInput, Integer, AckermannState)
      15. End Class
      16. Private Shared Sub NonRecursiveAckermann(Context As TaskContext(Of AckermannInput, Integer, AckermannState))
      17. Select Case Context.State.Number
      18. Case AckermannStateNumber.Initial
      19. 'Auch hier kann man sicherheitshalber nochmal prüfen, ob die Argumente überhaupt gültig sind.
      20. If Context.Input.m < 0 OrElse Context.Input.n < 0 Then
      21. Throw New NopeException
      22. End If
      23. 'Trivialer Fall
      24. If Context.Input.m = 0 Then
      25. Context.Finish(Context.Input.n + 1)
      26. Return
      27. End If
      28. 'Rekursion einleiten
      29. Context.State.Number = AckermannStateNumber.Recurse1
      30. If Context.Input.n = 0 Then
      31. Context.State.RecursionTask = Context.QueueTask(New AckermannInput With {.m = Context.Input.m - 1, .n = 1})
      32. Return
      33. End If
      34. Context.State.RecursionTask = Context.QueueTask(New AckermannInput With {.m = Context.Input.m, .n = Context.Input.n - 1})
      35. Return
      36. Case AckermannStateNumber.Recurse1
      37. 'Hier muss ein zweites Mal geprüft werden, ob Ackermann(m - 1, 1) oder Ackermann(m - 1, RecursiveAckermann(m, n - 1)) berechnet wird.
      38. If Context.Input.n = 0 Then
      39. 'In ersterem Falle ist der Task nämlich schon fertig.
      40. Context.Finish(Context.State.RecursionTask.Result)
      41. Return
      42. End If
      43. 'In letzterem muss nochmal gewartet werden.
      44. Context.State.Number = AckermannStateNumber.Recurse2
      45. Context.State.RecursionTask = Context.QueueTask(New AckermannInput With {.m = Context.Input.m - 1, .n = Context.State.RecursionTask.Result})
      46. Return
      47. Case AckermannStateNumber.Recurse2
      48. Context.Finish(Context.State.RecursionTask.Result)
      49. Return
      50. End Select
      51. End Sub

      Und der Aufruf:

      VB.NET-Quellcode

      1. Dim AckermannTaskExecutor As New RecursiveTaskIteration.TaskExecutor(Of AckermannInput, Integer, AckermannState)(AddressOf NonRecursiveAckermann, Function() New AckermannState)
      2. For m = 0 To 4
      3. For n = 0 To 4
      4. Console.WriteLine("Ackermann({0}, {1}) = {2}", m, n, AckermannTaskExecutor.Execute(New AckermannInput With {.m = m, .n = n}))
      5. Next
      6. Next



      Yield in C#

      Die Funktion wird auf diese Weise leider etwas auseinandergerissen. Das lässt sich auch nicht einfach vermeiden. Je komplexer die Funktion, desto schlimmer wird es. C# hat Iterator-Funktionen. Damit könnte man vermutlich was basteln, wenn man das alles etwas umschreibt. Könnte inetwa so aussehen:

      C#-Quellcode

      1. public class TaskContext<TInput, TOutput> { ... }
      2. private static IEnumerable<int?> FibonacciTest(TaskContext<int, int> Context)
      3. {
      4. if (Context.Input == 0)
      5. {
      6. yield return 0; //0 als Ergebnis festlegen.
      7. yield break; //Vermutlich unnötig, weil die Funktion sowieso nicht mehr aufgerufen wird, nachdem ein Ergebnis zurückgeliefert wurde.
      8. }
      9. if (Context.Input == 1)
      10. {
      11. yield return 1; //1 als Ergebnis festlegen.
      12. yield break;
      13. }
      14. var Task1 = Context.QueueTask(Context.Input - 1); //Tasks einreihen.
      15. var Task2 = Context.QueueTask(Context.Input - 2);
      16. yield return null; //Rekursion benötigt.
      17. yield return Task1.Result + Task2.Result; //Summe der Ergebnisse als Ergebnis festlegen.
      18. yield break;
      19. }

      Das sieht wesentlich übersichtlicher aus.
      Der Compiler generiert automatisch Code, mit dem er sich "merkt", wo aktuell Code ausgeführt wird:

      C#-Quellcode

      1. //Gaaaaaanz stark vereinfacht!
      2. private static IEnumerable<int?> FibonacciTest(TaskContext<int, int> Context)
      3. {
      4. <FibonacciTest>d__0 <FibonacciTest>d__ = new <FibonacciTest>d__0(-2);
      5. <FibonacciTest>d__.Context = Context;
      6. return <FibonacciTest>d__;
      7. }
      8. private class <FibonacciTest>d__0 : IEnumerable<int?>, IEnumerator<int?>
      9. {
      10. private int? <>2__current;
      11. private int <>1__state;
      12. public TaskContext<int, int> Context;
      13. public TaskContext<int, int> <Task1>5__1;
      14. public TaskContext<int, int> <Task2>5__2;
      15. public <FibonacciTest>d__0(int <>1__state)
      16. {
      17. this.<>1__state = <>1__state;
      18. }
      19. int? IEnumerator<int?>.Current { get { return this.<>2__current; } }
      20. IEnumerator<int?> IEnumerable<int?>.GetEnumerator()
      21. {
      22. //Extrem vereinfacht!
      23. return this;
      24. }
      25. bool IEnumerator.MoveNext()
      26. {
      27. switch (this.<>1__state)
      28. {
      29. case 0:
      30. this.<>1__state = -1;
      31. if (this.Context.Input == 0)
      32. {
      33. this.<>2__current = new int?(0);
      34. this.<>1__state = 1;
      35. return true;
      36. }
      37. if (this.Context.Input == 1)
      38. {
      39. this.<>2__current = new int?(1);
      40. this.<>1__state = 2;
      41. return true;
      42. }
      43. this.<Task1>5__1 = this.Context.QueueTask(this.Context.Input - 1);
      44. this.<Task2>5__2 = this.Context.QueueTask(this.Context.Input - 2);
      45. this.<>2__current = null;
      46. this.<>1__state = 3;
      47. return true;
      48. case 1:
      49. this.<>1__state = -1;
      50. break;
      51. case 2:
      52. this.<>1__state = -1;
      53. break;
      54. case 3:
      55. this.<>1__state = -1;
      56. this.<>2__current = new int?(this.<Task1>5__1.Result + this.<Task2>5__2.Result);
      57. this.<>1__state = 4;
      58. return true;
      59. case 4:
      60. this.<>1__state = -1;
      61. break;
      62. }
      63. return false;
      64. }
      65. }

      Das hat eine verblüffende Ähnlichkeit zur VB-Implementierung oben.
      Man beachte, dass der Compiler hier absichtlich Bezeichner verwendet, die man in C# normalerweise nicht hinschreiben kann. Das <FibonacciTest> gehört zum Bezeichner und stellt keinen generischen Typenparameter dar. Dadurch soll verhindert werden, dass der Compiler einen Namen generiert, den man schon woanders im Programm verwendet.

      Eventuell könnte sowas ähnliches auch mit Await/Async funktionieren. Damit habe ich aber nicht genug Erfahrung.


      Hausaufgabe

      Als Hausaufgabe für den Leser ;) :
      1.: Die Fibonacci-Funktion optimieren, indem man ein Dictionary verwendet, um bereits berechnete Ergebnisse zwischenzuspeichern. Wie man beim Beispiel oben erkennt, wird bei Fibonacci(4) mehrmals Fibonacci(2) berechnet. Erhöht man n, wird das umso schlimmer. In einem Dictionary kann gespeichert werden, was bereits berechnet wurde.
      2.: Das Ganze so umschreiben, dass man nicht nur eine Funktion rekursiv verwenden kann, sondern beliebige, die auch nicht die gleiche Signatur haben müssen. Also z.B. Function A(n As Integer) As String verwendet Function B(s As String) As Integer und umgekehrt.
      3.: So umschreiben, dass in C# Iterator-Funktionen damit verwendet werden können. Nur doof, dass es dann in VB noch schlimmer wird.


      Anhang
      Ich habe ein Projekt mit einer Bibliothek und Beispielanwendung angehängt. Im Code oben ist das eine oder andere als Friend deklariert (internal in C#). Diese Sachen sind nicht dazu gedacht, von außen angegriffen zu werden. Wenn sich TaskExecutor, TaskContext und TaskMethod in einer eigenen Bibliothek befinden, wie in der Projektmappe, dann sind diese Sachen von außen nicht sichtbar.
      Es kann sein, dass beim ersten Öffnen das falsche Startprojekt eingestellt ist. Hier einfach im Projektmappen-Explorer mit der rechten Maustaste auf TestApplication klicken und dann "Als Startprojekt festlegen".
      RecursiveTaskIteration.zip (19.9 KB gepackt, 50.2 KB entpackt)
      Bilder
      • Fibonacci4_01.png

        2,76 kB, 1.149×308, 875 mal angesehen
      • Fibonacci4_02.png

        3,7 kB, 1.149×308, 843 mal angesehen
      • Fibonacci4_03.png

        4,41 kB, 1.149×308, 842 mal angesehen
      • Fibonacci4_04.png

        5,51 kB, 1.149×308, 853 mal angesehen
      • Fibonacci4_05.png

        5,51 kB, 1.149×308, 843 mal angesehen
      • Fibonacci4_06.png

        5,53 kB, 1.149×308, 855 mal angesehen
      • Fibonacci4_07.png

        6,15 kB, 1.149×308, 850 mal angesehen
      • Fibonacci4_08_09.png

        12,24 kB, 1.149×616, 938 mal angesehen
      • Fibonacci4_10.png

        6,24 kB, 1.149×308, 851 mal angesehen
      • Fibonacci4_11_12.png

        12,29 kB, 1.149×616, 844 mal angesehen
      • Fibonacci4_13_14.png

        12,21 kB, 1.149×616, 877 mal angesehen
      • Fibonacci4_15_16.png

        12,19 kB, 1.149×616, 895 mal angesehen
      • Fibonacci4_17.png

        6,22 kB, 1.149×308, 845 mal angesehen
      "Luckily luh... luckily it wasn't poi-"
      -- Brady in Wonderland, 23. Februar 2015, 1:56
      Desktop Pinner | ApplicationSettings | OnUtils