C# バックエンド面接対策

C# 面接質問と回答 2026

バックエンド開発者向けの C# 面接対策セットです。各回答には、面接官が期待する考え方も含まれており、多くの項目に、その場で口頭説明できる小さなコード例があります。

C# の模擬面接を開始クレジットカードは不要です。1回分の無料セッションがあります。
英語での技術面接練習非ネイティブ話者が技術面接の合格を目指して練習できるモードです。

型と基本構文

1値型と参照型はどのように異なりますか?

値型の変数は通常、値そのものを保持するため、代入時には値がコピーされます。一方、参照型の変数はオブジェクトへの参照を保持するため、代入時には参照がコピーされ、両方の変数から同一オブジェクトの変更が観察できます。重要なニュアンスとして、格納場所は型単体ではなく、生存期間(寿命)と文脈に依存する点が挙げられます。メソッドのローカル変数としての値型はスタック上に配置されますが、クラスのフィールド、配列の要素、クロージャのキャプチャ、ボックス化された値、あるいはasyncステートマシンのフィールドとして保持される値型は、そのオブジェクトとともに生存します。

int a = 10;
int b = a;
b = 20;

// a is still 10

var first = new User { Name = "Ann" };
var second = first;
second.Name = "Kate";

// first.Name is also "Kate"
AI コーチを使ってこの質問に答えてみる

2class、struct、record の違いは何ですか?

classはオブジェクトの同一性を持つ参照型です。structは値型であり、座標、範囲、識別子などの小さなイミュータブルな値に最適です。record classは値ベースの等価性を持つ参照型であり、record structはその値型バージョンです。recordは通常のCLR型に対するコンパイラ構文であり、等価性比較、GetHashCode、ToString、分解(deconstruction)、および非破壊的変更を行うwith式を含むコピー支援機能を自動生成します。バックエンドコードにおいて、recordはDTO(Data Transfer Object)のようなモデルに適していますが、大きな変更可能なstructはコピーやコピー側の変更によって予期しない挙動を引き起こしやすいためリスクがあります。

public record User(int Id, string Name);

var first = new User(1, "Alice");
var second = new User(1, "Alice");

Console.WriteLine(first == second); // true
AI コーチを使ってこの質問に答えてみる

3ref、out、およびinパラメータにはどのような違いがありますか?

refは引数を参照渡しし、呼び出し前に初期化されている必要があります。メソッドはその値を読み取ることも変更することもできます。outも参照渡しですが、メソッドが復帰する前に必ず値を割り当てる必要があります。inは読み取り専用の参照渡しであり、サイズの大きな構造体のコピーを回避できますが、ディープな不変性を保証するわけではありません。変更可能な構造体や読み取り専用でないメンバーが存在すると、防御的コピーが発生する可能性があります。IL(Intermediate Language)レベルでは、ref、out、inはいずれもマネージド参照パラメータ(managed byref)であり、C#コンパイラの規則が異なるだけです。

void Increment(ref int value) => value++;

bool TryRead(string text, out int value) =>
    int.TryParse(text, out value);

double Length(in Vector vector) =>
    Math.Sqrt(vector.X * vector.X + vector.Y * vector.Y);
AI コーチを使ってこの質問に答えてみる

4ボックス化(boxing)とアンボックス化(unboxing)とは何ですか?

ボックス化とは、ヒープ上にオブジェクトを割り当てて値をコピーすることにより、値型を参照型のオブジェクトでラップする処理です。アンボックス化はその値を再び取り出す処理であり、完全に互換性のある型を指定する必要があります。頻繁なボックス化はメモリ割り当てを増やし、GC (Garbage Collector)への負荷を高めます。ジェネリクスを使用すると、例えばList<int>では各値をobjectに変換せずにint値をそのまま保持できるため、多くのボックス化を回避できます。

int number = 42;
object boxed = number;      // boxing
int restored = (int)boxed;  // unboxing

object value = 42;
// long wrong = (long)value; // InvalidCastException
long ok = (long)(int)value;
AI コーチを使ってこの質問に答えてみる

5null許容値型(nullable value types)とnull許容参照型(nullable reference types)とは何ですか?

null許容値型は `Nullable<T>` または `T?` を使用し、値型の実際のランタイムラッパーを表します。null許容参照型は主にコンパイラによる静的解析であり、`string?` は参照がnullになる可能性があることをコンパイラに伝えますが、新しいランタイム型を作成するわけではありません。null免除演算子(null-forgiving operator)`!` は警告を抑制するだけであり、`NullReferenceException` の発生を防ぐものではありません。

int? age = null;

string name = "Alice";
string? optionalName = null;

var length = optionalName?.Length;
var actualName = optionalName ?? "Unknown";
optionalName ??= "Default";

string trusted = optionalName!;
AI コーチを使ってこの質問に答えてみる

6const、readonly、および static readonly の違いは何ですか?

const はプリミティブ型、列挙型の値、string、または null に制限されたコンパイル時定数であり、コンパイル時に参照元のアセンブリ内に値がインライン展開(直接置換)されます。readonly は宣言時またはコンストラクタ内で代入可能なインスタンスフィールドです。static readonly は型ごとに1つ存在するフィールドであり、宣言時または静的コンストラクタ内で代入されます。ライブラリのバージョン間で変更される可能性がある公開値(パブリックな値)は、public const よりも static readonly にする方が安全です。クライアントが再コンパイルされるまで、古いインライン化された const の値を保持し続けてしまうためです。

public const int MaxAttempts = 3;

private readonly Guid _id = Guid.NewGuid();

public static readonly TimeSpan Timeout =
    TimeSpan.FromSeconds(30);
AI コーチを使ってこの質問に答えてみる

7Equals、==、およびGetHashCodeはどのように連携して動作しますか。

通常のクラスでは、Equalsと==はデフォルトで参照を比較します。組み込み型の多くやrecordは値によって比較します。Equalsをオーバーライドする場合、GetHashCodeの整合性を保つ必要があります。つまり、a.Equals(b)がtrueであれば、両方のオブジェクトは同じハッシュコードを持たなければなりません。その逆は保証されません。オブジェクトがDictionaryのキーやHashSetの要素として使われている間は、等値性やハッシュ計算に使用されるフィールドを決して変更しないでください。

public sealed class User : IEquatable<User>
{
    public required int Id { get; init; }

    public bool Equals(User? other) =>
        other is not null && Id == other.Id;

    public override bool Equals(object? obj) =>
        obj is User other && Equals(other);

    public override int GetHashCode() =>
        Id.GetHashCode();
}
AI コーチを使ってこの質問に答えてみる

8overrideとnewの違いは何ですか?

`override`は仮想メソッドの動作を変更するため、呼び出されるメソッドはオブジェクトの実行時の型に依存します。一方、`new`は基底クラスのメンバーを隠蔽するため、選択されるメンバーは変数の静的な型に依存します。ポリモーフィズムを実現したい場合は通常`virtual`と`override`を使用します。`new`による隠蔽はあまり一般的ではなく、挙動が分かりにくくなる可能性があります。

class Base
{
    public virtual void First() => Console.WriteLine("Base.First");
    public void Second() => Console.WriteLine("Base.Second");
}

class Derived : Base
{
    public override void First() => Console.WriteLine("Derived.First");
    public new void Second() => Console.WriteLine("Derived.Second");
}

Base value = new Derived();
value.First();  // Derived.First
value.Second(); // Base.Second
AI コーチを使ってこの質問に答えてみる

9抽象クラスとインターフェイスはどのように異なりますか?

抽象クラスは、状態、コンストラクタ、フィールド、実装済みメソッド、抽象メソッド、および protected メンバーを持つことができます。一方、インターフェイスは規約(コントラクト)を定義します。近年のインターフェイスはデフォルト実装や静的抽象メンバーを含めることができますが、インスタンスの状態を持たせる場所としては通常使用されません。クラスが継承できる基底クラスは1つのみですが、インターフェイスは複数実装できます。共通の基底クラスや実装の共有には抽象クラスを使用し、機能(ケイパビリティ)や境界の定義にはインターフェイスを使用します。

public interface IAsyncRepository<T>
{
    Task<T?> FindAsync(
        int id,
        CancellationToken cancellationToken);
}
AI コーチを使ってこの質問に答えてみる

ジェネリクス、コレクション、LINQ

10ジェネリクスとは何ですか?また、なぜ有用なのですか?

ジェネリクスを使用すると、再利用可能で型安全なコードを記述できます。多くの型チェックをコンパイル時に移行し、手動の型キャストを不要にするほか、値型に対するボックス化を回避できます。ジェネリック制約では、型引数が満たすべき要件(クラス、構造体、特定のインターフェイスの実装、基底クラス、アンマネージド型、または引数のないパブリックコンストラクタを持つことなど)を定義します。

public T? Find<T>(
    IEnumerable<T> items,
    Predicate<T> predicate)
{
    return items.FirstOrDefault(item => predicate(item));
}

public T Create<T>() where T : class, new()
{
    return new T();
}
AI コーチを使ってこの質問に答えてみる

11共変性(covariance)と反変性(contravariance)とは何ですか?

共変性により、より汎用的な生成型が期待される場所で、より具体的な型を使用できるようになります(例: IEnumerable<object> が求められる箇所での IEnumerable<string>)。インターフェイスやデリゲートにおいて out キーワードで指定します。反変性は消費される値に対してその逆方向に働き、in キーワードで指定します。変性(variance)が機能するのは参照型を扱うインターフェイスおよびデリゲートのみです。IEnumerable<int> を IEnumerable<object> として扱うことはボックス化が必要になるため不可能であり、List<string> を List<object> に代入することもできません。

IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // covariance

Action<object> printObject = Console.WriteLine;
Action<string> printString = printObject; // contravariance

interface IProducer<out T> { T Produce(); }
interface IConsumer<in T> { void Consume(T value); }
AI コーチを使ってこの質問に答えてみる

12IEnumerable<T> と IQueryable<T> の違いは何ですか?

`IEnumerable<T>` は.NETの列挙を表し、LINQ演算子は通常 `Func<T, bool>` などのデリゲートを受け取ります。一方、`IQueryable<T>` は式木(例えば `Expression<Func<T, bool>>`)を保持し、Entity Framework Core などのプロバイダがこれをSQL (Structured Query Language) に変換できます。早い段階で `ToList` を呼び出すとデータが実体化され、それ以降のフィルタリングがインメモリ処理になってしまいます。また、すべてのC#の式をクエリプロバイダが変換できるわけではありません。意図的な設計判断でない限り、アプリケーションのすべての層に `IQueryable` が漏出することは避けるべきです。

IEnumerable<User> loaded = loadedUsers;
var inMemoryAdults = loaded.Where(x => x.Age >= 18);

IQueryable<User> query = dbContext.Users;
var adults = await query
    .Where(x => x.Age >= 18)
    .ToListAsync();
AI コーチを使ってこの質問に答えてみる

13LINQ (Language Integrated Query) における遅延実行とは何ですか?

多くのLINQ演算子は、クエリを即座に実行することなく構築します。実行は、クエリが列挙されたとき、または `ToList`、`Count`、`First` などの終端操作が呼び出されたときに開始されます。これにより、列挙前に行われたデータソースの変更が結果に影響を与える可能性があり、複数回列挙すると処理やSQLクエリが重複して実行されるほか、クエリ構築時ではなく列挙時に例外が発生することがあります。

var query = numbers.Where(x => x > 10);

numbers.Add(42);

foreach (var number in query)
{
    Console.WriteLine(number);
}

var materialized = query.ToList();
AI コーチを使ってこの質問に答えてみる

14配列(array)、List<T>、Dictionary<TKey,TValue>、HashSet<T> はそれぞれどのように異なりますか?

配列は固定長で、インデックスによる高速なアクセスが可能です。List<T> は動的配列であり、インデックスアクセスは O(1) です。末尾への Add はならし(amortized)計算量で O(1) となります。これは容量拡張時に、より大きな配列が割り当てられ既存の要素がコピーされるためです。一方で途中への挿入は O(n) です。Dictionary<TKey,TValue> はキーと値をマッピングし、通常は O(1) でのキー検索を提供します。HashSet<T> は一意な要素を保持し、要素の存在確認に適しています。ハッシュベースのコレクションでは、Equals と GetHashCode の適切な実装が不可欠です。

var ids = new HashSet<int> { 1, 2, 3 };

if (ids.Contains(userId))
{
    // Fast membership check
}
AI コーチを使ってこの質問に答えてみる

15IEnumerable<T>、ICollection<T>、IList<T>、IReadOnlyCollection<T> の違いは何ですか?

IEnumerable<T> は順次列挙のみを保証します。ICollection<T> は Count プロパティと Add や Remove などの変更操作メソッドを追加します。IList<T> はインデックスによるアクセスを追加します。IReadOnlyCollection<T> は列挙と Count を公開しますが、そのインターフェイス経由での変更は公開しません。API設計では必要最小限の契約(インターフェイス)を返すのが望ましいですが、読み取り専用インターフェイスであっても、背後にあるオブジェクト自体の完全な不変性(ディープイミュータビリティ)を保証するわけではありません。

private readonly List<Order> _orders = new();

public IReadOnlyCollection<Order> Orders => _orders;
AI コーチを使ってこの質問に答えてみる

16yield returnはどのように動作しますか?

yield returnは、遅延実行(deferred execution)を伴うイテレータを生成します。コンパイラは対象メソッドをステートマシンに変換し、MoveNextが呼び出されたタイミングで値が1つずつ生成されます。これにより、中間コレクションの生成を回避でき、大規模なシーケンスに対して効率的に動作します。注意すべき落とし穴は、オブジェクトの寿命と複数回の反復評価です。すでに破棄(Dispose)されたDbContextやストリームに依存する遅延シーケンスを返してはいけません。また、Countの呼び出し後にforeachを実行するとイテレータが2回実行され、副作用やI/O処理が重複して発生する点に注意が必要です。

IEnumerable<int> GetEvenNumbers(IEnumerable<int> source)
{
    foreach (var number in source)
    {
        if (number % 2 == 0)
            yield return number;
    }
}
AI コーチを使ってこの質問に答えてみる

デリゲート、イベント、例外

17デリゲートとラムダ式とは何ですか?

デリゲートはメソッドに対する型安全な参照です。ラムダ式は簡潔なインライン関数を提供し、一般的にはFunc、Action、Predicateデリゲートに割り当てられます。ラムダ式は値のスナップショットではなく変数をキャプチャするため、後続のループや変数の値を参照することがあります。ラムダ式が何もキャプチャしない場合、コンパイラは静的デリゲートをキャッシュしてクロージャの割り当てを回避できます。性能重視のコードでは、クロージャの生成やオブジェクト寿命の長期化が影響を与える可能性があります。

public delegate int Operation(int x, int y);

Operation operation = (x, y) => x + y;
Console.WriteLine(operation(2, 3));

int factor = 10;
Func<int, int> multiply = x => x * factor;
AI コーチを使ってこの質問に答えてみる

18eventはdelegateとどのように異なりますか?

eventは通常delegateを内部実装として持ちますが、外部コードが実行できる操作を制限します。外部コードは購読(登録)および購読解除を行えますが、イベントを発行(呼び出し)できるのはそれを定義した所有クラスのみです。パブリックなdelegateフィールドを公開すると、呼び出し元が呼び出しリスト全体を置き換えたり、直接呼び出したりできてしまいます。また、eventは発行元から購読者への参照を保持するため、購読を解除しない場合、寿命の長い発行元が原因でメモリリークが発生する可能性があります。

public event EventHandler<OrderEventArgs>? OrderCreated;

service.OrderCreated += Handler;
service.OrderCreated -= Handler;

OrderCreated?.Invoke(this, args);
AI コーチを使ってこの質問に答えてみる

19C#では例外をどのように処理すべきですか?

通常の制御フローではなく、例外的な状況に対してのみ例外を使用します。対処可能な例外のみを捕捉し、空の `catch` ブロックを避け、例外をラップする際にはコンテキストを追加し、利用者に内部詳細を漏洩させないようにします。元のスタックトレースを保持したまま再送出するには `throw;` を使用します。例外をラップする場合は、例えば `throw new DomainException("...", ex)` のように、元の例外を `InnerException` として渡します。予期される失敗に対しては、`TryParse` スタイルのAPIを優先して使用します。

try
{
    await service.ProcessAsync();
}
catch (ValidationException exception)
{
    // Expected domain error
}
finally
{
    // Cleanup
}

catch
{
    throw; // preserves stack trace
}
AI コーチを使ってこの質問に答えてみる

20例外フィルター(Exception Filters)とは何ですか?

例外フィルターを使用すると、追加の条件が真(true)である場合にのみcatchブロックを実行させることができます。フィルターの評価はスタックの巻き戻し(stack unwinding)やcatchブロック内への進入の前に行われるため、falseが返された場合でも元のスタックトレースが保持され、障害診断やクラッシュダンプの採取に役立ちます。例外フィルターは、HTTPステータスコードやデータベースのエラーコードの判定、一時的なエラーの識別、条件付きロギングなどに有効です。なお、フィルター処理はシンプルに保ち、複雑な副作用を避けるべきです。

try
{
    await SendAsync();
}
catch (HttpRequestException ex)
    when (ex.StatusCode == HttpStatusCode.NotFound)
{
    // Handle only 404
}
AI コーチを使ってこの質問に答えてみる