Grundlagen

Funktionen sind den uns schon bekannten Methoden nicht ganz unähnlich. Sie bieten eine Möglichkeit immer wiederkehrende Anweisungen auszulagern.

Bei Methoden ist es so, dass diese nur dazu nutzbar sind, um Anweisungen auszulagern. Funktionen hingegen bieten die Möglichkeit einen Rückgabewert an die aufrufende Anweisung zurückzugeben.

Wenn wir uns die Methode addiere() aus dem vorhergehenden Kapitel ansehen, dann addiert diese zwei Zahlen und gibt uns in der Konsole die Rechnung aus:

1
2
3
4
5
static void addiere(double Zahl1, double Zahl2)
{
   double Ergebnis = Zahl1 + Zahl2;
   Console.WriteLine("Die Rechnung " + Zahl1 + " + " + Zahl2 + " ergibt: " + Ergebnis);
}

Wir wollen jetzt aber nicht mehr, dass die Methode addiere() Text ausgibt, sondern dass diese als Funktion das Ergebnis zurückgibt. Das heißt: wir entfernen die Anweisung Console.WriteLine(...); und fügen stattdessen ein return Ergebnis; ein. Dadurch gibt die Funktion jetzt das Ergebnis an die aufrufende Anweisung zurück.

Das einzige was noch fehlt, ist der Funktion zu sagen, dass sie auch eine Funktion ist. Das geschieht im Funktionskopf static void addiere(double Zahl1, double Zahl2). Das static void hatte uns bisher nicht interessiert.

  • static werden wir später bei der Objektorientierung thematisieren
  • void ist hier der Typ, den die Funktion zurück gibt. Bisher stand dort void, was soviel heißt, dass es keinen Rückgabewert gibt. Wir wollen nun aber eine Zahl zurückgeben, das heißt wir schreiben statt dem void zum Beispiel als Typ double hin.

Unsere Funktion sieht dann wie folgt aus:

1
2
3
4
5
static double addiere(double Zahl1, double Zahl2)
{
   double Ergebnis = Zahl1 + Zahl2;
   return Ergebnis;
}

Wenn wir jetzt addiere wie im letzten Kapitel aufrufen, wird das Programm nichts mehr ausgeben, stattdessen müssen wir uns jetzt um die Ausgabe wieder selber kümmern. Das ist so gesehen ersteinmal nicht von Vorteil, im unteren Beispiel wird aber der Vorteil des Zurückückgeben eines Wertes durch eine Funktion deutlicher.

Um die Ergebnisse unserer Rechnungen auszugeben, gibt es mehrere Möglichkeiten:

  • wir können die Ergebnisse in einer anderen Variablen zwischenspeichern: double zwischenErgebnis = addiere(1, 1); und diese dann ausgeben: Console.WriteLine(zwischenErgebnis);
  • alternativ können wir die Funktion auch direkt in der Anweisung zur Ausgabe aufrufen: Console.WriteLine(addiere(4, 200));

Unsere Hauptmethode könnte dann zum Beispiel so aussehen:

1
2
3
4
5
6
7
8
9
10
11
12
static void Main(string[] args)
{
    double zwischenErgebnis = addiere(1, 1);
    Console.WriteLine(zwischenErgebnis);

    /* alternativ direkt während der Ausgabe aufrufen */
    Console.WriteLine(addiere(4, 200));
    Console.WriteLine(addiere(67, 22));
    Console.WriteLine(addiere(22.3, 47));
    /* Vor dem Beenden warten */
    Console.ReadLine();
}

Am Beispiel der Fehlerbehandlung

Wenn wir bisher Werte eingelesen haben, die Zahlen darstellen sollten, mussten wir diese Zahlen konvertieren, wenn wir mit diesen weiterarbeiten wollten. War die Zahl falsch eingegeben (z. B. es war ein Buchstabe enthalten), dann hat die IDE das Programm gestoppt und uns angezeigt, dass der Befehl Convert.To...() nicht ordnungsgemäß ausgeführt werden konnte. Das Programm war beendet und wir mussten das Programm neu starten und alle Eingaben wiederholen. Um diesen Umstand zu umschiffen, werden wir uns die try-catch-Anweisung anschauen und diese dann später in eine Funktion auslagern.

Als Problemstellung nehmen wir das Beispiel aus den Kapitel while-Schleifen. In dem wir uns haben ausgeben lassen, welche Zahlen durch eine andere teilbar sind:

1
2
3
4
5
6
7
8
9
10
11
12
13
int divisor = 2;
int intervallEnde = 20;
int ueberpruef = 1;

while (ueberpruef <= intervallEnde)
{
    int rest = ueberpruef % divisor;

    if ( rest == 0 )
        Console.WriteLine(ueberpruef);

    ueberpruef++;
}

Diesen Quellcode wollen wir jetzt so erweitern, dass sowohl der Divisor als auch das Intervallende vom Nutzer eingegeben werden sollen. Dazu müssen wir dem Nutzer sagen, was er eingeben soll. Danach müssen wir das Programm anweisen auf eine Eingabe zu warten und wiederum danach müssen wir die Eingabe konvertieren, da die Eingabe eine Zahl ist und wir mit dieser auch weiterarbeiten wollen. Für den Divisor sähe das dann so aus:

1
2
3
4
5
6
/* Aufforderung: gib was ein */
Console.WriteLine("Bitte Divisor eingeben:");
/* Einlesen der Eingabe in eingabeDivisor */
string eingabeDivisor = Console.ReadLine();
/* Konvertieren der Eingabe in eine ganze Zahl */
int divisor = Convert.ToInt32(eingabeDivisor);

Für das Intervallende geht das natürlich komplett analog.

Wenn der Nutzer jetzt eine Fehleingabe tätigt, dann bricht das Programm ab. Um dies zu verhindern, können wir unserem Programm mit try sagen, dass es versuchen soll einen Anweisungsblock auszuführen. Mit einem darauf folgenden catch können wir dem Programm sagen, was es tun soll, wenn während des Ausführens ein Fehler aufgetreten ist.

Sobald im try-Block eine Anweisung einen Fehler hervorruft, wird direkt zum catch-Block gesprungen. Das heißt, dass die Anweisungen nach der fehlerhaften Anweisung nicht mehr ausgeführt werden. Wir müssen dieses try-catch dann solange ausführen, bis alles erfolgreich ausgeführt wurde.

Fangen wir ersteinmal mit dem Konvertieren der Eingabe an. Die Konvertierung könnte prinzipiell Fehler produzieren. Dementsprechend gehört diese in den try-Block:

1
2
3
4
5
6
7
try
{
    int einlesen = Convert.ToInt32(Console.ReadLine());
}
catch
{
}

Obwohl hier noch nicht viel drin steht, sieht das schon nach Quellcode aus, den wir nicht unbedingt in unseren Hauptmethoden stehen haben wollen. Das heißt wir wollen das später auslagern, dementsprechend werden wir uns das weitere Vorgehen anschauen. Würden wir diesen Quellcode in der Hauptmethode stehen lassen wollen, müssten wir noch einen Handstand mehr machen, wodurch der Quellcode noch komplexer würde.

Wir werden also eine Funktion schreiben, die uns unseren Eingabewert als Zahl herausgibt bzw. solange nach einer Eingabe fordert, bis die Konvertierung erfolgreich war.

Das return einer Funktion gibt immer sofort den angegeben Wert an die aufrufende Anweisung zurück, ohne dass die darauf folgenden Anweisungen der Funktion ausgeführt werden. Wir können also prinzipiell das Einlesen unserer Zahl über eine Endlosschleife realisieren, die nur durch ein erfolgreich erreichtes return beendet wird:

1
2
3
4
5
6
7
8
9
10
11
while (true)
  try
  {
      int einlesen = Convert.ToInt32(Console.ReadLine());

      /* wird nur erreicht, wenn Konvertierung erfolgreich */
      return einlesen;
  }
  catch
  {
  }

Im catch-Block fordern wir noch den Nutzer auf, seine Eingabe zu wiederholen, da diese ja fehlerhaft war:

1
2
3
4
5
6
7
8
9
10
11
12
while (true)
  try
  {
      int einlesen = Convert.ToInt32(Console.ReadLine());

      /* wird nur erreicht, wenn Konvertierung erfolgreich */
      return einlesen;
  }
  catch
  {
      Console.WriteLine("Eingabe fehlerhaft, bitte wiederholen:");
  }

Das war es im Prinzip schon. Die Anweisungen im try werden solange ausgeführt, bis das Ergebnis einer Konvertierung zurück gegeben wird: return ...;. Ist die Konvertierung nicht erfolgreich, wird das return nie erreicht und der Nutzer wird aufgefordert seine Eingabe zu wiederholen.

Wir müssen das ganze nur noch mit dem Funktionsgerüst umschließen. Also zum Beispiel mit einem static int einlesenInt32() und den entsprechenden Klammern.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int einlesenInt32()
{
  while (true)
    try
    {
        int einlesen = Convert.ToInt32(Console.ReadLine());
  
        /* wird nur erreicht, wenn Konvertierung erfolgreich */
        return einlesen;
    }
    catch
    {
        Console.WriteLine("Eingabe fehlerhaft, bitte wiederholen:");
    }
}

In unserer Hauptmethode müssen wir also nur noch den Nutzer auffordern den Divisor anzugeben und der Funktion die Fehlerbehandlung zu überlassen:

1
2
3
4
/*  Aufforderung: gib was ein */
Console.WriteLine("Bitte Divisor eingeben:");
/* divisor über die Funktion einlesen lassen */
int divisor = einlesenInt32();

Das können wir jetzt analog auch für das Intervallende nutzen, ohne dass wir das ganze try-catch nochmal schreiben müssen. Unsere Hauptmethode würde dann so aussehen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Console.WriteLine("Bitte Divisor eingeben:");
int divisor = einlesenInt32();
Console.WriteLine("Bitte Divisor eingeben:");
int intervallEnde = einlesenInt32();

int ueberpruef = 1;

while (ueberpruef <= intervallEnde)
{
    int rest = ueberpruef % divisor;

    if ( rest == 0 )
        Console.WriteLine(ueberpruef);

    ueberpruef++;
}

Wie ihr sicherlich gemerkt habt, haben wir hier und da geschweifte Klammern weg gelassen, die wir sonst gesetzt haben. Generell kann man bei Schleifen und Bedingungen die geschweiften Klammern immer dann weglassen, wenn in den Klammern nur eine einzige Anweisung stehen würde. Das try-catch-Konstrukt zählt hier auch als eine einzelne Anweisung, da der Compiler nach einem try direkt nach einem catch oder finally sucht und diese für sich zu einem Block zusammenfasst.