CodeGym /Kursy /C# SELF /Strumienie wejścia-wyjścia:

Strumienie wejścia-wyjścia: Stream

C# SELF
Poziom 36 , Lekcja 1
Dostępny

1. Wprowadzenie

Wyobraź sobie czajnik z wodą. Otwierasz kran — woda zaczyna lecieć. Możesz nalać dużo wody na raz, a możesz napełniać czajnik po trochu. Tak samo jest z plikami — nie zawsze wygodnie lub możliwe jest od razu załadować cały plik do pamięci. Pliki bywają duże, a czasem źródłem danych może być nie plik, a na przykład połączenie sieciowe, gdzie dane przychodzą stopniowo.

Gdybyśmy zawsze próbowali pracować tylko z tablicami bajtów, to przy dużych plikach szybko skończyłaby się pamięć, a dla "nieskończonych" strumieni danych (np. wideo lub audio) takie podejście po prostu nie zadziała. I tu właśnie z pomocą przychodzi koncepcja strumienia!

W .NET strumień to abstrakcja do sekwencyjnego dostępu do danych: nie ma znaczenia, co jest źródłem — plik, sieć, pamięć, czy nawet coś zupełnie egzotycznego jak skompresowane archiwum. Strumień pozwala ci czytać i zapisywać dane po kawałkach, zwykle blokami lub bajtami.

Główna idea:

  • Strumień — to kanał do przesyłania danych. Jest jak taśma produkcyjna: możesz "kłaść" (zapisywać) lub "zabierać" (czytać) dane, nie martwiąc się bezpośrednio o szczegóły, gdzie i jak są przechowywane.
  • Dane przychodzą sekwencyjnie: możesz przeczytać następny kawałek dopiero po poprzednim (albo odwrotnie, jeśli obsługiwane jest przewijanie).
  • W większości przypadków nie trzymasz wszystkich danych naraz w pamięci (i komputer ci za to podziękuje).

Ta abstrakcja leży u podstaw praktycznie wszystkich operacji wejścia-wyjścia w .NET: praca z plikami, sieciami, archiwami, nawet z konsolą!

2. Strumienie System.IO.Stream

Dziedziczenie i architektura: System.IO.Stream

Prawie wszystkie strumienie w .NET dziedziczą po abstrakcyjnej klasie System.IO.Stream. Ona definiuje podstawowe metody do czytania, zapisywania, przemieszczania się po strumieniu i zarządzania nim.


classDiagram
    class Stream {
        +Read()
        +Write()
        +Seek()
        +CanRead
        +CanWrite
        +CanSeek
        +Length
        +Position
    }
    class FileStream
    class MemoryStream
    class NetworkStream
    class CryptoStream
    Stream <|-- FileStream
    Stream <|-- MemoryStream
    Stream <|-- NetworkStream
    Stream <|-- CryptoStream
Schemat dziedziczenia strumieni w .NET
  • Stream — bazowa klasa abstrakcyjna
  • FileStream — do pracy z plikami
  • MemoryStream — do pracy z danymi w pamięci
  • NetworkStream — do komunikacji sieciowej
  • CryptoStream — do szyfrowania/deszyfrowania

Krótkie zapoznanie z kluczowymi właściwościami i metodami strumienia

Właściwość / Metoda Opis
CanRead
Czy można czytać z tego strumienia
CanWrite
Czy można pisać do tego strumienia
CanSeek
Czy można przemieszczać się po strumieniu (nie wszystkie to obsługują)
Length
Długość strumienia (jeśli obsługiwane — nie każdy strumień to ma)
Position
Aktualna pozycja w strumieniu
Read(...)
Czytanie danych
Write(...)
Zapisywanie danych
Seek(...)
Przemieszczanie się po strumieniu
Flush()
Opróżnienie bufora (zapisanie wszystkiego co się nagromadziło do strumienia)
Close()
/
Dispose()
Zamknięcie strumienia i zwolnienie zasobów

Zobaczmy, jak to wygląda "w praktyce".

3. Przykład: czytanie i zapisywanie plików przez Stream

Oto dość minimalny przykład, żeby zobaczyć strumień "w akcji":


// Otwieramy plik do zapisu
using var stream = new FileStream("numbers.bin", FileMode.Create);

// Załóżmy, że chcemy zapisać liczby od 1 do 10 do pliku
for (int i = 1; i <= 10; i++)
{
    byte val = (byte)i;
    stream.WriteByte(val); // Zapisujemy po jednym bajcie
}

// Jawnie zamykamy plik, żeby otworzyć go do czytania
stream.Close(); 

// Teraz spróbujemy odczytać te liczby z powrotem
using var stream2 = new FileStream("numbers.bin", FileMode.Open);
int value;
while ((value = stream2.ReadByte()) != -1)
{
    Console.WriteLine(value); // Wypisze 1, 2, ... 10
}

Tutaj używamy FileStream, który jest prawdziwym strumieniem w pełnym tego słowa znaczeniu: czytasz i zapisujesz dane blokami lub bajtami.

Rodzaje strumieni: gdzie można je spotkać?

Strumień to niekoniecznie tylko plik na dysku. Oto kilka przykładów, gdzie używa się koncepcji strumienia:

  • Plik na dysku (np. FileStream — najczęstszy przypadek)
  • Strumień w pamięci RAM (MemoryStream — wygodne dla tymczasowych lub pośrednich danych)
  • Połączenie sieciowe (NetworkStream)
  • Kompresja/archiwizacja (GZipStream, DeflateStream)
  • Szyfrowanie (CryptoStream)
  • Wejście/wyjście konsolowe (tak, tak!) — technicznie też strumienie

To pozwala pisać kod bez martwienia się o konkretne źródło/odbiornik danych: jeśli twój kod działa ze strumieniem, to jest uniwersalny!

4. Przydatne niuanse

Czytanie i zapisywanie to operacje przesyłania danych po kawałkach. Zwykle przez tablice bajtów i metody Read, Write.

Przykład: czytanie pliku blokami

byte[] buffer = new byte[1024]; // Bufor na 1024 bajty (1 KB)
using var stream = new FileStream("bigfile.bin", FileMode.Open);

int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
    // Przetwarzamy tylko bytesRead bajtów w buffer
    int sum = 0;
    for (int i = 0; i < bytesRead; i++)
        sum += buffer[i];

    Console.WriteLine($"Suma bloku: {sum}");
}

To podejście jest używane wszędzie — od antywirusów po odtwarzacze muzyki.

Pozycjonowanie w strumieniu (Position, Seek)

W większości implementacji strumieni (np. plikowych) można przemieszczać się po danych — czytać nie tylko "następny kawałek", ale przejść do konkretnej pozycji i stamtąd pracować z danymi.

using var stream = new FileStream("numbers.bin", FileMode.Open);
stream.Position = 5; // Przechodzimy na 6-ty bajt (liczenie od 0)
int value = stream.ReadByte();
Console.WriteLine($"6-ty bajt w pliku: {value}");

Strumienie mogą być tylko do czytania, tylko do zapisu, albo oba naraz

Niektóre strumienie obsługują tylko jedną z opcji:

  • Plik otwarty do zapisu: tylko Write()
  • Strumień do czytania danych sieciowych: tylko Read()
  • W niektórych egzotycznych przypadkach (np. strumień do drukarki) w ogóle nie da się "cofnąć" ani pozycjonować po strumieniu (nie można przewinąć wstecz).

Sprawdź obsługiwane operacje przez właściwości CanRead, CanWrite, CanSeek:

using var stream = new FileStream("myfile.txt", FileMode.OpenOrCreate);
if (stream.CanRead)
    Console.WriteLine("Czytanie obsługiwane");
if (stream.CanWrite)
    Console.WriteLine("Zapis obsługiwany");
if (stream.CanSeek)
    Console.WriteLine("Można przemieszczać się po pliku");

Buforowanie w strumieniach

Prawie wszystkie strumienie używają wewnętrznych buforów dla zwiększenia wydajności. Buforowanie oszczędza dostęp do dysku/sieci: dane zbierają się wewnętrznie, a potem są przekazywane/zapisywane hurtowo.

Metoda Flush() pozwala opróżnić bufor (np. żeby mieć pewność, że wszystko zostało zapisane na dysk):

using var stream = new FileStream("log.txt", FileMode.Append);
byte[] bytes = Encoding.UTF8.GetBytes("Hello, Stream!\n");
stream.Write(bytes, 0, bytes.Length);
stream.Flush(); // Gwarantuje, że zapis na pewno trafił na dysk

Jeśli zapisujesz bardzo ważne dane (np. transakcje płatnicze!), wywołanie Flush() to twój przyjaciel.

5. Typowe błędy przy pracy ze strumieniami

Bardzo często początkujący popełniają takie błędy:

Zapominają zamknąć strumień (i mają wycieki pamięci, "zawieszone" pliki i inne atrakcje).

Mylą strumienie tekstowe i binarne — próbują zapisać string metodą bajtową, a potem dostają "krzaczki".

Używają zbyt małego bufora (albo w ogóle bez bufora) — operacje są wtedy wolne.

Myślą, że Read() zawsze czyta dokładnie tyle bajtów, ile proszą — w rzeczywistości może zwrócić mniej; zawsze trzeba sprawdzać zwracaną wartość.

Nie biorą pod uwagę, że nie wszystkie strumienie obsługują pozycjonowanie (Seek), szczególnie sieciowe.

Na przykład:


// Zły przykład: czytanie wszystkich bajtów pliku bez sprawdzania ile faktycznie przeczytano
byte[] buffer = new byte[1024];
using (var stream = new FileStream("data.bin", FileMode.Open))
{
    int bytesRead = stream.Read(buffer, 0, 1024);
    // bytesRead może być mniejsze niż 1024, jeśli plik jest krótszy!
}
2
Zadanie
C# SELF, poziom 36, lekcja 1
Niedostępne
Sprawdzanie właściwości strumienia
Sprawdzanie właściwości strumienia
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION