C# using の3形態 — using ステートメント / using 宣言 / await using で業務SE が踏む使い分け

C# using の3形態 — using ステートメント / using 宣言 / await using で業務SE が踏む使い分け

みなさんこんにちは!ヒロポンです!!

using (var conn = new SqlConnection(...))、業務 SE なら毎日のように書いてるやつ。

でもさ、なんで using を付けてるんだっけ?? ブロック抜けたら自動で Dispose が呼ばれる、そこまでは多分わかってる。

じゃあ C#で増えた using 宣言 (using var conn = ...;) と await using、この違いを 1 文で言える? ん? 詰まったらこの記事の出番。

副業や転職で .NET 6+ のコードを開いた瞬間、ここで止まる業務 SE はマジで多い。俺も最初に using var 見た時、「セミコロンで終わってる using って何?」で 30 分固まった口です。

この記事では C# の using 3 形態 (ステートメント / 宣言 / await using) の使い分けを、try-finally への展開・例外時の挙動・スコープの違いまで、コピペで動くコードと比較表で整理する。

目次

忙しい人向けに最初にまとめ

  • using には ステートメント (C# 1.0+) / 宣言 (C# 8+) / await using (C# 8+ / IAsyncDisposable) の 3 形態がある
  • どれも内部的には try-finally + Dispose に展開される・スコープと書きやすさが違うだけ
  • .NET Framework 4.7.2 (C# 7.3) は宣言形不可・副業/転職で .NET 6+ に触れる時に「これ何?」になりがち
  • 対処目安: 30 分で 3 形態の挙動と落とし穴を頭に入れられる
  • 同僚や後輩から「using var って何が違うんですか?」と聞かれて答えられないと、業務側の信頼回復より先輩としての面目が崩れるので潰しておく

そもそも using とは何をしているのか

C# の using ステートメント・宣言は、IDisposable を実装したオブジェクトを確実に Dispose するための糖衣構文。

内部的には try-finally に展開される。これだけ。

📖『Effective C#』(第5章 例外処理) ではこう書かれている:

メソッド中で IDisposable オブジェクトを使用する場合、そのオブジェクトを確実に破棄するには using ステートメントを使用する方法が最も簡単です。using ステートメントを使用すると、ステートメント内で生成されるオブジェクトを囲うように try…finally ブロックが用意されます。次の2つのコードからは同じ IL が生成されます。

言い換えるとこう。以下 2 つは同じ IL に展開される:

// 形① using ステートメント
using (var reader = new StreamReader("data.txt"))
{
    var line = reader.ReadLine();
    Console.WriteLine(line);
}

// 形② try-finally で手書き (同じ IL)
StreamReader reader = null;
try
{
    reader = new StreamReader("data.txt");
    var line = reader.ReadLine();
    Console.WriteLine(line);
}
finally
{
    if (reader != null) ((IDisposable)reader).Dispose();
}

書きやすさが違うだけで挙動は完全に同じ。例外が出ても finally で確実に Dispose が呼ばれる。

IDisposable は超シンプルなインターフェイス

📖 同書 (第2章 リソース管理) より:

public interface IDisposable { void Dispose(); }

Dispose() メソッド 1 つだけ。

SqlConnection / StreamReader / FileStream など、OS リソース (ハンドル・接続・ファイル) を持つ全クラスは IDisposable を実装してる。これを忘れるとリソースリーク → 本番で「接続プール枯渇」「ファイルロック残存」で詰む。

業務 SE が踏む 3 形態の使い分け

順に書くとこう。

  1. 形態①: using ステートメント (C# 1.0+ / 全環境で使える)
  2. 形態②: using 宣言 (C# 8+ / 関数末尾まで自動 Dispose)
  3. 形態③: await using (C# 8+ / IAsyncDisposable / 非同期解放)

3 つとも目的は同じ「Dispose を確実に呼ぶ」。違いはスコープ非同期対応だけ。

形態①: using ステートメント (C# 1.0+ 全環境 OK)

レガシー WinForms / .NET Framework 4.7.2 の現場で見るやつ。C# 1.0 から使えて、ブロックスコープが明確。

using System;
using System.Data.SqlClient;

class Program
{
    static void Main()
    {
        // ブロックスコープで Dispose
        using (var conn = new SqlConnection("Server=...;Database=...;Trusted_Connection=True;"))
        {
            conn.Open();
            Console.WriteLine($"State: {conn.State}");
            // この } を抜けた瞬間に Dispose が呼ばれる
        }

        // conn はここでアクセス不可 (スコープ外)
        Console.WriteLine("Dispose 後");
    }
}

実行結果:

using ステートメントの実行結果

特徴:

  • C# 1.0+ で使える (.NET Framework 1.1 以降の全環境)
  • ブロック { } でスコープが明確
  • } を抜けた瞬間に Dispose
  • 例外が出ても finally で確実に Dispose
  • 入れ子で書くとピラミッド構造になって読みにくくなる ← 形態② が解決する課題

try-finally との等価性 (diff で確認)

「using って結局 try-finally じゃん」と思う人向けに、同じ意味のコードを diff で並べる。

using ステートメントと try-finally の等価性 (同じ IL に展開)

Effective C# でも「同じ IL が生成される」と明言されてる。

つまり書き方が短いだけで挙動は完全に同じ。業務 SE 視点だと「読みやすさのために using 書く」で OK。

形態②: using 宣言 (C# 8+ / 関数末尾まで自動 Dispose)

C# 8 (2019 年・.NET Core 3.0+ / .NET 5+) で追加。using の後に (...) を書かず、セミコロンで終わる。

関数末尾まで自動で Dispose される、ってのがミソ。

using System;
using System.Data.SqlClient;

class Program
{
    static void Main()
    {
        // 関数スコープで Dispose
        using var conn = new SqlConnection("Server=...;Trusted_Connection=True;");
        conn.Open();
        Console.WriteLine($"State: {conn.State}");

        // ... 関数末尾まで conn は生きてる
        DoWork(conn);

    } // ここで Main を抜ける瞬間に conn.Dispose() が呼ばれる
}

実行結果:

using 宣言の実行結果

特徴:

  • C# 8+ 専用 (.NET Framework 4.7.2 / 4.8 では使えない・.NET Core 3.0+ / .NET 5+ のみ)
  • インデントが 1 段浅くなる (ピラミッド回避)
  • スコープは関数末尾まで
  • 例外時も同じく finally で Dispose

using 宣言の落とし穴: スコープが関数末尾になる

これ、関数が長いと変数スコープが分かりにくくなるんよ。こんなコードに遭遇すると詰まる。

static void ProcessOrders()
{
    using var conn = new SqlConnection(connStr);
    conn.Open();

    // ... 50 行ほど何かの処理 ...

    using var cmd = conn.CreateCommand();
    cmd.CommandText = "UPDATE Orders SET ...";

    // ... さらに 30 行 ...

    // ここで関数終わり → conn と cmd が一気に Dispose される
    // ★ 順序: cmd が先・conn が後 (宣言と逆順で Dispose)
}

俺の現場でやられたやつ。using var を関数末尾でぜんぶ Dispose する流れになってて、cmd と conn の Dispose 順序が逆だと接続プール詰まる、ってパターンで本番夜中に起こされた。

SqlConnection を Dispose する前に SqlCommand を Dispose しないと挙動がおかしくなるケースがある。マジでタチ悪い。

回避策: スコープを明確にしたい時は形態① (ブロック) に戻す、もしくは関数を分割する。

形態③: await using (C# 8+ / IAsyncDisposable)

非同期解放が必要なリソース (DbContext / NetworkStream / Pipeline 系) を非同期で解放するための形。これも C# 8+。

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

class Program
{
    static async Task Main()
    {
        // IAsyncDisposable を実装した DbContext を非同期 Dispose
        await using var db = new MyDbContext();
        var orders = await db.Orders.ToListAsync();

        Console.WriteLine($"取得件数: {orders.Count}");

    } // ここで await db.DisposeAsync() が呼ばれる
}

実行結果:

await using の実行結果

特徴:

  • C# 8+ かつ IAsyncDisposable を実装した型のみ
  • 通常の Dispose() ではなく DisposeAsync() が await される
  • DB 接続のクローズ通信・ネットワークソケットの shutdown 等で I/O 待ちが発生する場合に使う
  • 通常の using で IAsyncDisposable オブジェクトを書くと警告 CS0840 が出る (CA2007 関連)

いつ await using を使う?

判断は単純。IAsyncDisposable を実装してるなら await using、そうでなければ using。EF Core の DbContext / IAsyncEnumerable / .NET 6+ の HttpClient 系などが対象。

書き間違いを防ぐコツは IDE 任せでいい。VS / Rider で IDisposable だけ実装してる型に await using を書くとエラー、逆に IAsyncDisposable 実装してる型に using を書くと「await using にしませんか」と警告が出る。

3 形態 × 5 観点 比較表

3 形態の使い分けを 1 枚にまとめる。

C# using 3形態の比較表 — 言語バージョン × スコープ × 例外時挙動 × 非同期 × 非適用ケース

table1

業務 SE 視点だと所属現場の .NET バージョンで選択肢が決まる。.NET Framework 4.7.2 メインなら形態① 一択、.NET 6+ 新規案件なら形態② を主軸 + 必要時に形態③。

罠①: 例外が出たら Dispose は呼ばれない?

using の中で例外が出たら Dispose は呼ばれないんじゃない??」と聞かれることが多い。

答えは「呼ばれる」。内部的に try-finally に展開されてて、例外が出ても finally で Dispose が走るから。

Effective C# 第5章でも「ステートメント内で生成されるオブジェクトを囲うように try…finally ブロックが用意される」と明言されてる。

ところが以下のケースは Dispose が呼ばれない:

  • using の ( ) 内 (オブジェクト生成中) で例外が出た場合 (= まだ try 句に入ってない)
  • コンストラクタ内で例外 (オブジェクト自体が完成してない)
  • プロセス強制終了 (Environment.FailFast / kill -9 / 電源断)

最初の 2 つは「まだリソース確保してないから Dispose 不要」という設計。最後はそもそも GC も動かないので諦める領域。

罠②: 複数 IDisposable の冗長性 — using ネスト or try-finally 手書きか

📖『Effective C#』(第5章) では複数オブジェクトを扱う時のヒントもある:

using ステートメントそれぞれに対して try…finally ブロックが生成されます。幸いなことに、IDisposable を実装している 2 つの別々のオブジェクトを 1 つのメソッド内で生成することは滅多にありません。上のコードの場合、正しく機能するためこのままで問題ありません。とはいえ、このコードは冗長であるため、複数の IDisposable オブジェクトを同じブロック内で生成するような try…finally ブロックを自分で用意した方がよい場合もあるでしょう。

つまり「using ネスト 3 段以上になったら try-finally 手書きを検討」。C# 8+ なら形態② (using 宣言) で 1 段浅くする選択もあり。

入れ子の解消パターン

// ❌ ピラミッド (using ネスト 3 段)
using (var conn = new SqlConnection(connStr))
using (var cmd = conn.CreateCommand())
using (var reader = cmd.ExecuteReader())
{
    while (reader.Read()) { /* ... */ }
}

実は C# 1.0+ でも上の書き方は OK で、これは「括弧省略」として認められてる。ピラミッド回避のテクニック。

C# 8+ ならもっと短く:

// ✅ using 宣言 (C# 8+)
using var conn = new SqlConnection(connStr);
using var cmd = conn.CreateCommand();
using var reader = cmd.ExecuteReader();
while (reader.Read()) { /* ... */ }

罠③: using 宣言の Dispose 順序は宣言の逆順

形態② の落とし穴をもう一段掘り下げる。

複数の using var がある時、Dispose は宣言の逆順で呼ばれる。

static void Main()
{
    using var a = new ResourceA(); // 1番目に作る
    using var b = new ResourceB(); // 2番目に作る
    using var c = new ResourceC(); // 3番目に作る

    // 関数末尾:
    // ① c.Dispose()
    // ② b.Dispose()
    // ③ a.Dispose()
} // ← この順で Dispose

業務 SE がハマるのが、SqlConnection と SqlCommand の順序。conn を先に宣言して cmd を後に宣言すれば、関数末尾で cmd.Dispose()conn.Dispose() の順になって安全。

これを逆にすると、cmd が死んだ conn に対して Dispose を呼ぼうとしておかしくなる場面がある。

俺もこれで本番夜中に「接続プール枯渇 → 全 API が 500 連発」のトレースで 2 時間溶かしたことがある。マジでやらかした。

業務 SE が using で迷ったら見るフロー

ここまでの判断軸を 1 枚にまとめる。

C# using 3形態の選択フロー (使うべきパターンを言語バージョンと非同期要否で分岐)

動作確認メモ: ここで紹介した C# コードは .NET 8 SDK の Docker container でビルド + 実行確認済 (using 3 形態すべての Dispose 呼び出しログが出ることを検証)。.NET Framework 4.7.2 環境で形態② / ③ を書くと CS8370 (Feature 'using declarations' is not available in C# 7.3.) で構文エラーになる。所属現場の <LangVersion>.csproj で確認しておくのが定石。

俺の現場メモ

数年前、物流系の基幹システム保守をやってた時の話。

.NET Framework 4.7.2 メインの現場で、副業で受けた .NET 6 のコードを開いた瞬間、using var conn = ...; を見て手が止まった。

「これ、セミコロンで終わってる using って何??」で 30 分検索した記憶。

結局 C# 8 で増えた using 宣言 だとわかったんやけど、当時の俺は .NET Framework 4.7.2 の現場知識しか持ってなかったので、新文法を見ただけで「これは違う言語か?」って一瞬本気で思った。マジで未熟。

しかも同僚から「await using db = new DbContext(); って何?」とも聞かれて、「あ、EF Core の DbContext は IAsyncDisposable なんで…」って説明できず、翌日「すみません、ちゃんと調べてきました」って謝った経験もある。

技術的にはこれで解決。

でも一番きつかったのは復旧時間より、後輩から先輩としての面目が一段落ちた瞬間。こっちの方が重い、ってのは業務 SE やってる人ならわかると思う。

その後 3 形態を頭に入れてからは、副業先で using var を見ても瞬時に「あ、C# 8 宣言形ね」って判定できるようになった。30 分の投資が、副業/転職市場での読みやすさ確保に効いた、って感じ。

まとめ

ここまでで C# using 3 形態の使い分けは揃った。要点を並べると:

  • using には ステートメント (C# 1.0+) / 宣言 (C# 8+) / await using (C# 8+) の 3 形態
  • 内部的にはすべて try-finally + Dispose に展開・例外時も Dispose 呼ばれる
  • .NET Framework 4.7.2 メインの現場は形態①一択・.NET 6+ 新規案件は形態②主軸+IAsyncDisposable で形態③
  • 入れ子 3 段以上なら形態② か try-finally 手書きで読みやすく
  • using 宣言の Dispose 順序は宣言の逆順・SqlConnection と SqlCommand の順序ミスに注意

同僚や後輩から「using var って何?」と聞かれて答えられるかどうかは、業務 SE の年次が上がるほど効いてくる。

ここで詰まると後輩から「先輩、こんなこと知らないんすか」って言われて、朝礼後の信頼回復に丸 1 日かかるやつ。30 分で潰せる論点なので、平時に押さえておくのが筋。

こんな感じで使い分けを 1 周頭に入れておけば、.NET バージョン違いの現場を渡り歩いても迷わない。

よくある質問

Q1. using を書き忘れて Dispose し忘れたらどうなる?

A. ファイナライザ (デストラクタ) が GC のタイミングで呼ばれて Dispose 相当の処理が走るが、タイミングが不定。接続プールが解放されるまで数秒〜数分のラグが出て、高負荷時に「接続プール枯渇」や「ファイルロック残存」を引き起こす。IDisposable を持つオブジェクトは using 書く、が鉄則。

Q2. C# 8 の using 宣言を .NET Framework 4.8 で使えますか?

A. 使えませんusing 宣言は C# 8 言語機能で、C# 8 を完全サポートするのは .NET Core 3.0+ / .NET 5+ のみ。.NET Framework 4.x は C# 7.3 まで。csproj で <LangVersion>8.0</LangVersion> を指定しても一部機能だけ動く / 動かないケースがあるので、.NET Framework 案件では形態① (ステートメント) に統一するのが安全。

Q3. using の中で例外が出たら Dispose は呼ばれますか?

A. 呼ばれます。using は内部的に try-finally に展開されるため、try 句内で例外が出ても finally 句で確実に Dispose が走る。ただし「using の ( ) 内 (オブジェクト生成中) の例外」「コンストラクタ内の例外」はオブジェクトがまだ完成していないので Dispose 不要 (というか呼べない)。

Q4. await usingusing を間違えて書いたらどうなる?

A. IAsyncDisposable のみ実装してる型に using を書くと、警告 (CA2007 / CS0840 系) が出るがコンパイル自体は通るケースがある。とはいえ Dispose() が呼ばれて DisposeAsync() が呼ばれないので、非同期解放が必要な処理 (DB 接続のクローズ通信等) がスキップされる。IAsyncDisposable 実装型にはかならず await using を使う。

Q5. 業務 SE が using var を見たら、すぐ何をすべきですか?

A. 3 ステップで頭を切り替える。①「これは C# 8 の using 宣言だな」→ ②「この変数は関数末尾まで生きる」→ ③「Dispose は関数末尾で宣言の逆順で呼ばれる」。この 3 ステップを 30 秒で回せれば、副業/転職先のコードレビューでも詰まらない。

関連記事

以上!

同じ罠でハマってる人いたら、どんどんシェア待ってるぜ!!


執筆者

バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。現職は SEO 直轄部の AI アドバイザー兼 PL、副業で中小 SIer の CTO。SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る

この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次