C# Interface の継承判定と暗黙キャストの定石(is / as / 暗黙キャスト 3パターン)

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

今回は C# 業務系で Interface 触る時にガチでよく迷うやつ!!の話。

DataAccess 層を抽象化したくて IDataLoader みたいな Interface を切ったはいいけど、「呼び出し元で is 使うべき?? それとも as?? それとも普通に (IDataLoader)obj でいい??」って迷ったこと、ないっすか??

俺も2年目くらいの流通系の基幹システム保守で、(IDataLoader)obj の直接キャストを書いて、実装クラスを差し替えた瞬間に InvalidCastException で本番が落ちた事故を踏んで、半日デバッガで追って詰まった経験があります。値型に Interface かけて Boxing 起こしたバッチで GC 負荷が爆増して夕方の運用報告で気づいたこともあって、Interface 周りはガチでハマりやすい領域です。

この記事では VS2019 / .NET Framework 4.7.2 / C# 7.3 環境で、Interface の型判定 + キャストの 3つの定石パターン と、値型 Boxing や GetType() 誤用のハマりどころを、コード5本でまとめます。後ろの「現場メモ」で、業務系チームでルール化した時の話も書いてる。

3行で結論:

  • 判定 + 利用を一緒にやるif (obj is IFoo foo) { foo.Do(); }(C# 7 パターンマッチング)
  • null 許容で複数チェックを並べるvar foo = obj as IFoo; if (foo != null) { ... }
  • 型が確実な時だけ 暗黙キャスト or 直接キャスト、それ以外は上の2パターン寄せ
目次

パターン1: is + パターンマッチング(C# 7 以降の本命)

C# 7 で導入された is パターンマッチングが、今の業務SE現場の本命。判定と変数束縛が1行で書けて、Null 排除込み:

// ✅ パターン1: is + パターンマッチ(C# 7 以降の本命)
public void Process(object data)
{
    if (data is IDataLoader loader)
    {
        var rows = loader.Load();
        // loader はここで IDataLoader 型に確定、Null でないことも保証される
    }
    else if (data is IDataValidator validator)
    {
        validator.Validate();
    }
    else
    {
        Console.WriteLine("対応する Interface 実装ではありません");
    }
}

if (data is IDataLoader loader) の1行で「dataIDataLoader を実装しているか?」「実装していたら loader 変数に束縛」「Null だったら false で抜ける」を全部やってくれる。

C# 7.3 + .NET Framework 4.7.2 で問題なく使えるので、業務系の保守現場でも導入できます。古いコードベースで as + null チェック2行が書かれている箇所を見つけたら、is パターンマッチに書き換えるリファクタはいい感じに読みやすくなるので地味におすすめ。

ただし欠点として、複数のインターフェース判定を並べるパターン(switch 文的な分岐)では、後述の as のほうが読みやすいケースもあります。

パターン2: as + null チェック(複数候補を並べる時)

as 演算子は「キャストできなければ Null を返す」非例外型のキャスト。複数の候補をフラットに並べたい時に効きます:

// ✅ パターン2: as + null チェック(複数候補をフラットに並べる)
public string Describe(object obj)
{
    var loader = obj as IDataLoader;
    if (loader != null) return $"Loader: {loader.SourceName}";

    var validator = obj as IDataValidator;
    if (validator != null) return $"Validator: {validator.RuleSet}";

    var formatter = obj as IDataFormatter;
    if (formatter != null) return $"Formatter: {formatter.OutputType}";

    return "(unknown)";
}

as参照型と Null 許容値型のみ に使えるので、as int のように非 Null 値型には使えない(コンパイルエラー)。Interface は参照型扱いなので問題なく使えます。

is パターンマッチでも同じことは書けますが、3つ以上の候補を並べる時は as のほうがフラットで読みやすい。書き味の好みで選ぶといい感じです。

ん?ペアで書くって面倒じゃない??って思うかもだけど、as は Null チェックを忘れると次の行で NullReferenceException を吐くので、ペアで書く運用が前提になります。書き忘れ事故が起きやすい構文なので、コードレビューで見ておきたいポイントっす。

パターン3: 暗黙キャストと直接キャスト(型が確定している時だけ)

「この obj は確実に IFoo を実装している」と保証できる場合だけ使うパターン:

// ✅ パターン3-a: 暗黙キャスト(継承関係が静的型レベルで明確)
public class CsvLoader : IDataLoader { /* 実装 */ }

CsvLoader csvLoader = new CsvLoader();
IDataLoader loader = csvLoader;  // ✅ 暗黙キャスト OK(CsvLoader → IDataLoader)

// ❌ パターン3-b: 直接キャスト(失敗すると InvalidCastException)
object data = GetData();
IDataLoader bad = (IDataLoader)data;  // data が IDataLoader 実装でないと例外

暗黙キャストは「変数の静的型から見て、変換先 Interface を実装していることがコンパイラに分かる」場合のみ書ける。新規インスタンスを Interface 型変数に代入する時の素直なパターンっす。

直接キャスト (IFoo)obj は実行時にチェックが入り、失敗すると InvalidCastException。型が確実な時だけ使う。「型が確実」って判断が業務SE現場では難しいので、原則 isas 寄せにして、直接キャストは新規にはあまり書かないのが安全です。

ハマりポイント — Boxing と GetType() 誤用

ここが業務SEで地味にハマるところ。Interface 周りで踏みやすい4つの罠を出します。

// ❌ 罠1: 値型 Interface で Boxing 大量発生(性能劣化)
public struct Point : IComparable<Point>
{
    public int X { get; set; }
    public int Y { get; set; }
    public int CompareTo(Point other) => /* 比較 */;
}

IComparable<Point> cmp = new Point { X = 1, Y = 2 };  // ← Boxing 発生
// Point は struct(値型)なので、Interface 型変数に代入した瞬間にヒープへコピー
// ループ内で大量に発生すると GC 負荷で性能劣化

罠1: 値型 Boxingstruct が Interface を実装してると、Interface 型変数に代入した瞬間にヒープ上にコピーが作られる。ホットパスのループ内で大量発生すると GC 負荷で 10万件規模なら体感で2〜3倍遅くなるケースもある。値型 + Interface はループ核では避けるのが鉄則っす。

罠2: GetType() == typeof(IFoo) の誤用。これは判定として効きません:

// ❌ 罠2: GetType() で Interface 判定はできない
if (obj.GetType() == typeof(IDataLoader))  // ← 常に false
{
    // ここには到達しない(常に false)
}

// ✅ 正しい書き方
if (obj is IDataLoader)
{
    // OK
}

GetType()実行時の具体型 を返すので、Interface 型は返ってこない。Interface の判定は isas で行う。これ、Reflection 入門書に載ってないことが多くてハマる業務SE多いんですよね。

罠3: dynamic との混同。「型判定が要らない dynamic で書けばいいじゃん」って思うかもだけど、業務系では原則使わない。コンパイル時の型チェックが効かなくなる・IntelliSense が使えない・実行時例外で初めて気づく、の3点で保守性が大きく落ちる。Interface + is/as で対処するのが現実的です。

罠4: 直接キャストで実装差し替え時に落ちる。実装クラスを差し替えた時、(IDataLoader)obj で書いてた箇所が InvalidCastException本番ビルド後に落ちる。これ、俺が 半日デバッガで追って一晩潰したやつ。直接キャストの代わりに is パターンマッチで書いておけば、判定で false に倒れて分岐先でいい感じに処理できた。

比較表 — どれを使うべきか

パターン 用途 失敗時の挙動 Null 安全 性能
is + パターンマッチ(推奨) 判定 + 利用を1行で false で分岐 ✅(Null は弾かれる) 標準
as + null チェック 複数候補をフラットに並べる Null 返却 ⚠️(Null チェック必要) 標準
暗黙キャスト 静的型で明確な代入 コンパイルエラー n/a 最速
直接キャスト (IFoo)obj 型が確定している時のみ InvalidCastException 標準
GetType() == typeof(IFoo) ❌ Interface には使えない 常に false n/a n/a

業務系の判断軸:

  • 判定 + 利用is パターンマッチ(最優先)
  • 複数候補の分岐as + null チェック
  • 新規インスタンスの Interface 化 → 暗黙キャスト
  • 直接キャスト・GetType 比較 → 原則使わない

著者の現場メモ — 業務系チームでの Interface 判定ルール

流通系の基幹システム保守チームに移った時、過去コードを grep -rE "as I[A-Z]|is I[A-Z]" . で grep してチェックしたら、60箇所近く出てきたんですよね。書き方がバラバラで、as IFoo 後の Null チェック忘れ・(IFoo)obj の直接キャスト・GetType() == typeof(IFoo) の誤用、全部混在。

んで、後輩と一緒に 3行ルール にまとめた:

  1. 判定 + 利用は is パターンマッチ寄せ(C# 7.3 で書ける)
  2. as を使う時は次の行で Null チェックをペアで書く(書き忘れ事故防止)
  3. 直接キャスト (IFoo)obj は新規禁止(既存箇所はリファクタ時に is 化)

このルール化で、実装差し替え時の InvalidCastException 事故が消えた。Interface 判定は1パターンに揃えるだけで、コードレビューの指摘工数も減るので、業務系チームには結構おすすめのルールっす。

C# 7.3 + .NET Framework 4.7.2 のレガシー業務系って、パターンマッチングが普通に使える環境なのに、書き方が C# 5 時代から進化してないコードベースが本当に多い。書き方のアップデートだけで保守工数が下がる実例だと思ってます。

まとめ

C# で Interface を扱う時の判定 + キャストは、3パターンの使い分けで整理できる:

状況 推奨パターン
判定 + 利用を1行で if (obj is IFoo foo)
複数候補をフラットに分岐 var foo = obj as IFoo; if (foo != null)
静的型で明確な代入 暗黙キャスト IFoo foo = csvLoader;
値型 + Interface(ホットパス) 避ける(Boxing で性能劣化)
Interface 判定 GetType() ではなく is / as
型確定時の直接キャスト 原則禁止(実装差し替えで落ちる)

**「is パターンマッチ寄せ、as は Null チェックとペア、直接キャストは新規禁止」**の3行ルールに揃えるだけで、業務系の Interface 周りの事故はだいぶ減ります。

よくある質問

Q1. is と as と直接キャスト、どれを使えばいい?

A. 判定と利用を一緒にやるなら is パターンマッチ(C# 7 以降の if (obj is IFoo foo) 構文)。Null 許容で複数チェックを並べたい時は as + Null チェック。型が確実な時だけ暗黙キャストや直接キャストを使ってください。一番事故りにくいのは is パターンマッチ寄せです。

Q2. GetType() == typeof(IFoo) で Interface 判定できますか?

A. できません。GetType() は実行時の具体型を返すので、Interface 型は返ってきません。Interface の判定は obj is IFooobj as IFoo != null で行ってください。GetType()== 比較は具体クラス(class Foo : IFoo の Foo 側)にしか使えないと覚えるのが安全です。

Q3. 値型に Interface を実装すると Boxing が起きるって本当?

A. 本当です。struct が Interface を実装している場合、Interface 型変数に代入した瞬間にヒープ上にコピーが作られて参照型として扱われます。これが Boxing で、ループ内で大量発生すると GC 負荷で性能劣化の原因になる。値型 + Interface はホットパスでは避けるのが鉄則です。

Q4. 暗黙キャストと明示キャスト(IFoo foo = (IFoo)obj)の違いは?

A. 暗黙キャストは継承関係が明確な場合に書ける形(IFoo foo = obj; が成り立つのは obj の静的型が IFoo を実装している時のみ)。明示キャスト (IFoo)obj は実行時にチェックが入って、失敗すると InvalidCastException を投げます。「型が確実なら暗黙、自信ない時は isas」が判断軸です。

Q5. dynamic を使えば型判定もキャストも要らなくなりますか?

A. 確かに dynamic 型は実行時バインディングなので型判定をスキップできますが、業務SE現場では原則使わない方がいい。コンパイル時の型チェックが効かなくなる、IntelliSense が使えなくなる、性能が落ちる、デバッグが困難になる、の4点で保守性が大きく落ちます。Interface + is / as の組み合わせで対処するのが現実的です。

ここまでで Interface 判定とキャストの定石は押さえた。データアクセス層の抽象化を考える時の隣接トピックも貼っておきます。

関連記事

以上!

同じ罠でハマってる業務SE仲間いたら、どんどんシェア待ってるぜ!!


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

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

コメント

コメントする

CAPTCHA


目次