C#のダウンキャストとは?アップキャストとの違い・安全な変換方法・InvalidCastException対策を初心者向けに解説

はじめに

C#の「ダウンキャスト」は、オブジェクト指向プログラミングを学び始めた人がつまずきやすいテーマのひとつです。

特に、次のような疑問を持つ人は多いです。

「アップキャストとダウンキャストの違いがわからない」
「なぜキャストに失敗してInvalidCastExceptionが出るのか知りたい」
「isやasを使うべき場面がわからない」
「基底クラス型の変数を派生クラス型に戻すとはどういう意味?」

C#では、継承関係にあるクラス同士で型を変換できます。しかし、派生クラスから基底クラスへ変換するアップキャストと、基底クラスから派生クラスへ変換するダウンキャストでは、安全性が大きく異なります。

この記事では、C#のダウンキャストについて、アップキャストとの違い、安全な書き方、InvalidCastExceptionが発生する原因と対策を、初心者向けにわかりやすく解説します。

1. C#のダウンキャストとは?

1-1. ダウンキャストの意味:基底クラスから派生クラスへ変換すること

C#のダウンキャストとは、基底クラス型の変数を、派生クラス型に変換することです。

たとえば、次のような継承関係があるとします。

C#
class Animal
{
public string Name { get; set; } = "";
}

class Dog : Animal
{
public void Bark()
{
Console.WriteLine("ワン!");
}
}

DogクラスはAnimalクラスを継承しています。

このとき、Animal型の変数に入っている実体がDogであれば、次のようにDog型へ戻すことができます。

C#
Animal animal = new Dog();

Dog dog = (Dog)animal;
dog.Bark();

このように、基底クラス型で扱っていたオブジェクトを、派生クラス型として扱えるようにする変換がダウンキャストです。

1-2. ダウンキャストが必要になる典型的な場面

ダウンキャストが必要になる典型的な場面は、基底クラス型やobject型として受け取ったオブジェクトを、具体的な型として扱いたいときです。

たとえば、次のようなケースがあります。

C#
Animal animal = new Dog();

この変数animalの型はAnimalです。

そのため、Animalクラスに定義されているメンバーにはアクセスできますが、Dogクラス固有のBarkメソッドにはそのままではアクセスできません。

C#
animal.Name = "ポチ";

// animal.Bark(); // コンパイルエラー

Barkメソッドを使いたい場合は、Animal型からDog型へダウンキャストします。

C#
Dog dog = (Dog)animal;
dog.Bark();

このように、基底クラス型として扱っていたオブジェクトに対して、派生クラス固有の機能を使いたいときにダウンキャストが使われます。

1-3. 初心者が混乱しやすい「変数の型」と「実体の型」の違い

ダウンキャストを理解するうえで重要なのが、「変数の型」と「実体の型」の違いです。

次のコードを見てください。

C#
Animal animal = new Dog();

このコードでは、左辺と右辺で型が異なります。

左辺のAnimal animalは、変数の型です。
右辺のnew Dog()は、実体の型です。

つまり、この変数は「Animal型の変数だけれど、中身の実体はDog」という状態です。

この状態であれば、Dog型へのダウンキャストは成功します。

C#
Dog dog = (Dog)animal;

一方、次のコードではどうでしょうか。

C#
Animal animal = new Animal();

Dog dog = (Dog)animal;

この場合、変数の型はAnimalで、実体の型もAnimalです。

実体はDogではないため、Dog型へダウンキャストしようとすると実行時エラーになります。

ダウンキャストで重要なのは、「変数の型」ではなく「実体の型」です。

1-4. ダウンキャストは常に成功するわけではない

ダウンキャストは、書けば必ず成功するわけではありません。

次のコードはコンパイルは通ります。

C#
Animal animal = new Animal();

Dog dog = (Dog)animal;

しかし、実行するとInvalidCastExceptionが発生します。

なぜなら、実体がDogではなくAnimalだからです。

C#では、継承関係があるからといって、基底クラスのインスタンスを派生クラスとして扱えるわけではありません。

DogAnimalですが、すべてのAnimalDogとは限りません。

この考え方が、ダウンキャストを理解するうえで非常に重要です。

2. アップキャストとダウンキャストの違い

2-1. アップキャストとは:派生クラスから基底クラスへの変換

アップキャストとは、派生クラス型のオブジェクトを、基底クラス型として扱う変換です。

C#
Dog dog = new Dog();

Animal animal = dog;

この例では、Dog型のオブジェクトをAnimal型の変数に代入しています。

DogAnimalを継承しているため、DogオブジェクトはAnimalとして扱うことができます。

この変換がアップキャストです。

2-2. アップキャストが安全な理由

アップキャストは基本的に安全です。

なぜなら、派生クラスは基底クラスの機能を必ず持っているからです。

C#
class Animal
{
public string Name { get; set; } = "";
}

class Dog : Animal
{
public void Bark()
{
Console.WriteLine("ワン!");
}
}

DogAnimalを継承しているため、AnimalNameプロパティを持っています。

そのため、DogAnimalとして扱っても問題ありません。

C#
Dog dog = new Dog();
Animal animal = dog;

animal.Name = "ポチ";

このように、派生クラスは基底クラスの要素を含んでいるため、アップキャストは自然で安全な変換です。

2-3. ダウンキャストが危険になりやすい理由

ダウンキャストは、基底クラス型から派生クラス型への変換です。

しかし、基底クラス型の変数に入っている実体が、必ずしも目的の派生クラスとは限りません。

C#
Animal animal = new Animal();

Dog dog = (Dog)animal; // 実行時エラー

このコードでは、animalの実体はAnimalです。

Dogではないため、Dog型として扱うことはできません。

また、次のようなケースにも注意が必要です。

C#
class Cat : Animal
{
public void Meow()
{
Console.WriteLine("ニャー!");
}
}

Animal animal = new Cat();

Dog dog = (Dog)animal; // 実行時エラー

CatAnimalを継承していますが、CatDogではありません。

そのため、Animal型の変数に入っているからといって、Dogに変換できるとは限らないのです。

2-4. アップキャストとダウンキャストのコード例で比較

アップキャストとダウンキャストをコードで比較してみましょう。

C#
Dog dog = new Dog();

// アップキャスト:Dog → Animal
Animal animal = dog;

// ダウンキャスト:Animal → Dog
Dog castedDog = (Dog)animal;

castedDog.Bark();

この例では、最初にDog型のオブジェクトを作成しています。

その後、Animal型にアップキャストし、さらにDog型にダウンキャストしています。

実体は最初からDogなので、最後のダウンキャストは成功します。

一方、次のコードは失敗します。

C#
Animal animal = new Cat();

Dog dog = (Dog)animal; // InvalidCastException

変数の型はAnimalですが、実体はCatです。

そのため、Dogへのダウンキャストはできません。

2-5. 暗黙的変換と明示的変換の違い

アップキャストは、通常、暗黙的に行えます。

C#
Dog dog = new Dog();

Animal animal = dog;

明示的にキャストを書く必要はありません。

C#
Animal animal = (Animal)dog;

このように書くこともできますが、通常は不要です。

一方、ダウンキャストでは明示的なキャストが必要です。

C#
Animal animal = new Dog();

Dog dog = (Dog)animal;

なぜなら、ダウンキャストは失敗する可能性があるからです。

C#では、危険性のある変換については、開発者が明示的に「この型に変換する」と書く必要があります。

3. C#でダウンキャストする基本的な書き方

3-1. キャスト演算子「(型名)」を使う方法

C#でダウンキャストする基本的な書き方は、キャスト演算子を使う方法です。

C#
Dog dog = (Dog)animal;

(Dog)のように、変換したい型名を丸かっこの中に書きます。

具体例は次のとおりです。

C#
Animal animal = new Dog();

Dog dog = (Dog)animal;
dog.Bark();

このコードでは、animalの実体がDogなので、Dog型へのダウンキャストは成功します。

ただし、失敗する可能性がある場合は注意が必要です。

C#
Animal animal = new Cat();

Dog dog = (Dog)animal; // InvalidCastException

キャスト演算子はシンプルですが、型が合わない場合に例外が発生します。

3-2. object型から具体的な型へダウンキャストする例

C#では、すべての型はobject型として扱うことができます。

そのため、object型の変数から具体的な型へ戻す場面でもダウンキャストが使われます。

C#
object obj = "Hello";

string text = (string)obj;

Console.WriteLine(text);

この例では、objの実体は文字列なので、string型へのキャストは成功します。

一方、次のコードは失敗します。

C#
object obj = 123;

string text = (string)obj; // InvalidCastException

objの実体はintなので、string型へキャストすることはできません。

object型から具体的な型へ戻す場合も、実体の型を意識する必要があります。

3-3. 基底クラス型の変数から派生クラス型へ戻す例

基底クラス型の変数から派生クラス型へ戻す例を見てみましょう。

C#
class Animal
{
public string Name { get; set; } = "";
}

class Dog : Animal
{
public void Bark()
{
Console.WriteLine($"{Name}:ワン!");
}
}

次のように、DogAnimal型として扱います。

C#
Animal animal = new Dog();
animal.Name = "ポチ";

この時点では、変数の型がAnimalなので、Dog固有のBarkメソッドは呼び出せません。

C#
// animal.Bark(); // コンパイルエラー

そこで、Dog型にダウンキャストします。

C#
Dog dog = (Dog)animal;
dog.Bark();

実体がDogであれば、この変換は成功します。

3-4. ダウンキャスト後に派生クラス固有のメンバーを使う方法

ダウンキャストの主な目的は、派生クラス固有のメンバーを使えるようにすることです。

C#
class Dog : Animal
{
public int Age { get; set; }

public void Bark()
{
Console.WriteLine("ワン!");
}
}

Animal型の変数では、Dog固有のAgeBarkにはアクセスできません。

C#
Animal animal = new Dog();

// animal.Age = 3; // コンパイルエラー
// animal.Bark(); // コンパイルエラー

Dog型にダウンキャストすれば、これらを使えるようになります。

C#
Dog dog = (Dog)animal;

dog.Age = 3;
dog.Bark();

ただし、ダウンキャストする前に、実体が本当にDogであることを確認するのが安全です。

3-5. コンパイルエラーになるケースと実行時エラーになるケース

ダウンキャストでは、コンパイルエラーになるケースと実行時エラーになるケースがあります。

たとえば、継承関係がまったくない型同士のキャストは、コンパイルエラーになることがあります。

C#
class Dog
{
}

class Car
{
}

Dog dog = new Dog();

// Car car = (Car)dog; // コンパイルエラー

DogCarには継承関係がないため、C#はこの変換を許可しません。

一方、継承関係がある場合は、コンパイルは通ることがあります。

C#
Animal animal = new Cat();

Dog dog = (Dog)animal; // コンパイルは通るが、実行時エラー

この場合、AnimalDogには継承関係があるため、コンパイルは通ります。

しかし、実体がCatなので、実行時にInvalidCastExceptionが発生します。

つまり、コンパイル時に「可能性がある」と判断されても、実行時に成功するとは限らないのです。

4. InvalidCastExceptionが発生する原因と対策

4-1. InvalidCastExceptionとは?

InvalidCastExceptionとは、無効な型変換を行ったときに発生する例外です。

ダウンキャストでは、実体の型が変換先の型と一致しない場合に発生します。

C#
object obj = 123;

string text = (string)obj; // InvalidCastException

この例では、objの実体はintです。

それをstringとして扱おうとしているため、InvalidCastExceptionが発生します。

4-2. 実体の型が変換先の型と一致しないケース

ダウンキャストで最も多い失敗原因は、実体の型が変換先の型と一致していないことです。

C#
Animal animal = new Cat();

Dog dog = (Dog)animal;

このコードでは、animalの変数の型はAnimalです。

しかし、実体の型はCatです。

CatDogではないため、Dog型へのダウンキャストは失敗します。

次のように考えるとわかりやすいです。

C#
Animal animal = new Cat();

これは「Animalとして扱っているCat」です。

「Animalとして扱っているDog」ではありません。

そのため、Dogに戻すことはできません。

4-3. 継承関係があっても失敗するダウンキャストの例

継承関係があるからといって、すべてのダウンキャストが成功するわけではありません。

C#
class Animal
{
}

class Dog : Animal
{
}

class Cat : Animal
{
}

DogCatAnimalを継承しています。

しかし、DogCatは別の型です。

C#
Animal animal = new Cat();

Dog dog = (Dog)animal; // 失敗

animalAnimal型ですが、実体はCatです。

CatDogに変換することはできません。

これは、「猫は動物であり、犬も動物だが、猫は犬ではない」という関係と同じです。

4-4. InvalidCastExceptionを防ぐ基本方針

InvalidCastExceptionを防ぐ基本方針は、キャストする前に型を確認することです。

C#では、主に次の方法があります。

C#
if (animal is Dog)
{
Dog dog = (Dog)animal;
dog.Bark();
}

より新しい書き方では、パターンマッチングを使えます。

C#
if (animal is Dog dog)
{
dog.Bark();
}

また、as演算子を使って、失敗時にnullを受け取る方法もあります。

C#
Dog? dog = animal as Dog;

if (dog != null)
{
dog.Bark();
}

安全にダウンキャストしたい場合は、いきなり(Dog)animalと書くのではなく、isasを使って型を確認しましょう。

4-5. 例外が出るコードと安全に書き換えたコードの比較

まず、例外が発生する可能性があるコードです。

C#
Animal animal = new Cat();

Dog dog = (Dog)animal;
dog.Bark();

このコードは、animalの実体がDogでなければ失敗します。

安全に書き換えると、次のようになります。

C#
Animal animal = new Cat();

if (animal is Dog dog)
{
dog.Bark();
}
else
{
Console.WriteLine("Dogではありません");
}

この書き方であれば、animalの実体がDogの場合だけBarkを呼び出します。

Dogでない場合は、例外を発生させずに別の処理を行えます。

as演算子を使う場合は、次のように書けます。

C#
Animal animal = new Cat();

Dog? dog = animal as Dog;

if (dog != null)
{
dog.Bark();
}
else
{
Console.WriteLine("Dogではありません");
}

どちらの方法でも、無効なダウンキャストによるInvalidCastExceptionを避けることができます。

5. 安全にダウンキャストする方法

5-1. is演算子で型を確認してからキャストする

is演算子を使うと、オブジェクトが指定した型かどうかを確認できます。

C#
if (animal is Dog)
{
Dog dog = (Dog)animal;
dog.Bark();
}

このコードでは、animalDog型として扱える場合だけ、キャストを実行しています。

そのため、InvalidCastExceptionを防げます。

ただし、この書き方では、型チェックとキャストを別々に書く必要があります。

そのため、最近のC#では次に紹介するパターンマッチングの書き方がよく使われます。

5-2. パターンマッチング「is 型 変数」を使う

パターンマッチングを使うと、型チェックと変数への代入を同時に行えます。

C#
if (animal is Dog dog)
{
dog.Bark();
}

このコードでは、animalDogとして扱える場合にだけ、dogという変数が使えるようになります。

dog変数は、ifブロックの中でDog型として扱えます。

C#
if (animal is Dog dog)
{
dog.Bark();
dog.Age = 3;
}

この書き方は、読みやすく安全なので、C#でダウンキャストを書くときによく使われます。

5-3. as演算子で失敗時にnullを受け取る

as演算子を使うと、キャストに失敗したときに例外ではなくnullを返します。

C#
Dog? dog = animal as Dog;

animalDogとして扱える場合は、dogDog型の参照が入ります。

扱えない場合は、nullになります。

C#
Dog? dog = animal as Dog;

if (dog != null)
{
dog.Bark();
}

as演算子を使う場合は、必ずnullチェックを行うことが大切です。

5-4. nullチェックと組み合わせて安全に処理する

as演算子を使った場合、結果がnullになる可能性があります。

そのため、次のようにnullチェックをしてから使います。

C#
Animal animal = new Dog();

Dog? dog = animal as Dog;

if (dog != null)
{
dog.Bark();
}
else
{
Console.WriteLine("Dogではありません");
}

nullチェックを忘れると、NullReferenceExceptionが発生する可能性があります。

C#
Animal animal = new Cat();

Dog? dog = animal as Dog;

dog.Bark(); // dogがnullなので危険

asは例外を避けるために便利ですが、結果がnullになることを前提に書く必要があります。

5-5. 「(型名)」「is」「as」の使い分け

ダウンキャストには、主に次の3つの書き方があります。

C#
Dog dog = (Dog)animal;
C#
if (animal is Dog dog)
{
dog.Bark();
}
C#
Dog? dog = animal as Dog;

if (dog != null)
{
dog.Bark();
}

それぞれの使い分けは、次のように考えるとわかりやすいです。

(型名)によるキャストは、必ずその型であると確信できる場合に使います。

C#
Animal animal = new Dog();

Dog dog = (Dog)animal;

isは、型を確認しながら処理を分岐したい場合に使います。

C#
if (animal is Dog dog)
{
dog.Bark();
}
else if (animal is Cat cat)
{
cat.Meow();
}

asは、キャストに失敗しても例外にしたくない場合や、nullで判定したい場合に使います。

C#
Dog? dog = animal as Dog;

if (dog != null)
{
dog.Bark();
}

初心者には、まずis 型 変数のパターンマッチングを使う方法がおすすめです。

安全で読みやすく、キャスト失敗による例外も避けやすいからです。

5-6. null許容参照型を使う場合の注意点

C#では、null許容参照型を有効にしている場合、as演算子の結果はnullになる可能性があるものとして扱います。

C#
Dog? dog = animal as Dog;

この場合、dogDog?です。

つまり、Dog型のオブジェクトかもしれないし、nullかもしれないという意味です。

そのため、次のようにnullチェックしてから使います。

C#
if (dog != null)
{
dog.Bark();
}

または、null条件演算子を使うこともできます。

C#
dog?.Bark();

ただし、dog?.Bark()は、dognullの場合に何もしないだけです。

Dogではなかった場合に別の処理をしたいなら、if文で分岐したほうがわかりやすいです。

C#
if (animal is Dog dog)
{
dog.Bark();
}
else
{
Console.WriteLine("Dogではありません");
}

null許容参照型を使う場合は、「キャストに失敗したらnullになる」という点をコード上でも明確に扱いましょう。

6. ダウンキャストを使うべき場面・避けるべき場面

6-1. ダウンキャストが有効な場面

ダウンキャストは、必要な場面で正しく使えば便利です。

たとえば、複数の派生クラスを基底クラス型のリストで管理している場合があります。

C#
List<Animal> animals = new List<Animal>
{
new Dog(),
new Cat()
};

このようなリストの中から、特定の型だけに固有の処理を行いたい場合、ダウンキャストが役立ちます。

C#
foreach (Animal animal in animals)
{
if (animal is Dog dog)
{
dog.Bark();
}
}

また、外部ライブラリやフレームワークからobject型として値を受け取る場合にも、具体的な型に戻すためにキャストが必要になることがあります。

ただし、ダウンキャストは便利な一方で、使いすぎるとコードが複雑になりやすい点に注意が必要です。

6-2. ダウンキャストを多用すると設計が複雑になる理由

ダウンキャストを多用すると、コードのあちこちで「このオブジェクトはDogか?Catか?」と型を判定することになります。

C#
if (animal is Dog dog)
{
dog.Bark();
}
else if (animal is Cat cat)
{
cat.Meow();
}
else
{
Console.WriteLine("不明な動物です");
}

このようなコードが増えると、新しい派生クラスを追加するたびに、多くの場所を修正しなければならなくなります。

たとえば、Birdクラスを追加した場合、すべての型判定にBird用の分岐を追加する必要が出てくるかもしれません。

C#
else if (animal is Bird bird)
{
bird.Fly();
}

このような設計は、変更に弱くなりがちです。

ダウンキャストが何度も出てくる場合は、ポリモーフィズムやインターフェイスで置き換えられないか検討しましょう。

6-3. ポリモーフィズムで置き換えられるケース

ポリモーフィズムを使うと、ダウンキャストを減らせる場合があります。

たとえば、動物ごとに鳴き声を出したい場合、型ごとに分岐するのではなく、基底クラスに共通のメソッドを用意できます。

C#
class Animal
{
public virtual void Speak()
{
Console.WriteLine("何かの鳴き声");
}
}

class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("ワン!");
}
}

class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("ニャー!");
}
}

このようにしておけば、次のように書けます。

C#
List<Animal> animals = new List<Animal>
{
new Dog(),
new Cat()
};

foreach (Animal animal in animals)
{
animal.Speak();
}

DogCatかを判定しなくても、それぞれのクラスに応じた処理が実行されます。

このような書き方のほうが、オブジェクト指向らしく、拡張にも強い設計になります。

6-4. virtual・overrideを使って型判定を減らす

C#では、基底クラスにvirtualメソッドを定義し、派生クラスでoverrideすることで、型判定を減らせます。

C#
class Animal
{
public virtual void Move()
{
Console.WriteLine("移動します");
}
}

class Dog : Animal
{
public override void Move()
{
Console.WriteLine("犬が走ります");
}
}

class Bird : Animal
{
public override void Move()
{
Console.WriteLine("鳥が飛びます");
}
}

この場合、呼び出し側は具体的な型を知る必要がありません。

C#
Animal animal = new Bird();

animal.Move();

実体がBirdであれば、BirdクラスのMoveが呼ばれます。

つまり、ダウンキャストしなくても、派生クラスごとの振る舞いを実現できます。

ダウンキャストが必要に見える場面でも、virtualoverrideで置き換えられることは多いです。

6-5. インターフェイスを使って安全に処理を共通化する

インターフェイスを使うことで、特定の機能を持つオブジェクトを安全に扱える場合があります。

たとえば、走ることができるクラスに共通のインターフェイスを定義します。

C#
interface IRunnable
{
void Run();
}

class Dog : Animal, IRunnable
{
public void Run()
{
Console.WriteLine("犬が走ります");
}
}

class Cat : Animal, IRunnable
{
public void Run()
{
Console.WriteLine("猫が走ります");
}
}

この場合、DogCatという具体的な型にダウンキャストするのではなく、IRunnableとして扱えます。

C#
if (animal is IRunnable runnable)
{
runnable.Run();
}

これにより、「Dogかどうか」ではなく、「走る機能を持っているかどうか」で処理できます。

具体的なクラスに依存しにくくなるため、柔軟な設計になります。

6-6. ジェネリクスでダウンキャストを避ける考え方

ジェネリクスを使うと、object型や基底クラス型からキャストして戻す処理を減らせる場合があります。

たとえば、次のようなコードでは、object型を使っているためキャストが必要になります。

C#
object value = "Hello";

string text = (string)value;

ジェネリクスを使えば、最初から型を保ったまま扱えます。

C#
class Box<T>
{
public T Value { get; set; }

public Box(T value)
{
Value = value;
}
}

使う側は次のように書けます。

C#
Box<string> box = new Box<string>("Hello");

string text = box.Value;

この場合、box.Valueは最初からstring型なので、ダウンキャストが不要です。

object型に入れてあとでキャストする設計より、ジェネリクスを使って型情報を保つ設計のほうが安全です。

7. よくあるダウンキャストの間違い

7-1. 基底クラスのインスタンスを派生クラスに変換しようとする

初心者がよく間違えるのが、基底クラスのインスタンスを派生クラスに変換しようとするケースです。

C#
Animal animal = new Animal();

Dog dog = (Dog)animal;

このコードは失敗します。

なぜなら、animalの実体はAnimalであり、Dogではないからです。

DogAnimalですが、Animalは必ずしもDogではありません。

ダウンキャストできるのは、実体が変換先の型である場合です。

C#
Animal animal = new Dog();

Dog dog = (Dog)animal; // 成功

この違いをしっかり理解しておくことが大切です。

7-2. 同じ基底クラスを持つ別の派生クラスへ変換しようとする

同じ基底クラスを持つ派生クラス同士でも、相互に変換できるわけではありません。

C#
Animal animal = new Cat();

Dog dog = (Dog)animal;

CatDogAnimalを継承しています。

しかし、CatDogではありません。

そのため、このダウンキャストは失敗します。

安全に処理するには、型を確認してからキャストします。

C#
if (animal is Dog dog)
{
dog.Bark();
}
else
{
Console.WriteLine("Dogではありません");
}

同じ基底クラスを持っていることと、同じ派生クラスであることは別です。

7-3. List<Base>をList<Derived>にそのまま変換しようとする

List<Base>List<Derived>にそのまま変換しようとするのも、よくある間違いです。

C#
List<Animal> animals = new List<Animal>
{
new Dog(),
new Dog()
};

// List<Dog> dogs = (List<Dog>)animals; // 変換できない

たとえ中身がすべてDogだったとしても、List<Animal>そのものをList<Dog>として扱うことはできません。

必要な場合は、要素ごとに変換します。

C#
List<Dog> dogs = animals.Cast<Dog>().ToList();

ただし、Cast<Dog>()は、要素の中にDogではないものが含まれていると例外が発生します。

安全にDogだけ取り出したい場合は、OfType<Dog>()を使います。

C#
List<Dog> dogs = animals.OfType<Dog>().ToList();

OfType<Dog>()は、Dog型として扱える要素だけを抽出します。

7-4. as演算子の結果をnullチェックせずに使う

as演算子は、キャストに失敗したときにnullを返します。

そのため、nullチェックせずに使うと危険です。

C#
Animal animal = new Cat();

Dog? dog = animal as Dog;

dog.Bark(); // NullReferenceExceptionの可能性

animalの実体はCatなので、animal as Dogの結果はnullです。

その状態でdog.Bark()を呼び出すと、NullReferenceExceptionが発生します。

正しくは、次のように書きます。

C#
Dog? dog = animal as Dog;

if (dog != null)
{
dog.Bark();
}
else
{
Console.WriteLine("Dogではありません");
}

asを使うときは、必ずnullになる可能性を考えましょう。

7-5. キャストすればオブジェクトの中身が変わると誤解する

キャストは、オブジェクトの中身を別の型に作り変える処理ではありません。

キャストは、「そのオブジェクトを別の型として参照する」ための操作です。

たとえば、次のコードを見てください。

C#
Animal animal = new Dog();

Dog dog = (Dog)animal;

このコードでは、animalの実体であるDogオブジェクトは変わっていません。

Animal型として見ていたものを、Dog型として見られるようにしているだけです。

一方、次のような変換はできません。

C#
Animal animal = new Animal();

Dog dog = (Dog)animal; // AnimalがDogに変化するわけではない

キャストを書いたからといって、AnimalオブジェクトがDogオブジェクトに変わるわけではありません。

ダウンキャストは、実体がすでにその型である場合にだけ成功します。

8. 実践サンプルで理解するC#のダウンキャスト

8-1. Animal・Dog・Catクラスを使った基本例

ここでは、AnimalDogCatクラスを使って、ダウンキャストを実践的に確認します。

C#
class Animal
{
public string Name { get; set; } = "";

public void Eat()
{
Console.WriteLine($"{Name}が食べています");
}
}

class Dog : Animal
{
public void Bark()
{
Console.WriteLine($"{Name}:ワン!");
}
}

class Cat : Animal
{
public void Meow()
{
Console.WriteLine($"{Name}:ニャー!");
}
}

DogCatは、どちらもAnimalを継承しています。

そのため、Animal型として扱うことができます。

C#
Animal animal1 = new Dog { Name = "ポチ" };
Animal animal2 = new Cat { Name = "タマ" };

ただし、Animal型の変数からは、Dog固有のBarkCat固有のMeowは直接呼び出せません。

8-2. アップキャストしたDogをDogに戻す例

まず、DogAnimalにアップキャストし、その後Dogにダウンキャストしてみます。

C#
Dog originalDog = new Dog { Name = "ポチ" };

// アップキャスト
Animal animal = originalDog;

// ダウンキャスト
Dog dog = (Dog)animal;

dog.Bark();

実行結果は次のようになります。

C#
ポチ:ワン!

この例では、animalの実体はDogです。

そのため、Dog型へのダウンキャストは成功します。

8-3. CatをDogにダウンキャストして失敗する例

次に、失敗する例を見てみましょう。

C#
Animal animal = new Cat { Name = "タマ" };

Dog dog = (Dog)animal;

dog.Bark();

このコードはコンパイルできる場合がありますが、実行時にInvalidCastExceptionが発生します。

animalの実体はCatです。

CatDogとして扱うことはできません。

正しく処理するには、型を確認してからキャストする必要があります。

8-4. is演算子で分岐して安全に処理する例

is演算子を使うと、型を確認してから安全に処理できます。

C#
Animal animal = new Cat { Name = "タマ" };

if (animal is Dog)
{
Dog dog = (Dog)animal;
dog.Bark();
}
else
{
Console.WriteLine("Dogではありません");
}

実行結果は次のようになります。

C#
Dogではありません

このように、isで確認してからキャストすれば、InvalidCastExceptionを防げます。

8-5. as演算子でnull判定する例

as演算子を使うと、キャストに失敗したときにnullを受け取れます。

C#
Animal animal = new Cat { Name = "タマ" };

Dog? dog = animal as Dog;

if (dog != null)
{
dog.Bark();
}
else
{
Console.WriteLine("Dogではありません");
}

このコードでも、animalの実体はCatなので、dognullになります。

そのため、else側の処理が実行されます。

C#
Dogではありません

asを使う場合は、nullチェックを忘れないことが重要です。

8-6. パターンマッチングで読みやすく書く例

最もおすすめしやすい書き方は、パターンマッチングを使う方法です。

C#
Animal animal = new Dog { Name = "ポチ" };

if (animal is Dog dog)
{
dog.Bark();
}
else if (animal is Cat cat)
{
cat.Meow();
}
else
{
Console.WriteLine("不明な動物です");
}

この書き方では、型チェックと変数宣言を同時に行えます。

animalDogならdog変数が使えます。

animalCatならcat変数が使えます。

さらに、switch式やswitch文を使って書くこともできます。

C#
Animal animal = new Cat { Name = "タマ" };

switch (animal)
{
case Dog dog:
dog.Bark();
break;

case Cat cat:
cat.Meow();
break;

default:
Console.WriteLine("不明な動物です");
break;
}

複数の型に応じて処理を分けたい場合は、パターンマッチングを使うと読みやすくなります。

9. ダウンキャストに関するよくある質問

9-1. ダウンキャストと型変換は同じ意味?

ダウンキャストは、型変換の一種です。

型変換には、数値型の変換、文字列への変換、参照型のキャスト、ボックス化・アンボックス化など、さまざまな種類があります。

その中で、継承関係にある型において、基底クラス型から派生クラス型へ変換することをダウンキャストと呼びます。

C#
Animal animal = new Dog();

Dog dog = (Dog)animal;

このような変換がダウンキャストです。

つまり、すべてのダウンキャストは型変換ですが、すべての型変換がダウンキャストというわけではありません。

9-2. ダウンキャストとボックス化・アンボックス化の違いは?

ダウンキャストは、主に継承関係にある参照型で使われる考え方です。

一方、ボックス化とアンボックス化は、値型とobject型の間で起こる変換です。

たとえば、次のコードではボックス化が行われます。

C#
int number = 123;

object obj = number;

intは値型ですが、object型として扱われています。

これがボックス化です。

逆に、object型からint型へ戻す処理がアンボックス化です。

C#
int result = (int)obj;

見た目はキャストに似ていますが、ダウンキャストとは仕組みが異なります。

ダウンキャストは、たとえばAnimalからDogへ戻すような参照型の変換です。

C#
Animal animal = new Dog();

Dog dog = (Dog)animal;

9-3. as演算子とキャスト演算子はどちらを使うべき?

必ずその型であると確信できる場合は、キャスト演算子を使っても問題ありません。

C#
Animal animal = new Dog();

Dog dog = (Dog)animal;

ただし、実体の型が不確かな場合は、isasを使ったほうが安全です。

特に初心者には、パターンマッチングを使ったis 型 変数の書き方がおすすめです。

C#
if (animal is Dog dog)
{
dog.Bark();
}

as演算子も便利ですが、結果がnullになる可能性があります。

C#
Dog? dog = animal as Dog;

if (dog != null)
{
dog.Bark();
}

型が違うことが通常の流れとしてあり得るなら、isasを使いましょう。

型が違うこと自体がプログラム上の異常であれば、キャスト演算子を使って例外を発生させる設計もあります。

9-4. is演算子とGetTypeの違いは?

is演算子は、指定した型として扱えるかどうかを判定します。

C#
if (animal is Dog)
{
Console.WriteLine("Dogとして扱えます");
}

一方、GetType()は、実体の正確な型を取得します。

C#
if (animal.GetType() == typeof(Dog))
{
Console.WriteLine("実体の型はDogです");
}

たとえば、Dogを継承したShibaDogクラスがあるとします。

C#
class ShibaDog : Dog
{
}

この場合、次のような違いがあります。

C#
Animal animal = new ShibaDog();

Console.WriteLine(animal is Dog); // true

Console.WriteLine(animal.GetType() == typeof(Dog)); // false

ShibaDogDogを継承しているため、is Dogtrueです。

しかし、実体の正確な型はShibaDogなので、GetType() == typeof(Dog)falseになります。

ダウンキャストの可否を確認したい場合は、通常はisを使うのが適しています。

9-5. ダウンキャストは使わない方がよい?

ダウンキャストは、使ってはいけないものではありません。

必要な場面では有効です。

たとえば、基底クラス型のコレクションから特定の派生クラスだけを取り出したい場合や、外部からobject型として渡された値を具体的な型に戻したい場合には、ダウンキャストが必要になることがあります。

ただし、ダウンキャストを多用している場合は、設計を見直したほうがよいかもしれません。

型ごとに処理を分けるコードが増えているなら、次のような方法で改善できる可能性があります。

C#
public virtual void Speak()
{
}
C#
public override void Speak()
{
Console.WriteLine("ワン!");
}

virtualoverrideを使えば、呼び出し側で型を判定せずに、派生クラスごとの処理を実行できます。

ダウンキャストは便利ですが、ポリモーフィズムやインターフェイスで置き換えられる場合は、そちらを検討しましょう。

9-6. InvalidCastExceptionとNullReferenceExceptionの違いは?

InvalidCastExceptionは、無効なキャストを行ったときに発生する例外です。

C#
Animal animal = new Cat();

Dog dog = (Dog)animal; // InvalidCastException

この例では、CatDogとして扱おうとしているため、InvalidCastExceptionが発生します。

一方、NullReferenceExceptionは、nullに対してメンバーを呼び出そうとしたときに発生する例外です。

C#
Animal animal = new Cat();

Dog? dog = animal as Dog;

dog.Bark(); // NullReferenceExceptionの可能性

この例では、animal as Dogの結果がnullになります。

そのnullに対してBark()を呼び出そうとしているため、NullReferenceExceptionが発生する可能性があります。

つまり、違いは次のとおりです。

InvalidCastExceptionは、キャストそのものに失敗したときの例外です。

NullReferenceExceptionは、nullの変数を使おうとしたときの例外です。

as演算子を使うとInvalidCastExceptionは避けられますが、nullチェックを忘れるとNullReferenceExceptionが起こる可能性があります。

まとめ

C#のダウンキャストとは、基底クラス型の変数を派生クラス型に変換することです。

たとえば、Animal型として扱っている実体がDogであれば、Dog型へダウンキャストできます。

C#
Animal animal = new Dog();

Dog dog = (Dog)animal;

一方で、実体がDogでない場合は、ダウンキャストに失敗します。

C#
Animal animal = new Cat();

Dog dog = (Dog)animal; // InvalidCastException

ダウンキャストで重要なのは、変数の型ではなく実体の型です。

Animal型の変数に入っているからといって、必ずDogに変換できるわけではありません。

安全にダウンキャストするには、is演算子やパターンマッチングを使うのがおすすめです。

C#
if (animal is Dog dog)
{
dog.Bark();
}

as演算子を使う場合は、失敗時にnullが返るため、必ずnullチェックを行いましょう。

C#
Dog? dog = animal as Dog;

if (dog != null)
{
dog.Bark();
}

また、ダウンキャストを多用している場合は、設計を見直すサインかもしれません。

virtualoverride、インターフェイス、ジェネリクスなどを使うことで、ダウンキャストを減らし、より安全で読みやすいコードにできる場合があります。

C#でダウンキャストを使うときは、「実体の型は何か」「キャストに失敗する可能性はあるか」「isやasで安全に書けないか」を意識することが大切です。