Przejdź do treści stopki
Iron Academy Logo
Naucz się C#
Naucz się C#

Inne Kategorie

Async Zip w .NET 10: Jedna Linia Tworzenie lub Wyodrębnianie

Tim Corey
~12m

Operacje na plikach Zip w C# zawsze były możliwe poprzez System.IO.Compression, ale każde wywołanie było synchroniczne, co oznaczało, że wątek był blokowany do czasu pełnego zapisania lub odczytania archiwum. .NET 10 zmienia to dzięki zestawowi przeciążeń asynchronicznych, które pozwalają tworzyć, wyodrębniać i wypełniać pliki zip bez zajmowania wątku wywołującego.

Ten przewodnik demonstruje nowe asynchroniczne przeciążenia zip w .NET 10, oparte na niedawnym przewodniku Tima Coreya. Zajrzymy na trzy coraz bardziej szczegółowe podejścia: jednoliniowe do zbudowania archiwum, pojedyncze wywołanie do jego rozpakowania oraz metoda ręczna dla pełnej kontroli, przy jednoczesnym badaniu problemu z zakresu using.

Konfiguracja: Ścieżki i folder źródłowy

[0:28 - 1:55] Konfiguracja zaczyna się od aplikacji konsolowej celującej w .NET 10 i pojedynczej dyrektywy używania:

using System.IO.Compression;
using System.IO.Compression;

Trzy zmienne string definiują ścieżki używane w całym demie:

string sourceDirectory    = @"C:\temp\test";
string destinationZipFile = @"C:\temp\archive.zip";
string destinationDirectory = @"C:\temp\extracted";
string sourceDirectory    = @"C:\temp\test";
string destinationZipFile = @"C:\temp\archive.zip";
string destinationDirectory = @"C:\temp\extracted";

Prefiks łańcucha tekstowego literalnego (@) eliminuje konieczność podwajania ukośników wstecznych. sourceDirectory to folder do spakowania. destinationZipFile to pełna ścieżka do tworzonego archiwum. destinationDirectory to miejsce, gdzie po wyodrębnieniu trafią zawartości.

Jedno praktyczne ostrzeżenie: nigdy nie kieruj destinationZipFile na ścieżkę wewnątrz sourceDirectory. Zapisanie zip do folderu, który jest zipowany powoduje rekurencyjną pętlę odczytu, która przerywa proces.

Folder testowy zawiera dwa pliki w katalogu głównym i trzeci plik w podfolderze, co jest istotne przy eksploracji opcji includeBaseDirectory oraz obsługi ścieżek względnych w podejściu manualnym.

Tworzenie archiwum w jednej linii

[2:35 - 4:20] Asynchroniczne wywołanie tworzenia jest bezpośrednim zamiennikiem dla synchronicznego ZipFile.CreateFromDirectory:

await ZipFile.CreateFromDirectoryAsync(
    sourceDirectory,
    destinationZipFile,
    CompressionLevel.SmallestSize,
    includeBaseDirectory: false);
await ZipFile.CreateFromDirectoryAsync(
    sourceDirectory,
    destinationZipFile,
    CompressionLevel.SmallestSize,
    includeBaseDirectory: false);

CompressionLevel.SmallestSize priorytetyzuje najmniejszy wynik kosztem nieco dodatkowego czasu przetwarzania. CompressionLevel.Fastest robi odwrotnie. Dla większości scenariuszy deweloperskich różnica jest znikoma, ale na serwerze web przetwarzającym wiele archiwów jednocześnie, warto ocenić ten kompromis.

includeBaseDirectory kontroluje, czy nazwa katalogu źródłowego pojawia się jako wpis główny w archiwum. Przy false (domyślnie), zip otwiera się bezpośrednio do plików. Przekazanie true zamiast tego umieszcza folder test na początku, z rzeczywistymi plikami umieszczonymi wewnątrz niego. Większość przypadków użycia korzysta na ustawieniu tego na false.

Wyodrębnianie archiwum w jednej linii

[4:45 - 5:55] Wyodrębnianie śledzi ten sam wzorzec:

await ZipFile.ExtractToDirectoryAsync(
    destinationZipFile,
    destinationDirectory,
    overwriteFiles: false);
await ZipFile.ExtractToDirectoryAsync(
    destinationZipFile,
    destinationDirectory,
    overwriteFiles: false);

destinationDirectory jest tworzony automatycznie, jeśli jeszcze nie istnieje. Parametr overwriteFiles domyślnie ustawiony jest na false, co powoduje wyjątek, jeśli którykolwiek plik w archiwum już istnieje w docelowej ścieżce. Ustawienie go na true pozwala na ciche zastąpienie istniejących plików. Uruchomienie ekstrakcji dwukrotnie ilustruje to: pierwsze przejście kończy się sukcesem i tworzy folder, podczas gdy drugie rzuca wyjątkiem, chyba że zostanie wskazany overwriteFiles: true.

Wyborcze zipowanie: Dodawanie plików po jednym

[6:15 - 9:50] Podejście jednowierszowe zipuje cały katalog bez filtrowania. Gdy potrzebujesz uwzględnić tylko określone pliki, ręcznie tworzysz archiwum korzystając z FileStream i ZipArchive:

await using FileStream zipStream = new FileStream(
    destinationZipFile,
    FileMode.Create,
    FileAccess.Write,
    FileShare.None,
    bufferSize: 4096,
    useAsync: true);

using ZipArchive archive = await ZipArchive.CreateAsync(
    zipStream,
    ZipArchiveMode.Create,
    leaveOpen: false,
    entryNameEncoding: null);
await using FileStream zipStream = new FileStream(
    destinationZipFile,
    FileMode.Create,
    FileAccess.Write,
    FileShare.None,
    bufferSize: 4096,
    useAsync: true);

using ZipArchive archive = await ZipArchive.CreateAsync(
    zipStream,
    ZipArchiveMode.Create,
    leaveOpen: false,
    entryNameEncoding: null);

Kilka parametrów tutaj jest wartych zrozumienia. FileMode.Create nadpisuje każdy istniejący plik w tej ścieżce. FileMode.CreateNew rzuca wyjątkiem, jeśli plik już istnieje. FileShare.None blokuje plik wyłącznie podczas zapisu archiwum, uniemożliwiając innym procesom jego odczyt lub zapis w trakcie operacji.

useAsync: true na FileStream umożliwia asynchroniczne I/O na poziomie systemu operacyjnego. Zauważ, że ustawienie tego bez rzeczywistego używania wywołań asynchronicznych w dół może znacznie spowolnić proces, czasami nawet dziesięć razy. Ponieważ pętla zapisu wpisów używa await, właściwym wyborem tutaj jest useAsync: true. Na synchronicznej ścieżce kodu pozostaw to na domyślne false.

leaveOpen: false na ZipArchive mówi jej, aby zamykała i opróżniała strumień bazowy po jej wykorzystaniu. entryNameEncoding: null pozostawia domyślne kodowanie, które dokumentacja zaleca pozostawić, chyba że masz konkretny powód do jego zmiany.

Gdy archiwum jest gotowe, pobierz treści źródłowe i zapisz każdy wpis:

string[] files = Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories);

foreach (string filePath in files)
{
    string relativePathAndName = Path.GetRelativePath(sourceDirectory, filePath);
    await archive.CreateEntryFromFileAsync(filePath, relativePathAndName);
}
string[] files = Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories);

foreach (string filePath in files)
{
    string relativePathAndName = Path.GetRelativePath(sourceDirectory, filePath);
    await archive.CreateEntryFromFileAsync(filePath, relativePathAndName);
}

Directory.GetFiles z SearchOption.AllDirectories pobiera pliki z zagnieżdżonych podfolderów, co jest sposobem, w jaki struktura podfolderów jest zachowywana w archiwum. Argument wzorca ("*") to miejsce, w którym można filtrować po rozszerzeniu: "*.txt" ograniczyłby archiwum do plików tekstowych, na przykład.

Path.GetRelativePath usuwa absolutny prefiks z każdej ścieżki pliku, pozostawiając tylko część względną do sourceDirectory. To jest to, co jest przechowywane jako nazwa wpisu wewnątrz zip, wiernie powielając hierarchię podfolderu. Jeśli zamiast tego prześlesz Path.GetFileName(filePath), każdy wpis trafi na poziom główny zip niezależnie od swojego pierwotnego położenia. To tworzy płaskie archiwum, ale ryzykuje kolizję nazw, jeśli dwa wpisy dzielą nazwę pliku w różnych podfolderach.

Pułapka z użyciem zasięgowym

[9:50 - 11:30] Jeśli spróbujesz dodać wywołanie ekstrakcji po ręcznym bloku zip, napotkasz wyjątek blokady pliku: The process cannot access the file because it is being used by another process. Dzieje się tak, ponieważ instrukcja using na zipStream używa składni ograniczonej do pliku, co oznacza, że strumień pozostaje otwarty do końca pliku zamiast zamykania się przy nawiasie bloku zip. Próba ekstrakcji działa, gdy blokada nadal jest trzymana.

Konwersja do formy z nawiasami rozwiązuje to:

// Before (file-scoped: stream stays open until end of file)
await using FileStream zipStream = new FileStream(...);

// After (block-scoped; stream released at the closing brace)
await using (FileStream zipStream = new FileStream(...))
{
    // zip operations here
}
// stream is now closed; extraction can proceed safely
// Before (file-scoped: stream stays open until end of file)
await using FileStream zipStream = new FileStream(...);

// After (block-scoped; stream released at the closing brace)
await using (FileStream zipStream = new FileStream(...))
{
    // zip operations here
}
// stream is now closed; extraction can proceed safely

Opakowanie pracy zip w nawiasy i usunięcie końcowego średnika ogranicza czas życia strumienia do tego explicit zasięgu. Gdy wykonanie opuszcza zamykający nawias, blokada jest zwalniana, a kolejne wywołanie ekstrakcji może otworzyć ten sam plik bez konfliktu.

Wnioski

[11:40 - koniec] Jednowierszowce obsługują typowe przypadki: CreateFromDirectoryAsync dla archiwizowania całego folderu i ExtractToDirectoryAsync do jego rozpakowania. Gdy potrzebujesz kontroli nad tym, które pliki wejdą do archiwum, podejście manualne FileStream i ZipArchive daje ci filtrowanie, renaming i przekształcanie ścieżek na poziomie wpisów.

Podsumowując: dodaj using System.IO.Compression, wywołaj await ZipFile.CreateFromDirectoryAsync lub await ZipFile.ExtractToDirectoryAsync dla prostych przypadków, a sięgnij po manualną ścieżkę ZipArchive gdy potrzebujesz filtrowania lub zmiany nazwy wpisów. Określaj swoje bloki using za pomocą nawiasów, jeśli strumień musi zostać zwolniony przed uruchomieniem kolejnego kodu. Te dodatki czynią async/await wzorce dostępne na całej workflow zip w .NET 10.

Obejrzyj pełne wideo na kanale YouTube Tima Coreya channel, aby śledzić na żywo kodowanie.

Hero Worlddot related to Async Zip w .NET 10: Jedna Linia Tworzenie lub Wyodrębnianie
Hero Affiliate related to Async Zip w .NET 10: Jedna Linia Tworzenie lub Wyodrębnianie

Zarabiaj więcej, dzieląc się tym, co kochasz

Tworzysz treści dla deweloperów pracujących z .NET, C#, Java, Python, czy Node.js? Zamień swoją wiedzę specjalistyczną na dodatkowy dochód!

Zespol wsparcia Iron

Jestesmy online 24 godziny, 5 dni w tygodniu.
Czat
Email
Zadzwon do mnie