Inhaltsverzeichnis         

Funktionale Programmierung in C#

Die Mathematiker entwickelten in Jahrhunderten eine eigene Sprache: die Sprache der Formeln und Funktionen. Diese Sprache besteht aus Operatoren wie z.B. + und *, Zahlendarstellungen und Symbolen für Konstanten (z.B. π) und Veränderliche (z.B. x, D). Beispiele sind die Formel zur Berechnung des Kreisumfanges:

U = π * D

Funktionen

Allgemeiner kann man den Zusammenhang zwischen Durchmesser D eines Kreises und seinem Umfang U durch eine Funktion darstellen:

f(D): D → π * D

Den Ausdruck kann man lesen als: die Funktion f(D) ordnet jedem Durchmesser D einen Kreisumfang f(D) = π * D zu. Diese Darstellung wurde in den 1930-er Jahren zu einem präzisen Formalismus ausgearbeitet (Alonzo Church, Stephen Kleene: Lambda- Kalkül), welcher die Grundlage für die Familie der funktionalen Programmiersprachen wie z.B. LISP oder F# bildet.

Während die Mathematiker Funktionen gerne mit einem einzigen Buchstaben bezeichnen, und dabei auch noch auf das griechische Alphabet zugreifen, bevorzugen die Programmierer lieber sprechende Namen wie:

Kreisumfang(D): D → π * D

Funktionen in C#

Eine Funktion wird in C# durch einen Funktionsblock dargestellt. Ein Funktionsblock gliedert sich in:

Funktionskopf

Typ des Funktionswertes

Name der Funktion

Parameterliste (...)



Funktionsrumpf { … }

Der Funktionskopf legt einen Namen und den Typ des Rückgabewertes für die Funktion fest. Weiter wird im Funktionskopf die Parameterliste definiert. Diese listet alle Veränderlichen auf, von denen der Funktionswert abhängt. Im Funktionsblock wird der Funktionswert berechnet und zurückgegeben. Beispielsweise kann die Kreisumfangsfunktion in C# wie folgt dargestellt werden:

  // C#
double Kreisumfang(double D) {
    return Math.Pi * D;
}

Das return entspricht dabei dem Abbildungspfeil → in der mathematischen Darstellung Kreisumfang(D): D → π * D.

Die Funktion kann nun angewendet werden, indem sie mit einem Argument aufgerufen wird:

// Der Umfang eines Kreises mit 250 cm Durchmesser wird berechnet
Kreisumfang(250);

Übungen

Implementieren Sie eine Funktion in C#, die einen gemessene Länge in cm auf eine Länge in m abbildet:

  CmInMeter(x): x → x*0,01

Funktionen speichern und übergeben mittels Delegates

Jedes Unterprogramm, jede Funktion hat im Arbeitsspeicher einen Startadresse, auch Einsprungadresse genannt. Ein Unterprogramm startet, indem der Befehlszähler der CPU mit dieser Einsprungadresse geladen wird.


Wie alle Daten können auch Einsprungadressen gespeichert werden. Variablen, die Einsprungadressen speichern, werden Delegates genannt.


Anwendung finden Delegates in Callback- Methoden bzw. zur Implementierung von Ereignissen.

Basisklasse System.Delegate

Im .NET Framework sind Delegates nichts weiter als Objekte spezieller Klassen. Da jeder Delegate eine individuelle Parameterliste aufweisen kann, können die Delegates nicht alle Objekte einer gemeinsamen Klasse sein. Der C#- Compiler generiert für jeden Delegate automatisch eine versiegelte Klasse die von der Klasse System.Delegate abgeleitet ist.

delegate- Deklaration

Delegate- Klassen werden vom Compiler automatisch aus Deklarationen erzeugt, die mit dem Schlüsselwort d elegate beginnen, und die Signatur der Invoke- Methoden definieren :

[Zugriffsmodifikator] delegate Methodensignatur;

z.B.:

// Klasse erzeugen, in der eine Einsprungadresse einer 
// binären Funktion abgelegt werden kann
delegate double DGBinär(double a, double b);

Aus der delegate- Deklaration erzeugt der Compiler zur Übersetzungszeit eine Klasse folgender Art:


Die Invoke Methode dient zum synchronen Aufruf aller im Einsprungadressen, die in einer Delegate- Variablen gespeichert sind.

Konstruktion

Eine Delegate- Variable wird wie ein Objekt angelegt, wobei die Einsprungadresse dem Konstruktor als Parameter zu übergeben ist.

double Add(double a, double b) {
    return a + b;
}

double Subtract(double a, double b) {
    return a - b;
}

void Main() {

    DGBinär op = null;

    // Adresse von Add dem Delegate als Parameter übergeben
     op = new DGBinär(Add);

    // Aufrufen eines Unterprogrammes über die Einsprungadresse
    var Summe = op.Invoke(3, 9);

    // Compiler erlaubt auch folgende Kurzform für einen Aufruf:
    Summe = op.(3, 9);

    // Weitere Einsprungadressen können mit += hinzugefügt werden
    op += new DGBinär(Subtract);

    // Jetzt werden zwei Methoden mit einem Invoke- Aufruf gestartet
    // Der letzte gewinnt...
    var res = op.Invoke(3, 2);

    // Einsprungadressen können mit -= aus dem delegate wieder
    // entfernt werden
    op += new DGBinär(Subtract);

}

Vordefinierte Delegate- Typen

In der .NET Standardbibliothek System gibt es bereits eine reihe vordefinierter, generischer Delegate- Typen, so das heute die Deklaration eigener Delegate- Typen mittels delegate - Schlüsselwort entfallen kann:

// vordefinierte Delegates für Unterprogramme
delegate void Action<T1, … >(T1 p1, … );

// vordefinierte Delegates für Funktionen
delegate Tresult Func<T1, … , Tresult>(T1 p1, … );

// Beispiel für Einsatz
double Kreisumfang(double D) {
    return Math.Pi * D;
}
static void Main() {

  // Delegate anlegen und Einsprungadresse abspeichern
  var f = new Func<double, double>(Kreisumfang);

  // über Delegate die Funktion aufrufen
  double u = f.Invoke(10.0);  
}

Asynchroner Methodenstart

Delegates ermöglichen Methoden asynchron auszuführen. Dabei wird ein Thread aus dem Threadpool genommen, und in diesem die Methode gestartet. Die geschieht durch die BeginInvoke Methode

  delegate.BeginInvoke([Parameterliste der Methode], AsyncCallback DelegateDerCallBackMethode, object ParameterDerCallBackMethode)

BeginInvoke werden dabei die Parameter der asynchron aufzurufenden Methode, und zusätzlich ein Delegate einer Callback- Methode und deren Parameter übergeben. Die Callback- Methode wird aufgerufen, wenn die asynchron aufgerufene Methode endet. Sie ist vom Typ (Delegate) AsyncCallback:

delegate void AsyncCallback(IAsyncResult ar);


Über BeginInvoke kann der Programmierer an die Callback Methode einen Parameter übergeben (letzter Parameter). Dieser wird in einem Objekt vom Typ IAsyncResult im Feld AsyncState verpackt. Das Objekt vom Typ IAsyncResult wird schließlich an die CallBack- Methode übergeben.

Anonyme Methoden (ab NET 2.0)

Bisher wurde davon ausgegangen, dass einem Delegate eine gewöhnliche sog. benannte Methode zugewiesen wird. In Fällen, wo ein Stück ausführbarer Kode nur an einer Stelle im Programm zugewiesen werden muss, kann der Overhead einer kompletten Methodendeklaration durch sog. Anonyme Methoden eingespart werden.

Eine anonyme Methode wird wie folgt definiert:

delegate(Parameterliste) { /* Methodenrumpf */ }

Im folgenden Beispiel wird dem Ereignis Error der Klasse CLog zwei Ereignishandler als anonyme Methoden zugewiesen:

CLog log = new CLog();

public Form1()
{
   InitializeComponent();

   // Zuweisen von anonymen Methoden an das Ereignis Error
   // Hiedurch wird der Quelltext kompakter, da zwei explizite
   // Methodendeklarationen entfallen
   log.Error += delegate(string msg) 
                { 
                  statusStrip1.Items["toolStripStatusLabel1"].Text = msg; 
                };

   log.Error += delegate(string msg) 
                { 
                  MessageBox.Show(msg); 
                };
}   

private void btnFireEvent_Click(object sender, EventArgs e)
{
   log.LogError("Ein Ereignis");
}

Anonyme Methoden sind ein Vorläufer der wesentlich ausdrucksstärkeren Lambda- Ausdrücke, wie sie im nächsten Kapitel vorgestellt werden. Sie sollten in neuen Projekten nicht mehr eingesetzt werden.

Lambda- Ausdrücke (ab .NET 3.5)

Der Begriff Lambda Ausdruck wurde in einem Formalismus der Mathematiker Alonzo Church und Stephe Kleene in den 1930 Jahren geprägt (Lambda Kalkül ). Dieser Kalkül wird auch als der Urahn der Funktionalen Programmierung betrachtet.

Ein Lambda- Ausdruck ist die Deklaration einer anonymen (= unbenannten) Funktion/Prozedur in Form eines Ausdrucks innerhalb eines Funktions- oder Programmblockes.

  // Klassische Deklaration einer Funktion durch Funktionsblock
// Vorteil:  wiederverwendbar
// Nachteil: Deklaration erfolgt "weitab vom Einsatz"
double Kreisumfang(double D) {
    return Math.Pi * D;
}
static void Main() {
  
  //  Delegate anlegen und Einsprungadresse abspeichern
  var f = new Func<double, double>(Kreisumfang);

  // über Delegate die Funktion aufrufen
  double u = f.Invoke(10.0);  

  // Funktion zur Berechnung des Kreisumfanges "inline" als 
  // Lambda- Ausdruck definieren und einem Delegate zuweisen
  Func<double, double> L = (D) => { return Math.PI * D;};

  // Der Lambda- Ausdruck ist wie eine gewöhnliche Funktion aufrufbar
  u = L(10.0);

  // Syntaktische Vereinfachungen für Lambdaausdrücke:
  // I)
  // Besteht die Lambda- Anweisung nur aus einer return- Anweisung, 
  // dann kann das Schlüsselwort return, das Semikolon zum 
  // Abschluss von return und die Blockklammern {} entfallen:
  Func<double, double> LL = (D) => Math.PI * D;

  // II)
  // Wenn die Parameterliste nur aus einem Parameter besteht, dann 
  // können die () um die Parameterliste weggelassen werden
  Func<double, double> LLL = D => Math.PI * D;
}

Der Sinn von Lambdaausdrücken erschließt sich, wenn diese z.B. über Parameterlisten an Unterprogramme oder Funktionen übergeben werden, um diese "funktional" zu Parametrieren:

// 1) Liste erzeugen mit natürlichen Zahlen
var Liste = new int[]{23, 99, 78, 66, 55, 44, 76, 73};

// 2) Lambda- Ausdruck definieren, der entscheidet, ob eine Zahl durch 3 teilbar ist
Func<int, bool> kriterium = prüfling => prüfling % 3 == 0;

// 3) Lambda- Ausdruck als Filterkriterium einsetzen in einer LINQ- Listenverarbeitungsfunktion
//    Die Linq- Funktion Where(...) gibt alle Listenelemente zurück, die das Kriterium in Form
//    eines Lambda- Ausdruckes erfüllen.
var ListeDerDurchDreiTeilbaren = Liste.Where(kriterium).ToArray();

// 4)
// Lambda deklarieren und Filtern in einer Zeile:
var ListeDerDurchDreiTeilbaren = Liste.Where(z => z % 3 == 0).ToArray();

Fallunterscheidungen

Neben den Ausdrücken wie π * D können die Abbilder von Funktionen auch durch Fallunterscheidungen definiert werden. Z.B. kann die Funktion, die jeder Zahl ihren absoluten Betrag zuordnet, wie folgt beschrieben werden:

  Fallunterscheidungen
                     -z      z < 0
|z| = Abs(z): z → 0  für z = 0
                      z      z > 0

z.B. Abs(-5) = 5, Abs(5) = 5, Abs(0) = 0

Fallunterscheidungen in C#

Die Fallunterscheidung wird mit dem if( ) { ...}- Block implementiert. Zwischen if( ) wird die Bedingung definiert. Wenn sie zur Laufzeit erfüllt wird, dann werden die im Block definierten Berechnungen ausgeführt, sonst nicht.

  // C#
long ABS(long z) 
{
  if(z < 0)
    return -z;
  else if (z > 0)
    return z;
  else
    return 0;
}

Übung

Implementieren Sie eine Funktion in C# mit zwei Parametern, die den Preis eines Wirtschaftsgutes und der Warengruppe, zu der es gehört, auf einen Mehrwertsteuerbetrag abbildet:

  Mehrwertsteuerberechnung
                               preis * 0,07       wg = "ermäßigt"
MwSt(preis, wg): (preis, wg) →               für
                               preis * 0,19       wg = "normal"

Funktionen hintereinander schalten

In der Mathematik kann man mittels Funktionen eine Menge {x} auf eine Menge {y}, und die Menge {y} wiederum auf die Menge {z} abbilden. Formal wird dies wie folgt beschrieben:

Wenn f1(x): x → y und f2(y): y → z dann ist f2(f1(x)) : x → z

Damit kann man z.B. die Berechnungen des Kreisumfanges an Durchmesser anpassen, die in verschiedenen Maßeinheiten gemessen wurden:

  Funktionen hintereinanderschalten

MillimeterInMeter(x): x → x * 0,001
CentimeterInMeter(x): x → x * 0,01

Kreisumfang(D): D →  π * D

                     D                           D
            /--------+--------\             /----+----\   
Kreisumfang(MillimeterInMeter(x)): x →  π * (x * 0,001)
Kreisumfang(CentimeterInMeter(x)): x →  π * (x * 0,01)

z.B. Kreisumfang(CentimeterInMeter(100)) = 3,142..., Kreisumfang(MillimeterInMeter(100)) = 0,3142...

Funktionen hintereinanderschalten in C#

Die Syntax der Hintereinanderschaltung ist identisch mit der bereits eingeführten allgemeinen Syntax.

  var UmfangInMeter = Kreisumfang(MillimeterInMeter(x))

Übung

Zu einem Netto- Preis soll erst der Brutto- Preis (inkl. MwSt) und dann der Skonto (3%) berechnet werden. Implementieren Sie die Stufenweise Preisermittlung durch Hintereinanderschalten von Funktionen.

Rekursion

Mathematiker sind findige Menschen. Sie hatten schnell erkannt, dass mit der Hintereinaderschaltung von Funktionen ein mächtiges Instrument bereitsteht, mit dem man sich weit in die Tiefen mathematischer Probleme vorwagen kann. Eine besondere Form ist die Rekursion. Auf dieser basieren die funktionalen Beschreibungen vieler mächtiger Algorithmen.

Z.B. soll ein Vertreter 3 Kunden nacheinander besuchen, wobei die gewählte Reihenfolge die mit der kleinsten Gesamtstrecke ist (Problem der Rundreise mit minimaler Streckenlänge).

Die Lösung benötigt zunächst einen Graphen, dessen Knoten den Startpunkt und die Kunden darstellen, und dessen Kanten die Entfernungen zwischen den Knoten angeben. Dann sind für alle möglichen Reihenfolgen der Besuche die Summen der Wege zu berechnen. Am Ende ist die Reihenfolge mit der kleinsten Summe als Lösung auszuwählen. Folgendes Bild veranschaulicht das Vorgehen:


Wie viele Reihenfolgen über dem Graphen, und wie viele Summen damit gebildet werden müssen, gibt die aus der Kombinatorik bekannte Fakultät- Funktion an:

  Fakultät berechnen

                   1     n = 0             
N!(n): n →           für 
           n*N!(n-1)     n > 0

Die Funktion wird mittels einer Fallunterscheidung definiert, wobei für den Fall n > 0 der Funktionswert durch einen Term beschrieben wird, der N! wiederum enthält. Man erhält eine Lösung, indem man auf eine Lösung für den Vorgänger zurückgreift. Diese Art von Funktionsdefinition wird Rekursion genannt.

Aufgelöst wird die Rekursion von N! z.B. für n = 3 wie folgt:

  N!(3) = 3*N!(2) = 3*2*N!(1) = 3*2*1*N!(0) = 3*2*1*1 = 6

Die Fakultät wächst mit n extrem. Die exakte Lösung des Rundreiseproblem für große n praktisch unmöglich.

Rekursion in C#

  public static long Fact(long n)
{
  if (n == 0)
    return 1;
  else
    return n * Fact(n - 1);
}

Übung

Implementiere eine rekursive Funktion, die eine gegebene Zahlenpaar Basis, Exponent die Potenz berechnet wie folgt:

Potenz(Basis, Exponent): (Basis, Exponent) → Basis Exponent

Beachte dabei die Beziehung: b e = b * b e-1 = b * b * b e-2

Listenverarbeitung mittels Rekursion

In den Naturwissenschaften werden oft mehrere Messwerte einer Beobachtung zugeordnet werden. Es entstehen Paare von Messwerten. Ein Beispiel ist die Beobachtung eines beschleunigenden Fahrzeugs. Dabei wird in festen Zeitabständen die Momentangeschwindigkeit notiert.

  Messung: {(v, t)} := {(1 m/s, 1s), (2 m/s, 2s), …, (12 m/s, 5s)}

Ein anderes Beispiel sind die möglichen Folgen, in denen die Kunden A, B, C besucht werden (siehe vorausgegangener Abschnitt)

Besuchsreihenfolgen: {(K1, K2, K3)} := {(A, B, C), (B, A, C), (B, C, A), (C, B, A), (C, A, B), (A, C, B)}

Die Permutationen über der dreielementigen Menge {A, B, C} bilden hier alle möglichen Reihenfolgen. Jede Reihenfolge wird als ein Triple dargestellt.

Paare, Tripel oder allgemein Tupel sind spezielle Formen von Listen.

  Liste := (e1, e2, …., eN)

Mit Listenverarbeitung werden allgemein Funktionen bezeichnet, die Listen auf Listen oder Listen auf skalare Werte abbilden

  Listenverarbeitung := {f mit f(Liste X) : Liste X → Liste Y oder f(Liste X): Liste X → Y}

Grundlegende Operatoren der Listenverarbeitung

An dieser Stelle soll die Listenverarbeitung mit Mitteln der funktionalen Programmierung in C# eingeführt werden. Dazu wird eine Klassenbibliothek (mko.Algo) mit den notwendigen Funktionen bereitgestellt. Die Funktionen aus der Bibliothek verbergen die "technischen" Details (Arrays, IEnumerable<T> Schnittstelle, Linq) der Listenverarbeitung in .NET. Die Bibliothek führt damit die Listenverarbeitung auf einer sehr abstrakten, und damit allgemeingültigen Art und Weise ein.

In der mko.Algo Klassenbibliothek findet man die Listenverarbeitungsfunktionen unter folgendem Namensraum in der Klasse Fn :

  using mko.Algo.Listprocessing

Folgenden Tabelle beschreibt die Listenverarbeitungsfunktionen aus mko.Algo.Listprocessing

Name

Beispiel

Beschreibung




Fn.L(...)

var ListeX = Fn.L(1,2,3,4);

Erzeugt eine Liste mit den angegebenen Elementen

IEnumerable<T>

Fn.L(1,2,3,4) is IEnumerable<int>

// mittels using kann für den

// langen Typnamen ein Alias

// vereinbart werden:

using Lint = System.Collections.Generic.IEnumarable<int>;



// Nun kann geschrieben werden:

Fn.L(1,2,3,4) is Lint

Grundlegende Schnittstelle zu allen Listenfunktionen. Alle Funktionen der Listenverarbeitung liefern Ergebnisse von diesem Typ. T ist durch den Typ der Listenelemente zu ersetzen

Fn.First(liste)

oder

liste.First()

1 == Fn.First(ListeX)

Gibt das erste Element einer Liste zurück

Fn.Last(liste)

oder

liste.Last()

4 == Fn.Last(ListeX)

Gibt das letzte Element einer Liste zurück.

Fn.Take(liste, i)

oder

liste.Take(i)

Fn.Take(ListeX, 2) == Fn.L(1,2)

Liefert eine Liste mit den ersten i Elemente der ursprünglichen Liste

Fn.Skip(liste, i)

oder

liste.Skip(i)

Fn.Skip(ListeX, 2) == Fn.L(3,4)

Liefert eine Liste mit den ersten i Elemente der ursprünglichen Liste

Fn.Get(liste, i)

Fn.Get(ListeX, 1) == 2

Liefert den Wert des i-ten Listenelementes

Fn.Set(liste, i, newValue)

Fn.Set(ListeX, 1, 29) == Fn.L(1,29,3,4)

Setzt den i-ten Platz der Liste auf einen neuen Wert.

Fn.Count(liste)

oder

liste.Count()

Fn.Count(ListeX) == 4

Gibt die Anzahl der Elemente in einer Liste zurück

Fn.Reverse(liste)

oder

liste.Reverse()

Fn.Reverse(ListeX) == Fn.L(4,3,2,1)

Liefert eine Liste mit allen Elemente der Eingangsliste in umgekehrter Reihenfolge

Fn.Concat(liste1, liste2)

oder

liste1.Concat(liste2)

Fn.Concat(ListeX, Fn.L(5,7)) == Fn.L(1,2,3,4,5,7)

Verkettet zwei Listen zu einer neuen Liste

Fn.ForEach(liste, f)

Fn.ForEach(ListeX, x => x*x) == Fn.L(1,4,9,16)

Liefert zu einer Liste die Liste mit den Funktionswerten f(x) für jedes x aus der eingegebenen Liste.



Spezielle Listenverarbeitungsfunktionen, rekursiv implementiert

Mittels der allgemeinen Listenverarbeitungsoperationen können spezielle, Anwendungsspezifische Listenoperationen bereitgestellt werden. Hier wird die Rekursion intensiv genutzt.

Bildet die Summe aller Werte in einer Liste
  /// <summary>
/// Summenbildung
/// Summe({a1, a2, …, aN}): {a1, a2, …, aN} → a1 + a2 + … + aN
/// </summary>
/// <param name="liste"></param>
/// <returns></returns>
public static double Sum(IEnumerable<double> liste)
{
  if (lisp.Fn.Count(liste) == 0)
    return 0;
  else if (lisp.Fn.Count(liste) == 1)
    return lisp.Fn.First(liste);
  else
    return lisp.Fn.First(liste) + Sum(lisp.Fn.Skip(liste, 1));
}
Sucht das Minimum in einer Liste
  Minima finden

Min({…}): {…} → min mit min ϵ {…} & Für alle x ϵ {…} gilt: min <= x

public static double Min(IEnumerable<int> list)
{
   if (lisp.Fn.Count(list) == 0)
      throw new Exception();
   else if (lisp.Fn.Count(list) == 1)
      return lisp.Fn.First(list);
   else if (lisp.Fn.First(list) < Min(lisp.Fn.Skip(list, 1)))
      return lisp.Fn.First(list);
   else
      return Min(lisp.Fn.Skip(list, 1));
}
Erzeugt eine Liste mit den Werten 1-N
  Liste mit Werten erzeugen

/// <summary>
/// Erste Permutation von n Elementen. Alle weiteren 
/// </summary>
/// <param name="n"></param>
/// <returns></returns>
public static IEnumerable<long> FirstPermutation(long n)
{
   if (n == 1)
      return lisp.Fn.L(n);
   else
      return lisp.Fn.Concat(FirstPermutation(n - 1), lisp.Fn.L(n));
}
Prüft zwei Listen auf vollständige Übereinstimmung
  Gleichheit von Listen prüfen

Equal({a1,a2,…,aN}, {b1,b2,…,bM}): ({a1, a2, …, aN}, {b1, b2, …, bM}) → true für 
                                         Count({a1,a2,…,aN})  = Count({b1, b2, …, bM}) und
                                         a1 = b1, a2 = b2, …, aN = bM

public static bool EqualPureFn<T>(IEnumerable<T> listA, IEnumerable<T> listB) {

  if (Count(listA) != Count(listB))
     // Ungleich lange Listen können nie gleich sein
     return false;
  else if (Count(listA) > 0)
     // Rückführung auf die Prüfung, das der Anfang gleich ist, und der Rest auch
     return First(listA).Equals(First(listB)) && EqualPureFn(Skip(listA, 1), Skip(listB, 1));
  else
     // zwei leere Listen sind immer gleich
     return true;
}

Beispiel: Primzahlscanner funktional realisieren

Teilaufgabe 1 kann z.B. durch folgende Funktion dargestellt werden (Pseudokode):

  IstPrimzahl(z): z → z Mod 2 <> 0 AND TesteWeiterAufPrimzahl(z, 3)

TesteWeiterAufPrimzahl(z, t): z,t → z Mod t <> 0 AND
                                    Wenn t = z/2 
                                    dann true 
                                    sonst TesteWeiterAufPrimzahl(z, t + 1)

Wir setzen die Lösung in C# um:

  public static bool IsPrime(long z)
{
   // Entscheidungen für Sonderfälle wie 1 und 2 werden direkt getroffen
   // Für alle anderen werte wird mittels Rekursion die Probe auf Teilbarkeit 
   // mit allen möglichen Teilern durchgeführt
   return z == 2 || (z != 1) && (z % 2 != 0 && IsNotFactor(z, 3, z/2));
}

private static bool IsNotFactor(long z, long factor, long maxPossibleFactor)
{
   if (factor >= maxPossibleFactor)
      // Fall: alle möglichen Prüfungen auf Teilbarkeit blieben erfolglos
      return true;
   else
      // Fall: eine Prüfung wird durchgeführt und das Prüfergebnis mit
      // den Ergebnissen aus den restlichen noch anstehenden Prüfungen 
      // logisch UND verknüpft.
      return z % factor != 0 && IsNotFactor(z, factor + 2, maxPossibleFactor);
}

Auch für Teilaufgabe 2 gibt es eine funktionale Lösung (Pseudokode):

  AllePrimzahlenIn(a, b): a, b → Alle x in [a, b] für die gilt: IstPrimzahl(x)

Die Funktion bildet dabei das Paar a,b auf eine Menge mit allen Primzahlen zwischen a und b ab. Hier die Implementierung in C#:

/// <summary>
/// Erzeugt eine Liste aller Primzahlen im Intervall [a, b]
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public static IEnumerable<long> Scan(long a, long b)
{
   if (a > b)
      return lisp.Fn.L<long>();
   else if (IsPrime(a))
      // Primzahliste besteht aus a und den restlichen Primzahlen zw. [a+1, b]
      return lisp.Fn.Concat(lisp.Fn.L(a), Scan(a + 1, b));
   else
      // Primzahliste besteht aus den restlichen Primzahlen zw. [a+1, b]
      return Scan(a + 1, b);
}