Odczyt i zapis plików w C#

Przestrzeń nazw System.IO

Wszystkie operacje na plikach w C# znajdują się w przestrzeni nazw System.IO. Musimy ją dołączyć na początku programu:

using System.IO;

Najważniejsze klasy do pracy z plikami:

  • File – statyczna klasa do prostych operacji na plikach
  • StreamReader – do odczytu plików tekstowych
  • StreamWriter – do zapisu plików tekstowych
  • FileStream – do zaawansowanych operacji na plikach

Słowniczek funkcji i ich działanie:

Klasa File (operacje na całych plikach):

  • File.ReadAllText(ścieżka) – odczytuje cały plik jako jeden string
  • File.ReadAllLines(ścieżka) – odczytuje plik i zwraca tablicę stringów (każda linia = element)
  • File.WriteAllText(ścieżka, tekst) – nadpisuje plik podanym tekstem (tworzy nowy jeśli nie istnieje)
  • File.WriteAllLines(ścieżka, tablica) – nadpisuje plik tablicą stringów (każdy element = nowa linia)
  • File.AppendAllText(ścieżka, tekst) – dopisuje tekst na końcu pliku
  • File.AppendAllLines(ścieżka, tablica) – dopisuje tablicę stringów na końcu pliku
  • File.Exists(ścieżka) – sprawdza czy plik istnieje (zwraca true/false)

Klasa StreamReader (odczyt linia po linii):

  • new StreamReader(ścieżka) – tworzy obiekt do czytania pliku
  • reader.ReadLine() – czyta jedną linię z pliku (zwraca null gdy koniec pliku)
  • reader.ReadToEnd() – czyta resztę pliku jako jeden string

Klasa StreamWriter (zapis linia po linii):

  • new StreamWriter(ścieżka) – tworzy obiekt do pisania do pliku (nadpisuje)
  • new StreamWriter(ścieżka, append: true) – tworzy obiekt do dopisywania na końcu pliku
  • writer.WriteLine(tekst) – zapisuje tekst i dodaje znak nowej linii
  • writer.Write(tekst) – zapisuje tekst bez znaku nowej linii

Obsługa zasobów:

  • using() – automatycznie zamyka plik po zakończeniu bloku kodu
  • try-catch – przechwytuje błędy podczas operacji na plikach

2. Odczyt plików – podstawowe metody

2.1 Odczyt całego pliku jednorazowo

// Metoda 1: ReadAllText() - cały plik jako string
string zawartoscPliku = File.ReadAllText("plik.txt");
Console.WriteLine(zawartoscPliku);

// Metoda 2: ReadAllLines() - każda linia jako element tablicy
string[] linie = File.ReadAllLines("plik.txt");
foreach (string linia in linie)
{
    Console.WriteLine(linia);
}

2.2 Odczyt pliku linia po linii (StreamReader)

using (StreamReader reader = new StreamReader("plik.txt"))
{
    string linia;
    while ((linia = reader.ReadLine()) != null)
    {
        Console.WriteLine(linia);
    }
}

Zapis do plików

3.1 Zapis całego tekstu jednorazowo

// Nadpisanie całego pliku
string tekst = "To jest przykładowy tekst";
File.WriteAllText("nowyPlik.txt", tekst);

// Zapis tablicy stringów (każdy jako nowa linia)
string[] linie = {"Pierwsza linia", "Druga linia", "Trzecia linia"};
File.WriteAllLines("linie.txt", linie);

3.2 Dopisywanie do pliku

// Dopisanie tekstu na końcu pliku
File.AppendAllText("plik.txt", "Nowy tekst na końcu");

// Dopisanie linii
string[] noweLinie = {"Nowa linia 1", "Nowa linia 2"};
File.AppendAllLines("plik.txt", noweLinie);

3.3 Zapis za pomocą StreamWriter

using (StreamWriter writer = new StreamWriter("plik.txt"))
{
    writer.WriteLine("Pierwsza linia");
    writer.WriteLine("Druga linia");
    writer.Write("Tekst bez nowej linii");
}

// Dopisywanie za pomocą StreamWriter
using (StreamWriter writer = new StreamWriter("plik.txt", append: true))
{
    writer.WriteLine("Ta linia zostanie dopisana");
}

4.Obsługa błędów

Operacje na plikach mogą generować wyjątki. Zawsze należy je obsłużyć:

try
{
    string zawartość = File.ReadAllText("plik.txt");
    Console.WriteLine(zawartość);
}
catch (FileNotFoundException)
{
    Console.WriteLine("Plik nie został znaleziony!");
}
catch (UnauthorizedAccessException)
{
    Console.WriteLine("Brak uprawnień do odczytu pliku!");
}
catch (IOException ex)
{
    Console.WriteLine($"Błąd operacji na pliku: {ex.Message}");
}

5.Sprawdzanie istnienia pliku

string nazwaPliku = "test.txt";

if (File.Exists(nazwaPliku))
{
    Console.WriteLine("Plik istnieje");
    string zawartość = File.ReadAllText(nazwaPliku);
    Console.WriteLine(zawartość);
}
else
{
    Console.WriteLine("Plik nie istnieje. Tworzę nowy...");
    File.WriteAllText(nazwaPliku, "Zawartość nowego pliku");
}

6.Dodatkowe informacje

Kodowanie znaków

// Odczyt z określonym kodowaniem
string tekst = File.ReadAllText("plik.txt", Encoding.UTF8);

// Zapis z określonym kodowaniem
File.WriteAllText("plik.txt", "Tekst z polskimi znakami: ąćęłńóśźż", Encoding.UTF8);

Wyjaśnienie funkcji:

  • Encoding.UTF8 – kodowanie UTF-8 (obsługuje polskie znaki)
  • Bez określenia kodowania system użyje domyślnego (może nie obsługiwać polskich znaków)
  • UTF-8 to uniwersalne kodowanie obsługujące wszystkie języki

Ścieżki plików

// Ścieżka bezwzględna
string sciezka = @"C:\dane\plik.txt";

// Łączenie ścieżek
string sciezka2 = Path.Combine("dane", "pliki", "dokument.txt");

// Bieżący katalog
string obecnyKatalog = Directory.GetCurrentDirectory();

Wyjaśnienie funkcji:

  • @"ścieżka" – verbatim string (nie interpretuje znaków specjalnych jak \n)
  • Path.Combine() – inteligentnie łączy części ścieżki (dodaje / lub \ automatycznie)
  • Directory.GetCurrentDirectory() – zwraca ścieżkę do folderu, z którego uruchomiono program
  • Path.Combine automatycznie dostosowuje się do systemu operacyjnego (Windows vs Linux)

7.Obsługa ciągów znaków – dzielenie tekstu

Podstawowe dzielenie po spacji

string tekst = "Ala ma kota i psa";
string[] slowa = tekst.Split(' ');

foreach (string slowo in slowa)
{
    Console.WriteLine(slowo);
}
// Wynik: Ala, ma, kota, i, psa

Wyjaśnienie funkcji:

  • Split(' ') – dzieli string na części używając spacji jako separatora
  • Zwraca tablicę stringów string[]
  • Każde słowo staje się osobnym elementem tablicy

Dzielenie po różnych separatorach

string dane = "Jan;Kowalski;30;Warszawa";
string[] elementy = dane.Split(';');

Console.WriteLine($"Imię: {elementy[0]}");
Console.WriteLine($"Nazwisko: {elementy[1]}");
Console.WriteLine($"Wiek: {elementy[2]}");
Console.WriteLine($"Miasto: {elementy[3]}");

Wyjaśnienie funkcji:

  • Split(';') – dzieli string po średniku
  • elementy[0] – dostęp do pierwszego elementu tablicy (indeks 0)
  • $"tekst {zmienna}" – interpolacja stringów (wstawianie zmiennej do tekstu)

Dzielenie po znakach nowej linii

// Przykład 1: Dzielenie po \n
string tekstWieloliniowy = "Pierwsza linia\nDruga linia\nTrzecia linia";
string[] linie = tekstWieloliniowy.Split('\n');

foreach (string linia in linie)
{
    Console.WriteLine($"Linia: '{linia}'");
}

// Przykład 2: Dzielenie po \r\n (Windows)
string tekstWindows = "Linia 1\r\nLinia 2\r\nLinia 3";
string[] linieWindows = tekstWindows.Split(new string[] { "\r\n" }, StringSplitOptions.None);

// Przykład 3: Uniwersalne dzielenie po znakach nowej linii
string tekst = "A\nB\r\nC\rD";
string[] wszystkieLinie = tekst.Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);

Wyjaśnienie funkcji:

  • \n – znak nowej linii (Unix/Linux)
  • \r\n – znak nowej linii (Windows)
  • \r – powrót karetki (stare systemy Mac)
  • Split('\n') – dzielenie po pojedynczym znaku
  • Split(new string[] { "\r\n" }) – dzielenie po ciągu znaków
  • Split(new char[] { '\n', '\r' }) – dzielenie po wielu znakach jednocześnie
  • StringSplitOptions.None – zachowuje puste elementy
  • StringSplitOptions.RemoveEmptyEntries – usuwa puste elementy z wyniku

Co to jest StringSplitOptions?

StringSplitOptions to enumeracja (lista stałych wartości) określająca jak funkcja Split() ma traktować puste elementy:

StringSplitOptions.None (domyślne):

string tekst = "a,,b,";
string[] wynik = tekst.Split(',');
// Wynik: ["a", "", "b", ""]
// Tablica ma 4 elementy, w tym 2 puste stringi

StringSplitOptions.RemoveEmptyEntries:

string tekst = "a,,b,";
string[] wynik = tekst.Split(',', StringSplitOptions.RemoveEmptyEntries);
// Wynik: ["a", "b"]  
// Tablica ma tylko 2 elementy, puste stringi zostały usunięte

Praktyczny przykład – dlaczego to ważne:

// Dane z pliku CSV mogą mieć puste komórki:
string linia = "Jan,Kowalski,,Warszawa,";

// Bez RemoveEmptyEntries:
string[] dane1 = linia.Split(',');
Console.WriteLine($"Liczba elementów: {dane1.Length}"); // 5
Console.WriteLine($"Element 2: '{dane1[2]}'"); // Pusty string
Console.WriteLine($"Element 4: '{dane1[4]}'"); // Pusty string

// Z RemoveEmptyEntries:
string[] dane2 = linia.Split(',', StringSplitOptions.RemoveEmptyEntries);
Console.WriteLine($"Liczba elementów: {dane2.Length}"); // 3
Console.WriteLine($"Elementy: {string.Join(" | ", dane2)}"); // Jan | Kowalski | Warszawa

Kiedy używać której opcji:

  • None – gdy struktura danych jest ważna (np. CSV z pustymi komórkami)
  • RemoveEmptyEntries – gdy chcesz tylko wartościowe elementy (np. dzielenie tekstu na słowa)

Zaawansowane opcje dzielenia

string tekst = "apple,,banana,cherry,";

// Domyślnie - pozostawia puste elementy
string[] owoce1 = tekst.Split(',');
// Wynik: ["apple", "", "banana", "cherry", ""]

// Usuwanie pustych elementów
string[] owoce2 = tekst.Split(',', StringSplitOptions.RemoveEmptyEntries);
// Wynik: ["apple", "banana", "cherry"]

// Ograniczenie liczby elementów
string[] owoce3 = tekst.Split(',', 3);
// Wynik: ["apple", "", "banana,cherry,"]

Wyjaśnienie funkcji:

  • Split(',') – podstawowe dzielenie, zachowuje puste elementy (domyślnie None)
  • StringSplitOptions.RemoveEmptyEntries – usuwa puste stringi z wyniku
  • Split(',', 3) – ogranicza wynik do maksymalnie 3 elementów
  • Gdy osiągnie limit, reszta tekstu trafia do ostatniego elementu

Szczegółowe porównanie StringSplitOptions:

string przykład = "apple,,banana,cherry,";

// 1. Domyślnie (None) - zachowuje wszystkie elementy:
string[] wynik1 = przykład.Split(',');
foreach (string element in wynik1)
    Console.WriteLine($"'{element}'");
// Wynik:
// 'apple'
// ''           <- pusty string
// 'banana'  
// 'cherry'
// ''           <- pusty string na końcu

// 2. RemoveEmptyEntries - usuwa puste elementy:
string[] wynik2 = przykład.Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (string element in wynik2)
    Console.WriteLine($"'{element}'");
// Wynik:
// 'apple'
// 'banana'
// 'cherry'

Różnica w liczbie elementów:

  • wynik1.Length = 5 (z pustymi)
  • wynik2.Length = 3 (bez pustych)
string tekstMieszany = "Ala,ma;kota:i psa";
char[] separatory = {',', ';', ':', ' '};
string[] slowa = tekstMieszany.Split(separatory, StringSplitOptions.RemoveEmptyEntries);

foreach (string slowo in slowa)
{
    Console.WriteLine(slowo);
}
// Wynik: Ala, ma, kota, i, psa

Wyjaśnienie funkcji:

  • char[] separatory – tablica znaków-separatorów
  • Split(separatory, opcje) – dzieli po dowolnym ze znaków z tablicy
  • Program traktuje każdy z separatorów jako miejsce podziału

Praktyczny przykład – analiza pliku CSV

using System;
using System.IO;

class AnalizatorCSV
{
    static void Main()
    {
        try
        {
            // Przykładowy plik CSV: imię,nazwisko,wiek,miasto
            string sciezka = "osoby.csv";
            
            // Utworzenie przykładowego pliku
            if (!File.Exists(sciezka))
            {
                string[] przykładoweDane = {
                    "Imię,Nazwisko,Wiek,Miasto",
                    "Jan,Kowalski,25,Warszawa",
                    "Anna,Nowak,30,Kraków",
                    "Piotr,Wiśniewski,22,Gdańsk"
                };
                File.WriteAllLines(sciezka, przykładoweDane);
            }
            
            string[] linie = File.ReadAllLines(sciezka);
            
            // Pierwsza linia to nagłówki
            string[] naglowki = linie[0].Split(',');
            Console.WriteLine("Nagłówki:");
            for (int i = 0; i < naglowki.Length; i++)
            {
                Console.WriteLine($"{i}: {naglowki[i]}");
            }
            
            Console.WriteLine("\nDane:");
            
            // Pozostałe linie to dane
            for (int i = 1; i < linie.Length; i++)
            {
                string[] dane = linie[i].Split(',');
                Console.WriteLine($"Osoba {i}:");
                
                for (int j = 0; j < dane.Length && j < naglowki.Length; j++)
                {
                    Console.WriteLine($"  {naglowki[j]}: {dane[j]}");
                }
                Console.WriteLine();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Błąd: {ex.Message}");
        }
    }
}

Wyjaśnienie kluczowych funkcji:

  • File.WriteAllLines(sciezka, tablicaStringów) – tworzy plik z tablicy (każdy element = linia)
  • linie.Length – właściwość zwracająca liczbę elementów w tablicy
  • linie[0] – dostęp do pierwszego elementu tablicy (nagłówki)
  • for (int i = 1; i < linie.Length; i++) – pętla od 1 (pomijamy nagłówki)
  • dane.Length && j < naglowki.Length – sprawdzenie obu warunków (operator AND)
  • Exception ex – przechwytuje dowolny wyjątek i zapisuje w zmiennej ex
  • ex.Message – właściwość zawierająca opis błędu

Przykład – Parser prostego formatu danych

static void PrzetworzPlikDanych(string nazwaPliku)
{
    try
    {
        string[] linie = File.ReadAllLines(nazwaPliku);
        
        foreach (string linia in linie)
        {
            // Pomijamy puste linie i komentarze
            if (string.IsNullOrWhiteSpace(linia) || linia.StartsWith("#"))
                continue;
            
            // Dzielimy po dwukropku (format: klucz: wartość)
            string[] czesci = linia.Split(':', 2); // Maksymalnie 2 części
            
            if (czesci.Length == 2)
            {
                string klucz = czesci[0].Trim();
                string wartosc = czesci[1].Trim();
                Console.WriteLine($"{klucz} = {wartosc}");
            }
            else
            {
                Console.WriteLine($"Nieprawidłowy format linii: {linia}");
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Błąd podczas przetwarzania pliku: {ex.Message}");
    }
}

Wyjaśnienie kluczowych funkcji:

  • string.IsNullOrWhiteSpace(tekst) – sprawdza czy string jest pusty, null lub zawiera tylko białe znaki
  • linia.StartsWith("#") – sprawdza czy string zaczyna się od podanego znaku/tekstu
  • continue – przechodzi do kolejnej iteracji pętli (pomija resztę kodu w pętli)
  • Split(':', 2) – dzieli maksymalnie na 2 części (reszta trafia do drugiej części)
  • czesci.Length == 2 – sprawdza czy tablica ma dokładnie 2 elementy
  • Trim() – usuwa białe znaki (spacje, taby) z początku i końca stringu

Obsługa różnych formatów końca linii

static string[] PodzielNaLinie(string tekst)
{
    // Normalizacja różnych formatów końca linii
    tekst = tekst.Replace("\r\n", "\n").Replace("\r", "\n");
    
    // Dzielenie po znaku nowej linii
    return tekst.Split('\n', StringSplitOptions.RemoveEmptyEntries);
}

// Użycie:
string zawartoscPliku = File.ReadAllText("plik.txt");
string[] linie = PodzielNaLinie(zawartoscPliku);

foreach (string linia in linie)
{
    Console.WriteLine($"Linia: {linia}");
}

Wyjaśnienie kluczowych funkcji:

  • Replace(staryTekst, nowyTekst) – zamienia wszystkie wystąpienia staryTekst na nowyTekst
  • Replace("\r\n", "\n") – najpierw zamienia Windows na Unix (ważna kolejność!)
  • Replace("\r", "\n") – potem zamienia Mac na Unix
  • return – zwraca wartość z funkcji
  • static – funkcja należy do klasy, nie do instancji obiektu
  • Funkcja normalizuje wszystkie formaty do \n, potem dzieli

8. Najczęstsze błędy i jak ich unikać

  1. Nie zamykanie plików – używaj using do automatycznego zamykania
  2. Brak obsługi wyjątków – zawsze sprawdzaj czy plik istnieje i obsługuj błędy
  3. Kodowanie znaków – określaj kodowanie dla polskich znaków
  4. Ścieżki plików – używaj Path.Combine() zamiast konkatenacji stringów
  5. Nieprawidłowe dzielenie – pamiętaj o StringSplitOptions.RemoveEmptyEntries
  6. Różne formaty końca linii – normalizuj przed przetwarzaniem (\r\n vs \n vs \r)
  7. Puste linie i białe znaki – używaj Trim() i sprawdzaj IsNullOrWhiteSpace()

9.Przykład praktyczny – Program do notatek

using System;
using System.IO;

class ProgramNotatek
{
    static string nazwaPliku = "notatki.txt";
    
    static void Main()
    {
        while (true)
        {
            Console.Clear();
            Console.WriteLine("=== PROGRAM DO NOTATEK ===");
            Console.WriteLine("1. Wyświetl notatki");
            Console.WriteLine("2. Dodaj notatkę");
            Console.WriteLine("3. Wyczyść wszystkie notatki");
            Console.WriteLine("0. Wyjście");
            Console.Write("Wybierz opcję: ");
            
            string wybor = Console.ReadLine();
            
            switch (wybor)
            {
                case "1":
                    WyswietlNotatki();
                    break;
                case "2":
                    DodajNotatke();
                    break;
                case "3":
                    WyczyscNotatki();
                    break;
                case "0":
                    return;
                default:
                    Console.WriteLine("Nieprawidłowa opcja!");
                    break;
            }
            
            Console.WriteLine("Naciśnij dowolny klawisz...");
            Console.ReadKey();
        }
    }
    
    static void WyswietlNotatki()
    {
        Console.Clear();
        Console.WriteLine("=== TWOJE NOTATKI ===");
        
        if (File.Exists(nazwaPliku))
        {
            string[] notatki = File.ReadAllLines(nazwaPliku);
            if (notatki.Length == 0)
            {
                Console.WriteLine("Brak notatek.");
            }
            else
            {
                for (int i = 0; i < notatki.Length; i++)
                {
                    Console.WriteLine($"{i + 1}. {notatki[i]}");
                }
            }
        }
        else
        {
            Console.WriteLine("Brak pliku z notatkami.");
        }
    }
    
    static void DodajNotatke()
    {
        Console.Clear();
        Console.WriteLine("=== DODAJ NOTATKĘ ===");
        Console.Write("Wpisz swoją notatkę: ");
        string notatka = Console.ReadLine();
        
        if (!string.IsNullOrWhiteSpace(notatka))
        {
            string notatkaCzas = $"[{DateTime.Now:yyyy-MM-dd HH:mm}] {notatka}";
            File.AppendAllText(nazwaPliku, notatkaCzas + Environment.NewLine);
            Console.WriteLine("Notatka została dodana!");
        }
        else
        {
            Console.WriteLine("Notatka nie może być pusta!");
        }
    }
    
    static void WyczyscNotatki()
    {
        Console.Clear();
        Console.WriteLine("=== WYCZYŚĆ NOTATKI ===");
        Console.Write("Czy na pewno chcesz usunąć wszystkie notatki? (t/n): ");
        string potwierdzenie = Console.ReadLine().ToLower();
        
        if (potwierdzenie == "t" || potwierdzenie == "tak")
        {
            File.WriteAllText(nazwaPliku, "");
            Console.WriteLine("Wszystkie notatki zostały usunięte!");
        }
        else
        {
            Console.WriteLine("Operacja anulowana.");
        }
    }
}

10. Zadania do wykonania

Zadanie 1 – Podstawowe operacje

Napisz program, który:

  1. Sprawdza czy plik „dane.txt” istnieje
  2. Jeśli nie – tworzy go z przykładowymi danymi
  3. Odczytuje zawartość i wyświetla ją
  4. Dopisuje aktualną datę i godzinę

Zadanie 2 – Licznik słów

Napisz program, który:

  1. Odczytuje plik tekstowy
  2. Liczy ilość linii, słów i znaków
  3. Zapisuje statystyki do nowego pliku „statystyki.txt”

Zadanie 3 – Książka adresowa

Napisz prostą książkę adresową, która:

  1. Przechowuje dane w pliku „kontakty.txt”
  2. Umożliwia dodawanie kontaktów (imię, nazwisko, telefon)
  3. Wyświetla wszystkie kontakty
  4. Wyszukuje kontakt po nazwisku

Zadanie 5 – Analizator tekstu

Napisz program, który:

  1. Odczytuje plik tekstowy
  2. Dzieli tekst na słowa (po spacjach)
  3. Liczy wystąpienia każdego słowa
  4. Zapisuje wyniki do pliku „analiza.txt”

Zadanie 6 – Parser pliku konfiguracyjnego

Utwórz program, który:

  1. Odczytuje plik „config.txt” w formacie „klucz=wartość” (jedna para na linię)
  2. Dzieli każdą linię po znaku „=”
  3. Przechowuje konfigurację w Dictionary<string, string>
  4. Umożliwia wyszukiwanie wartości po kluczu

Zadanie 7 – Konwerter formatów końca linii

Napisz program, który:

  1. Odczytuje plik tekstowy
  2. Wykrywa format końca linii (\n, \r\n, \r)
  3. Konwertuje na wybrany format
  4. Zapisuje do nowego pliku

Zadanie 8 – Prosty parser CSV

Utwórz program do analizy pliku CSV, który:

  1. Wczytuje plik z danymi oddzielonymi przecinkami
  2. Dzieli każdą linię na kolumny
  3. Wyświetla dane w formie tabeli
  4. Umożliwia wyszukiwanie po wybranej kolumnie