Ihr Notizbuch hat einen Haken: Beenden Sie es, sind alle Notizen weg. Die Liste lebt im Arbeitsspeicher, und den gibt das Betriebssystem frei, sobald das Programm endet. Was bleiben soll, muss in eine Datei. In dieser letzten Lektion lernt Ihr Notizbuch genau das, und dann ist es fertig.
In eine Datei schreiben und daraus lesen
.NET bringt mit File einen Baustein für Dateien mit. Für Textzeilen genügen drei seiner Methoden:
// datei.cs
List<string> zeilen = ["erste Zeile", "zweite Zeile"];
File.WriteAllLines("probe.txt", zeilen);
Console.WriteLine($"Gibt es probe.txt? {File.Exists("probe.txt")}");
string[] gelesen = File.ReadAllLines("probe.txt");
Console.WriteLine($"{gelesen.Length} Zeilen gelesen:");
foreach (string zeile in gelesen)
{
Console.WriteLine(zeile);
}
Gibt es probe.txt? True
2 Zeilen gelesen:
erste Zeile
zweite Zeile
File.WriteAllLines(name, liste)schreibt jeden Eintrag als eigene Zeile in die Datei. Gibt es die Datei schon, wird sie dabei überschrieben.File.Exists(name)sagt, ob es eine Datei mit diesem Namen gibt.File.ReadAllLines(name)liest alle Zeilen. Es liefert ein Arraystring[], das Sie aus Lektion 7 kennen. Die Länge eines Arrays heißtLength, nichtCount. Gibt es die Datei nicht, brichtReadAllLinesmit einer Ausnahme ab. Darum fragt man vorher mitExists.
Wo liegt probe.txt jetzt? Im Ordner, in dem Sie das Programm gestartet haben, also im aktuellen Ordner Ihres Terminals. Sehen Sie mit
ls nach. Die Datei ist reiner Text; öffnen Sie sie ruhig mit Ihrem Editor.
Das Notizbuch merkt sich alles
Zwei neue Methoden reichen. Lade liest die Notizen beim Start, falls es die Datei schon gibt, und liefert sonst eine leere Liste:
// Ausschnitt aus notizbuch.cs
static List<string> Lade(string datei)
{
List<string> notizen = [];
if (File.Exists(datei))
{
foreach (string zeile in File.ReadAllLines(datei))
{
notizen.Add(zeile);
}
}
return notizen;
}
Speichere schreibt die ganze Liste in die Datei:
// Ausschnitt aus notizbuch.cs
static void Speichere(string datei, List<string> notizen)
{
File.WriteAllLines(datei, notizen);
}
Das Hauptprogramm lädt die Notizen beim Start und speichert nach jeder Änderung, also nach den Menüpunkten 1 und 4. Das kostet bei ein
paar hundert Notizen keine spürbare Zeit und hat einen großen Vorteil: Schließt jemand das Terminalfenster, statt mit 0 zu beenden, ist
trotzdem nichts verloren. Speichere ruft das Hauptprogramm auf, nicht NeueNotiz oder Loesche. So behält jede Methode ihre eine
Aufgabe: Die einen ändern die Liste, die andere schreibt sie weg.
Der Dateiname steht nur an einer Stelle, in der Variablen datei, und wird an die Methoden übergeben. Das ist Ihr fertiges Notizbuch:
// notizbuch.cs
string datei = "notizen.txt";
List<string> notizen = Lade(datei);
Console.WriteLine($"Ihr Notizbuch. Gespeicherte Notizen: {notizen.Count}");
bool weiter = true;
while (weiter)
{
ZeigeAuswahl();
string wahl = (Console.ReadLine() ?? "0").Trim();
if (wahl == "1")
{
NeueNotiz(notizen);
Speichere(datei, notizen);
}
else if (wahl == "2")
{
ZeigeAlle(notizen);
}
else if (wahl == "3")
{
Suche(notizen);
}
else if (wahl == "4")
{
Loesche(notizen);
Speichere(datei, notizen);
}
else if (wahl == "0")
{
weiter = false;
}
else
{
Console.WriteLine("Diese Auswahl gibt es nicht.");
}
}
Console.WriteLine("Bis bald!");
static void ZeigeAuswahl()
{
Console.WriteLine();
Console.WriteLine("1 Neue Notiz");
Console.WriteLine("2 Alle Notizen zeigen");
Console.WriteLine("3 Suchen");
Console.WriteLine("4 Notiz löschen");
Console.WriteLine("0 Beenden");
Console.Write("Ihre Wahl: ");
}
static void NeueNotiz(List<string> notizen)
{
Console.Write("Notiz: ");
string text = (Console.ReadLine() ?? "").Trim();
if (text == "")
{
Console.WriteLine("Eine leere Notiz lege ich nicht an.");
return;
}
notizen.Add(text);
Console.WriteLine($"Notiert. Anzahl der Notizen: {notizen.Count}");
}
static void ZeigeAlle(List<string> notizen)
{
if (notizen.Count == 0)
{
Console.WriteLine("Noch keine Notizen.");
return;
}
for (int i = 0; i < notizen.Count; i++)
{
Console.WriteLine($"{i + 1}. {notizen[i]}");
}
}
static void Suche(List<string> notizen)
{
Console.Write("Suchwort: ");
string suchwort = (Console.ReadLine() ?? "").Trim();
if (suchwort == "")
{
Console.WriteLine("Ohne Suchwort suche ich nicht.");
return;
}
int treffer = 0;
for (int i = 0; i < notizen.Count; i++)
{
if (notizen[i].Contains(suchwort, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"{i + 1}. {notizen[i]}");
treffer++;
}
}
Console.WriteLine($"Treffer: {treffer}");
}
static void Loesche(List<string> notizen)
{
ZeigeAlle(notizen);
if (notizen.Count == 0)
{
return;
}
Console.Write("Welche Nummer löschen? ");
string eingabe = Console.ReadLine() ?? "";
if (!int.TryParse(eingabe, out int nummer) || nummer < 1 || nummer > notizen.Count)
{
Console.WriteLine($"Bitte eine Nummer von 1 bis {notizen.Count}.");
return;
}
string text = notizen[nummer - 1];
notizen.RemoveAt(nummer - 1);
Console.WriteLine($"Gelöscht: {text}");
}
static List<string> Lade(string datei)
{
List<string> notizen = [];
if (File.Exists(datei))
{
foreach (string zeile in File.ReadAllLines(datei))
{
notizen.Add(zeile);
}
}
return notizen;
}
static void Speichere(string datei, List<string> notizen)
{
File.WriteAllLines(datei, notizen);
}
Starten Sie es, legen Sie zwei Notizen an und beenden Sie mit 0. Starten Sie es dann ein zweites Mal:
Ihr Notizbuch. Gespeicherte Notizen: 2
1 Neue Notiz
2 Alle Notizen zeigen
3 Suchen
4 Notiz löschen
0 Beenden
Ihre Wahl: 2
1. Zahnarzt am Dienstag
2. Brot kaufen
Ihre Notizen haben das Programmende überlebt. Sie stehen in notizen.txt im selben Ordner, eine Notiz pro Zeile. Ein Hinweis, der
für jedes Programm gilt: Schreiben Sie nichts Geheimes hinein, keine Passwörter, keine PIN. Die Datei ist nicht verschlüsselt, und jeder,
der an Ihren Rechner kommt, kann sie lesen. Geheimnisse gehören nie in Code, nie in Logdateien und nie in einfache Textdateien.
Testen wie eine Prüferin
Ihr Notizbuch ist fertig, wenn es die unbequemen Fälle aushält. Gehen Sie diese Liste einmal ganz durch und haken Sie ab:
- Erster Start ohne
notizen.txt: „Gespeicherte Notizen: 0“, die 2 zeigt „Noch keine Notizen.“ - Zwei Notizen anlegen, mit 0 beenden, neu starten: beide sind da, in derselben Reihenfolge.
- Eine Notiz anlegen und das Terminalfenster schließen, ohne 0 zu wählen. Neues Terminal, neu starten: Die Notiz ist da.
- Suchen mit anderer Groß- und Kleinschreibung als in der Notiz: Sie wird gefunden.
- Löschen mit 0, mit einer zu hohen Nummer und mit einem Wort: jeweils ein Hinweis, nichts gelöscht.
- Die letzte Notiz löschen, neu starten: „Gespeicherte Notizen: 0“.
- Eine Notiz mit Umlauten, etwa „Grüße an Jörg“: Sie wird richtig gespeichert und nach dem Neustart richtig angezeigt.
Wenn alle sieben Punkte stimmen, haben Sie ein Programm geschrieben, das man benutzen kann. Herzlichen Glückwunsch.
Übung 1
Löschen geht im Moment sehr schnell, auch aus Versehen. Ändern Sie Loesche so, dass es vor dem Entfernen fragt:
"Brot kaufen" wirklich löschen? (j/n). Nur bei „j“ oder „ja“ wird gelöscht, egal ob groß oder klein geschrieben. Sonst meldet
die Methode „Nichts gelöscht.“
Lösung 1
Ersetzen Sie die Methode Loesche durch diese. Neu sind die Zeilen nach string text = notizen[nummer - 1];:
// Ausschnitt aus notizbuch.cs
static void Loesche(List<string> notizen)
{
ZeigeAlle(notizen);
if (notizen.Count == 0)
{
return;
}
Console.Write("Welche Nummer löschen? ");
string eingabe = Console.ReadLine() ?? "";
if (!int.TryParse(eingabe, out int nummer) || nummer < 1 || nummer > notizen.Count)
{
Console.WriteLine($"Bitte eine Nummer von 1 bis {notizen.Count}.");
return;
}
string text = notizen[nummer - 1];
Console.Write($"\"{text}\" wirklich löschen? (j/n) ");
string antwort = (Console.ReadLine() ?? "").Trim().ToLower();
if (antwort != "j" && antwort != "ja")
{
Console.WriteLine("Nichts gelöscht.");
return;
}
notizen.RemoveAt(nummer - 1);
Console.WriteLine($"Gelöscht: {text}");
}
Die Rückfrage folgt demselben Muster wie die Prüfungen davor: Erst der Fall, in dem nichts passieren soll, mit return, dann die
eigentliche Arbeit. Beachten Sie die Bedingung antwort != "j" && antwort != "ja". In Worten: „weder j noch ja“. Mit || wäre sie
immer wahr, denn jede Antwort ist von mindestens einem der beiden verschieden. Das ist ein Fehler, den fast jeder einmal macht.
Übung 2
Öffnen Sie notizen.txt mit Ihrem Editor, fügen Sie zwischen zwei Notizen eine leere Zeile ein und speichern Sie. Starten Sie das
Notizbuch und lassen Sie sich alle Notizen zeigen. Was sehen Sie? Ändern Sie Lade so, dass leere Zeilen und Zeilen aus lauter
Leerzeichen übersprungen werden.
Lösung 2
Vorher erscheint die leere Zeile als eigene Notiz: eine Nummer mit einem Punkt dahinter und sonst nichts. Das Notizbuch selbst legt keine leeren Notizen an, aber
eine Datei kann jeder ändern. Ein Programm sollte Daten, die von außen kommen, deshalb immer prüfen. So überspringt Lade die leeren
Zeilen:
// Ausschnitt aus notizbuch.cs
static List<string> Lade(string datei)
{
List<string> notizen = [];
if (File.Exists(datei))
{
foreach (string zeile in File.ReadAllLines(datei))
{
if (zeile.Trim() != "")
{
notizen.Add(zeile.Trim());
}
}
}
return notizen;
}
zeile.Trim() entfernt die Leerzeichen am Rand. Bleibt danach nichts übrig, war die Zeile leer. Abgelegt wird die Zeile ohne die
Leerzeichen am Rand, genauso wie NeueNotiz es mit neuen Notizen macht.
Übung 3
Für die Kür: Ein Menüpunkt 5 Notiz ändern. Er zeigt alle Notizen, fragt nach der Nummer und nach dem neuen Text und ersetzt die
Notiz. Ein leerer neuer Text ändert nichts. Tipp: Mit notizen[nummer - 1] = text; ersetzen Sie einen Eintrag der Liste.
Lösung 3
Die neue Methode Aendere liest die Nummer genauso wie Loesche. Hier ist das ganze Notizbuch mit allen drei Erweiterungen dieser
Lektion:
// notizbuch.cs
string datei = "notizen.txt";
List<string> notizen = Lade(datei);
Console.WriteLine($"Ihr Notizbuch. Gespeicherte Notizen: {notizen.Count}");
bool weiter = true;
while (weiter)
{
ZeigeAuswahl();
string wahl = (Console.ReadLine() ?? "0").Trim();
if (wahl == "1")
{
NeueNotiz(notizen);
Speichere(datei, notizen);
}
else if (wahl == "2")
{
ZeigeAlle(notizen);
}
else if (wahl == "3")
{
Suche(notizen);
}
else if (wahl == "4")
{
Loesche(notizen);
Speichere(datei, notizen);
}
else if (wahl == "5")
{
Aendere(notizen);
Speichere(datei, notizen);
}
else if (wahl == "0")
{
weiter = false;
}
else
{
Console.WriteLine("Diese Auswahl gibt es nicht.");
}
}
Console.WriteLine("Bis bald!");
static void ZeigeAuswahl()
{
Console.WriteLine();
Console.WriteLine("1 Neue Notiz");
Console.WriteLine("2 Alle Notizen zeigen");
Console.WriteLine("3 Suchen");
Console.WriteLine("4 Notiz löschen");
Console.WriteLine("5 Notiz ändern");
Console.WriteLine("0 Beenden");
Console.Write("Ihre Wahl: ");
}
static void NeueNotiz(List<string> notizen)
{
Console.Write("Notiz: ");
string text = (Console.ReadLine() ?? "").Trim();
if (text == "")
{
Console.WriteLine("Eine leere Notiz lege ich nicht an.");
return;
}
notizen.Add(text);
Console.WriteLine($"Notiert. Anzahl der Notizen: {notizen.Count}");
}
static void ZeigeAlle(List<string> notizen)
{
if (notizen.Count == 0)
{
Console.WriteLine("Noch keine Notizen.");
return;
}
for (int i = 0; i < notizen.Count; i++)
{
Console.WriteLine($"{i + 1}. {notizen[i]}");
}
}
static void Suche(List<string> notizen)
{
Console.Write("Suchwort: ");
string suchwort = (Console.ReadLine() ?? "").Trim();
if (suchwort == "")
{
Console.WriteLine("Ohne Suchwort suche ich nicht.");
return;
}
int treffer = 0;
for (int i = 0; i < notizen.Count; i++)
{
if (notizen[i].Contains(suchwort, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"{i + 1}. {notizen[i]}");
treffer++;
}
}
Console.WriteLine($"Treffer: {treffer}");
}
static void Loesche(List<string> notizen)
{
ZeigeAlle(notizen);
if (notizen.Count == 0)
{
return;
}
Console.Write("Welche Nummer löschen? ");
string eingabe = Console.ReadLine() ?? "";
if (!int.TryParse(eingabe, out int nummer) || nummer < 1 || nummer > notizen.Count)
{
Console.WriteLine($"Bitte eine Nummer von 1 bis {notizen.Count}.");
return;
}
string text = notizen[nummer - 1];
Console.Write($"\"{text}\" wirklich löschen? (j/n) ");
string antwort = (Console.ReadLine() ?? "").Trim().ToLower();
if (antwort != "j" && antwort != "ja")
{
Console.WriteLine("Nichts gelöscht.");
return;
}
notizen.RemoveAt(nummer - 1);
Console.WriteLine($"Gelöscht: {text}");
}
static void Aendere(List<string> notizen)
{
ZeigeAlle(notizen);
if (notizen.Count == 0)
{
return;
}
Console.Write("Welche Nummer ändern? ");
string eingabe = Console.ReadLine() ?? "";
if (!int.TryParse(eingabe, out int nummer) || nummer < 1 || nummer > notizen.Count)
{
Console.WriteLine($"Bitte eine Nummer von 1 bis {notizen.Count}.");
return;
}
Console.Write("Neuer Text: ");
string text = (Console.ReadLine() ?? "").Trim();
if (text == "")
{
Console.WriteLine("Leer lasse ich die Notiz nicht. Zum Entfernen gibt es Punkt 4.");
return;
}
notizen[nummer - 1] = text;
Console.WriteLine($"Geändert: {text}");
}
static List<string> Lade(string datei)
{
List<string> notizen = [];
if (File.Exists(datei))
{
foreach (string zeile in File.ReadAllLines(datei))
{
if (zeile.Trim() != "")
{
notizen.Add(zeile.Trim());
}
}
}
return notizen;
}
static void Speichere(string datei, List<string> notizen)
{
File.WriteAllLines(datei, notizen);
}
Loesche und Aendere lesen die Nummer jetzt mit fast denselben Zeilen; nur die Frage unterscheidet sich. Doppelter Code ist ein Fall
für eine Methode, wie in Lektion 8. Wenn Sie Lust haben: Schreiben Sie eine Methode static int LiesNummer(List<string> notizen, string frage),
die eine gültige Nummer zurückgibt oder 0, wenn die Eingabe nicht passt, und benutzen Sie sie in beiden Methoden. Prüfen Sie danach mit der Testliste von oben,
dass alles noch genauso funktioniert.
Was Sie jetzt können
- Sie schreiben Textzeilen mit
File.WriteAllLinesin eine Datei und lesen sie mitFile.ReadAllLines. - Sie prüfen mit
File.Exists, ob eine Datei da ist, bevor Sie sie lesen. - Sie wissen, dass eine Datei mit einem einfachen Namen im aktuellen Ordner landet und dass Geheimnisse nicht in Textdateien gehören.
- Sie prüfen Daten, die von außen kommen.
- Sie haben ein vollständiges Programm geschrieben, das man benutzen kann, und es planvoll getestet.
Wie es weitergeht
Machen Sie zuerst den Selbsttest, der zu diesem Kurs gehört. Er dauert etwa eine halbe Stunde und zeigt Ihnen, was sitzt.
Danach haben Sie mehrere Möglichkeiten:
- Üben. Bauen Sie Ihr Notizbuch nach eigenen Ideen aus: eine Notiz nach oben schieben, alle Notizen löschen nach einer Rückfrage, eine Notiz nachträglich ändern. Jede eigene Idee, die Sie umsetzen, bringt mehr als jede gelesene Seite.
- Weiterlernen mit Microsoft Learn. Die Lernpfade „Get started with C#“ Teil 2 bis 6 vertiefen, was Sie hier begonnen haben, mit eigenen Übungen und Projekten.
- Der Aufbaukurs der Akademie macht aus dem Notizbuch die Kladde mit Kontakten und Terminen: mit Klassen, Speichern als JSON, automatischen Tests und Git, so wie im Beruf. Wenn Sie erfahren möchten, wann er beginnt, tragen Sie sich auf der Seite der Akademie in die Warteliste ein.
Weiterlesen
- Build file-based apps: was Programme aus einer einzigen Datei noch können, etwa Argumente lesen und Pakete einbinden.
- Work with files and directories in a .NET app: ein Modul zu Dateien und Ordnern.
- Create and run simple C# console applications (Get started with C#, Part 2): der zweite Lernpfad der Reihe „Get started with C#“, von dem aus es bis Teil 6 weitergeht.