.NET 10での非同期Zip: ワンラインで作成または抽出
[[academy-video-youtube({"vid": "YgQ3ta6455A", "start_time": "0", "title": "Async Zip in .NET 10: One Line Create or Extract", "creator": "Tim Corey", "length": "~12m"})]]
C#のZipファイル操作は常にSystem.IO.Compressionを通じて可能でしたが、すべての呼び出しは同期的でした。つまり、アーカイブが完全に書き込まれたり読まれたりするまでスレッドはブロックされていました。 .NET 10はこれを変更し、一連の非同期オーバーロードによって、呼び出しスレッドをブロックせずにZipファイルを作成、抽出、ポピュレートできるようにします。
このウォークスルーでは、Tim Coreyの最近のガイドに基づいた.NET 10の新しい非同期Zipオーバーロードを紹介します。アーカイブを構築するためのワンライナー、解凍するための1回の呼び出し、完全なコントロールのためのマニュアルメソッドを段階的に詳述した3つのアプローチを見ていきます。また、スコープ付きのusingの落とし穴も検討します。
セットアップ: パスとソースフォルダ
[0:28 - 1:55] セットアップは、.NET 10をターゲットとするコンソールアプリケーションと1つの使用ディレクティブから始まります。
using System.IO.Compression;using System.IO.Compression;3つのストリング変数がデモ全体で使用されるパスを定義します:
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";逐語的な文字列接頭辞(@)はバックスラッシュを二重にする必要をなくします。 sourceDirectory はZipするフォルダです。destinationZipFile は作成されるアーカイブの完全なパスです。 destinationDirectory は抽出されるときにコンテンツが置かれる場所です。
1つの実用的な警告として、sourceDirectory内のパスに向けてはいけません。 zipを生成するフォルダに書き込むと、プロセスを破る再帰的な読み取りループを引き起こします。
テストフォルダにはルートに2つのファイルとサブフォルダ内に3番目のファイルが含まれています。このことは、includeBaseDirectoryオプションや相対パスの処理を手動で行う際に重要です。
ワンライナーでアーカイブを作成する
[2:35 - 4:20] 非同期作成呼び出しは同期のZipFile.CreateFromDirectoryの直接の代替です:
await ZipFile.CreateFromDirectoryAsync(
sourceDirectory,
destinationZipFile,
CompressionLevel.SmallestSize,
includeBaseDirectory: false);await ZipFile.CreateFromDirectoryAsync(
sourceDirectory,
destinationZipFile,
CompressionLevel.SmallestSize,
includeBaseDirectory: false);CompressionLevel.Fastestはその逆を行います。 ほとんどの開発シナリオではその違いは無視できるものですが、多くのアーカイブを同時に処理するウェブサーバーでは、トレードオフは評価に値します。
false(デフォルト)では、ZIPは直接ファイルに開きます。 testフォルダがルートに置かれ、その中に実際のファイルが入ります。 ほとんどの使用ケースでは、これをfalseに設定することが有益です。
ワンライナーでアーカイブを抽出する
[4:45 - 5:55] 抽出は同じパターンに従います:
await ZipFile.ExtractToDirectoryAsync(
destinationZipFile,
destinationDirectory,
overwriteFiles: false);await ZipFile.ExtractToDirectoryAsync(
destinationZipFile,
destinationDirectory,
overwriteFiles: false);存在しない場合、falseになり、ターゲットパスにアーカイブ内の任意のファイルがすでに存在する場合に例外をスローします。 既存のファイルを静かに置換するためにはそれをtrueに設定します。 抽出を2回実行するとこれが示されます:最初のパスは成功してフォルダを作成しますが、2回目はoverwriteFiles: trueが指定されていない限り例外をスローします。
選択的Zip化: ファイルを一つずつ追加
[6:15 - 9:50] ワンライナーアプローチはフィルタリングなしでディレクトリ全体をZip化します。 特定のファイルだけを含める必要がある場合、FileStreamと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);ここでいくつかのパラメータは理解する価値があります。 FileMode.Createは、そのパスに既に存在するファイルを上書きします。 ファイルが既に存在する場合、FileMode.CreateNewは代わりに例外をスローします。 FileShare.Noneはアーカイブが書き込まれている間、ファイルを独占的にロックし、他のプロセスが操作中にそれを読み書きするのを防ぎます。
useAsync: trueは、OSレベルで非同期I/Oを有効にします。 これを設定しても下流で実際に非同期呼び出しを使用しないと、プロセスが大幅に遅くなることがあります。時には10倍にもなります。 エントリ書き込みループがuseAsync: trueが正しい選択です。 同期的なコード経路では、デフォルトのfalseのままにします。
ZipArchive上のleaveOpen: falseは、破棄時に基になるストリームを閉じてフラッシュするように指示します。 entryNameEncoding: nullはデフォルトのエンコーディングを保持します。ドキュメントは、特定の理由がない限り変更することを推奨していません。
アーカイブの準備が整ったら、ソース内容を取得して各エントリーを書き込みます。
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);
}"</em>.txt"はアーカイブをテキストファイルに限定します。
sourceDirectoryに対する部分のみを残します。 これが、zip内にエントリー名として保存され、サブフォルダ階層が忠実に再現されるものです。 代わりにPath.GetFileName(filePath)を渡すと、元の場所に関係なくすべてのエントリがZipのルートに配置されます。 これはフラットなアーカイブを生成しますが、異なるサブフォルダーでファイル名を共有しているエントリーがあると名前の衝突が発生する可能性があります。
Scoped Using Pitfall
[9:50 - 11:30] 手動Zipブロックの後に抽出呼び出しを追加しようとすると、ファイルロック例外に遭遇します:The process cannot access the file because it is being used by another process。 これは、zipStream上のusingステートメントがファイルスコープの構文を使用しているために発生します。これにより、ストリームはZipブロックの中括弧で閉じるのではなく、ファイルの終わりまで開いています。 ロックがまだ保持されている間に抽出試行が行われます。
ブレース形式に変換することで、この問題が解決します。
// 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 safelyzip作業をブレースで囲み、末尾のセミコロンを削除することで、ストリームの寿命をその明示的なスコープに制限します。 実行が閉じる括弧を出たら、ロックが解除され、後続の抽出呼び出しで同じファイルを無矛盾で開けるようになります。
結論
[11:40 - end] ワンライナーは一般的なケースを処理します:フォルダ全体を圧縮するためのExtractToDirectoryAsyncです。 どのファイルがアーカイブに入るかを制御する必要がある場合、手動のZipArchiveのアプローチが、フィルタリング、リネーム、およびパスの書き換えをエントリーレベルで可能にします。
まとめますと:ZipArchiveパスを選びます。 ストリームを次のコードの前に解放する必要がある場合は、usingブロックを中括弧でスコープ化します。 これらの追加により、.NET 10全体でのフルzipワークフローでasync/awaitパターンが利用可能です。
ライブコーディングをフォローするには、Tim CoreyのYouTube動画を完全に視聴してください。

