Visitor- und Composite-Pattern
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 ind er 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 sicher im giültigen Bereich von 0 bis 15 der ForegroundColor-Eigenschaft 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.
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.
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!
wird fortgesetzt