Visitor- und Composite-Pattern
Neuronales Netzwerk
Parkhaus (Zusammenspiel von
Klassen, UML)
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();
}
}
}
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.
true
):
Die Methode gibt zahl
zurück und beendet sich durch return
.false
):
Der Benutzer wird informiert, dass die Eingabe ungültig war, und die
Schleife beginnt von vorn.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.
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.
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%.
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.
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:
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();
}
}
}
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.
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.
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.
*/
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
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.
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();
}
}
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;
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
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!
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.
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