C#面接の技術質問:LINQ・async・リソース管理をコードで説明する

C#面接の技術質問を、請求書のコードと32項目の検証で練習。LINQの遅延実行、参照と値の具体化、Taskの重複開始、取消しとawait usingを追います。選択時点、処理開始、非同期解放の完了を分け、実行結果と保証できない範囲を説明します。制御用の合図で、成功・取消し・失敗と所有権を確かめます。

Author: PracHub

Published: 10/11/2026

C#面接の技術質問:LINQ・async・リソース管理をコードで説明する

October 11, 2026

Quick Overview

C#面接を請求書の可変データと32項目の実行検証で練習します。LINQの遅延実行、値と参照の具体化、Taskを二度列挙する重複、取消しとawait usingの後処理を比較。選択・開始・解放完了の三つの時点からコードと所有権を説明します。

Software EngineerFree

C#面接でLINQ、async、リソース管理を説明するときは、**「いつ選ぶか」「いつ処理を始めるか」「いつ解放が終わるか」**を分けて追うと、短いコードの不具合が見えてきます。ToArray()を追加しても、何を配列にしたかで保証は変わります。キャンセルを要求しても、呼び出し元のTaskがすぐ完了するとは限りません。

この記事では、請求書の候補を選び、非同期で読み込み、セッションを解放する独自ケースを使います。最初に出力と呼出回数を予測し、それから修正が変える結果を確認します。キーワードの定義だけで終わらず、データを読む時点と、処理が終わる時点を説明する練習です。

根拠の区分: LINQ、非同期処理、取消し、await usingの仕組みはMicrosoftの公式資料に基づきます。請求書データとテスト用セッションはこの記事の演習です。.NET SDK 10.0.401、ランタイム10.0.12で32項目の検証を実行しました。設計上の助言はその結果からの推論であり、候補者報告や企業の出題基準を示すものではありません。データベース、HTTP、実際の請求・保存は使用していません。

LINQの選択、Taskの作成、非同期解放の三つの時点

LINQの質問:クエリを作ったとき、値は確定したか

最初のリストには、処理対象のI71が120、I72が80、対象外のI73が200という三件があります。金額の下限は100です。Invoiceは金額と対象フラグを変更できるクラスで、Idは文字列です。次のコードでは、変更前に選んだ値と、後から列挙した値を比べます。

var invoices = new List<Invoice> {
    new("I71", 120, true), new("I72", 80, true), new("I73", 200, false)
};
int minimum = 100;
var query = invoices.Where(x => x.Ready && x.Amount >= minimum)
                    .Select(x => (x.Id, x.Amount));
var early = query.ToArray();
var references = invoices.Where(x => x.Ready).ToArray();

minimum = 150;
invoices[0].Amount = 160;
invoices[1].Amount = 180;
invoices.Add(new("I74", 200, true));
var late = query.ToArray();

earlyは(I71,120)の一件、lateは(I71,160),(I72,180),(I74,200)の三件です。I73は金額が下限を超えてもReady=falseなので入りません。クエリの宣言時点と、列挙する時点を区別してください。

MicrosoftのLINQ入門は、遅延実行と、ToArray()などによる結果の具体化を説明しています。この例のWhereは宣言時にリストを調べません。列挙した時点で、そのときのリスト、各オブジェクトのプロパティ、捕捉したminimumを読みます。

捕捉した変数も最初の100に自動固定されません。今回は二回目に150を読みます。LINQの前に別のローカル変数へ100を保存し、その変数を変更しなければ条件を固定できますが、元のオブジェクトの金額まで固定できるわけではありません。

実行用コードでは述語の評価回数も数えました。宣言直後は0、最初の列挙後は3、リストに一件追加して再列挙した後は累計7でした。このカウンターは動作を観察するためのものです。実際の述語に送信や更新を入れると、列挙の回数が副作用の回数になるため、副作用を何回許すかまで決める必要があります。

ToArrayの質問:参照を集めたのか、必要な値を選んだのか

referencesには、最初にReadyだったI71とI72への参照が入ります。後でI74を追加しても要素数は二件のままですが、I71のAmountを読むと160、I72は180です。配列にしたことと、その中のオブジェクトの不変性は別です。

保持したもの後の変更を受けた結果固定できた範囲
queryという列挙の記述I71/160、I72/180、I74/200結果自体はまだ固定しない
earlyのIdとint金額の組I71/120今回投影した値
referencesのオブジェクト参照二件のまま、金額は160と180選ばれた参照の集合

ここでearlyが保持するのは文字列Idと整数金額の組です。一般のオブジェクトグラフを深くコピーした結果ではありません。投影先に可変オブジェクトへの参照を追加すれば、その先の変更を観察する可能性が戻ります。必要な値を明示することが、説明可能なスナップショットの第一歩です。

どちらの結果が正しいかは契約で決まります。「承認時点の金額を確認する」なら、その時点で必要な値を選びます。「今の候補一覧を表示する」なら、新しい列挙が意図どおりかもしれません。常にToArray()を入れるのではなく、選択時点、件数、メモリ量を先に確認してください。

この実験はLINQ to Objectsです。IQueryable、Entity Framework、SQLの分離レベルや接続の寿命を試した結果ではありません。面接でデータベースへ話を広げる場合は、プロバイダーと実際のクエリを別に確認すると宣言してください。

asyncの質問:同じSelectを二度渡すと何回始まるか

次に、後から選ばれた三件のIdを非同期で読み込みます。テスト用のBatchSession.FetchAsyncは、呼ばれると開始回数を増やし、制御用の合図を待ってIdを返します。待機前に実行される部分と、待機後の部分がある点が重要です。

var ids = new[] { "I71", "I72", "I74" };
var pending = ids.Select(id => session.FetchAsync(id, ct));
var first = Task.WhenAll(pending);
var second = Task.WhenAll(pending);

pendingを宣言した直後の開始回数は0です。一つ目のWhenAllが列挙すると三回、二つ目が同じクエリを再列挙すると累計六回になります。三件のTaskが自動的にキャッシュされたのではなく、三件のTaskを作る記述を二度使っています。読込が送信や課金なら、同じIdへの二重実行を引き起こす可能性があります。

Microsoftの非同期シナリオの説明でも、LINQで作る非同期処理の列挙時点に注意を促しています。今回の修正は、列挙を一度行い、作られたTaskの配列を保持することです。

var tasks = ids.Select(id => session.FetchAsync(id, ct)).ToArray();
var first = await Task.WhenAll(tasks);
var second = await Task.WhenAll(tasks);

同じ制御用セッションで確認すると、こちらの開始回数は三回のままでした。同じTaskを再び待っても、FetchAsyncを再度呼びません。これはリトライでも、サーバー側の冪等性でもありません。新しいTaskを作れば新たな実行になり、外部への副作用があるなら、その重複対策は別途必要です。

asyncは新しいスレッドを必ず作る指定ではありません。今回のFetchAsyncも、呼出回数を増やす部分は最初の未完了のawaitまで進みます。このコードからCPU処理の並列度、処理速度、同時接続の上限は分かりません。多数のIdへ広げるなら、件数制限や同時実行数の制御も別の課題になります。

リソース管理の質問:所有者は何の完了まで待つか

この演習のセッションはIAsyncDisposableを実装します。読込用の合図とは別に、解放を終えてよいことを知らせる合図があります。実際のデータベース接続ではなく、非同期の後処理を観察するためのオブジェクトです。

自分でセッションを開くメソッドは、読込と解放を自分で終える契約にします。usingの公式説明と非同期解放の説明に対応して、await usingで解放の完了を待ちます。

public static async Task<string[]> OwnedAsync(
    Func<BatchSession> open, string[] ids, CancellationToken ct) {
    ct.ThrowIfCancellationRequested();
    await using var session = open();
    var tasks = ids.Select(id => session.FetchAsync(id, ct)).ToArray();
    return await Task.WhenAll(tasks);
}

呼出前に取消し済みなら、先頭のチェックで取得を避けます。取得後は読込の成功、取消し、失敗のどの経路でもスコープを抜ける際に解放します。ただし、このテストでは取得が有効なセッションを返し、解放自体は成功するという前提を置いています。

読込結果ができた時点でも、メソッドのTaskが完了していない場合があります。 テストで読込の合図だけを開くと、dispose:startまで進み、解放の合図を待ちます。dispose:startを観察し、解放の合図を開く前に、呼出元が待つTaskのIsCompletedがfalseであることを確認しました。解放を許可するとdispose:endとなり、初めて結果を受け取れます。

この違いは「returnの直前まで進んだ」と「呼出元に正常終了が伝わった」を分けます。戻り値の計算が終わっても、スコープ終了時に必要な後処理が残っています。return awaitを単なる書き方の好みとして外す前に、そのメソッドが所有するものを確認してください。

一方、呼出元から借りたセッションを使うBorrowedAsyncは、この演習では解放しません。同じTask配列を待ち、結果を返し、外側の所有者が最後に解放します。「IDisposableなら何でもusingする」では、別の処理が使うリソースを閉じる可能性があります。所有権はメソッドの引数と契約から決める必要があります。

取消しの質問:待つのをやめたら元の処理も止まるか

.NETの取消しは協調的です。要求を出す側と、それを観察する側がいます。今回の読込は、テスト用のWork.Task.WaitAsync(ct)を待っています。WaitAsyncのAPI説明では、元のTaskの完了または指定トークンの取消しによって終わる待機を表します。

三件が待機に入った後で取消すと、各FetchAsyncの待機が取消されます。OwnedAsyncはスコープを抜け、非同期の解放を始めます。解放の合図が閉じている間、外側のTaskはまだ完了しません。後処理を終えると、呼出元はOperationCanceledExceptionを観察できます。

ただし、元のWork.Taskは未完了のままでした。後から別の合図で正常完了させることもできました。取消した待機と、待機対象を作る側の処理は同じではありません。 この結果から、外部サーバーの仕事を止めた、既に保存したデータを戻した、とは言えません。

今回のDisposeAsyncは、読込に使う取消し済みトークンを受け取りません。取消し要求で後処理まで省略しない様子を明確にするためです。その分、後処理が終わらなければ外側も終わりません。本番で必要な解放の期限、失敗時の診断、強制終了への扱いは、使用するリソースの仕様に合わせて決めます。

取消し後の非同期解放と元のTaskの完了を分ける時間線

テストの質問:何を観察すると三つの境界が分かるか

適当な秒数だけ待つテストでは、目的の地点に到達したか分かりません。今回の検証は、読込を進める合図、解放が始まった合図、解放を終える合図を分けました。解放開始を観察してから、外側のTaskが未完了であることを確認します。

制御した経路合図の途中で確認すること最後に確認すること
正常な三件の読込読込後も解放待ちで未完了三件のIdと一回の解放
呼出前に取消し済みセッション取得は0回取消しが観察できる
待機中に取消し解放待ち、元のWorkは未完了取消し状態と一回の解放
I72で合成した読込障害解放を終えるまで未完了IOExceptionを保持して失敗
借りたセッション借り手の終了後も未解放外側の所有者が一回解放
選択が空読込開始は不要空の成功結果と解放

32項目には、クエリの出力、評価回数、六回対三回の開始、上の経路の中間状態と最終状態が含まれます。解放回数はテスト用オブジェクトのカウンターで、OSのハンドル数や実ドライバーの漏れを測定した値ではありません。

取消しや障害を空配列へ変換しないことも検証しました。「対象がなかった成功」と「読み切れなかった結果」を分けるためです。また一件の読込失敗を、他の処理の自動取消しや全体のロールバックと同一視していません。今回の合図では全読込を再開させ、その後の結果を観察しています。

説明する際は、一つの結果へ証拠を一つ結びます。「二度列挙すると六回始まった」「解放待ちではTaskが未完了だった」「WaitAsyncの取消し後も元のTaskを完了できた」。この順で示せば、相手と同じ出力を見ながら、修正で何が変わり、何が残るかを話せます。

面接の追問を別のケースで練習する

最初の回答は「選択の値を固定し、Taskは一度作り、所有したセッションは解放完了まで待ちます」とまとめられます。次にI71の金額、開始回数、取消し後のTask状態を一つずつ説明してください。どのToArrayが何を保持するかを答えられることが、今回のポイントです。

以下はC#専用の出題実績ではなく、同じ判断を別の題材へ移すためのPracHub問題です。企業の選考内容や採用基準の予告として扱わないでください。

PracHubの問題このケースから広げる問い
Design a Workspace Resource Manager所有者と利用者の解放責任をどう分けるか
Name-Keyed Async Task Scheduler with Sequential Lanes, Cancel and Clear by Name待機中と開始済みで取消しの意味はどう違うか
Design async batched key-value fetcher一度作ったTaskと再実行をどう区別するか
Design a service aggregator with robust error handling一件の失敗と全体の結果をどう表現するか
Lowest-Price Book Aggregator Fanning Out to Hundreds of Async Bookstores件数が増えたとき何を制限し、何を待つか

三件のTaskを再利用しても開始回数が三回のままである理由を説明できたら、非同期のバッチ取得問題で、「待つのをやめる」と「元の取得を止める」を区別する契約を書いてみてください。実行結果、予測、未検証の保証を分けて答えると、短いコードでも判断の根拠が伝わります。

Sources and Further Reading


Comments (0)