Wat leer je in dit hoofdstuk?

Na dit hoofdstuk kan je:

werken met System.IO: paden, bestanden en folders

tekst lezen en schrijven met StreamReader, StreamWriter en de File-klasse

het using-blok inzetten om bestanden netjes vrij te geven

binaire bestanden verwerken met BinaryWriter en BinaryReader

info opvragen met FileInfo en DirectoryInfo

objecten serialiseren naar en deserialiseren uit JSON

System.IO

using System.IO;

Hiermee lees, schrijf, maak en verwijder je bestanden en folders.

Maar: je raakt nu echt de harde schijf.

Belangrijk

Tijd voor een back-up, en voor exception handling: bestanden luisteren minder goed dan jij. Eén foutje wist zomaar gegevens (ja, ook mij is dat al overkomen).

Mijn eerste programma met bestanden

Welke films hebben nog geen ondertitels?

Rare tekens en reclame uit mp3-namen strippen

Darude Sandstorm for life.

Bestands- en folderlocaties

Paden

Elk bestand heeft een uniek path, in C# steeds een string:

c:\temp\mijnData.txt

c:\

temp

mijnData.txt

Een path is niet hoofdlettergevoelig

Windows gebruikt \, macOS gebruikt /

File.WriteAllText("doem.txt", "Het einde is nabij");        // bij het programma
File.WriteAllText(@"c:\temp\doem.txt", "Het einde is nabij"); // vast pad

Veilig met paden: Path.Combine

Bouw paden platformonafhankelijk met de Path-klasse:

string fullPath = Path.Combine("data", "dagboek.txt");
// Windows: data\dagboek.txt   macOS: data/dagboek.txt

Tip

Path heeft ook GetTempPath, GetFileNameWithoutExtension, … En via Environment.GetFolderPath(Environment.SpecialFolder.Desktop) vind je het bureaublad van de gebruiker.

Eerst controleren

Controleer of iets bestaat vóór je het gebruikt, en overschrijf nooit zomaar:

if (File.Exists(path)) { /* werk met bestand */ }
if (Directory.Exists(path)) { /* werk met folder */ }

if (!Directory.Exists(path))
    Directory.CreateDirectory(path);

Schrijven en lezen

StreamWriter: schrijven

StreamWriter writer = new StreamWriter("dagboek.txt", true);
writer.WriteLine("Game over!");

Net als bij Console gebruik je WriteLine. De tweede parameter:

true toevoegen (append)

false overschrijft

Waarschuwing

Zo “bloot” gebruik je het beter niet: bij een fout blijft het bestand open en gelockt. Verderop lossen we dat op met using.

StreamReader: lezen

StreamReader reader = new StreamReader("dagboek.txt");
string regel;
while ((regel = reader.ReadLine()) != null)
{
    Console.WriteLine(regel);
}

ReadLine geeft null zodra het einde van het bestand bereikt is: zo weet de loop wanneer te stoppen.

Het using-blok

Een using-blok geeft een bestand gegarandeerd weer vrij, ook bij een fout:

using (StreamWriter writer = new StreamWriter("doem.txt"))
{
    writer.WriteLine("Het einde is nabij!");
    writer.WriteLine("Hou vol!");
}

Bij de sluitende accolade roept C# automatisch Dispose() aan: het bestand gaat netjes dicht.

Tip

Vanaf nu zetten we altijd een using rond een StreamWriter of StreamReader.

File.ReadAllText of StreamReader?

De File-klasse kan een bestand in één keer inlezen, zonder lus:

string alles = File.ReadAllText("dagboek.txt");
string[] regels = File.ReadAllLines("dagboek.txt");

Klein bestand, in één keer

File.ReadAllText / ReadAllLines (kort en leesbaar)

Groot bestand of regel per regel

StreamReader met de while-lus

Uitgewerkt: een dagboek

string path = "dagboek.txt";

Console.WriteLine("Dagboek:");
using (StreamReader reader = new StreamReader(path))
{
    string regel;
    while ((regel = reader.ReadLine()) != null)
        Console.WriteLine(regel);
}

Console.WriteLine("Geef je volgende schrijfsel:");
string entry = Console.ReadLine();

using (StreamWriter writer = new StreamWriter(path, true))
{
    writer.WriteLine($"\n{DateTime.Now}");
    writer.WriteLine(entry);
}

Binaire bestanden

BinaryWriter

Met een BinaryWriter schrijf je elk datatype weg:

var fs = File.Open("bond.dat", FileMode.Create);
using (BinaryWriter writer = new BinaryWriter(fs))
{
    writer.Write("Bond");
    writer.Write(7);
    writer.Write(true);
}

De FileMode-enum.

FileMode (Create, Append, Open, …) bepaalt hoe het bestand geopend wordt.

BinaryReader

var fs = File.Open("bond.dat", FileMode.Open);
using (BinaryReader reader = new BinaryReader(fs))
{
    string naam = reader.ReadString();
    int code = reader.ReadInt32();
    bool leeftNog = reader.ReadBoolean();
}

Write("Bond") ReadString()

Write(7) ReadInt32()

Write(true) ReadBoolean()

Belangrijk

De volgorde is cruciaal: lees in exact dezelfde volgorde als je schreef. Wissel je ReadInt32 en ReadBoolean om, dan crasht je programma met een EndOfStreamException.

De ruwe bytes bekijken

Elk bestand is uiteindelijk gewoon een rij bytes. Toon ze als hexadecimaal:

byte[] bytes = File.ReadAllBytes("bond.dat");
foreach (byte b in bytes)
{
    Console.Write($"{b:X2} "); // bv. 04 42 6F 6E 64 07 ...
}
04 42 6F 6E 64 07 00 00 00 01

Tip

{b:X2} toont elke byte als twee hexadecimale tekens. Zo zie je letterlijk wat een BinaryWriter op schijf zette.

Oefening: hoeveel bytes neemt een type in?

Schrijf een waarde weg en kijk hoe groot het bestand wordt:

using (BinaryWriter writer = new BinaryWriter(File.Open("test.bin", FileMode.Create)))
{
    writer.Write(true);   // hoeveel bytes?
}
long grootte = new FileInfo("test.bin").Length;
Console.WriteLine($"{grootte} byte(s)");
1 byte(s)

Tip

Probeer het met een bool, een int, een double, een char. Herken je de groottes uit hoofdstuk 2?

De FileInfo klasse

FileInfo

Een FileInfo-object geeft info over een bestand én bewerkingsmethoden:

FileInfo info = new FileInfo("bond.dat");
if (info.Exists)
{
    Console.WriteLine($"{info.Name}: {info.Length / 1024} kB");
    Console.WriteLine(info.CreationTime);
}

CopyTo MoveTo Delete doen wat je verwacht.

Tip

File vs FileInfo? File is een static klasse (snel, kort iets doen). FileInfo instantieer je met new (handig voor meerdere bewerkingen na elkaar).

DirectoryInfo

DirectoryInfo dir = new DirectoryInfo(@"C:\temp");
foreach (var bestand in dir.GetFiles("*.txt"))
{
    Console.WriteLine(bestand.Name);
}

GetFiles en GetDirectories geven de inhoud terug

Een searchPattern (*.txt, Tim?.*) filtert

SearchOption.AllDirectories zoekt ook in subfolders

Zippen en unzippen

Met System.IO.Compression pak je hele mappen in of uit:

ZipFile.CreateFromDirectory(@"c:\temp\data", "backup.zip");
ZipFile.ExtractToDirectory("backup.zip", @"c:\temp\hersteld");

c:\temp\data

backup.zip

CreateFromDirectory

c:\temp\hersteld

ExtractToDirectory

Tip

Handig voor back-ups of om veel bestanden in een keer te versturen. Voeg using System.IO.Compression; toe.

Bonus: downloaden van het web

Een bestand of API-antwoord ophalen doe je met HttpClient:

HttpClient client = new HttpClient();
string inhoud = await client.GetStringAsync("https://example.com/data.txt");
Console.WriteLine(inhoud);

Opmerking

HttpClient is de moderne opvolger van het oudere WebClient. Het await hoort bij asynchroon programmeren (verdiepingsstof voor later).

Klassen serialiseren

Serialiseren: het idee

Objecten met de hand wegschrijven (string-conversies, byte-volgorde) is lastig. Serialiseren doet het voor je: een object “in serie” naar een bestand en terug.

object

bestand

serialiseren

object

deserialiseren

Zo maak je letterlijk een savepoint van je programma.

JSON combineert het beste van beide werelden: compact én leesbaar.

{ "naam": "Barry", "leeftijd": 25, "uitgeschreven": true }

JSON serialiseren

Gebruik System.Text.Json. Serialiseren werkt op de publieke kant van je object:

public class Student
{
    public string Naam { get; set; }
    public int Leeftijd { get; set; }
}

var student = new Student { Naam = "Barry", Leeftijd = 25 };
string json = JsonSerializer.Serialize(student);
File.WriteAllText("student.json", json);

student.json

{"Naam":"Barry","Leeftijd":25}

Tip

Met new JsonSerializerOptions { WriteIndented = true } krijg je nette inspringing. Een hele List<Student> serialiseert net zo makkelijk.

JSON deserialiseren

De omgekeerde weg is generic: je geeft het verwachte type mee.

string json = File.ReadAllText("student.json");
Student ingeladen = JsonSerializer.Deserialize<Student>(json);

Opmerking

Met attributen stuur je het gedrag bij: [JsonIgnore] slaat een property over, [JsonPropertyName("...")] hernoemt ze, [JsonInclude] neemt iets privaat toch mee.

Bonus: Paste JSON as Classes

Onbekende JSON van bv. een web-API? Laat VS de klassen voor je schrijven:

kopieer de JSON

nieuw, leeg klasse-bestand

Edit - Paste Special - Paste JSON as Classes

BOEM!

De meest onderschatte VS-feature.

Conclusie

System.IO geeft toegang tot de harde schijf: werk voorzichtig en met exception handling

StreamReader/StreamWriter (in een using-blok) en de File-klasse lezen en schrijven tekst

BinaryWriter/BinaryReader verwerken elk datatype, maar de volgorde telt

FileInfo en DirectoryInfo geven info en bewerkingen

JSON-serialisatie maakt een leesbaar savepoint van je objecten

Tip

En dat was het laatste hoofdstuk. Je bent van “Hello World!” tot serialisatie geraakt: dik verdiend. Blijf vooral oefenen en bouwen.

Meer weten

Kennisclips

Oefeningen H18 · Quizlet flashcards

Zie verder

Hoe doen andere programmeertalen hetzelfde? En een korte zoek-de-fout, om zo kritisch mogelijk naar code te leren kijken.

Zie verder: een bestand lezen

In Python doet with open exact wat ons using-blok doet: het bestand gaat automatisch dicht aan het einde van het blok, ook bij een fout.

with open("dagboek.txt") as f:
    inhoud = f.read()
# f is hier al netjes gesloten

Waar C# een StreamReader plus using nodig heeft, vangt Python beide in één korte regel.

Zie verder: JSON in JavaScript

JSON staat voor JavaScript Object Notation en komt dus letterlijk uit JavaScript. Geen aparte JsonSerializer nodig: serialiseren zit ingebouwd.

const student = { naam: "Barry", leeftijd: 25 };
const jsonString = JSON.stringify(student);
// {"naam":"Barry","leeftijd":25}

Nóg directer dan C#: omdat JSON uit JavaScript stamt, is een object en zijn JSON-voorstelling bijna hetzelfde ding.

Zie verder: JSON in Python

Python gebruikt zijn ingebouwde json-module: json.dumps serialiseert, json.loads deserialiseert.

import json

student = {"naam": "Barry", "leeftijd": 25}
json_string = json.dumps(student)   # object -> JSON-tekst
terug = json.loads(json_string)     # JSON-tekst -> dict

Verschil met C#: je hoeft geen type mee te geven. Python geeft een dict terug, waar C# via Deserialize<Student> weet bij welke klasse de data hoort.

Zie verder: zoek de fout

Onderstaande Python-code schrijft een dagboekregel weg, maar het bestand blijft soms leeg of half geschreven, zeker als er verderop iets misloopt. Wat zou je beter doen?

f = open("dagboek.txt", "w")
f.write("Beste dagboek...")
# code die crasht voor de volgende regel
f.close()

Opmerking

Als de code tussen open en close crasht, wordt f.close() nooit bereikt en blijft het bestand open (en mogelijk leeg of gelockt). Gebruik een with open(...)-blok: dan sluit Python het bestand sowieso, net zoals een using-blok in C#.

with open("dagboek.txt", "w") as f:
    f.write("Beste dagboek...")

Samenvattende poster

Een handige samenvattende poster (Opgelet: gemaakt m.b.v. ChatGPT Image Gen 2).