Dr. Erhard Henkes Stand: 17.11.2024

Die Programmiersprache C# (C sharp)


Einstieg

    Quadratische Gleichung auflösen

    Collatz-Folge

    Primzahlen

     Mersenne-Primzahlen

    Mit Brüchen rechnen

    Text nach Morsecode

Quiz mit Trivia OpenDB

Ameisen-Algorithmus



Singleton-Pattern

Factory-Pattern

State-Pattern

Visitor- und Composite-Pattern

Observer Pattern


Neuronales Netzwerk

Parkhaus (Zusammenspiel von Klassen, UML)

Bibliotheksverwaltung

 

 

Erster Einstieg in Konsolen-Programme



Beginnen wir mit einer Konsolenanwendung - ja, es gibt diese noch!
Als IDE verwenden wir MS Visual Studio 2022.
Rüsten Sie dieses mittels Visual Studio Installer mit den notwendigen Komponenten für C# aus.

Man erstellt eine neues Projekt, wählt Konsolenanwendung und gibt in den Editor den gewünschten Sourcecode ein:



Wer die objektorientierten Programmiersprachen Java und C++ kennt, hat bei C# einen großen Vorteil, weil er die grundlegenden Elemente wie z.B. Namespaces, Klassen und Vererbung bereits versteht.

Das Programm zeigt nach dem Starten (Strg+F5) folgende Konsole:



Der Befehl Console.ReadKey() ist für die Praxis wichtig, damit sich das Konsolenfenster nicht sofort schließt, sondern uns in Ruhe den ausgegebenen Text "Hello world!" lesen lässt. Erst beim Drücken einer Taste wird das Programm geschlossen.

 

Das nächste Beispiel ist ein wenig komplizierter. Dieses Programm gibt “Das Ergebnis von 5 + 10 ist 15” auf dem Bildschirm aus.
Die Addition ist beispielhaft in eine Funktion ausgelagert:

using System;

namespace MethodExample
{
    class Program
    {
        static void Main(string[] args)
        {
            int result = Add(5, 10);
            Console.WriteLine("Das Ergebnis von 5 + 10 ist {0}", result);
        }

        static int Add(int num1, int num2)
        {
            return num1 + num2;
        }
    }
}

 

Hier wird eine Zahlenreihe sortiert und mit einer foreach-Schleife ausgegeben:

using System;
using System.Collections.Generic;

namespace SortExample
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> numbers = new List<int> { 5, 3, 8, 1, 4 };
            numbers.Sort();
           
            Console.WriteLine("Sortierte Liste: ");
            foreach (int number in numbers)
            {
                Console.Write(number + " ");
            }
            Console.ReadKey();
        }
    }
}

 

Foreach ist ein hilfreicher Iterator. Hier folgt ein kleines Beispiel, das auch die Farbgestaltung in der Konsole demonstriert. Der Wert der Index-Variablen i wird mittels Modulo-Operator % auf den Bereich von 0 bis 15 beschränkt, bevor man ihn in einen Wert vom Typ ConsoleColor umwandelt. So kann man ihn im gültigen Bereich von 0 bis 15 der ForegroundColor-Eigenschaft sicher zuweisen.

using System;

namespace Iterators
{
  public static class Foreach_and_Color_Example
  {
    public static void Main()
    {
      var collection = new List<string>
      {
        "Hello",
        "Programming",
        "World",
        "Csharp",
        "uses",
        "foreach.",
        "This",
        "is",
        "a",
        "really",
        "great",
        "iterator.",
        "Have",
        "fun!"
      };

      int i = 1;

      foreach (var item in collection)
      {
        i %= 16;
        Console.ForegroundColor = (ConsoleColor)i++;
        Console.WriteLine(item.ToString());
      }
      Console.ReadKey();
    }
  }
}

 

Das nachfolgende Programm gibt die aktuelle Uhrzeit im Format “HH:mm:ss” auf dem Bildschirm aus:

using System;

namespace TimeExample
{
    class Program
    {
        static void Main(string[] args)
        {
            DateTime currentTime = DateTime.Now;
            Console.WriteLine("Die aktuelle Uhrzeit ist {0}", currentTime.ToString("HH:mm:ss"));
            Console.ReadKey();
        }
    }
}


Dieses Programm fordert den Benutzer auf einen Reaktionstest zu starten.
Sobald der Benutzer nach "Los geht's!" eine Taste drückt, startet der Timer und stoppt, wenn der Benutzer erneut eine Taste drückt.
Die Reaktionszeit wird berechnet und in Millisekunden angezeigt. So kann man Zeitspannen messen.

using System;
using System.Diagnostics;

namespace ReactionTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Drücke eine beliebige Taste, wenn du bereit bist.");
            Console.ReadKey();
            Console.WriteLine("Los geht's!");
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            Console.ReadKey();
            stopwatch.Stop();
            Console.WriteLine("Deine Reaktionszeit betrug {0} Millisekunden.", stopwatch.ElapsedMilliseconds);
            Console.ReadKey();
        }
    }
}

 

 

Quadratische Gleichung auflösen

 

Nun probieren wir ein Programm zur Lösung einer quadratischen Gleichung ax^2 + bx + c = 0:

using System;

namespace QuadraticEquationSolver
{
    class Program
        {
        static void Main(string[] args)
                 {
            Console.WriteLine("Geben Sie den Koeffizienten a der quadratischen Gleichung ax^2 + bx + c = 0 ein:");
            double a = Convert.ToDouble(Console.ReadLine());
            Console.WriteLine("Geben Sie den Koeffizienten b der quadratischen Gleichung ax^2 + bx + c = 0 ein:");
            double b = Convert.ToDouble(Console.ReadLine());
            Console.WriteLine("Geben Sie den Koeffizienten c der quadratischen Gleichung ax^2 + bx + c = 0 ein:");
            double c = Convert.ToDouble(Console.ReadLine());

            double discriminant = b * b - 4 * a * c;
            if (discriminant < 0)
                           {
                Console.WriteLine("Die Gleichung hat keine reellen Lösungen.");
                           }
            else if (discriminant == 0)
                           {
                double x = -b / (2 * a);
                Console.WriteLine("Die Gleichung hat eine reelle Lösung: x = {0}", x);
                           }
            else
                           {
                double x1 = (-b + Math.Sqrt(discriminant)) / (2 * a);
                double x2 = (-b - Math.Sqrt(discriminant)) / (2 * a);
                Console.WriteLine("Die Gleichung hat zwei reelle Lösungen: x1 = {0} und x2 = {1}", x1, x2);
                           }
            Console.ReadKey();
                  }
         }
}

Probieren Sie es aus. Es funktioniert.

Um den reellen Bereich zu verlassen, müssen wir noch den komplexen Zahlenberich hinzu nehmen:

using System;
using System.Numerics;

namespace QuadraticEquationSolver
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Geben Sie den Koeffizienten a der quadratischen Gleichung ax^2 + bx + c = 0 ein:");
      double a = Convert.ToDouble(Console.ReadLine());
      Console.WriteLine("Geben Sie den Koeffizienten b der quadratischen Gleichung ax^2 + bx + c = 0 ein:");
      double b = Convert.ToDouble(Console.ReadLine());
      Console.WriteLine("Geben Sie den Koeffizienten c der quadratischen Gleichung ax^2 + bx + c = 0 ein:");
      double c = Convert.ToDouble(Console.ReadLine());

      double discriminant = b * b - 4 * a * c;
      if (discriminant < 0)
      {
        Complex x1 = (-b + Complex.Sqrt(discriminant)) / (2 * a);
        Complex x2 = (-b - Complex.Sqrt(discriminant)) / (2 * a);
        Console.WriteLine("Die Gleichung hat zwei komplexe Lösungen: x1 = {0} und x2 = {1}", x1, x2);
      }
      else if (discriminant == 0)
      {
        double x = -b / (2 * a);
        Console.WriteLine("Die Gleichung hat eine reelle Lösung: x = {0}", x);
      }
      else
      {
        double x1 = (-b + Math.Sqrt(discriminant)) / (2 * a);
        double x2 = (-b - Math.Sqrt(discriminant)) / (2 * a);
        Console.WriteLine("Die Gleichung hat zwei reelle Lösungen: x1 = {0} und x2 = {1}", x1, x2);
      }
      Console.ReadKey();
    }
  }
}
  

Hierzu binden wir System.Numerics ein. System.Numerics ist ein Namespace in .NET, der numerische Typen enthält, die die numerischen Primitive wie Byte, Double und Int32 ergänzen, die von .NET definiert sind. Einige der Typen, die in diesem Namespace definiert sind, umfassen BigInteger, der eine beliebig große Ganzzahl darstellt, Complex, der komplexe Zahlen darstellt und eine Reihe von SIMD-fähigen Typen. SIMD steht für “Single Instruction Multiple Data” und bietet Hardwareunterstützung für die parallele Ausführung eines Vorgangs mit einer einzigen Anweisung. Dies ist interessant für Vektor- und Matrix-Berechnungen.

Das Problem der Eingabe besteht noch darin, dass der benutzer "blabla" schreiben oder einfach ENTER drücken kann. Hierzu schreiben wir, gezeigt am ersten Beispiel, eine kleine Routine, die ungültige Eingaben abfängt und eine erneute Eingabe fordert:

using System;

namespace QuadraticEquationSolver
{
  class Program
  {
    static void Main(string[] args)
    {
      double a = EingabeZahl("Geben Sie den Koeffizienten a der quadratischen Gleichung ax^2 + bx + c = 0 ein:");
      double b = EingabeZahl("Geben Sie den Koeffizienten b der quadratischen Gleichung ax^2 + bx + c = 0 ein:");
      double c = EingabeZahl("Geben Sie den Koeffizienten c der quadratischen Gleichung ax^2 + bx + c = 0 ein:");

      double discriminant = b * b - 4 * a * c;
      if (discriminant < 0)
      {
        Console.WriteLine("Die Gleichung hat keine reellen Lösungen.");
      }
      else if (discriminant == 0)
      {
        double x = -b / (2 * a);
        Console.WriteLine("Die Gleichung hat eine reelle Lösung: x = {0}", x);
      }
      else
      {
        double x1 = (-b + Math.Sqrt(discriminant)) / (2 * a);
        double x2 = (-b - Math.Sqrt(discriminant)) / (2 * a);
        Console.WriteLine("Die Gleichung hat zwei reelle Lösungen: x1 = {0} und x2 = {1}", x1, x2);
      }
      Console.ReadKey();
    }

    static double EingabeZahl(string prompt)
    {
      double zahl;
      while (true)
      {
        Console.WriteLine(prompt);
        string? eingabe = Console.ReadLine();

        if (double.TryParse(eingabe, out zahl))
        {
          return zahl;
        }
        else
        {
          Console.WriteLine("Ungültige Eingabe. Bitte geben Sie eine Zahl ein.");
        }
      }
    }
  }
}

Der Code "EingabeZahl" definiert eine Methode, die den Benutzer auffordert, eine Zahl einzugeben und sicherstellt, dass die Eingabe tatsächlich eine gültige Zahl ist. Die Methode wird so oft wiederholt, bis eine gültige Eingabe erfolgt ist. Dies ist besonders nützlich, um Fehler durch falsche Eingaben (z. B. Texteingaben wie "blabla") zu vermeiden. Die Methode EingabeZahl ist vom Typ double, was bedeutet, dass sie eine Dezimalzahl im Format double zurückgibt. Sie akzeptiert einen Parameter prompt vom Typ string. Dieser Parameter ist der Text, der dem Benutzer angezeigt wird, um ihn zur Eingabe einer Zahl aufzufordern. In der ersten Zeile wird eine Variable zahl vom Typ double deklariert. Diese Variable wird verwendet, um die Eingabe zu speichern, falls sie eine gültige Zahl ist. Die while(true)-Schleife sorgt dafür, dass der Code in der Schleife so lange wiederholt wird, bis der Benutzer eine gültige Zahl eingegeben hat. Die Schleife läuft endlos, bis wir explizit mit return eine Zahl zurückgeben. Die Eingabeaufforderung (prompt) wird dem Benutzer angezeigt. Der Text dieser Aufforderung wird in der Main-Methode festgelegt, zum Beispiel: „Geben Sie den Koeffizienten a der quadratischen Gleichung ax^2 + bx + c = 0 ein:“. Anschließend wird die Eingabe des Benutzers als Text (string) gelesen und in der Variablen eingabe gespeichert. Das ? hinter string bedeutet, dass die Variable eingabe auch null sein kann, falls der Benutzer einfach Enter drückt. Es wird double.TryParse verwendet, um zu prüfen, ob die Eingabe in eine Zahl (double) umgewandelt werden kann. TryParse gibt true zurück, wenn die Konvertierung erfolgreich ist, und speichert den Wert in der Variable zahl. Ist die Eingabe ungültig, gibt TryParse false zurück.

Diese Methode sorgt folglich dafür, dass nur gültige Zahlenwerte als Eingaben akzeptiert werden. Der Benutzer wird so lange zur Eingabe aufgefordert, bis eine Zahl eingegeben wird. Diese Art der Eingabevalidierung ist nützlich, um Laufzeitfehler durch ungültige Daten zu vermeiden und eine sichere Eingabe zu gewährleisten. Daher lohnt es sich, diese Vorgehensweise bei Eingaben zu übernehmen.

 

Collatz-Folge


BigInteger ist natürlich sehr interessant, wenn man sehr große Zahlen untersuchen will, z.B. für die Collatz-Folge oder Goldbachvermutung.

Zunächst ein Einstiegsbeispiel für die Collatz-Folge:

using System;
using System.Numerics;

namespace Collatz
{
  class Program
  {
    static void Main(string[] args)
    {
      /*********************************************************** Eingabebereich ****************************/
      const ulong element_limit = 1000000;   // Maximum H(n)
      const ulong element_print_limit = 500; // Ausgabe nur, wenn H(n) > element_print_limit
      BigInteger start = BigInteger.Parse("1000000000000000000000000000000000000000000"); // Beginn der Berechnung bei start
      BigInteger end   = BigInteger.Parse("2000000000000000000000000000000000000000000"); // Ende der Berechnung bei end
      /*********************************************************** Eingabebereich ****************************/

      for (BigInteger j = start; j < end; j++)
      {
        BigInteger zahl = j;
        ulong i = 1;
        while ((zahl != 1) && (i <= element_limit))
        {
          if (zahl % 2 == 0)
            zahl /= 2;
          else
            zahl = 3 * zahl + 1;
          i++;
        }

        if (zahl == 1)
        {
          if (i > element_print_limit)
          {
            Console.WriteLine("Startzahl: " + j);
            Console.WriteLine("\tAnzahl: " + i);
          }
        }
        else
        {
          Console.WriteLine("Startzahl: " + j);
          Console.WriteLine("kein Resultat (Anzahl-Limit erhoehen)");
        }

        if (i > element_limit) Console.Error.WriteLine("Anzahl zu hoch");
      }

      Console.ReadKey();
    }
  }
}



und hier noch ein Beispiel:

using System;
using System.Numerics;

namespace Collatz
{
  class Program
  {
    static void Main(string[] args)
    {
      /*********************************************************** Eingabebereich ****************************/
      BigInteger element_limit = 1000000; // Maximum H(n)
      BigInteger element_print_limit = 3400; // Ausgabe nur, wenn H(n) > element_print_limit
      BigInteger start = BigInteger.Parse("10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); // Beginn der Berechnung bei start
      BigInteger end   = BigInteger.Parse("10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000"); // Ende der Berechnung bei end
/*********************************************************** Eingabebereich ****************************/

      for (BigInteger j = start; j < end; j++)
      {
        BigInteger zahl = j;
        BigInteger i = 1;
        while ((zahl != 1) && (i <= element_limit))
        {
          if (zahl % 2 == 0)
            zahl /= 2;
          else
            zahl = 3 * zahl + 1;
          i++;
        }

        if (zahl == 1)
        {
          if (i > element_print_limit)
          {
            Console.WriteLine("Startzahl: " + j);
            Console.WriteLine("\tAnzahl: " + i);
          }
        }
        else
        {
          Console.WriteLine("Startzahl: " + j);
          Console.WriteLine("kein Resultat (Anzahl-Limit erhoehen)");
        }
      }

      Console.ReadKey();
    }
  }
}

Den Beginn der Ausgaben sieht man hier:

Vergleichen Sie es mit den entsprechenden C++-Programmen. Die Übersichtlichkeit des Codes ist hier eindeutig erhöht. Übertragen Sie zur Übung anspruchsvollen C++ Konsolen-Code nach C# und vergleichen Sie Code-Struktur und Geschwindigkeit.

 

Primzahlen


Wenn wir gerade bei BigInteger sind, schauen wir uns noch ein Programm an für das Auffinden von Primzahlen im sehr hohen Zahlenbereich an. Dieses C#-Programm findet Primzahlen ab einem bestimmten Wert.

Die Main-Funktion nimmt zwei Argumente entgegen: args und start. args ist ein Array von Strings, das Kommandozeilenargumente darstellt, während start ein BigInteger ist, das den Startwert für die Suche nach Primzahlen darstellt. Die Main-Funktion ruft die Methode FindPrimes auf und übergibt den start-Wert und eine Ganzzahl count, die die Anzahl der zu findenden Primzahlen darstellt.

Die Methode FindPrimes verwendet eine parallele For-Schleife, um Primzahlen zu finden. Die Schleife iteriert von 0 bis count, und bei jeder Iteration ruft sie die Methode IsProbablePrime auf, um zu überprüfen, ob die aktuelle Zahl prim ist. Wenn sie es ist, wird die Zahl in der Konsole ausgegeben.

Die Methode IsProbablePrime nimmt zwei Argumente entgegen: ein BigInteger, das die zu überprüfende Zahl darstellt, ob sie prim ist, und eine Ganzzahl, die die Anzahl der Iterationen für den Miller-Rabin-Primzahltest darstellt. Diese Methode gibt einen booleschen Wert zurück, der angibt, ob die Eingabezahl wahrscheinlich prim ist oder nicht. Die Methode überprüft zunächst, ob die Eingabezahl durch 2 teilbar oder kleiner als 2 ist, in welchem Fall sie false zurückgibt. Dann berechnet sie zwei Werte, d und s, die im Miller-Rabin-Primzahltest verwendet werden. Die Methode führt dann den Miller-Rabin-Primzahltest k-mal durch. Bei jeder Iteration wird eine Zufallszahl a zwischen 2 und n-2 mit der Methode RandomInRange generiert. Dann wird x als a^d mod n berechnet. Wenn x gleich 1 oder n-1 ist, wird die Iteration übersprungen. Die Methode betritt dann eine Schleife, die von 1 bis s iteriert. In jeder Iteration wird x als x^2 mod n berechnet. Wenn x gleich 1 ist, gibt die Methode false zurück. Wenn x gleich n-1 ist, wird die Schleife abgebrochen. Wenn nach allen Iterationen der Schleife x nicht gleich n-1 ist, gibt die Methode false zurück. Wenn alle Iterationen des Miller-Rabin-Primzahltests abgeschlossen sind, ohne dass false zurückgegeben wurde, gibt die Methode true zurück.

Die Methode RandomInRange nimmt zwei Argumente entgegen: zwei BigIntegers, die den Mindest- und Höchstwert für das Generieren einer Zufallszahl darstellen. Diese Methode liefert eine zufällige BigInteger zwischen den angegebenen Mindest- und Höchstwerten. Hierbei wurde das MersenneTwister-Paket über NuGet installiert. Randoms.WellBalanced ist eine Instanz eines Zufallszahlengenerators, der auf dem Mersenne-Twister-Algorithmus basiert. Das MersenneTwister-Paket ist eine portable Klassenbibliothek, die Mersenne-Twister-Pseudozufallszahlengeneratoren bereitstellt. Es enthält verschiedene Varianten des Mersenne-Twister-Algorithmus, einschließlich MT19937ar, MT19937-64 und SFMT-199371. Die Klasse Randoms wird in diesem Paket bereitgestellt und bietet verschiedene Eigenschaften für den bequemen Zugriff auf verschiedene Implementierungen von Zufallszahlengeneratoren. Die Eigenschaft WellBalanced gibt eine Instanz eines Zufallszahlengenerators zurück, der für allgemeine Zwecke gut ausbalanciert ist.

using MersenneTwister;
using System;
using System.Numerics;
using System.Threading.Tasks;

namespace PrimeFinder
{
    class Program
    {
        static void Main(string[] args)
        {
            //double value = double.Parse("1E100");
            //BigInteger start = new BigInteger(value);

            BigInteger start = BigInteger.Pow(10, 1000);

            //BigInteger start = BigInteger.Pow(2, 82589933) - 1; // größte bisher gefundene Primzahl

            int count = 20;
            FindPrimes(start, count);
            Console.ReadKey();
        }

        static void FindPrimes(BigInteger start, int count)
        {
            BigInteger n = start;
            object lockObject = new object();
            var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount };

            Parallel.For(0, count, parallelOptions, (i) =>
            {
                BigInteger current;
                lock (lockObject)
                {
                    current = n;
                    n++;
                }
               
                while (!IsProbablePrime(current, 5))
                {
                    lock (lockObject)
                    {
                        current = n;
                        n++;
                    }
                }
               
                Console.WriteLine(current);
            });
        }

        static bool IsProbablePrime(BigInteger n, int k)
        {
            if (n != 2 && n % 2 == 0)
                return false;
            if (n < 2)
                return false;

            BigInteger d = n - 1;
            int s = 0;

            while (d % 2 == 0)
            {
                d /= 2; s += 1;
            }

            for (int i = 0; i < k; i++)
            {
                BigInteger a = RandomInRange(2, n - 2);
                BigInteger x = BigInteger.ModPow(a, d, n);

                if (x == 1 || x == n - 1)
                    continue;

                for (int r = 1; r < s; r++)
                {
                    x = BigInteger.ModPow(x, 2, n);
                    if (x == 1) return false;
                    if (x == n - 1) break;
                }

                if (x != n - 1)
                    return false;
            }
            return true;
        }

        static BigInteger RandomInRange(BigInteger min, BigInteger max)
        {
            byte[] bytes = max.ToByteArray();
            BigInteger result;
            var random = Randoms.WellBalanced;
           
            do
            {
                random.NextBytes(bytes);
                result = new BigInteger(bytes);
            } while (result < min || result > max);
           
            return result;
        }
    }
}

 

Hier zeige ich die Ausgabe des obigen Code-Beispiels, das mit der Suche bei der großen Zahl 10 hoch 1000 beginnt. Die CPU-Auslastung (bei mir 10 echte Kerne) liegt bei über 75%.
Die Geschwindigkeit der Suche ist in diesem Zahlenbereich noch akzeptabel. 

An die im Jahr 2018 vom GIMPS-Forschungsprojekt gefundene Mersenne-Primzahl (2 hoch 82589933) - 1 kommen wir mit diesem Programm bezüglich der Geschwindigkeit und der Ausgabemöglichkeiten nicht heran.
Man vermutet, dass es keine größte Primzahl gibt. Folgende Überlegung von Euklid von Alexandria führt zu diesem Schluss: Angenommen, es gäbe eine endliche Anzahl von Primzahlen. Multipliziert man alle diese Primzahlen und addiert 1, erhält man eine Zahl, die durch keine der Primzahlen teilbar ist. Diese Zahl ist entweder selbst eine Primzahl oder sie hat Primfaktoren, die nicht in der ursprünglichen Liste der Primzahlen enthalten sind. In beiden Fällen haben wir einen Widerspruch zur Annahme, dass es eine endliche Anzahl von Primzahlen gibt. Daher erwartet man unendlich viele Primzahlen.

 

Mersenne-Primzahlen finden

Das Thema Mersenne-Primzahlen wird hier gut vorgestellt. Im Oktober 2024 wurde wieder eine neue Mp gefunden, nämlich M_136279841. Zur Prüfung zieht man den Lucas-Lehmer-Test heran.
Das Programm erklärt durch seine Kommentare die jeweiligen Schritte recht gut:

using System.Numerics;
using System.Diagnostics;

namespace PrimeFinder
{
class Program
{
  static void Main(string[] args)
  {
    int startExponent = 2; // Standard-Startwert für den Exponenten p
    int count = 60; // Anzahl der zu findenden Mersenne-Primzahlen

    // Abfrage des Start-Exponenten in der ersten Zeile
    Console.Write("Bitte geben Sie den Start-Exponenten ein: ");
    string input = Console.ReadLine() ?? string.Empty; // Gibt ein leeres Zeichen als Standardwert, falls `null`

    if (!int.TryParse(input, out startExponent) || startExponent < 2)
    {
      Console.WriteLine("Ungültige Eingabe. Der Start-Exponent wird auf 2 gesetzt.");
      startExponent = 2;
    }

    FindMersennePrimes(startExponent, count);
    Console.ReadKey();
}

static void FindMersennePrimes(int startExponent, int count)
{
  int found = 0;
  int p = startExponent;
  int resultLine = 3; // Startzeile für die Ausgabe der gefundenen Primzahlen
  object consoleLock = new object();

  // Starten des Gesamttimers
  Stopwatch totalStopwatch = new Stopwatch();
  totalStopwatch.Start();

  while (found < count)
  {
    // Prüfe, ob p eine Primzahl ist
    if (IsPrime(p))
    {
      // Starten des Timers für die Lucas-Lehmer-Prüfung
      Stopwatch llStopwatch = new Stopwatch();
      llStopwatch.Start();

      // Führe den Lucas-Lehmer-Test durch
      bool isMersennePrime = LucasLehmerTest(p);

      // Stoppen des Timers für die Lucas-Lehmer-Prüfung
      llStopwatch.Stop();
      double findTimeSeconds = llStopwatch.Elapsed.TotalSeconds;

      // Berechne die verstrichene Gesamtzeit in Sekunden
      double elapsedTotalSeconds = totalStopwatch.Elapsed.TotalSeconds;

      // Aktualisiere die Prüfmeldung mit Laufzeit und Findezeit
      lock (consoleLock)
      {
        Console.SetCursorPosition(0, 2);
        Console.Write(new string(' ', Console.WindowWidth)); // Lösche die Zeile
        Console.SetCursorPosition(0, 2);
        Console.Write($"[Laufzeit: {elapsedTotalSeconds:F2}s] Prüfe M_{p} = 2^{p} - 1... (Findezeit: {findTimeSeconds:F2}s)");
      }

      if (isMersennePrime)
      {
        lock (consoleLock)
        {
          Console.SetCursorPosition(0, resultLine);
          Console.ForegroundColor = ConsoleColor.Yellow;
          Console.WriteLine($"Gefunden: M_{p} ist eine Mersenne-Primzahl. [Laufzeit: {elapsedTotalSeconds:F2}s, Findezeit: {findTimeSeconds:F2}s]");
          Console.ResetColor();
          resultLine++; // Nächste Zeile für die nächste Primzahl
        }
        found++;
      }
    }
    else
    {
      // Aktualisiere die Laufzeit
      double elapsedTotalSeconds = totalStopwatch.Elapsed.TotalSeconds;
      lock (consoleLock)
      {
        Console.SetCursorPosition(0, 2);
        Console.Write(new string(' ', Console.WindowWidth)); // Lösche die Zeile
        Console.SetCursorPosition(0, 2);
        Console.Write($"[Laufzeit: {elapsedTotalSeconds:F2}s] Überspringe p = {p}, da nicht prim.");
      }
    }

    p++;

    // Sicherheitsabbruch, wenn p einen bestimmten Wert überschreitet
    if (p > 100000000)
    {
      lock (consoleLock)
      {
        Console.SetCursorPosition(0, resultLine);
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine($"Abbruch bei p = {p}. Keine weiteren Mersenne-Primzahlen gefunden.");
        Console.ResetColor();
      }
      break;
    }
  }

  // Stoppen des Gesamttimers
  totalStopwatch.Stop();

  // Lösche die letzte Prüfmeldung
  lock (consoleLock)
  {
    Console.SetCursorPosition(0, 1);
    Console.Write(new string(' ', Console.WindowWidth));
    Console.SetCursorPosition(0, 1);
  }

  // Gesamtzeit ausgeben
  lock (consoleLock)
  {
    Console.SetCursorPosition(0, resultLine + 1);
    Console.WriteLine($"Gesamtlaufzeit: {totalStopwatch.Elapsed.TotalSeconds:F2} Sekunden");
  }
}

static bool IsPrime(int n)
{
  // Wenn n kleiner oder gleich 1 ist, ist es keine Primzahl
  if (n <= 1) return false;

  // Wenn n kleiner oder gleich 3 ist, ist es eine Primzahl (2 und 3 sind Primzahlen)
  if (n <= 3) return true;

  // Wenn n durch 2 oder 3 teilbar ist, ist es keine Primzahl
  if (n % 2 == 0 || n % 3 == 0) return false;

  // Schleife beginnt bei 5 und prüft bis zur Quadratwurzel von n
  int i = 5;
  while ((long)i * i <= n)
  {
    // Wenn n durch i oder i + 2 teilbar ist, ist es keine Primzahl
    if (n % i == 0 || n % (i + 2) == 0) return false;

    // i wird um 6 erhöht, um nur relevante Werte zu prüfen (Optimierung)
    i += 6;
   
    // Die Erhöhung um 6 in der Funktion ist eine Optimierung, die auf dem Muster von Primzahlen basiert:


    /*
    Nach den Zahlen 2 und 3 sind alle Primzahlen der Form 6k ± 1, wobei k eine positive ganze Zahl ist.
    Das liegt daran, dass alle anderen Zahlen entweder durch 2 oder durch 3 teilbar sind und somit keine Primzahlen sein können.
    In der Schleife wird zunächst geprüft, ob n durch 5 oder 7 teilbar ist (also i = 5 und i + 2 = 7).
    Danach wird i um 6 erhöht, sodass im nächsten Schleifendurchlauf auf die nächsten potenziellen Primteiler geprüft wird:
    11 und 13, dann 17 und 19, und so weiter.
    Dadurch überspringt die Schleife alle Werte, die keine potenziellen Primzahlen sein können, da sie durch 2 oder 3 teilbar wären.
    Diese Optimierung reduziert die Anzahl der durchgeführten Divisionen erheblich und beschleunigt die Primzahlprüfung.
    */
  }

  // Wenn keine Teilbarkeit gefunden wurde, ist n eine Primzahl
  return true;
}


// https://de.wikipedia.org/wiki/Lucas-Lehmer-Test

static bool LucasLehmerTest(int p)
{
  if (p == 2)
    return true;

  BigInteger s = 4;
  BigInteger M = BigInteger.Pow(2, p) - 1;

  for (int i = 0; i < p - 2; i++)
  {
    s = (s * s - 2) % M;
  }

  return s == 0;
}
}
}

So sieht das bei mir aus:

 

 

Mit Brüchen rechnen


Unser nächstes Programm definiert eine 'Fraction' Struktur mit einem 'Numerator' (Zähler) und einem 'Denominator' (Nenner).
Es überschreibt den '+' und '-' Operator, um die Addition und Substraktion von Brüchen zu ermöglichen.
Die Funktion ToString() wird in der Struktur überschrieben, um einen Bruch direkt ausgeben zu können.
In der `Main`-Methode werden zwei Brüche erstellt, addiert und subtrahiert.
Das Ergebnis wird auf der Konsole ausgegeben.



using System;

namespace FractionAddition
{
  public struct Fraction
  {
    public int Numerator;
    public int Denominator;

    public Fraction(int numerator, int denominator)
    {
      Numerator = numerator;
      Denominator = denominator;
    }

    public static Fraction operator +(Fraction a, Fraction b)
    {
      int numerator = a.Numerator * b.Denominator + b.Numerator * a.Denominator;
      int denominator = a.Denominator * b.Denominator;
      return new Fraction(numerator, denominator);
    }

    public static Fraction operator -(Fraction a, Fraction b)
    {
      int numerator = a.Numerator * b.Denominator - b.Numerator * a.Denominator;
      int denominator = a.Denominator * b.Denominator;
      return new Fraction(numerator, denominator);
    }
   
    public override string ToString()
    {
      return $"{Numerator}/{Denominator}";
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      Fraction a = new Fraction(1, 2);
      Fraction b = new Fraction(1, 3);
      Fraction c = a + b;
      Console.WriteLine($"{a} + {b} = {c}\n");
      c = a - b;
      Console.WriteLine($"{a} - {b} = {c}");
      Console.ReadKey();
    }
  }
}

 

Nutzen Sie dieses Programm zur Übung durch Erweitern mit weiteren Operatoren, Ein- und Ausgaben, bevor Sie weiterlesen.

Hier ist eine Version, die den Bruch vor der Ausgabe kürzt, nur 0 ausgibt, wenn der Zähler 0 ist, und vor allem darauf achtet, dass nicht durch Null dividiert wird:


using System;

namespace FractionAddition
{
  public struct Fraction
  {
    public int Numerator;
    public int Denominator;

    public Fraction(int numerator, int denominator)
    {
      Numerator = numerator;
      Denominator = denominator;
    }

    public static Fraction operator +(Fraction a, Fraction b)
    {
      int numerator = a.Numerator * b.Denominator + b.Numerator * a.Denominator;
      int denominator = a.Denominator * b.Denominator;
      return new Fraction(numerator, denominator);
    }

    public static Fraction operator -(Fraction a, Fraction b)
    {
      int numerator = a.Numerator * b.Denominator - b.Numerator * a.Denominator;
      int denominator = a.Denominator * b.Denominator;
      return new Fraction(numerator, denominator);
    }

    public void Simplify()
    {
      int gcd = GCD(Numerator, Denominator);
      Numerator /= gcd;
      Denominator /= gcd;
    }

    private int GCD(int a, int b)
    {
      while (b != 0)
      {
        int temp = b;
        b = a % b;
        a = temp;
      }
      return a;
    }

    public override string ToString()
    {
      if (Numerator == 0)
      {
        return "0";
      }
      else
      {
        return $"{Numerator}/{Denominator}";
      }
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      int denominator, numerator;
      do
      {
        Console.WriteLine("Enter the first fraction in the format numerator/denominator: ");
        string input = Console.ReadLine();
        string[] parts = input.Split('/');
        numerator = int.Parse(parts[0]);
        denominator = int.Parse(parts[1]);

        if (denominator == 0)
        {
          Console.WriteLine("Denominator cannot be 0. Please enter a valid value.");
        }
      } while (denominator == 0);

      Fraction a = new Fraction(numerator, denominator);

      do
      {
        Console.WriteLine("Enter the second fraction in the format numerator/denominator: ");
        string input = Console.ReadLine();
        string[] parts = input.Split('/');
        numerator = int.Parse(parts[0]);
        denominator = int.Parse(parts[1]);

        if (denominator == 0)
        {
          Console.WriteLine("Denominator cannot be 0. Please enter a valid value.");
        }
      } while (denominator == 0);

      Fraction b = new Fraction(numerator, denominator);

      Fraction c = a + b;
      c.Simplify();
      Console.WriteLine($"{a} + {b} = {c}\n");
      c = a - b;
      c.Simplify();
      Console.WriteLine($"{a} - {b} = {c}");
      Console.ReadKey();
    }
  }
}

 

 

Text nach Morsecode

 

Im nächsten Programm geben wir Text ein und wandeln ihn in Morsecode um.
Es liest Text von der Konsole ein und wandelt ihn mithilfe einer Dictionary in Morsecode um.
Buchstaben werden durch Morsecode und Leerzeichen durch Schrägstriche (/) ersetzt.

Es lohnt die Mühe, sich mit den Inhalten und Möglichkeiten von System.Collections.Generic zu beschäftigen.

 

using System;
using System.Collections.Generic;

namespace ConsoleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.Write("Enter text: ");
      string input     = Console.ReadLine();
      string morseCode = ToMorseCode(input);
      Console.WriteLine(morseCode);
      Console.ReadKey();
    }

    static string ToMorseCode(string input)
    {
      Dictionary<char, string> morseAlphabet = new Dictionary<char, string>()
      {
        {'A', ".-"},    {'B', "-..."},  {'C', "-.-."},  {'D', "-.."},   {'E', "."},
        {'F', "..-."},  {'G', "--."},   {'H', "...."},  {'I', ".."},    {'J', ".---"},
        {'K', "-.-"},   {'L', ".-.."},  {'M', "--"},    {'N', "-."},    {'O', "---"},
        {'P', ".--."},  {'Q', "--.-"},  {'R', ".-."},   {'S', "..."},   {'T', "-"},
        {'U', "..-"},   {'V', "...-"},  {'W', ".--"},   {'X', "-..-"},  {'Y', "-.--"},
        {'Z', "--.."},  {'0', "-----"}, {'1', ".----"}, {'2', "..---"}, {'3', "...--"},
        {'4', "....-"}, {'5', "....."}, {'6', "-...."}, {'7', "--..."}, {'8',"---.."},
        {'9',"----."}
      };

      string output = "";
      foreach (char c in input.ToUpper())
      {
        if (morseAlphabet.ContainsKey(c))
        {
          output += morseAlphabet[c] + " ";
        }
        else if (c == ' ')
        {
          output += "/ ";
        }
      }
      return output;
    }
  }
}

Wie Sie sehen, wird das Ausrufezeichen ignoriert, da es im Dictionary nicht vorkommt.

 

 

Quiz-Anwendung mit API-Abfrage bei Quiz-Datenbanken

 

Nun zu einem ganz anderen Thema. Wir wollen Quiz spielen und dabei eine Datenbank aus dem Netz nutzen. Hier ist ein Vorschlag, der mit chatGPT-4 als Partner erzeugt wurde:

using System;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace TriviaExample
{
  class Program
  {
    static async Task Main(string[] args)
    {
      string url = "";

      // Abfrage von Themengebiet und Schwierigkeit
      // Zeigen Sie die verfügbaren Kategorien an
      Console.WriteLine("Verfügbare Kategorien:");
      Console.WriteLine("0. All Categories");
      Console.WriteLine("1. General Knowledge");
      Console.WriteLine("2. History");
      Console.WriteLine("3. Geography");
      Console.WriteLine("4. Science & Nature");
      Console.WriteLine("5. Science & Computers");
      Console.WriteLine("6. Science & Mathematics");
      Console.WriteLine("7. Animals");
      Console.WriteLine("8. Politics");

      // Fordern Sie den Benutzer auf, eine Kategorie auszuwählen
      string? categorySelection = "";
      do
      {
        Console.Write("Wählen Sie eine Kategorie (0-8): ");
        categorySelection = Console.ReadLine();
      }
      while
      // Überprüfen Sie die Eingabe des Benutzers
      (
        categorySelection != "0" && categorySelection != "1" && categorySelection != "2" &&
        categorySelection != "3" && categorySelection != "4" && categorySelection != "5" &&
        categorySelection != "6" && categorySelection != "7" && categorySelection != "8"
      );

      if (categorySelection == "0")
      {
        url = $"https://opentdb.com/api.php?amount=1";
      }
      else
      {
        // Zuordnung von Kategorie-IDs zu Kategorienamen
        Dictionary<string, string> categoryDictionary = new Dictionary<string, string>
        {
          {"1", "9"},
          {"2", "23"},
          {"3", "22"},
          {"4", "17"},
          {"5", "18"},
          {"6", "19"},
          {"7", "27"},
          {"8", "24"}
        };

        string category = categoryDictionary[categorySelection];
        url = $"https://opentdb.com/api.php?amount=1&category={category}";
      }

      // Zeigen Sie die verfügbaren Schwierigkeitsgrade an
      Console.WriteLine("Verfügbare Schwierigkeitsgrade:");
      Console.WriteLine("1. easy");
      Console.WriteLine("2. medium");
      Console.WriteLine("3. hard");

      // Fordern Sie den Benutzer auf, einen Schwierigkeitsgrad auszuwählen
      string? difficultySelection = "";
      do
      {
        Console.Write("Wählen Sie einen Schwierigkeitsgrad (1-3): ");
        difficultySelection = Console.ReadLine();
      }
      while
      // Überprüfen Sie die Eingabe des Benutzers
      (
        difficultySelection != "1" && difficultySelection != "2" && difficultySelection != "3"
      );

      // Zuordnung von Schwierigkeitsgraden zu Schwierigkeitsnamen
      Dictionary<string, string> difficultyDictionary = new Dictionary<string, string>
      {
        {"1", "easy"},
        {"2", "medium"},
        {"3", "hard"}
      };

      string difficulty = difficultyDictionary[difficultySelection];
      url += $"&difficulty={difficulty}";

      Console.Write("\n");

      // Erstelle einen HttpClient
      var client = new HttpClient();

      // Statistik
      int totalQuestions = 0;
      int correctAnswers = 0;

      // Doppelte Fragen vermeiden     
      bool skipQuestion = false;
      List<string> askedQuestions = new List<string>();

      bool keepRunning = true;
      while (keepRunning) // Schleife läuft, solange keepRunning true ist
      {
        // Sende eine GET-Anfrage an die Open Trivia Database API
        // Open Trivia DB: Free to use, user-contributed trivia question database. (opentdb.com)
        // Hier wählt man Themengebiet, Schwierigkeitsgrad und Art als nachfolgende API-Anfrage
        var response = await client.GetAsync("https://opentdb.com/api.php?amount=1");

        // Überprüfe, ob die Anfrage erfolgreich war
        if (response.IsSuccessStatusCode)
        {
          // Lese die Antwort als String
          var responseString = await response.Content.ReadAsStringAsync();

#pragma warning disable CS8602 // Dereferenzierung eines möglichen Nullverweises
#pragma warning disable CS8604 // Compiler erkennt, dass ein möglicherweise NULL-Wert an eine Methode oder einen Delegaten übergeben wird,
                               // der einen Non-Nullable-Parameter erwartet.
          try
          {
            // Parse die Antwort als JSON-Objekt
            var json = JObject.Parse(responseString);

            // Überprüfe, ob die Antwort das erwartete Format hat
            if (json["results"] != null && json["results"].HasValues &&
            json["results"][0]["question"] != null && json["results"][0]["correct_answer"] != null &&
            json["results"][0]["incorrect_answers"] != null)
            {
              // Extrahiere die Frage und Antworten aus dem JSON-Objekt
              var question = WebUtility.HtmlDecode(json["results"][0]["question"].ToString());
              var correctAnswer = WebUtility.HtmlDecode(json["results"][0]["correct_answer"].ToString());
              var incorrectAnswers = json["results"][0]["incorrect_answers"].ToObject<string[]>();

#pragma warning restore CS8602 // Dereferenzierung eines möglichen Nullverweises

              // Füge alle Antworten in eine Liste ein und mische sie
              var allAnswers = new List<string>(incorrectAnswers.Select(a => WebUtility.HtmlDecode(a)));
              allAnswers.Add(correctAnswer);
              allAnswers = allAnswers.OrderBy(a => Guid.NewGuid()).ToList();

#pragma warning restore CS8604

              // Überprüfe, ob die Frage bereits gestellt wurde
              if (askedQuestions.Contains(question))
              {
                // Überspringe die Frage und fordere eine neue an
                skipQuestion = true;
              }
              else
              {
                // Füge die Frage zur Liste der gestellten Fragen hinzu und stelle die Frage dem Benutzer
                askedQuestions.Add(question);
              }

              if (!skipQuestion)
              {
                // Gebe die Frage und Antworten aus
                Console.WriteLine("Frage: " + question);
                for (int i = 0; i < allAnswers.Count; i++)
                {
                  Console.WriteLine($"{i + 1}: {allAnswers[i]}");
                }

                // Frage den Benutzer nach seiner Antwort
                Console.Write("Ihre Antwort (Nummer eingeben): ");
                int userAnswerIndex;
                while (!int.TryParse(Console.ReadLine(), out userAnswerIndex) || userAnswerIndex < 1 || userAnswerIndex > allAnswers.Count)
                {
                  Console.Write("Ungültige Eingabe. Bitte geben Sie eine gültige Antwortnummer ein: ");
                }

                // Überprüfe, ob die Antwort des Benutzers korrekt ist
                if (allAnswers[userAnswerIndex - 1] == correctAnswer)
                {   
                  Console.WriteLine("Richtig!");
                  correctAnswers++;
                }
                else
                {
                  Console.WriteLine("Falsch. Die richtige Antwort war: " + correctAnswer);
                }
                totalQuestions++;
              }
              else
              {
                // Flag zum Überspringen der Frage zurücksetzen
                skipQuestion = false;
                //Console.WriteLine("Debug Message: Die letzte abgerufene Frage wurde übersprungen, da bereits gestellt.");
              }
            }
            else
            {
              Console.WriteLine("Die Antwort von der Open Trivia Database API hatte nicht das erwartete Format.");
            }
          }
          catch (JsonException)
          {
            Console.WriteLine("Fehler beim Parsen der Antwort von der Open Trivia Database API als JSON-Objekt.");
          }
        }
        else
        {
          Console.WriteLine("Fehler beim Abrufen der Daten von der Open Trivia Database API");
        }

        Console.WriteLine("\n'quit' beendet das Programm. Die Eingabetaste stellt eine weitere Frage.");
        string? input = Console.ReadLine();
        if (input == "quit")
        {
          keepRunning = false;
        }
      }

      // Gebe die Gesamtauswertung aus
      double percentageCorrect = (double)correctAnswers / totalQuestions * 100;
      percentageCorrect = Math.Round(percentageCorrect, 1);
      Console.WriteLine($"Sie haben {correctAnswers} von {totalQuestions} Fragen richtig beantwortet ({percentageCorrect}%).");

      Console.ReadKey();
    }
  }
}

Das fertige Programm findet man hier.


Mein "Programmiergehilfe" chatGPT-4 erklärt den Ablauf des Programms wie folgt:

Dieses Programm verwendet die Open Trivia Database API, um Quizfragen abzurufen und dem Benutzer anzuzeigen.
Der Benutzer kann dann eine Antwort auswählen und das Programm gibt Feedback, ob die Antwort korrekt war oder nicht.
Am Ende wird eine Gesamtauswertung der richtigen Antworten in Prozent angezeigt.

Das Programm beginnt damit, dass es einen HttpClient erstellt,
der zum Senden von HTTP-Anfragen an die Open Trivia Database API verwendet wird.
Es werden auch einige Variablen initialisiert, um die Anzahl der gestellten Fragen und die Anzahl der korrekten Antworten zu verfolgen.
Dann tritt das Programm in eine Schleife ein, die so lange läuft, bis der Benutzer das Programm beendet.
In jedem Durchlauf der Schleife wird eine GET-Anfrage an die Open Trivia Database API gesendet, um eine Quizfrage abzurufen.
Wenn die Anfrage erfolgreich war, wird die Antwort als String gelesen und als JSON-Objekt geparst.
Das Programm überprüft dann, ob die Antwort das erwartete Format hat (d. h. ob sie die erforderlichen Felder enthält).
Wenn die Antwort das erwartete Format hat, extrahiert das Programm die Frage und Antworten aus dem JSON-Objekt
und gibt sie auf dem Bildschirm aus.
Der Benutzer wird dann aufgefordert, eine Antwort auszuwählen, indem er die entsprechende Nummer eingibt.
Das Programm überprüft dann, ob die Antwort des Benutzers korrekt ist und gibt entsprechendes Feedback.
Nachdem der Benutzer eine Antwort ausgewählt hat, wird er aufgefordert, das Programm zu beenden oder fortzufahren.
Wenn der Benutzer das Programm beenden möchte, wird die Schleife beendet
und das Programm gibt eine Gesamtauswertung der richtigen Antworten in Prozent aus.

 

Ameisen-Algorithmus mit C#

Der Ameisen-Algorithmus ist ein interessantes Beispiel für die nachahmende Anwendung natürlicher biologischer Systeme in einem Computerprogramm.
Ich lasse hier vor allem chatGPT-4 "sprechen", dass mich bei der Erstellung dieses kleinen Beispiels und der technischen Dokumentation tatkräftig unterstützte.

//using System;


namespace AntAlgorithm
{
    /// <summary>
    /// Es gibt eine Klasse namens Program mit zwei Methoden: ShowMap und Main.
    /// </summary>
    class Program
       {
    /// <summary>
    /// Die ShowMap-Methode nimmt eine Liste von City-Objekten als Eingabe und zeigt eine visuelle Darstellung der Städte auf einer Karte an.
    /// Die Karte ist ein 2D-Array von Zeichen mit 21 Zeilen und 80 Spalten.Jede Stadt wird durch den ersten Buchstaben ihres Namens dargestellt.
    /// </summary>
    /// <param name="cities"></param>
    static void ShowMap(List<City> cities)
        {
        char[,] map = new char[21, 80];
        for (int i = 0; i < 21; i++)
            for (int j = 0; j < 80; j++)
                map[i, j] = ' ';
        foreach (City city in cities)
                 {
            int x = (int)city.X;
            int y = (int)city.Y;
            if (x >= 0 && x < 80 && y >= 0 && y < 21)
                map[y, x] = city.Name[0];
                 }
        for (int i = 0; i < 21; i++)
                {
            for (int j = 0; j < 80; j++)
                Console.Write(map[i, j]);
            Console.WriteLine();
                }
         }

    /// <summary>
    /// Die Main-Methode erstellt eine Liste von City-Objekten und ruft die ShowMap-Methode auf, um die Städte auf der Karte anzuzeigen.
    /// Dann werden einige Parameter für den Ameisenalgorithmus festgelegt, einschließlich der Anzahl der Ameisen, der maximalen Iterationen, Alpha, Beta, Rho und Q.
    /// Schließlich wird ein neues AntColonyOptimization-Objekt erstellt.
    /// In der Main-Methode wird ein Wartesymbol angezeigt, um anzuzeigen, dass der Ameisenalgorithmus ausgeführt wird.
    /// Dann wird ein neuer Thread gestartet, um regelmäßig neue Ameisen hinzuzufügen.
    /// Der Algorithmus wird ausgeführt und das beste Ergebnis wird angezeigt.
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
        {
        // Städte
        var cities = new List<City>
                 {
            new City("A", 0, 5, 0),
            new City("B", 1, 43, 15),
            new City("C", 2, 72, 7),
            new City("D", 3, 36, 11),
            new City("E", 4, 6, 3)
        };

        ShowMap(cities);

        // Parameter
        int numberOfAnts = 1000;
        int maxIterations = 5000;

        /// <summary>
        /// Der Ameisenalgorithmus, auch bekannt als Ant Colony Optimization (ACO), ist eine probabilistische Technik zur Lösung von Berechnungsproblemen, die auf die Suche nach guten Pfaden durch Graphen reduziert werden können.
        /// Der Algorithmus wurde von der Nahrungssuche von Ameisenkolonien inspiriert und verwendet künstliche Ameisen, die für Multi-Agenten-Methoden stehen.
        /// In der Natur suchen Ameisen nach Nahrungsquellen und hinterlassen auf ihrem Weg Pheromonspuren.
        /// Andere Ameisen folgen diesen Spuren und verstärken sie, wenn sie ebenfalls Nahrung finden.
        /// Mit der Zeit entsteht so ein Pfad mit hoher Pheromonkonzentration, der die kürzeste Verbindung zwischen dem Nest und der Nahrungsquelle darstellt.
        /// Der Ameisenalgorithmus ahmt dieses Verhalten nach, indem er eine Population von künstlichen Ameisen verwendet, die Touren durch eine Menge von Knoten(z.B.Städte) durchführen.
        /// Die Ameisen wählen ihren Weg basierend auf den Pheromonwerten der Kanten und den Entfernungen zwischen den Knoten.
        /// Nach jeder Iteration des Algorithmus werden die Pheromonwerte aktualisiert, um die besten gefundenen Touren zu belohnen.
        /// Der Algorithmus kann für verschiedene Anwendungen angepasst werden, indem die Parameter Alpha, Beta, Rho und Q entsprechend eingestellt werden.

        /// Ant colony optimization algorithms - Wikipedia. https://en.wikipedia.org/wiki/Ant_colony_optimization_algorithms.
        /// Ameisenalgorithmus – Wikipedia. https://de.wikipedia.org/wiki/Ameisenalgorithmus.
        /// Ant Colony Optimization - an overview | ScienceDirect Topics. https://www.sciencedirect.com/topics/engineering/ant-colony-optimization.
        ///
        /// Alpha(α) : Dieser Parameter bestimmt die relative Bedeutung der Pheromonspur bei der Wahl der nächsten Stadt durch eine Ameise.
        /// Ein hoher Wert von Alpha legt mehr Wert auf die Pheromonspur und führt dazu, dass die Ameisen stärker von den bisherigen Entscheidungen anderer Ameisen beeinflusst werden.
        ///
        /// Beta(β): Dieser Parameter bestimmt die relative Bedeutung der Entfernung zwischen den Städten bei der Wahl der nächsten Stadt durch eine Ameise.
        /// Ein hoher Wert von Beta legt mehr Wert auf die Entfernung und führt dazu, dass die Ameisen kürzere Wege bevorzugen.
        ///
        /// Rho (ρ): Dieser Parameter bestimmt die Verdunstungsrate der Pheromone.
        /// In jeder Iteration des Algorithmus verdunsten die Pheromone auf den Pfaden zwischen den Städten um einen Faktor von Rho.
        /// Ein hoher Wert von Rho führt zu einer schnelleren Verdunstung der Pheromone und ermöglicht es dem Algorithmus, sich schneller an veränderte Bedingungen anzupassen.
        ///
        /// Q: Dieser Parameter bestimmt die Menge an Pheromonen, die von einer Ameise auf ihrem Pfad abgelegt wird.
        /// Ein hoher Wert von Q führt dazu, dass mehr Pheromone abgelegt werden und die Entscheidungen der Ameisen stärker beeinflusst werden.
        /// </summary>
        double alpha = 1;
        double beta = 5;
        double rho = 0.5;
        double Q = 100;

        // Ameisenalgorithmus
        var antColonyOptimization = new AntColonyOptimization(cities, numberOfAnts, maxIterations, alpha, beta, rho, Q);

        // Wartesymbol
        Console.WriteLine("Ameisenalgorithmus läuft...\n");
        Console.WriteLine(" \\/ \\/");
        Console.WriteLine(" ___ _@@ @@_ ___");
        Console.WriteLine(" (___)(_) (_)(___)");
        Console.WriteLine(" //|| || || ||\\\\");

        // Neue Ameisen hinzufügen
        var addAntsThread = new Thread(() =>
                 {
             while (true)
                          {
                antColonyOptimization.AddAntSynchronized();
                Thread.Sleep(2000);
                           }
        });
        addAntsThread.Start();

        // Algorithmus ausführen
        var bestTour = antColonyOptimization.Run();

        // Ausgabe des besten Pfades
        Console.WriteLine();
        Console.WriteLine("Beste Tour: ");
        foreach (var city in bestTour)
                 {
            Console.Write(city.Name + " ");
                  }

        Console.ReadKey();
        }
}

/// <summary>
/// Die AntColonyOptimization-Klasse enthält private Felder für die Städte, die Anzahl der Ameisen, die maximalen Iterationen, Alpha, Beta, Rho und Q.
/// Es gibt auch Felder für die Entfernungen zwischen den Städten, die Pheromonwerte und eine Liste von Ameisen.
/// </summary>
public class AntColonyOptimization
{
    private readonly object _antsLock = new object();
    private List<City> Cities;
    private int NumberOfAnts;
    private int MaxIterations;

    private double Alpha;
    private double Beta;
    private double Rho;
    private double Q;

    private double[,] Distances;
    private double[,] Pheromones;
    private List<Ant> Ants;

    /// <summary>
    /// Der Konstruktor nimmt die Städte, die Anzahl der Ameisen, die maximalen Iterationen, Alpha, Beta, Rho und Q als Eingabe.
    /// Die Entfernungen zwischen den Städten werden berechnet und in einem 2D-Array gespeichert.
    /// Die Pheromonwerte werden initialisiert und eine Liste von Ameisen wird erstellt.
    /// </summary>
    /// <param name="cities"></param>
    /// <param name="numberOfAnts"></param>
    /// <param name="maxIterations"></param>
    /// <param name="alpha"></param>
    /// <param name="beta"></param>
    /// <param name="rho"></param>
    /// <param name="q"></param>
    public AntColonyOptimization(List<City> cities, int numberOfAnts, int maxIterations, double alpha, double beta, double rho, double q)
        {
        Cities = cities;
        NumberOfAnts = numberOfAnts;
        MaxIterations = maxIterations;
        Alpha = alpha;
        Beta = beta;
        Rho = rho;
        Q = q;

        // Distanzen berechnen
        Distances = new double[Cities.Count, Cities.Count];
        for (int i = 0; i < Cities.Count; i++)
                 {
            for (int j = i + 1; j < Cities.Count; j++)
                          {
                var distance = Cities[i].DistanceTo(Cities[j]);
                Distances[i, j] = distance;
                Distances[j, i] = distance;
                           }
                  }

        // Pheromone initialisieren
        Pheromones = new double[Cities.Count, Cities.Count];
        for (int i = 0; i < Cities.Count; i++)
                 {
            for (int j = 0; j < Cities.Count; j++)
                           {
                Pheromones[i, j] = 0.1;
                            }
                  }

        // Ameisen initialisieren
        Ants = new List<Ant>();
        for (int i = 0; i < NumberOfAnts; i++)
                 {
            Ants.Add(new Ant(Cities));
                  }
          }

    /// <summary>
    /// Die AddAntSynchronized-Methode fügt eine neue Ameise zur Liste der Ameisen hinzu.
    /// Diese Methode ist threadsicher, da sie das _antsLock-Objekt verwendet, um den Zugriff auf die Liste der Ameisen zu synchronisieren.
    /// </summary>
    public void AddAntSynchronized()
        {
        lock (_antsLock)
                 {
            AddAnt();
                 }
        }

    /// <summary>
    /// Die Run-Methode führt den Ameisenalgorithmus für eine bestimmte Anzahl von Iterationen aus.
    /// In jeder Iteration führen die Ameisen ihre Touren durch und die beste Tour wird gefunden.
    /// Dann werden die Pheromonwerte aktualisiert.
    /// Die Methode gibt die beste gefundene Tour zurück.
    /// </summary>
    /// <returns></returns>
    public List<City> Run()
        {
        var bestTourLength = double.MaxValue;
        var bestTour = new List<City>();

        for (int iteration = 0; iteration < MaxIterations; iteration++)
                 {
            // Ameisen ihre Touren durchführen lassen
            lock (_antsLock)
                          {
                foreach (var ant in Ants)
                                   {
                    ant.MakeTour(Distances, Pheromones, Alpha, Beta);
                                    }
                           }

            // Beste Tour der Iteration finden
            lock (_antsLock)
                          {
                foreach (var ant in Ants)
                                   {
                    if (ant.TourLength < bestTourLength)
                                            {
                        bestTourLength = ant.TourLength;
                        bestTour = ant.Tour;
                                            }
                                    }
                            }

            // Pheromone aktualisieren
            for (int i = 0; i < Cities.Count; i++)
                           {
                for (int j = i + 1; j < Cities.Count; j++)
                                   {
                    var deltaPheromone = 0.0;

                    lock (_antsLock)
                                            {
                        foreach (var ant in Ants)
                                                    {
                            if (ant.Visited(i) && ant.Visited(j))
                                                            {
                                deltaPheromone += Q / ant.TourLength;
                                                            }
                                                     }
                           }

            Pheromones[i, j] *= (1 - Rho);
            Pheromones[i, j] += deltaPheromone;

            Pheromones[j, i] *= (1 - Rho);
            Pheromones[j, i] += deltaPheromone;
                   }
              }
        }
    return bestTour;
    }

  /// <summary>
  /// Die AddAnt-Methode fügt eine neue Ameise zur Liste der Ameisen hinzu.
  /// </summary>
  public void AddAnt()
     {
      Ants.Add(new Ant(Cities));
      }
}

/// <summary>
/// Die Ant-Klasse enthält Felder für die Städte, die Tour und die Länge der Tour.
/// Es gibt auch einen Konstruktor und eine MakeTour-Methode.
/// </summary>
public class Ant
{
    private List<City> Cities;
    public List<City> Tour { get; private set; }
    public double TourLength { get; private set; }

    public Ant(List<City> cities)
        {
        Cities = cities;
        Tour = new List<City>();
        TourLength = 0;
        }

    /// <summary>
    /// Die MakeTour-Methode führt eine Tour für die Ameise durch. Die Tour wird zurückgesetzt und eine Startstadt wird zufällig ausgewählt.
    /// Dann werden die restlichen Städte besucht, wobei die Wahrscheinlichkeiten für den Besuch jeder Stadt berechnet werden.
    /// In der MakeTour-Methode wird die nächste Stadt basierend auf den berechneten Wahrscheinlichkeiten ausgewählt.
    /// Die Tour wird aktualisiert und die Länge der Tour wird berechnet.
    /// Schließlich kehrt die Ameise zur Startstadt zurück.
    /// </summary>
    /// <param name="distances"></param>
    /// <param name="pheromones"></param>
    /// <param name="alpha"></param>
    /// <param name="beta"></param>
    public void MakeTour(double[,] distances, double[,] pheromones, double alpha, double beta)
        {
        // Tour zurücksetzen
        Tour.Clear();
        TourLength = 0;

        // Startstadt wählen
        var currentCity = Cities[new Random().Next(Cities.Count)];
        Tour.Add(currentCity);

        // Restliche Städte besuchen
        for (int i = 1; i < Cities.Count; i++)
                 {
            // Wahrscheinlichkeiten berechnen
            var probabilities = new List<double>();
            foreach (var city in Cities)
                           {
                if (!Visited(city))
                                   {
                    var probability = Math.Pow(pheromones[currentCity.Index, city.Index], alpha) * Math.Pow(1 / distances[currentCity.Index, city.Index], beta);
                    probabilities.Add(probability);
                                    }
                else
                                    {
                    probabilities.Add(0);
                                    }
                            }

            // Nächste Stadt wählen
            var totalProbability = probabilities.Sum();
            var randomProbability = new Random().NextDouble() * totalProbability;
            var cumulativeProbability = 0.0;
            for (int j = 0; j < probabilities.Count; j++)
                           {
                cumulativeProbability += probabilities[j];
                if (cumulativeProbability >= randomProbability)
                                   {
                    currentCity = Cities[j];
                    Tour.Add(currentCity);
                    TourLength += distances[Tour[i - 1].Index, currentCity.Index];
                    break;
                                    }
                            }
                    }

        // Zurück zur Startstadt
        TourLength += distances[Tour.Last().Index, Tour.First().Index];
            }
      public bool Visited(int index)
            {
        return Tour.Any(c => c.Index == index);
             }
      public bool Visited(City city)
             {
        return Tour.Contains(city);
             }
        }

    /// <summary>
    /// Die City-Klasse enthält Felder für den Namen, den Index, die X- und Y-Koordinaten einer Stadt.
    /// Es gibt auch einen Konstruktor und eine DistanceTo-Methode, um die Entfernung zu einer anderen Stadt zu berechnen.
    /// </summary>
    public class City
        {
        public string Name { get; private set; }
        public int Index { get; private set; }
        public double X { get; private set; }
        public double Y { get; private set; }
        public City(string name, int index, double x, double y)
                 {
            Name = name;
            Index = index;
            X = x;
            Y = y;
                 }
        public double DistanceTo(City other)
                 {
            return Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
                 }
         }
}

/*
Technische Dokumentation:

Kurzfassung:
Dieses Programm implementiert einen Ameisenalgorithmus zur Lösung des Problems des Handlungsreisenden.
Es gibt eine City-Klasse, um Städte zu repräsentieren, eine Ant-Klasse, um Ameisen zu repräsentieren, und eine AntColonyOptimization-Klasse, um den Algorithmus auszuführen.
Die AntColonyOptimization-Klasse enthält Methoden zum Hinzufügen von Ameisen, zum Ausführen des Algorithmus und zum Aktualisieren der Pheromonwerte.
Die Ant-Klasse enthält Methoden zum Durchführen einer Tour und zum Überprüfen, ob eine Stadt bereits besucht wurde.
In der Main-Methode werden einige Städte erstellt und ein neues AntColonyOptimization-Objekt wird erstellt.
Der Algorithmus wird ausgeführt und das beste Ergebnis wird angezeigt.

Langfassung:
Dieses Programm implementiert einen Ameisenalgorithmus zur Lösung des Problems des Handlungsreisenden.
Der Ameisenalgorithmus ist ein Optimierungsverfahren, das von der Nahrungssuche von Ameisenkolonien inspiriert ist.
Der Algorithmus verwendet eine Population von Ameisen, die Touren durch eine Menge von Städten durchführen, um die kürzeste Tour zu finden.
Das Programm besteht aus mehreren Klassen: `City`, `Ant`, `AntColonyOptimization` und `Program`.
Die `City`-Klasse repräsentiert eine Stadt mit einem Namen, einem Index und X- und Y-Koordinaten.
Die Klasse enthält auch eine `DistanceTo`-Methode, um die Entfernung zu einer anderen Stadt zu berechnen.
Die `Ant`-Klasse repräsentiert eine Ameise mit einer Liste von Städten, einer Tour und der Länge der Tour.
Die Klasse enthält auch eine `MakeTour`-Methode, um eine Tour für die Ameise durchzuführen.
In dieser Methode wird die Tour zurückgesetzt und eine Startstadt wird zufällig ausgewählt.
Dann werden die restlichen Städte besucht, wobei die Wahrscheinlichkeiten für den Besuch jeder Stadt berechnet werden.
Schließlich kehrt die Ameise zur Startstadt zurück. Die Klasse enthält auch `Visited`-Methoden, um zu überprüfen, ob eine Stadt bereits besucht wurde.
Die `AntColonyOptimization`-Klasse implementiert den Ameisenalgorithmus.
Die Klasse enthält Felder für die Städte, die Anzahl der Ameisen, die maximalen Iterationen, Alpha, Beta, Rho und Q.
Es gibt auch Felder für die Entfernungen zwischen den Städten, die Pheromonwerte und eine Liste von Ameisen.
Der Konstruktor der Klasse nimmt die Städte, die Anzahl der Ameisen, die maximalen Iterationen, Alpha, Beta, Rho und Q als Eingabe.
Die Entfernungen zwischen den Städten werden berechnet und in einem 2D-Array gespeichert. Die Pheromonwerte werden initialisiert und eine Liste von Ameisen wird erstellt.
Die Klasse enthält auch `AddAnt`- und `AddAntSynchronized`-Methoden zum Hinzufügen von Ameisen zur Liste der Ameisen. Die `AddAntSynchronized`-Methode ist threadsicher.
Die `Run`-Methode führt den Ameisenalgorithmus für eine bestimmte Anzahl von Iterationen aus. In jeder Iteration führen die Ameisen ihre Touren durch und die beste Tour wird gefunden.
Dann werden die Pheromonwerte aktualisiert. Die Methode gibt die beste gefundene Tour zurück.
Die `Program`-Klasse enthält die `Main`-Methode des Programms. In dieser Methode werden einige Städte erstellt und ein neues `AntColonyOptimization`-Objekt wird erstellt.
Der Algorithmus wird ausgeführt und das beste Ergebnis wird angezeigt.
*/

 

 

Singleton-Pattern mit C#

Das Singleton-Pattern ist ein Entwurfsmuster in der Softwareentwicklung und gehört zur Kategorie der Erzeugungsmuster. Es stellt sicher, dass von einer Klasse genau ein Objekt existiert.
Seine Aufgabe besteht darin, zu verhindern, dass von einer Klasse mehr als ein Objekt erstellt werden kann. Das wird dadurch erreicht, dass das gewünschte Objekt in einer Klasse selbst erzeugt dann als statische Instanz abgerufen wird. Das Singleton zählt zu den einfachsten, aber dafür mächtigsten Patterns in der Software-Entwicklung. 

Nachfolgend schauen wir uns ein kleines Beispiel als Vorbild für die Verwendung dieses Singleton-Entwurfsmuster in C# an. Die Klasse SettingsManager ist als sealed deklariert, was bedeutet, dass sie nicht von anderen Klassen abgeleitet werden kann. Es gibt eine private statische Variable namens instance, die die einzige Instanz der Klasse enthält. Diese Variable wird mit null initialisiert. Es gibt auch eine private statische Variable namens padlock, die ein Objekt vom Typ object enthält. Diese Variable wird verwendet, um den Zugriff auf die Instance-Eigenschaft zu synchronisieren. Die Klasse enthält auch eine private Variable namens settings, die ein Wörterbuch vom Typ Dictionary<string, string> enthält. Dieses Wörterbuch speichert die Einstellungen der Anwendung. Der Konstruktor der Klasse ist privat und kann daher nur innerhalb der Klasse aufgerufen werden. Im Konstruktor werden die Einstellungen aus einer Datei oder Datenbank geladen und im Wörterbuch gespeichert. Die Instance-Eigenschaft ist öffentlich und statisch. Sie gibt die einzige Instanz der Klasse zurück. Die Eigenschaft verwendet das lock-Statement, um sicherzustellen, dass nur ein Thread gleichzeitig auf den Codeblock zugreifen kann. Die Klasse enthält auch zwei öffentliche Methoden: GetSetting und SetSetting. Die GetSetting-Methode gibt den Wert einer Einstellung zurück, während die SetSetting-Methode den Wert einer Einstellung ändert oder hinzufügt.

In der Main-Methode wird auf die Singleton-Instanz von SettingsManager zugegriffen. Dies geschieht durch Aufruf der statischen Instance-Eigenschaft der SettingsManager-Klasse. Die Main-Methode ruft dann die GetSetting-Methode auf, um den Wert der Einstellung “Theme” abzurufen. Der Wert wird dann auf der Konsole ausgegeben. Die Main-Methode ändert dann den Wert der Einstellung “Theme” auf “Light”, indem sie die SetSetting-Methode aufruft. Der neue Wert wird dann erneut abgerufen und auf der Konsole ausgegeben.

Dieses Programm zeigt beispielhaft, wie man auf die alleinige Singleton-Instanz von SettingsManager zugreift und wie man Einstellungen abruft und ändert:

 

public sealed class SettingsManager
{
    private static SettingsManager? instance = null;
    private static readonly object padlock = new();

    private Dictionary<string, string> settings;

    private SettingsManager()
        {
        // Lade die Einstellungen aus einer Datei oder Datenbank
        settings = new Dictionary<string, string>
                {
            { "Theme", "Dark" },
            { "Language", "English" }
        };
        }

    /// <summary>
    /// Die SettingsManager-Klasse ist threadsicher aufgebaut.
    /// Die Instance-Eigenschaft verwendet das lock-Statement.
    /// Dies stellt sicher, dass nur ein Thread gleichzeitig auf den Codeblock zugreifen kann.
    /// Dies verhindert, dass mehrere Threads gleichzeitig eine Instanz der Klasse erstellen.
    /// </summary>
    public static SettingsManager Instance
        {
        get
                 {
            lock (padlock)
                          {
                instance ??= new SettingsManager();
                return instance;
                           }
                 }
        }

    public string? GetSetting(string key)
        {
        if (settings.ContainsKey(key))
                 {
            return settings[key];
                 }
        else
                 {
            return null;
                  }
         }

    public void SetSetting(string key, string value)
        {
        if (settings.ContainsKey(key))
                 {
            settings[key] = value;
                 }
        else
                 {
            settings.Add(key, value);
                 }
        }
}

class Program
{
    static void Main(string[] args)
         {
        // Zugriff auf die Singleton-Instanz von SettingsManager
        SettingsManager settings = SettingsManager.Instance;

        // Abrufen einer Einstellung
        string? theme = settings.GetSetting("Theme");
        Console.WriteLine("Current theme: " + theme);

        string? language = settings.GetSetting("Language");
        Console.WriteLine("Current language: " + language);

        // Ändern einer Einstellung
        settings.SetSetting("Theme", "Light");
        theme = settings.GetSetting("Theme");
        Console.WriteLine("New theme: " + theme);

        settings.SetSetting("Language", "German");
        language = settings.GetSetting("Language");
        Console.WriteLine("New language: " + language);
         }
}

 

Factory-Pattern mit C#

 

Der nachfolgende Code implementiert das Factory Pattern in C#. Es ist ein Entwurfsmuster, das die Erstellung von Objekten ermöglicht, ohne dass der konkrete Typ des zu erstellenden Objekts angegeben werden muss.
Der erste Teil des Codes definiert eine IProduct-Schnittstelle mit einer Operation-Methode. Diese Schnittstelle definiert das Verhalten, das alle Produktklassen implementieren müssen.
Dann werden als Beispiele drei konkrete Produktklassen definiert: ConcreteProduct1, ConcreteProduct2 und ConcreteProduct3. Jede dieser Klassen implementiert die IProduct-Schnittstelle und gibt hier zur Unterscheidung einen anderen Text zurück, wenn die Operation-Methode aufgerufen wird.

Schließlich wird eine abstrakte Creator-Klasse definiert. Diese Klasse hat eine abstrakte FactoryMethod-Methode, die von den konkreten Creator-Klassen implementiert werden muss. Die Creator-Klasse hat auch eine SomeOperation-Methode, die die FactoryMethod aufruft, um ein neues IProduct-Objekt zu erstellen und damit zu arbeiten.

Drei konkrete Creator-Klassen: ConcreteCreator1, ConcreteCreator2 und ConcreteCreator3 werden definiert. Jede dieser Klassen erbt von der abstrakten Creator-Klasse und implementiert die FactoryMethod-Methode. Die individuellen FactoryMethod-Implementierungen geben ein neues Objekt einer anderen konkreten Produktklasse zurück.

Im Hauptprogramm wird ein Array von Creator-Objekten erstellt und durch diese Objekte iteriert. Für jedes Creator-Objekt wird die SomeOperation-Methode aufgerufen und der individuelle Text wird ausgegeben.

Dieses kurze Beispiel zeigt, wie man das Factory Pattern in C# erstellen und verwenden kann, um Objekte zu generieren, ohne dass der konkrete Typ des zu erstellenden Objekts angegeben werden muss. Stattdessen wird die FactoryMethod der entsprechenden Creator-Klasse aufgerufen, um das gewünschte Objekt zu erstellen.

public interface IProduct
{
    string Operation();
}

public class ConcreteProduct1 : IProduct
{
    public string Operation()
        {
        return "Ich bin ein Auto";
        }
}

public class ConcreteProduct2 : IProduct
{
    public string Operation()
        {
        return "Ich bin ein Fahrrad";
        }
}

public class ConcreteProduct3 : IProduct
{
    public string Operation()
        {
        return "Ich bin ein Motorrad";
        }
}

public abstract class Creator
{
    public abstract IProduct FactoryMethod();

    public string SomeOperation()
        {
        var product = FactoryMethod();
        var result = "Ersteller: Der gleiche Code hat gerade mit folgendem funktioniert: " + product.Operation();
        return result;
        }
}

public class ConcreteCreator1 : Creator
{
    public override IProduct FactoryMethod()
        {
        return new ConcreteProduct1();
        }
}

public class ConcreteCreator2 : Creator
{
    public override IProduct FactoryMethod()
        {
        return new ConcreteProduct2();
        }
}

public class ConcreteCreator3 : Creator
{
    public override IProduct FactoryMethod()
        {
        return new ConcreteProduct3();
        }
}

class Program
{
    static void Main(string[] args)
        {
        Creator[] creators =
        {
            new ConcreteCreator1(),
            new ConcreteCreator2(),
            new ConcreteCreator3()
        };

        foreach (Creator creator in creators)
                 {
            Console.WriteLine(creator.SomeOperation());
                 }
        Console.ReadKey();
        }
}

 


State-Pattern mit C#

Im nachfolgenden Programm verwenden wir das State Pattern, um das Verhalten einer Tür zu modellieren.
Die Tür kann entweder verschlossen (LockedState) oder geöffnet (UnlockedState) sein. Das Verhalten der Tür wird durch die Verwendung eines Schlüssels gesteuert.

Das Programm besteht aus mehreren Klassen und Schnittstellen:

IState: Dies ist eine Schnittstelle, die von den Zustandsklassen implementiert wird. Sie definiert eine Handle-Methode, die von den Zustandsklassen überschrieben wird, um das Verhalten der Tür in Abhängigkeit von ihrem Zustand zu ändern.

LockedState: Dies ist eine Klasse, die den verschlossenen Zustand der Tür repräsentiert. Sie implementiert die IState-Schnittstelle und überschreibt die Handle-Methode. Wenn die Handle-Methode aufgerufen wird und der Schlüssel verwendet wurde (context.KeyUsed == true), öffnet sich die Tür und der Zustand wechselt zu UnlockedState. Wenn ein Schlüssel nicht passt oder falsch verwendet wurde (context.KeyUsed == false), bleibt die Tür verschlossen, und es wird eine Nachricht ausgegeben (Schlüssel passt nicht ins Schloss.).

UnlockedState: Dies ist eine Klasse, die den geöffneten Zustand der Tür repräsentiert. Sie implementiert die IState-Schnittstelle und überschreibt die Handle-Methode. Wenn die Handle-Methode aufgerufen wird und der Schlüssel verwendet wurde (context.KeyUsed == true), wird die Tür verschlossen und der Zustand wechselt zu LockedState. Wenn der Schlüssel nicht passt oder falsch verwendet wurde (context.KeyUsed == false), bleibt die Tür geöffnet, und es wird eine Nachricht ausgegeben (Schlüssel passt nicht ins Schloss.).

Context: Dies ist eine Klasse, die den Kontext des State Patterns darstellt. Sie enthält eine Referenz auf den aktuellen Zustand der Tür (_state) und eine Eigenschaft (KeyUsed), die angibt, ob der Schlüssel verwendet wurde oder nicht. Die Klasse definiert auch zwei Methoden: UseKey und CheckState. Die UseKey-Methode wird verwendet, um die Handlung (Öffnen oder Schließen der Tür mit einem Schlüssel) auszuführen. Die CheckState-Methode wird verwendet, um den aktuellen Zustand der Tür abzufragen.

Das Hauptprogramm (Program) erstellt ein Context-Objekt und initialisiert es mit dem verschlossenen Zustand (new LockedState()). Dann ruft es mehrmals die UseKey- und CheckState-Methoden auf, um das Verhalten der Tür zu steuern und ihren aktuellen Zustand abzufragen.




namespace StatePattern

{
    public interface IState
        {
        void Handle(Context context);
        }

    public class LockedState : IState
        {
        public void Handle(Context context)
                 {
            if (context.KeyUsed)
                          {
                Console.WriteLine("Schlüssel öffnet.");
                context.State = new UnlockedState();
                context.KeyUsed = false;
                           }
            else
                           {
                Console.WriteLine("Schlüssel passt nicht ins Schloss.");
                            }
                   }
          }

    public class UnlockedState : IState
        {
        public void Handle(Context context)
                 {
            if (context.KeyUsed)
                          {
                Console.WriteLine("Schlüssel verschließt.");
                context.State = new LockedState();
                context.KeyUsed = false;
                           }
            else
                          {
                Console.WriteLine("Schlüssel passt nicht ins Schloss.");
                           }
                  }
          }

     public class Context
           {
        private IState _state;
        public bool KeyUsed { get; set; }

        public Context(IState state)
                 {
            this.State = state;
            this.KeyUsed = false;
                  }

        public IState State
                 {
            get => _state;
            set
                          {
                _state = value;
                Console.WriteLine("State: " + _state.GetType().Name);
                          }
                  }

        public void UseKey(bool keyUsed)
                 {
            this.KeyUsed = keyUsed;
            _state.Handle(this);
                 }

        public void CheckState()
                 {
            Console.WriteLine("State: " + _state.GetType().Name);
                  }
          }

    class Program
         {
        static void Main(string[] args)
                 {
            Context c = new Context(new LockedState());
            c.CheckState();
            c.UseKey(false);
            c.CheckState();
            c.UseKey(true);
            c.CheckState();
            c.UseKey(false);
            c.CheckState();
            c.UseKey(true);
            c.CheckState();

            Console.ReadKey();
                 }
        }
}






Visitor- und Composite-Pattern mit C#

Im nächsten Beispiel setzen wir zwei Pattern, nämlich Visitor und Composite ein.

Das Programm verwendet das Visitor- und das Composite-Pattern, um eine Hierarchie von Mitarbeitern in einem Unternehmen darzustellen.
Die Hierarchie besteht aus Manager- und Employee-Elementen, die beide das IElement-Interface implementieren.
Das IElement-Interface definiert eine Accept-Methode, die einen IVisitor akzeptiert.

Das IVisitor-Interface definiert zwei Methoden: Visit(Manager manager) und Visit(Employee employee).
Diese Methoden werden aufgerufen, wenn ein IVisitor ein Manager- oder Employee-Element besucht.

Die Manager-Klasse implementiert das IElement-Interface und stellt einen Manager in der Hierarchie dar.
Ein Manager hat einen Namen (Name) und eine Liste von untergeordneten Elementen (Subordinates).
Die Accept-Methode eines Managers ruft die Visit(Manager manager)-Methode des übergebenen IVisitor auf und ruft dann die Accept-Methode jedes untergeordneten Elements auf.

Die Employee-Klasse implementiert ebenfalls das IElement-Interface und stellt einen Mitarbeiter in der Hierarchie dar.
Ein Mitarbeiter hat nur einen Namen (Name). Die Accept-Methode eines Mitarbeiters ruft einfach die Visit(Employee employee)-Methode des übergebenen IVisitor auf.

Die NameVisitor-Klasse implementiert das IVisitor-Interface. Wenn ein NameVisitor ein Manager- oder Employee-Element besucht, gibt er den Namen des Elements zusammen mit seiner Position aus.

Im Hauptprogramm wird eine Hierarchie von Mitarbeitern erstellt, die aus einem CEO (ceo) besteht, der einen CTO (cto) als Untergebenen hat. Der CTO hat wiederum drei Entwickler (dev1, dev2, dev3) als Untergebene. Ein NameVisitor wird erstellt und der CEO akzeptiert ihn. Dies führt dazu, dass der Name jedes Elements in der Hierarchie zusammen mit seiner Position ausgegeben wird.

Die Ausgabe des Programms sieht folgendermaßen aus:

Alice    (Manager)
Bob      (Manager)
Charlie  (Employee)
Dave     (Employee)
John     (Employee)

 

 

using System;
using System.Collections.Generic;

namespace VisitorCompositeExample
{
    public interface IElement
        {
        void Accept(IVisitor visitor);
        }

    public interface IVisitor
        {
        void Visit(Manager manager);
        void Visit(Employee employee);
        }

    public class Manager : IElement
        {
        public string? Name { get; set; }
        public List<IElement> Subordinates { get; set; } = new List<IElement>();

        public void Accept(IVisitor visitor)
                 {
            visitor.Visit(this);
            foreach (var subordinate in Subordinates)
                          {
                subordinate.Accept(visitor);
                           }
                 }
        }

    public class Employee : IElement
        {
        public string? Name { get; set; }

        public void Accept(IVisitor visitor)
                 {
            visitor.Visit(this);
                  }
         }

    public class NameVisitor : IVisitor
        {
        public void Visit(Manager manager)
                 {
            Console.WriteLine(manager.Name + "\t (Manager)");
                  }

        public void Visit(Employee employee)
                 {
            Console.WriteLine(employee.Name + "\t (Employee)");
                  }
         }

    class Program
        {
        static void Main(string[] args)
                 {
            var ceo = new Manager { Name = "Alice" };
            var cto = new Manager { Name = "Bob" };
            var dev1 = new Employee { Name = "Charlie" };
            var dev2 = new Employee { Name = "Dave" };
            var dev3 = new Employee { Name = "John" };
           
            cto.Subordinates.Add(dev1);

            cto.Subordinates.Add(dev2);
            cto.Subordinates.Add(dev3);
            ceo.Subordinates.Add(cto);

            var visitor = new NameVisitor();
            ceo.Accept(visitor);

            Console.ReadKey();
                   }
        }
}

 

Observer Pattern


Das nachfolgende Programm implementiert das Observer-Pattern in C#, das verwendet wird, um eine Eins-zu-viele-Abhängigkeitsbeziehung zwischen Objekten zu definieren, so dass bei Zustandsänderung eines Objektes alle seine von ihm abhängigen Objekte automatisch benachrichtigt und aktualisiert werden.

Das Programm definiert zwei Schnittstellen: IObserver und ISubject.

Die IObserver-Schnittstelle definiert eine Methode Update, die von Beobachterobjekten implementiert wird. Diese Methode wird aufgerufen, wenn das beobachtete Objekt seinen Zustand ändert.
Die ISubject-Schnittstelle definiert Methoden zum Registrieren und Entfernen von Beobachterobjekten sowie zum Benachrichtigen aller registrierten Beobachter über Zustandsänderungen.

Die WeatherData-Klasse implementiert die ISubject-Schnittstelle. Sie verwaltet eine Liste von Beobachterobjekten und benachrichtigt sie über Änderungen in Temperatur, Luftfeuchtigkeit und Luftdruck.
Die Klasse enthält auch Methoden zum Festlegen neuer Messwerte und zum Abrufen der aktuellen Messwerte.
Die WeatherData-Klasse beinhaltet auch Methoden zum Abrufen der aktuellen Temperatur, Luftfeuchtigkeit und des Luftdrucks.

Die CurrentConditionsDisplay-Klasse implementiert die IObserver-Schnittstelle. Sie registriert sich bei einem ISubject-Objekt, in diesem Fall bei einem WeatherData-Objekt, als Beobachter. Wenn das WeatherData-Objekt seinen Zustand ändert, d.h. wenn neue Messwerte festgestellt werden, wird die Update-Methode der CurrentConditionsDisplay-Klasse aufgerufen. Diese Methode aktualisiert die Anzeigewerte und ruft die Display-Methode auf, um die aktuellen Bedingungen anzuzeigen.

Die Program-Klasse enthält die Main-Funktion, die den Einstiegspunkt für das Programm darstellt. In dieser Methode wird ein WeatherData-Objekt erstellt und ein CurrentConditionsDisplay-Objekt als Beobachter registriert. Dann werden einige Messwerte festgelegt, um das Verhalten des Programms zu demonstrieren.

Insgesamt zeigt dieses kleine Programm, wie das Observer-Muster in C# verwendet werden kann, um eine lose Kopplung zwischen Objekten zu erreichen. Das WeatherData-Objekt ist nicht direkt von der CurrentConditionsDisplay-Klasse abhängig und kann ohne Änderungen mit anderen Klassen verwendet werden. Ebenso kann die CurrentConditionsDisplay-Klasse ohne Änderungen mit anderen Klassen verwendet oder durch eine andere Klasse ersetzt werden, die die IObserver-Schnittstelle implementiert.

 

using System;
using System.Collections.Generic;

namespace ObserverPattern
{
    public interface IObserver
    {
        void Update(WeatherData data);
    }

    public interface ISubject
    {
        void RegisterObserver(IObserver observer);
        void RemoveObserver(IObserver observer);
        void NotifyObservers();
    }

    public class WeatherData : ISubject
    {
        private List<IObserver> observers;
        private float temperature;
        private float humidity;
        private float pressure;

        public WeatherData()
        {
            observers = new List<IObserver>();
        }

        public void RegisterObserver(IObserver observer)
        {
            observers.Add(observer);
        }

        public void RemoveObserver(IObserver observer)
        {
            observers.Remove(observer);
        }

        public void NotifyObservers()
        {
            foreach (IObserver observer in observers)
            {
                observer.Update(this);
            }
        }

        public void MeasurementsChanged()
        {
            NotifyObservers();
        }

        public void SetMeasurements(float temperature, float humidity, float pressure)
        {
            this.temperature = temperature;
            this.humidity = humidity;
            this.pressure = pressure;
            MeasurementsChanged();
        }

        public float GetTemperature()
        {
            return temperature;
        }

        public float GetHumidity()
        {
            return humidity;
        }

        public float GetPressure()
        {
            return pressure;
        }
    }

    public class CurrentConditionsDisplay : IObserver
    {
        private float temperature;
        private float humidity;
        private float pressure;

        public CurrentConditionsDisplay(ISubject weatherData)
        {
            weatherData.RegisterObserver(this);
        }

        public void Update(WeatherData data)
        {
            this.temperature = data.GetTemperature();
            this.humidity = data.GetHumidity();
            this.pressure = data.GetPressure();
            Display();
        }

        public void Display()
        {
            Console.WriteLine("Aktuelle Bedingungen: " + temperature + "°C, " + humidity + "% Luftfeuchtigkeit und " + pressure +" mbar");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            WeatherData weatherData = new WeatherData();

            CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);

            weatherData.SetMeasurements(27, 65, 1013.2f);
            weatherData.SetMeasurements(28, 70, 1012.5f);
            weatherData.SetMeasurements(26, 90, 1012.7f);
        }
    }
}

 

Ausgabe:

Aktuelle Bedingungen: 27°C, 65% Luftfeuchtigkeit und 1013,2 mbar
Aktuelle Bedingungen: 28°C, 70% Luftfeuchtigkeit und 1012,5 mbar
Aktuelle Bedingungen: 26°C, 90% Luftfeuchtigkeit und 1012,7 mbar

 

 

Neuronale Netzwerke mit C#

Heute werden neuronale Netzwerke vielfach eingesetzt. Wir werden nun ein einfaches Beispiel in C# aufbauen.
Hierzu muss man die Accord.Neuro und Accord.Statistics Pakete mittels NuGet-Projektmappe installieren.
Man erreicht NuGet mittels Rechtsklick auf das Projekt.

Das nachstehende C# Programm verwendet ein “künstliches neuronales Netz”, um eine Art von Aufgabe zu lösen.
Ein künstliches neuronales Netz ist eine Art von Computerprogramm, das versucht, wie das menschliche Gehirn zu arbeiten.
Es besteht aus vielen kleinen Teilen, die “Neuronen” genannt werden und die miteinander verbunden sind.
Diese Neuronen können Informationen verarbeiten und das Netzwerk kann lernen, bestimmte Aufgaben zu lösen.

In diesem speziellen Programm wird das künstliche neuronale Netz verwendet, um eine Art von Aufgabe zu lösen, die man “logisches XOR” nennt.
Das bedeutet, dass das Netzwerk lernt, zwei Zahlen zu nehmen und eine bestimmte Antwort zu geben, abhängig davon, ob die Zahlen gleich oder unterschiedlich sind.

Das Programm erledigt dies, indem es einige Beispieldaten verwendet, um das Netzwerk zu trainieren.
Es zeigt dem Netzwerk einige Beispiele von Zahlenpaaren und den richtigen Antworten und lässt das Netzwerk lernen, wie es diese Antworten selbst vorhersagen kann.

Nachdem das Netzwerk trainiert wurde, kann es dann getestet werden, indem es neue Zahlenpaare erhält und versucht, die richtigen Antworten vorherzusagen.
Das Programm zeigt dann an, wie gut das Netzwerk diese Aufgabe lösen kann.

 

using System;
using System.Linq;
using Accord.Neuro;
using Accord.Neuro.Learning;

namespace NeuralNetworkExample
{
  class Program
  {
    static void Main(string[] args)
    {
      // Eingabe- und Ausgabedaten
      double[][] input =
      {
        new[] {0.0, 0.0},
        new[] {1.0, 0.0},
        new[] {0.0, 1.0},
        new[] {1.0, 1.0}
      };

      double[][] output =
      {
        new[] {0.0},
        new[] {1.0},
        new[] {1.0},
        new[] {0.0}
      };

      // Aufteilen der Daten in Trainings- und Validierungsdaten
      int splitIndex = (int)(input.Length * 0.8);
      double[][] trainingInput    = input.Take(splitIndex).ToArray();
      double[][] trainingOutput   = output.Take(splitIndex).ToArray();
      double[][] validationInput  = input.Skip(splitIndex).ToArray();
      double[][] validationOutput = output.Skip(splitIndex).ToArray();

      // Erstelle ein künstliches neuronales Netz
      var function = new SigmoidFunction();
      var network = new ActivationNetwork(function, 2, 2, 1);

      // Erstelle einen Backpropagation-Lernalgorithmus
      var teacher = new BackPropagationLearning(network);

      // Trainiere das Netzwerk
      int iteration = 0;
      double error = double.PositiveInfinity;
      while (error > 1e-5 && iteration < 10000)
      {
        error = teacher.RunEpoch(trainingInput, trainingOutput);
        iteration++;
      }

      // Validiere das trainierte Netzwerk
      int correctPredictions = 0;
      for (int i = 0; i < validationInput.Length; i++)
      {
        double[] predicted = network.Compute(validationInput[i]);
        if (Math.Round(predicted[0]) == validationOutput[i][0])
        {
          correctPredictions++;
        }
      }
      double accuracy = (double)correctPredictions / validationInput.Length;
      Console.WriteLine($"Validation accuracy: {accuracy}");

      // Teste das trainierte Netzwerk
      for (int i = 0; i < input.Length; i++)
      {
        double[] predicted = network.Compute(input[i]);
        Console.WriteLine($"Input: {string.Join(", ", input[i])} | Output: {predicted[0]} | Expected: {output[i][0]}");
      }

      Console.ReadKey();
    }
  }
}

 

 

Ah, die letzte Ausgabe ist ziemlich daneben? OK, da müssen wir etwas nachschärfen und weitere Funktionen und andere Netzwerke testen:


using System;
using System.Linq;
using Accord.Neuro;
using Accord.Neuro.Learning;
using AForge;

namespace NeuralNetworkExample
{
  public class ReLUFunction : IActivationFunction
  {
    public double Function(double x)
    {
      return x > 0 ? x : 0;
    }

    public double Derivative(double x)
    {
      return x > 0 ? 1 : 0;
    }

    public double Derivative2(double y)
    {
      return y > 0 ? 1 : 0;
    }

    public void Randomize() { }
  }

   public class LeakyReLUFunction : IActivationFunction
  {
    private double _alpha;

    public LeakyReLUFunction(double alpha = 0.01)
    {
      _alpha = alpha;
    }

    public double Function(double x)
    {
      return x > 0 ? x : _alpha * x;
    }

    public double Derivative(double x)
    {
      return x > 0 ? 1 : _alpha;
    }

    public double Derivative2(double y)
    {
      return y > 0 ? 1 : _alpha;
    }

    public void Randomize() { }
  }

  class Program
  {
     static void Main(string[] args)
     {
       //Eingabe- und Ausgabedaten
       double[][] input =
       {
         new[] {0.0, 0.0},
         new[] {1.0, 0.0},
         new[] {0.0, 1.0},
         new[] {1.0, 1.0}
       };

       double[][] output =
       {
         new[] {0.0},
         new[] {1.0},
         new[] {1.0},
         new[] {0.0}
       };

       // Aufteilen der Daten in Trainings- und Validierungsdaten
       int splitIndex = (int)(input.Length * 0.8);
       double[][] trainingInput = input.Take(splitIndex).ToArray();
       double[][] trainingOutput = output.Take(splitIndex).ToArray();
       double[][] validationInput = input.Skip(splitIndex).ToArray();
       double[][] validationOutput = output.Skip(splitIndex).ToArray();

       // Erstelle ein künstliches neuronales Netz
    // var function = new SigmoidFunction();
    // var function = new ReLUFunction();
       var function = new LeakyReLUFunction();
       var network = new ActivationNetwork(function, 2, 100, 1);

       // Erstelle einen Backpropagation-Lernalgorithmus
       var teacher = new BackPropagationLearning(network);

       // Trainiere das Netzwerk
       int iteration = 0;
       double error = double.PositiveInfinity;
       while (error > 1e-10 && iteration < 1000000)
       {
         error = teacher.RunEpoch(trainingInput, trainingOutput);
         iteration++;
       }

       // Validiere das trainierte Netzwerk
       int correctPredictions = 0;
       for (int i = 0; i < validationInput.Length; i++)
       {
         double[] predicted = network.Compute(validationInput[i]);
         if (Math.Round(predicted[0]) == validationOutput[i][0])
         {
           correctPredictions++;
         }
       }
       double accuracy = (double)correctPredictions / validationInput.Length;
       Console.WriteLine($"Validation accuracy: {accuracy}");

       // Teste das trainierte Netzwerk
       for (int i = 0; i < input.Length; i++)
       {
         double[] predicted = network.Compute(input[i]);
         Console.WriteLine($"Input: {string.Join(", ", input[i])} | Output: {predicted[0]} | Expected: {output[i][0]}");
       }
      
       Console.ReadKey();
    } 
  }
}

 

Das Ergebnis ist immer noch daneben? Ja, da hilft nur weiter forschen zum Thema NN. ;)
Es ist eine große Kunst.

OK, dann probieren wir noch einen sehr übersichtlichen Ansatz in C#:

using System;
using Accord.Neuro;
using Accord.Neuro.Learning;

namespace BackPropagationXor
{
  class Program
  {
    static void Main(string[] args)
    {
      train();
      Console.ReadKey();
    }

    private static void train()
    {
      // input and output
      double[][] inputs =
      {
        new double[] { 0, 0},
        new double[] { 0, 1},
        new double[] { 1, 0},
        new double[] { 1, 1}
      };

      double[][] results =
      {
        new double[] { 0 },
        new double[] { 1 },
        new double[] { 1 },
        new double[] { 0 }
      };

      // neural network
      ActivationNetwork network = new ActivationNetwork(new SigmoidFunction(), 2, 2, 1);

      // teacher
      BackPropagationLearning teacher = new BackPropagationLearning(network);

      // loop
      for (int i = 0; i < 10000; i++)
      {
        teacher.RunEpoch(inputs, results);
      }

      // test
      for (int i = 0; i < inputs.Length; i++)
      {
        Console.WriteLine("{0} xor {1} = {2}", inputs[i][0], inputs[i][1], network.Compute(inputs[i])[0]);
      }
    }
  }
}

 

Das sieht doch schon interessant aus?

Manchmal bleibt es allerdings bei 0,5 stecken. Interessantes NN-Thema. ;)
XOR ist keine anspruchslose Aufgabe.

Also manchmal klappt es und manchmal nicht?
Das schreit danach, dass wir gelungen trainierte Netzwerke abspeichern und später wieder laden können.
Man muss die Load-/Save-Routinen nur an den richtigen Stellen in unseren Code einbauen:

 

using System;
using System.IO;
using Accord.Neuro;
using Accord.Neuro.Learning;

namespace BackPropagationXor
{
  class Program
  {
    static void Main(string[] args)
    {
      train();
      Console.ReadKey();
    }

    private static void train()
    {
      ActivationNetwork network = null;
      string load = "";
      bool trainNN = false;

      // input and output
      double[][] inputs =
      {
        new double[] { 0, 0},
        new double[] { 0, 1},
        new double[] { 1, 0},
        new double[] { 1, 1}
      };

      double[][] results =
      {
        new double[] { 0 },
        new double[] { 1 },
        new double[] { 1 },
        new double[] { 0 }
      };

      // check if network file exists
      if (File.Exists("network.bin"))
      {
        // ask user if they want to load an existing network
        Console.Write("Möchtest du ein vorhandenes Netzwerk laden? (j/n): ");
        load = Console.ReadLine();

        // load network if user wants to
        if (load == "j")
        {
          network = (ActivationNetwork)Network.Load("network.bin");
          Console.WriteLine("Netzwerk geladen.");
        }
        else
        {
          trainNN = true;
        }
      }
      else
      {
        trainNN = true; 
      }

       if(trainNN)
      {
        // neural network
        network = new ActivationNetwork(new SigmoidFunction(), 2, 2, 1);

        // teacher
        BackPropagationLearning teacher = new BackPropagationLearning(network);

        // loop
        for (int i = 0; i < 10000; i++)
        {
          teacher.RunEpoch(inputs, results);
        }
      }

      // test
      for (int i = 0; i < inputs.Length; i++)
      {
        Console.WriteLine("{0} xor {1} = {2}", inputs[i][0], inputs[i][1], network.Compute(inputs[i])[0]);
      }

      if (!File.Exists("network.bin") && load != "j")
      {
        // ask user if they want to save the network
        Console.Write("Möchtest du das Netzwerk speichern? (j/n): ");
        string save = Console.ReadLine();

        // save network if user wants to
        if (save == "j")
        {
          network.Save("network.bin");
          Console.WriteLine("Netzwerk gespeichert.");
        }
      }
      else if(load != "j")
      {
        // inform user that a network file already exists
        Console.WriteLine("Es existiert bereits eine Datei namens 'network.bin'.");

        // ask user if they want to overwrite the existing file
        Console.Write("Möchtest du die vorhandene Datei überschreiben? (j/n): ");
        string action = Console.ReadLine();

        if (action == "j")
        {
          if (File.Exists("network.bak"))
          {
            File.Delete("network.bak");
          }
          File.Move("network.bin", "network.bak");
          network.Save("network.bin");
          Console.WriteLine("Aktuelles Netzwerk gespeichert und altes Netzwerk gesichert");
        }
      }
    }
  }
}

 

Inzwischen haben sich einige Dateien um unsere exe versammelt, und das Netzwerk wird in network.bin gespeichert.

Damit haben wir nun eine Ausgangsbasis für erwünschte Input-/Output-Szenarien und weitere Versuche.
Arbeiten Sie mit obigem Code, um mehr über C# und neuronale Netzwerke zu erfahren.
Viel Spaß dabei!

 

 

Parkhaus (Zusammenspiel von Klassen, UML)

Nachdem wir nun die Grundzüge von C# kennengelernt haben, gehen wir die Umsetzung eines Projektes mit mehreren Klassen an. Wir simulieren ein Parkhaus im Kern. Was finden wir dort? Einen Parkautomaten, vielleicht noch Tickets und Bargeldzahlung, eine Schranke für Ein- und Ausfahrt. Wir bauen entsprechende "Module", das sind die Files mit Endung cs, auf:

Program.cs

using System;

namespace Parkhaus
{
  class Program
  {
    static void Main(string[] args)
    {
      ParkAutomat automat = new ParkAutomat();
      automat.StarteAutomat();
    }
  }
}

Automatenstatus.cs

namespace Parkhaus
{
  public enum AutomatenStatus
  {
    Idle,
    TicketGezogen,
    ParkzeitBezahlt,
    AusfahrtErlaubt
  }
}


Geldwechsel.cs

using System;
using System.Collections.Generic;

namespace Parkhaus
{
  public class GeldWechsel
  {
    private readonly List<decimal> _verfuegbareScheine = new List<decimal> { 50m, 20m, 10m, 5m, 2m, 1m };
    private readonly List<decimal> _verfuegbareMuenzen = new List<decimal> { 0.50m, 0.20m, 0.10m, 0.05m, 0.02m, 0.01m };

    public Dictionary<decimal, int> BerechneWechselgeld(decimal betrag)
    {
      decimal rest = betrag;
      var wechselgeld = new Dictionary<decimal, int>();

      foreach (var s in _verfuegbareScheine)
      {
        if (rest >= s)
        {
          int anzahl = (int)(rest / s);
          wechselgeld[s] = anzahl;
          rest -= anzahl * s;
          rest = Math.Round(rest, 2);
        }
      }

      foreach (var m in _verfuegbareMuenzen)
      {
        if (rest >= m)
        {
          int anzahl = (int)(rest / m);
          wechselgeld[m] = anzahl;
          rest -= anzahl * m;
          rest = Math.Round(rest, 2);
        }
      }

      return wechselgeld;
    }

    public void AusgabeWechselgeld(Dictionary<decimal, int> wechselgeld)
    {
      Console.WriteLine("Wechselgeld:");
      foreach (var eintrag in wechselgeld)
      {
        if (eintrag.Value > 0)
        {
          // Betrag mit zwei Nachkommastellen anzeigen
          Console.WriteLine($"{eintrag.Value} x {eintrag.Key:F2} EUR");
        }
      }
    }
  }
}


Parkautomat.cs

using System;

namespace Parkhaus
{
  public class ParkAutomat
  {
    private Ticket? _aktuellesTicket; // Nullable, da es initial null ist
    private Zahlung _zahlung;
    private GeldWechsel _geldWechsel;
    private Schranke _schranke;
    private AutomatenStatus _status { get; set; } = AutomatenStatus.Idle;
    public AutomatenStatus Status => _status; // Öffentlicher Getter

    public ParkAutomat()
    {
      _zahlung = new Zahlung();
      _geldWechsel = new GeldWechsel();
      _schranke = new Schranke(this); // Übergibt die aktuelle Instanz an Schranke
      _status = AutomatenStatus.Idle;
    }

    public void StarteAutomat()
    {
      Console.WriteLine("Willkommen beim Parkautomaten!");

      while (true)
      {
        Console.WriteLine("\nBitte wählen Sie eine Option:");
        switch (_status)
        {
          case AutomatenStatus.Idle:
            Console.WriteLine("1. Parkticket ziehen");
            Console.WriteLine("3. Beenden");
            break;
          case AutomatenStatus.TicketGezogen:
            Console.WriteLine("2. Parkzeit bezahlen");
            Console.WriteLine("3. Beenden");
            break;
          case AutomatenStatus.ParkzeitBezahlt:
            Console.WriteLine("4. Schranke öffnen (ausfahren)");
            Console.WriteLine("3. Beenden");
            break;
          case AutomatenStatus.AusfahrtErlaubt:
            Console.WriteLine("3. Beenden");
            break;
        }
       
        Console.WriteLine("----------------------");

        string? auswahl = Console.ReadLine();

        switch (_status)
        {
          case AutomatenStatus.Idle:
            HandleIdleState(auswahl);
            break;
          case AutomatenStatus.TicketGezogen:
            HandleTicketGezogenState(auswahl);
            break;
          case AutomatenStatus.ParkzeitBezahlt:
            HandleParkzeitBezahltState(auswahl);
            break;
          case AutomatenStatus.AusfahrtErlaubt:
            HandleAusfahrtErlaubtState(auswahl);
            break;
        }
      }
    }

    private void HandleIdleState(string? auswahl)
    {
      switch (auswahl)
      {
        case "1":
          ZieheTicket();
          break;
        case "3":
          Console.WriteLine("Auf Wiedersehen!");
          Environment.Exit(0);
          break;
        default:
          Console.WriteLine("Ungültige Auswahl. Bitte versuchen Sie es erneut.");
          break;
      }
    }

    private void HandleTicketGezogenState(string? auswahl)
    {
      switch (auswahl)
      {
        case "2":
          BezahleParkzeit();
          break;
        case "3":
          Console.WriteLine("Auf Wiedersehen!");
          Environment.Exit(0);
          break;
        default:
          Console.WriteLine("Ungültige Auswahl. Bitte versuchen Sie es erneut.");
          break;
      }
    }

    private void HandleParkzeitBezahltState(string? auswahl)
    {
      switch (auswahl)
      {
        case "4":
          FahreAus();
          break;
        case "3":
          Console.WriteLine("Auf Wiedersehen!");
          Environment.Exit(0);
          break;
        default:
          Console.WriteLine("Ungültige Auswahl. Bitte versuchen Sie es erneut.");
          break;
      }
    }

    private void HandleAusfahrtErlaubtState(string? auswahl)
    {
      switch (auswahl)
      {
        case "3":
          Console.WriteLine("Auf Wiedersehen!");
          Environment.Exit(0);
          break;
        default:
          Console.WriteLine("Ungültige Auswahl. Bitte versuchen Sie es erneut.");
          break;
      }
    }

    private void ZieheTicket()
    {
      _aktuellesTicket = new Ticket();
      _aktuellesTicket.DruckeTicket();
      _status = AutomatenStatus.TicketGezogen;
      _schranke.ÖffneSchranke();
      _schranke.SchließeSchranke();

      Console.WriteLine("Bitte suchen Sie Ihren Parkplatz und kehren Sie später zurück, um die Parkzeit zu bezahlen.");
    }

    private void BezahleParkzeit()
    {
      if (_status != AutomatenStatus.TicketGezogen || _aktuellesTicket == null)
      {
        Console.WriteLine("Bitte ziehen Sie zuerst ein Parkticket.");
        return;
      }

      Console.WriteLine("Bezahlvorgang gestartet...");

      double parkStunden = _aktuellesTicket.Parkzeit;
      decimal gesamtPreis = _zahlung.BerechnePreis(parkStunden);

      // Gesamtpreis mit zwei Nachkommastellen anzeigen
      Console.WriteLine($"Gesamtpreis für {parkStunden} Stunden: {gesamtPreis:F2} EUR");
      Console.WriteLine("Bitte geben Sie das Geld ein (z.B. 1, 2, 5, 10, 20, 50):");
      while (_zahlung.EntgegengenommenesGeld < gesamtPreis)
      {
        Console.Write("Betrag eingeben: ");
        string? eingabe = Console.ReadLine();
        if (decimal.TryParse(eingabe, out decimal betrag))
        {
          if (IstGültigesGeld(betrag))
          {
            _zahlung.ZahlEingeben(betrag);

            // Prüfen, ob die Zahlung noch nicht vollständig ist
            if (_zahlung.EntgegengenommenesGeld < gesamtPreis)
            {
              decimal restbetrag = gesamtPreis - _zahlung.EntgegengenommenesGeld;
              Console.WriteLine($"Restbetrag: {restbetrag:F2} EUR");
            }
          }
          else
          {
            Console.WriteLine("Ungültiger Betrag. Akzeptierte Scheine/Münzen: 0.01, 0.02, 0.05, 0.10, 0.20, 0.50, 1, 2, 5, 10, 20, 50 EUR.");
          }
        }
        else
        {
          Console.WriteLine("Ungültiger Betrag. Bitte versuchen Sie es erneut.");
        }
      }

      decimal wechsel = _zahlung.EntgegengenommenesGeld - gesamtPreis;
      if (wechsel > 0)
      {
        var wechselgeld = _geldWechsel.BerechneWechselgeld(wechsel);
        _geldWechsel.AusgabeWechselgeld(wechselgeld);
      }

      Console.WriteLine("Vielen Dank für Ihre Zahlung.");

      // Aktualisiere den Status
      _status = AutomatenStatus.ParkzeitBezahlt;

      // Reset der Zahlung für zukünftige Zahlungen
      _zahlung = new Zahlung();
    }

    private bool IstGültigesGeld(decimal betrag)
    {
      List<decimal> akzeptierteBeträge = new List<decimal> { 0.01m, 0.02m, 0.05m, 0.10m, 0.20m, 0.50m, 1m, 2m, 5m, 10m, 20m, 50m };
      return akzeptierteBeträge.Contains(betrag);
    }

    private void FahreAus()
    {
      if (_status != AutomatenStatus.ParkzeitBezahlt)
      {
        Console.WriteLine("Sie müssen zuerst die Parkzeit bezahlen.");
        return;
      }

      Console.WriteLine("Bitte stecken Sie Ihr Ticket in die Schranke, um auszufahren.");
      Console.Write("Ticket ID eingeben: ");
      string? ticketEingabe = Console.ReadLine();

      if (Guid.TryParse(ticketEingabe, out Guid ticketId))
      {
        if (_aktuellesTicket != null && _aktuellesTicket.TicketId == ticketId)
        {
          _schranke.ÖffneSchranke();
          Console.WriteLine("\nVielen Dank für Ihren Besuch!");
          // Reset des Automaten für den nächsten Benutzer
          _aktuellesTicket = null;
          _status = AutomatenStatus.Idle;
        }
        else
        {
          Console.WriteLine("Ungültiges Ticket.");
        }
      }
      else
      {
        Console.WriteLine("Ungültige Ticket-ID.");
      }
    }
  }
}


Schranke.cs

using System;
using System.Net.NetworkInformation;

namespace Parkhaus
{
  public class Schranke
  {
    private ParkAutomat p; // Feld als privat deklarieren

    // Neuer Konstruktor, der eine ParkAutomat-Instanz entgegennimmt
    public Schranke(ParkAutomat parkAutomat)
    {
      p = parkAutomat;
    }

    public void ÖffneSchranke()
    {
      if (p.Status == AutomatenStatus.ParkzeitBezahlt)
      {
        Console.WriteLine("Die Schranke öffnet sich. Sie können jetzt ausfahren.");
      }
      else if (p.Status == AutomatenStatus.TicketGezogen)
      {
        Console.WriteLine("Die Schranke öffnet sich. Sie können jetzt einfahren.");
      }
    }

    public void SchließeSchranke()
    {
      Console.WriteLine("Die Schranke ist geschlossen.");
    }
  }
}


Ticket.cs

using System;

namespace Parkhaus
{
  public class Ticket
  {
    public Guid TicketId { get; private set; }
    public DateTime Einfahrtszeit { get; private set; }
    public double Parkzeit { get; private set; } // in Stunden

    public Ticket()
    {
      TicketId = Guid.NewGuid();
      Einfahrtszeit = DateTime.Now;
      Parkzeit = GeneriereParkzeit();
    }

    private double GeneriereParkzeit()
    {
      Random rnd = new Random();
      // Generiere eine Parkzeit zwischen 0,5 und 10 Stunden, in 0,5-Stunden-Schritten
      int halbstunden = rnd.Next(1, 21); // 0,5 h bis 10 h
      return halbstunden * 0.5;
    }

    public void DruckeTicket()
    {
      Console.WriteLine("----- Parkticket -----");
      Console.WriteLine($"Ticket ID: {TicketId}");
      Console.WriteLine($"Einfahrtszeit: {Einfahrtszeit}");
      Console.WriteLine("----------------------");
    }
  }
}


Zahlung.cs

using System;

namespace Parkhaus
{
  public class Zahlung
  {
    public decimal PreisProStunde { get; private set; } = 2.50m; // Preis pro Stunde

    public decimal BerechnePreis(double stunden)
    {
      return (decimal)stunden * PreisProStunde;
    }

    public decimal EntgegengenommenesGeld { get; private set; } = 0m;

    public void ZahlEingeben(decimal betrag)
    {
      EntgegengenommenesGeld += betrag;
      Console.WriteLine($"Entgegengenommen: {betrag} EUR");
    }
  }
}


Führen wir das Programm aus, könnte es wie folgt verlaufen:




Wir hätten gerne noch ein UML-Diagramm für dieses Projekt? Kein Problem. ChatGPT liefert uns sofort folgenden Text:

@startuml
!define ENUM class

enum AutomatenStatus {
Idle
TicketGezogen
ParkzeitBezahlt
AusfahrtErlaubt
}

class ParkAutomat {
- Ticket? _aktuellesTicket
- Zahlung _zahlung
- GeldWechsel _geldWechsel
- Schranke _schranke
- AutomatenStatus _status { get; set; } = AutomatenStatus.Idle
+ AutomatenStatus Status { get }

+ ParkAutomat()
+ void StarteAutomat()
- void HandleIdleState(string? auswahl)
- void HandleTicketGezogenState(string? auswahl)
- void HandleParkzeitBezahltState(string? auswahl)
- void HandleAusfahrtErlaubtState(string? auswahl)
- void ZieheTicket()
- void BezahleParkzeit()
- bool IstGültigesGeld(decimal betrag)
- void FahreAus()
}

class Schranke {
- ParkAutomat p

+ Schranke(ParkAutomat parkAutomat)
+ void ÖffneSchranke()
+ void SchließeSchranke()
}

class Ticket {
+ Guid TicketId { get; }
+ DateTime Einfahrtszeit { get; }
+ double Parkzeit { get; }

+ Ticket()
+ void DruckeTicket()
}

class Zahlung {
+ decimal PreisProStunde { get; private set; } = 2.50m
+ decimal EntgegengenommenesGeld { get; private set; } = 0m

+ BerechnePreis(double stunden) : decimal
+ ZahlEingeben(decimal betrag) : void
}

class GeldWechsel {
- List<decimal> _verfuegbareScheine
- List<decimal> _verfuegbareMuenzen

+ GeldWechsel()
+ BerechneWechselgeld(decimal betrag) : Dictionary<decimal, int>
+ AusgabeWechselgeld(Dictionary<decimal, int> wechselgeld) : void
}

class Program {
+ static void Main(string[] args)
}

' Beziehungen
ParkAutomat "1" -- "0..1" Ticket : besitzt >
ParkAutomat "1" --> "1" Zahlung : verwendet >
ParkAutomat "1" --> "1" GeldWechsel : verwendet >
ParkAutomat "1" --> "1" Schranke : verwendet >
Schranke "1" --> "1" ParkAutomat : referenziert >

Program "1" --> "1" ParkAutomat : erstellt >
@enduml



Wir fügen diesen Text unter folgendem Link ein: https://www.planttext.com/

Wir erhalten folgende grafische Darstellung:

Dies ist ein sogenanntes Unified Modeling Language (UML) Diagramm. UML dient zur Modellierung objektorientierter Projekte. Man erhält hiermit eine gute Übersicht über die enthaltenen Klassen mit ihren Daten und Methoden und ihre Beziehungen untereinander.

Vorschlag: Bauen Sie dieses Projekt um auf ticketloses Parken. An der Schranke wird das Kennzeichen erfasst, das beim Bezahlen eingegeben wird. Bei der Ausfahrt wird das Kennzeichen überprüft. Falls bezahlt, wird die Schranke geöffnet. Passen Sie das UML-Diagramm entsprechend an.

 

Bibliotheksverwaltung

Nachfolgend erstellen wir eine Verwaltung für die Erfassung und den Verleih von Büchern sowie von Mitgliedern der Bücherei. Zunächst das Klassendesign:


Im Modul Program.cs stellen wir die Klassen und ihre Funktionen beispielhaft vor:

using System;
using System.Collections.Generic;

namespace LibraryManagement
{
  public static class LibraryKonstanten
  {
    /// <summary>
    /// Ausleihefrist in Tagen
    /// </summary>
    public const int LoanPeriodDays = 14;
  }
 
  class Program
  {
    static void Main(string[] args)
    {
      // Bibliothek erstellen
      Library library = new();

      // Bücher hinzufügen
      Book book1 = new Book("1984", "George Orwell", "1234567890");
      Book book2 = new Book("To Kill a Mockingbird", "Harper Lee", "0987654321");
      library.AddBookToCatalog(book1);
      library.AddBookToCatalog(book2);

      Console.WriteLine(); // Leerzeile für bessere Lesbarkeit
      Console.ReadKey();

      // Mitglieder registrieren
      Member member1 = new Member("Alice", "M001");
      Member member2 = new Member("Bob", "M002");
      library.RegisterMember(member1);
      library.RegisterMember(member2);

      Console.WriteLine(); // Leerzeile für bessere Lesbarkeit
      Console.ReadKey();

      // Buch ausleihen
      library.LoanBook("1234567890", "M001");

      Console.WriteLine(); // Leerzeile für bessere Lesbarkeit
      Console.ReadKey();

      // Versuch, das gleiche Buch erneut auszuleihen
      library.LoanBook("1234567890", "M002");

      Console.WriteLine(); // Leerzeile für bessere Lesbarkeit
      Console.ReadKey();

      // Buch zurückgeben
      library.ReturnBook("1234567890", "M001");

      Console.WriteLine(); // Leerzeile für bessere Lesbarkeit
      Console.ReadKey();

      // Jetzt kann das Buch von Bob ausgeliehen werden
      library.LoanBook("1234567890", "M002");

      Console.WriteLine(); // Leerzeile für bessere Lesbarkeit
      Console.ReadKey();

      // Bücher suchen
      Console.WriteLine("Suche nach Büchern mit '1984':");
      List<Book> searchResults = library.SearchBooks(title: "1984");
      foreach (var book in searchResults)
      {
        Console.WriteLine($"- {book.Title} von {book.Author}");
      }

      Console.WriteLine(); // Leerzeile für bessere Lesbarkeit
      Console.ReadKey();

      // Optional: Alle Mitglieder auflisten
      library.ListAllMembers();

      Console.WriteLine(); // Leerzeile für bessere Lesbarkeit
      Console.ReadKey();

      // Optional: Alle Ausleihen auflisten
      library.ListAllLoans();

      // Warten, bis der Benutzer eine Taste drückt
      Console.WriteLine("\nDrücken Sie eine beliebige Taste, um das Programm zu beenden...");
      Console.ReadKey();
    }
  }
}


Das zentrale Element der Bücherei ist das Buch. Wir setzen in Book.cs die Eigenschaften, das Erzeugen und Ausleihe/Rückgabe um.
Hier folgt die Klasse für diese Objekte:

using System;

namespace LibraryManagement
{
  public class Book
  {
    // Eigenschaften
    public string Title { get; private set; }
    public string Author { get; private set; }
    public string ISBN { get; private set; }
    public bool IsAvailable { get; private set; }

    // Konstruktor
    public Book(string title, string author, string isbn)
    {
      Title = title;
      Author = author;
      ISBN = isbn;
      IsAvailable = true;
    }

    // Methode zum Ausleihen des Buches
    public void Checkout()
    {
      if (IsAvailable)
      {
        IsAvailable = false;
        Console.WriteLine($"'{Title}' wurde ausgeliehen.");
      }
      else
      {
        Console.WriteLine($"'{Title}' ist derzeit nicht verfügbar.");
      }
    }

    // Methode zur Rückgabe des Buches
    public void ReturnBook()
    {
      IsAvailable = true;
      Console.WriteLine($"'{Title}' wurde zurückgegeben.");
    }
  }
}


Nun folgt der Katalog, der die Bücher in einer Liste erfasst, in Catalog.cs:

using System;
using System.Collections.Generic;
using System.Linq;

namespace LibraryManagement
{
  public class Catalog
  {
    // Liste aller Bücher im Katalog
    private List<Book> books;

    // Konstruktor
    public Catalog()
    {
      books = new List<Book>();
    }

    // Methode zum Hinzufügen eines Buches
    public void AddBook(Book book)
    {
      books.Add(book);
      Console.WriteLine($"'{book.Title}' wurde zum Katalog hinzugefügt.");
    }

    // Methode zum Entfernen eines Buches nach ISBN
    public void RemoveBook(string isbn)
    {
      Book? book = FindBookByISBN(isbn);
      if (book != null)
      {
        books.Remove(book);
        Console.WriteLine($"'{book.Title}' wurde aus dem Katalog entfernt.");
      }
      else
      {
        Console.WriteLine($"Buch mit ISBN {isbn} nicht gefunden.");
      }
    }

    // Methode zur Suche eines Buches nach ISBN
    public Book? FindBookByISBN(string isbn)
    {
      return books.FirstOrDefault(b => b.ISBN == isbn);
    }

    // Methode zur Suche von Büchern nach Titel und/oder Autor
    public List<Book> SearchBooks(string? title = null, string? author = null)
    {
      IEnumerable<Book> results = books;

      if (!string.IsNullOrEmpty(title))
      {
        results = results.Where(b => b.Title.IndexOf(title, StringComparison.OrdinalIgnoreCase) >= 0);
      }

      if (!string.IsNullOrEmpty(author))
      {
        results = results.Where(b => b.Author.IndexOf(author, StringComparison.OrdinalIgnoreCase) >= 0);
      }

      return results.ToList();
    }

    // Methode zur Auflistung aller Bücher (optional)
    public void ListAllBooks()
    {
      Console.WriteLine("Alle Bücher im Katalog:");
      foreach (var book in books)
      {
        Console.WriteLine($"- {book.Title} von {book.Author} (ISBN: {book.ISBN})");
      }
    }
  }
}

Die eigentliche Bücherei setzen wir im Modul Library.cs um:

using System;
using System.Collections.Generic;
using System.Linq; // Erleichtert die Suche und Filterung von Listen

namespace LibraryManagement
{
  public class Library
  {
    // Eigenschaften
    private Catalog catalog;
    private List<Member> members;
    private List<Loan> loans;

    // Konstruktor
    public Library()
    {
      catalog = new Catalog();
      members = new List<Member>();
      loans = new List<Loan>();
    }

    // Methode zum Hinzufügen eines Buches zum Katalog
    public void AddBookToCatalog(Book book)
    {
      catalog.AddBook(book);
    }

    // Methode zum Entfernen eines Buches aus dem Katalog
    public void RemoveBookFromCatalog(string isbn)
    {
      catalog.RemoveBook(isbn);
    }

    // Methode zur Registrierung eines Mitglieds
    public void RegisterMember(Member member)
    {
      members.Add(member);
      Console.WriteLine($"Mitglied '{member.Name}' wurde registriert.");
    }

    // Methode zum Ausleihen eines Buches
    public void LoanBook(string isbn, string memberId)
    {
      Book? book = catalog.FindBookByISBN(isbn);
      Member? member = FindMemberById(memberId);

      if (book != null && member != null)
      {
        if (book.IsAvailable)
        {
          member.BorrowBook(book);
          Loan loan = new Loan(book, member);
          loans.Add(loan);
          Console.WriteLine($"Ausleihe registriert: {member.Name} - '{book.Title}' (Fällig am: {loan.DueDate.ToShortDateString()})");
        }
        else
        {
          Console.WriteLine($"'{book.Title}' ist derzeit nicht verfügbar.");
        }
      }
      else
      {
        Console.WriteLine("Buch oder Mitglied nicht gefunden.");
      }
    }

    // Methode zur Rückgabe eines Buches
    public void ReturnBook(string isbn, string memberId)
    {
      Book? book = catalog.FindBookByISBN(isbn);
      Member? member = FindMemberById(memberId);

      if (book != null && member != null)
      {
        member.ReturnBook(book);
        Loan? loan = FindLoan(book, member);
        if (loan != null)
        {
          loans.Remove(loan);
          Console.WriteLine($"Ausleihe entfernt: {member.Name} - '{book.Title}'");
        }
        else
        {
          Console.WriteLine("Keine entsprechende Ausleihe gefunden.");
        }
      }
      else
      {
        Console.WriteLine("Buch oder Mitglied nicht gefunden.");
      }
    }

    // Methode zur Suche eines Mitglieds nach ID
    private Member? FindMemberById(string memberId)
    {
      return members.FirstOrDefault(m => m.MemberID == memberId);
    }

    // Methode zur Suche einer Ausleihe
    private Loan? FindLoan(Book book, Member member)
    {
      return loans.FirstOrDefault(l => l.Book == book && l.Member == member);
    }

    // Methode zur Suche von Büchern
    public List<Book> SearchBooks(string? title = null, string? author = null)
    {
      return catalog.SearchBooks(title, author);
    }

    // Optionale Methode zur Auflistung aller Mitglieder
    public void ListAllMembers()
    {
      Console.WriteLine("Alle Mitglieder:");
      foreach (var member in members)
      {
        Console.WriteLine($"- {member.Name} (ID: {member.MemberID})");
      }
    }

    // Optionale Methode zur Auflistung aller Ausleihen
    public void ListAllLoans()
    {
      Console.WriteLine("Alle Ausleihen:");
      foreach (var loan in loans)
      {
        string status = loan.IsOverdue() ? "Überfällig" : "In Ordnung";
        Console.WriteLine($"- {loan.Member.Name} hat '{loan.Book.Title}' ausgeliehen. Fällig am: {loan.DueDate.ToShortDateString()} ({status})");
      }
    }
  }
}


Die Ausleihe wird im Modul Loan.cs realisiert:

using System;

namespace LibraryManagement
{
  public class Loan
  {
    // Eigenschaften
    public Book Book { get; private set; }
    public Member Member { get; private set; }
    public DateTime LoanDate { get; private set; }
    public DateTime DueDate { get; private set; }

    // Ausleihfrist in Tagen
    private const int LoanPeriodDays = LibraryKonstanten.LoanPeriodDays;

    // Konstruktor
    public Loan(Book book, Member member)
    {
      Book = book;
      Member = member;
      LoanDate = DateTime.Today;
      DueDate = LoanDate.AddDays(LoanPeriodDays);
    }

    // Methode zur Überprüfung, ob die Ausleihe überfällig ist
    public bool IsOverdue()
    {
      return DateTime.Today > DueDate;
    }
  }
}


Die Benutzer der Bücherei werden in der Klasse Member im Modul Member.cs beschrieben:

using System;
using System.Collections.Generic;

namespace LibraryManagement
{
  public class Member
  {
    // Eigenschaften
    public string Name { get; private set; }
    public string MemberID { get; private set; }
    public List<Book> BorrowedBooks { get; private set; }

    // Konstruktor
    public Member(string name, string memberId)
    {
      Name = name;
      MemberID = memberId;
      BorrowedBooks = new List<Book>();
    }

    // Methode zum Ausleihen eines Buches
    public void BorrowBook(Book book)
    {
      if (book.IsAvailable)
      {
        book.Checkout();
        BorrowedBooks.Add(book);
        Console.WriteLine($"{Name} hat '{book.Title}' ausgeliehen.");
      }
      else
      {
        Console.WriteLine($"'{book.Title}' ist nicht verfügbar.");
      }
    }

    // Methode zur Rückgabe eines Buches
    public void ReturnBook(Book book)
    {
      if (BorrowedBooks.Contains(book))
      {
        book.ReturnBook();
        BorrowedBooks.Remove(book);
        Console.WriteLine($"{Name} hat '{book.Title}' zurückgegeben.");
      }
      else
      {
        Console.WriteLine($"{Name} hat '{book.Title}' nicht ausgeliehen.");
      }
    }
  }
}


Nach Start und mehrfacher Tasteneingabe ergibt sich folgender Ablauf:

 

Der Code für dieses kleine Projekt findet man hier (incl. UML Klassen-Diagram).

Vorschlag: Bauen Sie dieses Programm so aus, dass Sie eine echte Bibliotheksverwaltung erhalten. Beachtung sollte finden, dass nicht alle Bücher eine ISBN besitzen. 

 

wird fortgesetzt