C# で派生クラスに基底クラスと同じ名前のメソッドやプロパティを定義すると、コンパイラが次の警告を出します。
CS0108: 'ExtendedCircle.Draw()' hides inherited member 'Circle.Draw()'.
Use the new keyword if hiding was intended.
この警告は 「あなたが基底クラスのメンバーを隠そうとしている」 ことをコンパイラが検出したものです。意図的なら new キーワードを付けてその意思を明示し、意図的でないなら設計を見直す必要があります。
本記事では、new 修飾子による シャドーイング(hiding) の仕組みと、virtual / override との本質的な違い、そして「どんな場面で使うべきか」を解説します。
シャドーイングとは何か
問題の発生
まず、警告が発生するコードを見てみましょう。
public class Circle
{
public void Draw()
{
Console.WriteLine("Circle.Draw");
}
}
public class ExtendedCircle : Circle
{
// CS0108 警告: 'ExtendedCircle.Draw()' hides inherited member 'Circle.Draw()'.
public void Draw()
{
Console.WriteLine("ExtendedCircle.Draw");
}
}
Circle クラスの Draw() は virtual ではない通常のメソッドです。ExtendedCircle が同じシグネチャの Draw() を定義すると、基底クラスのメンバーが「隠される」 ことになります。これをシャドーイング(shadowing)、あるいはメンバーの隠蔽(hiding)と呼びます。
new 修飾子で意図を明示する
new キーワードを付けることで、「基底クラスのメンバーを意図的に隠している」とコンパイラに宣言し、警告を消します。
public class ExtendedCircle : Circle
{
// new を付けて意図的な隠蔽を明示 → 警告は出ない
public new void Draw()
{
Console.WriteLine("ExtendedCircle.Draw");
}
}
重要: new を付けても付けなくても 実行時の振る舞いは同じ です。new はコンパイラに対する意思表示であり、生成される IL(中間言語)に違いはありません。
キャストによる振る舞いの変化
シャドーイングの最も重要な特性は、変数の型(コンパイル時の型)によって呼ばれるメソッドが変わる ことです。
ExtendedCircle extended = new ExtendedCircle();
extended.Draw(); // ExtendedCircle.Draw
// 基底クラスにキャストすると、基底クラスのメソッドが呼ばれる
((Circle)extended).Draw(); // Circle.Draw
// 変数の型が Circle なら Circle.Draw() が呼ばれる
Circle circle = extended;
circle.Draw(); // Circle.Draw
これは virtual / override の動作とは 根本的に異なります。override の場合、変数の型に関係なく、常にオブジェクトの実際の型のメソッドが呼ばれます。
シャドーイング vs オーバーライド — 決定的な違い
この違いを並べて確認しましょう。
オーバーライド(virtual / override)
public class Circle
{
public virtual void Draw()
{
Console.WriteLine("Circle.Draw");
}
}
public class ExtendedCircle : Circle
{
public override void Draw()
{
Console.WriteLine("ExtendedCircle.Draw");
}
}
ExtendedCircle extended = new ExtendedCircle();
extended.Draw(); // ExtendedCircle.Draw
Circle circle = extended;
circle.Draw(); // ExtendedCircle.Draw ← オブジェクトの型で決まる
((Circle)extended).Draw(); // ExtendedCircle.Draw ← キャストしても同じ
シャドーイング(new)
public class Circle
{
public void Draw()
{
Console.WriteLine("Circle.Draw");
}
}
public class ExtendedCircle : Circle
{
public new void Draw()
{
Console.WriteLine("ExtendedCircle.Draw");
}
}
ExtendedCircle extended = new ExtendedCircle();
extended.Draw(); // ExtendedCircle.Draw
Circle circle = extended;
circle.Draw(); // Circle.Draw ← 変数の型で決まる
((Circle)extended).Draw(); // Circle.Draw ← キャストで基底の実装が呼ばれる
比較表
| 特性 | virtual / override |
new(シャドーイング) |
|---|---|---|
| メソッドの解決 | 実行時(オブジェクトの型) | コンパイル時(変数の型) |
基底クラスの virtual 宣言 |
必須 | 不要 |
| ポリモーフィズム | あり | なし |
| キャストした場合 | 派生クラスのメソッドが呼ばれる | 基底クラスのメソッドが呼ばれる |
| 主な目的 | 振る舞いの差し替え | 名前の衝突の解決 |
核心的な違い: override は「基底クラスの振る舞いを 置き換える」のに対し、new は「基底クラスのメンバーを 隠すだけ で、基底クラスの実装はそのまま残る」。
シャドーイングの仕組み — IL レベルで何が起きているか
override と new の違いは、生成される IL コードに明確に現れます。
overrideメソッドは、IL 上でvirtualフラグを持ち、vtable(仮想メソッドテーブル)のスロットを基底クラスのメソッドと共有します。メソッド呼び出しはcallvirt命令で行われ、実行時にオブジェクトの型から適切なメソッドが解決されます。newメソッドは、vtable 上で 新しいスロット を占めます(newslotフラグ)。基底クラスの元のスロットはそのまま残るため、基底クラスの型を通じたアクセスでは元のメソッドが呼ばれます。
// override の場合 — vtable のスロットを上書き
.method public hidebysig virtual instance void Draw() cil managed
// Circle の vtable スロットを ExtendedCircle の実装で置換
// new の場合 — 新しいスロットを追加
.method public hidebysig instance void Draw() cil managed
// Circle の vtable スロットはそのまま、新しいスロットに ExtendedCircle の実装を配置
メソッド以外のシャドーイング
new 修飾子はメソッドだけでなく、プロパティ、フィールド、イベント、インデクサー、さらには入れ子型にも適用できます。
プロパティのシャドーイング
public class BaseConfig
{
public string ConnectionString { get; set; } = "";
}
public class SecureConfig : BaseConfig
{
// 戻り値の型を変えることはできないが、実装を変えられる
public new string ConnectionString
{
get => Decrypt(base.ConnectionString);
set => base.ConnectionString = Encrypt(value);
}
private static string Encrypt(string value) => /* 暗号化 */;
private static string Decrypt(string value) => /* 復号 */;
}
戻り値の型を変えるシャドーイング
override では戻り値の型を変更できませんが(C# 9 以降の共変戻り値型を除く)、new によるシャドーイングでは シグネチャを完全に変更 できます。メソッド名が同じでも、引数の型や数、戻り値の型を変えたメンバーを定義できます。
public class DataSource
{
public object GetData() => new { Value = 42 };
}
public class TypedDataSource : DataSource
{
// 戻り値の型を object から int に変更
public new int GetData() => 42;
}
ただし、これは ポリモーフィズムが効かない ことを意味します。基底クラスの型を通じてアクセスすると object を返す元のメソッドが呼ばれるため、混乱の原因になりやすい設計です。
フィールドのシャドーイング
public class Base
{
public int Value = 10;
}
public class Derived : Base
{
public new int Value = 20;
}
var d = new Derived();
Console.WriteLine(d.Value); // 20
Console.WriteLine(((Base)d).Value); // 10 — 基底クラスのフィールドがそのまま残っている
フィールドのシャドーイングは特に混乱を招きやすいため、通常は避けるべきです。
シャドーイングが適切な場面
シャドーイングは一般的に「設計の問題を示す兆候」として扱われがちですが、正当な使いどころも存在します。
1. 基底クラスを変更できない場合の名前衝突の解決
サードパーティのライブラリやフレームワークが提供する基底クラスが、後のバージョンで自分が既に定義していたメンバーと同じ名前のメンバーを追加した場合、new で明示的に隠すことで既存のコードを壊さずに対応できます。
// ライブラリ v1.0 — Format() メソッドはない
public class LibraryWidget
{
public void Render() { /* ... */ }
}
// 自分のコード — Format() を独自に追加
public class MyWidget : LibraryWidget
{
public string Format() => "custom format";
}
// ライブラリ v2.0 — Format() が追加された!
public class LibraryWidget
{
public void Render() { /* ... */ }
public string Format() => "library format"; // 名前が衝突!
}
// new で衝突を明示的に解決
public class MyWidget : LibraryWidget
{
public new string Format() => "custom format";
}
これはまさにコンパイラ警告 CS0108 が想定している本来のシナリオです。
2. 戻り値の型をより具体的にしたい場合
基底クラスが virtual でないメソッドを定義しており、派生クラスでより具体的な型を返したい場合に使うことがあります。
public class Builder
{
public Builder WithTimeout(int seconds)
{
// タイムアウトを設定
return this;
}
}
public class HttpBuilder : Builder
{
// メソッドチェーンで HttpBuilder 型を維持するために new でシャドーイング
public new HttpBuilder WithTimeout(int seconds)
{
base.WithTimeout(seconds);
return this;
}
public HttpBuilder WithBaseUrl(string url)
{
// URL を設定
return this;
}
}
// シャドーイングにより、メソッドチェーンが自然に書ける
var builder = new HttpBuilder()
.WithTimeout(30) // HttpBuilder を返す(new で隠した版)
.WithBaseUrl("https://api.example.com"); // HttpBuilder のメソッドを呼べる
ただし、この設計は Fluent Interface パターンの既知の課題であり、ジェネリクスを使った CRTP(Curiously Recurring Template Pattern) で解決する方が型安全です。
3. インターフェースの明示的実装との組み合わせ
public interface IResettable
{
void Reset();
}
public class Component : IResettable
{
public void Reset()
{
Console.WriteLine("Component.Reset");
}
}
public class AdvancedComponent : Component
{
// Component.Reset() を隠して、より詳細なリセット処理を提供
public new void Reset()
{
Console.WriteLine("AdvancedComponent.Reset (full reset)");
base.Reset(); // 基底のリセットも呼ぶ
}
}
var comp = new AdvancedComponent();
comp.Reset(); // AdvancedComponent.Reset (full reset)
((Component)comp).Reset(); // Component.Reset
((IResettable)comp).Reset(); // Component.Reset ← 注意!
注意: インターフェースを通じた呼び出しでは、基底クラスの実装が呼ばれます。new はポリモーフィズムを提供しないため、インターフェース経由のアクセスではシャドーイングが効きません。これはバグの温床になりやすい点です。
シャドーイングを避けるべき場面
1. ポリモーフィズムが必要な場面
基底クラスの型を通じてアクセスされる可能性がある場合、シャドーイングはバグの原因になります。
// アンチパターン: ポリモーフィズムが必要な場面で new を使う
public class Logger
{
public void Log(string message)
{
Console.WriteLine($"[LOG] {message}");
}
}
public class FileLogger : Logger
{
public new void Log(string message)
{
File.AppendAllText("log.txt", $"[FILE] {message}\n");
}
}
// 基底クラスの型で受け取ると、ファイルへの出力が行われない!
void ProcessWithLogging(Logger logger)
{
logger.Log("Processing started"); // Logger.Log() が呼ばれる
}
ProcessWithLogging(new FileLogger()); // コンソールに出力される(ファイルではない)
この場合は基底クラスで virtual を宣言し、派生クラスで override するのが正しい設計です。
2. 基底クラスのメンバーを意識せず同じ名前を使ってしまった場合
コンパイラ警告 CS0108 が出たとき、まず new を付けるのではなく、なぜ名前が衝突しているのかを考える べきです。
- 派生クラスのメソッド名を変更すべきではないか?
- 基底クラスのメソッドを
overrideすべきではないか? - そもそも継承関係が正しいのか?
CS0108 警告を new で黙らせるのは「問題を隠す」行為になりかねません。
3. フィールドのシャドーイング
フィールドのシャドーイングは、基底クラスのフィールドと派生クラスのフィールドが 同時に存在 するため、値の不整合が起きやすくなります。
public class Base
{
public int Count = 0;
public void Increment()
{
Count++; // Base.Count をインクリメント
}
}
public class Derived : Base
{
public new int Count = 0; // Base.Count とは別のフィールド
public void Show()
{
Increment(); // Base.Count が 1 になる
Console.WriteLine(Count); // 0 ← Derived.Count は変わっていない!
Console.WriteLine(base.Count); // 1
}
}
new を付け忘れるとどうなるか
new を付けなくてもコードはコンパイルされ、実行できます。ただし CS0108 の 警告 が出ます。
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> を設定しているプロジェクトでは、この警告はコンパイルエラーに昇格します。意図的なシャドーイングなら new を付け、意図的でないなら設計を見直しましょう。
<!-- .csproj: 警告をエラーとして扱う設定 -->
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
base キーワードによる基底クラスの呼び出し
シャドーイングしたメソッドの中から、基底クラスの元のメソッドを呼ぶには base キーワードを使います。
public class ExtendedCircle : Circle
{
public new void Draw()
{
Console.WriteLine("ExtendedCircle: before draw");
base.Draw(); // Circle.Draw() を呼ぶ
Console.WriteLine("ExtendedCircle: after draw");
}
}
外部からは、前述のとおりキャストで基底クラスの実装にアクセスできます。
ExtendedCircle circle = new ExtendedCircle();
circle.Draw(); // ExtendedCircle の Draw()
((Circle)circle).Draw(); // Circle の Draw()
判断フロー: override か new か
メンバーの名前が基底クラスと衝突したとき、どちらを選ぶべきかの判断基準です。
基底クラスのメソッドは virtual / abstract / override か?
├── YES → override を使う(ポリモーフィズムが機能する)
│ │
│ そのメソッドの振る舞いを派生クラスで変えたいか?
│ ├── YES → override
│ └── NO → そもそも同じ名前を定義しない
│
└── NO(非 virtual)
│
基底クラスを変更できるか?
├── YES → 基底クラスに virtual を追加して override する
└── NO → new で明示的にシャドーイングする
(ただしポリモーフィズムが効かないことを理解したうえで)
まとめ
new 修飾子によるシャドーイングは、C# の継承メカニズムにおける 名前衝突の解決手段 です。
newはポリモーフィズムを提供しない: 呼び出されるメソッドは変数の型(コンパイル時の型)で決まるoverrideは実行時のポリモーフィズムを提供する: 呼び出されるメソッドはオブジェクトの型(実行時の型)で決まる- CS0108 警告が出たら、まず設計を見直す:
newを付ける前に、overrideすべきか、メソッド名を変更すべきか検討する - シャドーイングの正当な使いどころ: ライブラリの基底クラスが後のバージョンで名前を追加した場合の互換性維持
前回の記事で取り上げた sealed override は「オーバーライドチェーンを止める」ための機能でしたが、new はそもそもオーバーライドチェーンに 参加しない 別のメカニズムです。この 2 つを混同しないことが、C# の継承を正しく使いこなすための鍵です。