C#のsealedとは?継承を禁止する理由・使い方・overrideとの違いを初心者向けに解説
はじめに
C#でクラス設計を学んでいると、sealedというキーワードを見かけることがあります。
sealedは、簡単にいうと「これ以上、継承させない」「これ以上、オーバーライドさせない」ためのキーワードです。C#では、クラスを継承して機能を拡張したり、virtualやoverrideを使ってメソッドの動きを変更したりできます。しかし、すべてのクラスやメソッドが自由に継承・変更されてよいわけではありません。
たとえば、重要な処理を持つクラスや、設計上これ以上拡張されたくないクラスでは、意図しない継承によってバグやセキュリティ上の問題が発生することがあります。そのような場合に使うのがsealedです。
この記事では、C#のsealedについて、初心者にもわかりやすく解説します。sealed classの基本的な書き方、sealed overrideとoverrideの違い、virtual・abstract・newとの違い、実際に使うべき場面や注意点まで、順番に見ていきましょう。
1. C#のsealedとは?まずは意味を初心者向けに解説
1-1. sealedは「継承を禁止する」ためのキーワード
C#のsealedは、日本語では「封印された」「閉じられた」という意味を持つキーワードです。
C#のプログラムでは、あるクラスをもとにして新しいクラスを作る「継承」ができます。
C#class Animal
{
public void Eat()
{
Console.WriteLine("食べます");
}
}
class Dog : Animal
{
}
この例では、DogクラスがAnimalクラスを継承しています。
しかし、クラスにsealedを付けると、そのクラスは継承できなくなります。
C#sealed class Animal
{
}
このように書くと、Animalクラスを親クラスとして使うことはできません。つまり、sealedは「このクラスはここで完成しているので、これ以上継承しないでください」という設計上の意思表示になります。
1-2. sealedを使うと何ができなくなるのか
sealedを使うと、主に次のことができなくなります。
まず、sealedを付けたクラスは継承できません。
C#sealed class User
{
}
class AdminUser : User
{
}
このコードはコンパイルエラーになります。Userクラスがsealedで宣言されているため、AdminUserはUserを継承できません。
また、sealed overrideを使うと、派生クラスでさらにオーバーライドできなくなります。
C#class BaseClass
{
public virtual void Show()
{
}
}
class ChildClass : BaseClass
{
public sealed override void Show()
{
}
}
class GrandChildClass : ChildClass
{
public override void Show()
{
}
}
この場合、ChildClassでShowメソッドがsealed overrideされているため、GrandChildClassでは再度overrideできません。
つまり、sealedには大きく分けて次の2つの役割があります。
C#// クラスの継承を禁止する
sealed class MyClass
{
}
// メソッドの再オーバーライドを禁止する
public sealed override void MyMethod()
{
}
1-3. sealedが使える対象:クラス・メソッド・プロパティ
C#のsealedは、主に次の対象に使えます。
| 対象 | 使い方 | 意味 |
|---|---|---|
| クラス | sealed class MyClass | クラスの継承を禁止する |
| メソッド | sealed override void Method() | それ以上のオーバーライドを禁止する |
| プロパティ | sealed override string Name { get; } | それ以上のオーバーライドを禁止する |
クラスに使う場合は、単純にclassの前にsealedを付けます。
C#sealed class Sample
{
}
メソッドやプロパティに使う場合は、必ずoverrideと一緒に使います。
C#public sealed override void Execute()
{
}
重要なのは、通常のメソッドにいきなりsealedを付けることはできないという点です。sealedメソッドは、「すでにオーバーライドしたメソッドを、これ以上オーバーライドさせない」ために使います。
1-4. Javaのfinalとの違いを簡単に比較
Javaを知っている人であれば、C#のsealedはJavaのfinalに近いと考えると理解しやすいです。
Javaでは、クラスにfinalを付けると継承を禁止できます。
Javafinal class User {
}
C#では、同じような役割をsealedが持ちます。
C#sealed class User
{
}
ただし、Javaのfinalは変数にも使えます。たとえば、値を再代入できない変数を作るときにもfinalを使います。
一方、C#のsealedは変数の再代入禁止には使いません。C#で再代入できない値を表す場合は、readonlyやconstを使います。
| 言語 | キーワード | 主な意味 |
|---|---|---|
| C# | sealed | 継承や再オーバーライドを禁止する |
| Java | final | 継承禁止、オーバーライド禁止、再代入禁止など |
そのため、C#のsealedはJavaのfinalと似ていますが、完全に同じではありません。
2. sealedクラスの基本的な使い方
2-1. sealed classの書き方
sealed classの基本的な書き方はとてもシンプルです。
C#sealed class クラス名
{
}
具体例は次のとおりです。
C#sealed class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
public void ShowName()
{
Console.WriteLine(Name);
}
}
このUserクラスはsealedで宣言されているため、他のクラスから継承できません。
C#sealed class User
{
}
このように書くことで、「Userクラスはこれ以上派生クラスを作らせない」という意味になります。
2-2. sealedクラスを継承しようとした場合のエラー
sealedクラスを継承しようとすると、コンパイルエラーになります。
C#sealed class User
{
}
class AdminUser : User
{
}
このコードでは、AdminUserがUserを継承しようとしています。しかし、Userはsealedクラスなので継承できません。
エラーの内容は環境によって多少異なりますが、意味としては「sealed型から派生することはできない」という内容になります。
修正するには、次のどちらかを選びます。
C#// 方法1:継承をやめる
class AdminUser
{
}
// 方法2:基底クラス側のsealedを外す
class User
{
}
class AdminUser : User
{
}
ただし、sealedが付いているクラスは、設計上「継承させない」意図がある場合が多いです。そのため、外部ライブラリのsealedクラスを無理に継承しようとするのではなく、別の設計を考える必要があります。
2-3. sealedクラスのインスタンス化はできる
初心者が混乱しやすい点として、sealedクラスは継承できないだけで、インスタンス化はできます。
C#sealed class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
}
class Program
{
static void Main()
{
User user = new User("Taro");
Console.WriteLine(user.Name);
}
}
このコードは問題なく動作します。
sealedは「newできない」という意味ではありません。あくまで「継承できない」という意味です。
インスタンス化を禁止したい場合は、privateコンストラクターやstatic classなど、別の仕組みを使います。
2-4. sealedクラスのサンプルコード
次の例では、消費税計算を行うTaxCalculatorクラスをsealedにしています。
C#sealed class TaxCalculator
{
private const decimal TaxRate = 0.10m;
public decimal Calculate(decimal price)
{
return price + price * TaxRate;
}
}
class Program
{
static void Main()
{
TaxCalculator calculator = new TaxCalculator();
decimal result = calculator.Calculate(1000m);
Console.WriteLine(result);
}
}
このTaxCalculatorクラスは、外部から継承できません。
C#class CustomTaxCalculator : TaxCalculator
{
}
このような継承はできません。
消費税計算のように、決められたルールに従って処理したいクラスでは、意図しない継承によって処理内容が変わると困ることがあります。そのような場合、sealedを使うことでクラスの振る舞いを固定できます。
3. C#でsealedを使って継承を禁止する理由
3-1. クラスの設計意図を守るため
sealedを使う大きな理由のひとつは、クラスの設計意図を守るためです。
クラスには、それぞれ想定された使い方があります。あるクラスが「この機能だけを提供する完成したクラス」として作られている場合、継承によって勝手に動作を変更されると、設計者が意図していない使われ方をされる可能性があります。
たとえば、次のようなクラスを考えてみます。
C#sealed class EmailAddress
{
public string Value { get; }
public EmailAddress(string value)
{
if (!value.Contains("@"))
{
throw new ArgumentException("メールアドレスの形式が不正です。");
}
Value = value;
}
}
このクラスは、メールアドレスとして正しい値だけを扱うためのクラスです。もし継承が許可されていると、派生クラスによってバリデーションの意味が変わってしまう可能性があります。
sealedを付けることで、「このクラスはこの仕様で完結している」という設計意図を明確にできます。
3-2. 予期しない継承によるバグを防ぐため
継承は便利ですが、使い方を間違えるとバグの原因になります。
基底クラスの開発者が想定していない形で継承されると、内部状態が壊れたり、処理の順番が変わったりすることがあります。
C#class PaymentService
{
public virtual void Pay(int amount)
{
Validate(amount);
ExecutePayment(amount);
}
protected void Validate(int amount)
{
if (amount <= 0)
{
throw new ArgumentException("金額が不正です。");
}
}
protected void ExecutePayment(int amount)
{
Console.WriteLine($"{amount}円を支払いました。");
}
}
このクラスでPayメソッドが自由にオーバーライドされると、検証処理を通さずに支払い処理を実行する派生クラスが作られるかもしれません。
C#class CustomPaymentService : PaymentService
{
public override void Pay(int amount)
{
Console.WriteLine($"{amount}円を支払いました。");
}
}
このようなコードでは、本来必要な検証処理が抜けてしまいます。
クラスを継承させる必要がない場合は、最初からsealedにしておくことで、こうした予期しない拡張を防げます。
3-3. セキュリティや不変性を保つため
認証、権限、検証、暗号化、金額計算など、重要な処理を含むクラスでは、継承によって動作が変わることが問題になる場合があります。
たとえば、権限チェックを行うクラスが自由に継承できると、派生クラスでチェック処理を弱めることができてしまうかもしれません。
C#sealed class PermissionChecker
{
public bool CanAccess(string role)
{
return role == "Admin";
}
}
このようなクラスをsealedにしておけば、継承によってCanAccessの意味が変えられることを防げます。
また、不変オブジェクトでもsealedはよく使われます。不変オブジェクトとは、作成後に状態が変わらないオブジェクトのことです。
C#sealed class Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
このようなクラスでは、継承によって状態変更の仕組みを追加されると、不変性が崩れる可能性があります。sealedを付けることで、クラスの性質を保ちやすくなります。
3-4. パフォーマンス最適化につながる場合があるため
sealedは、場合によってはパフォーマンス最適化につながることがあります。
C#では、virtualメソッドやoverrideメソッドは、実行時にどのメソッドを呼び出すかを判断する仕組みがあります。これを動的ディスパッチと呼びます。
一方、クラスがsealedであれば、そのクラスを継承する派生クラスは存在しません。そのため、実行環境やコンパイラが「これ以上オーバーライドされない」と判断しやすくなります。
その結果、条件によってはメソッド呼び出しの最適化が行われることがあります。
ただし、sealedを付ければ必ず速くなるわけではありません。現代の.NET実行環境は多くの最適化を行っているため、通常のアプリケーションではsealedによる速度差を意識する場面は多くありません。
パフォーマンス目的だけでsealedを使うよりも、まずは設計上の意図を明確にする目的で使うのが基本です。
3-5. ライブラリやAPI設計で互換性を保ちやすくするため
ライブラリやAPIを公開する場合、クラスを継承可能にするかどうかは重要な設計判断です。
一度公開したクラスを多くの利用者が継承し始めると、後から内部実装を変更しにくくなります。基底クラスの変更が、利用者側の派生クラスに影響する可能性があるためです。
たとえば、公開クラスに新しいvirtualメソッドを追加したり、既存メソッドの呼び出し順序を変えたりすると、派生クラスの動作が壊れる可能性があります。
そのため、外部に公開するクラスでは、継承を前提として設計していない限り、sealedを使って継承を禁止することがあります。
C#public sealed class JsonSettings
{
public bool Indented { get; set; }
}
このようにしておくと、ライブラリ開発者は将来的に内部実装を変更しやすくなります。利用者にとっても、「このクラスは継承ではなく、そのまま使うものだ」と理解しやすくなります。
4. sealed overrideとは?overrideとの違いを解説
4-1. overrideは基底クラスのvirtualメンバーを上書きする
C#では、基底クラスのメソッドにvirtualを付けると、派生クラスでoverrideできます。
C#class Animal
{
public virtual void Speak()
{
Console.WriteLine("鳴きます");
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("ワン");
}
}
この例では、AnimalクラスのSpeakメソッドを、Dogクラスで上書きしています。
overrideを使うことで、基底クラスで定義された動作を、派生クラスに合わせて変更できます。
C#Animal animal = new Dog();
animal.Speak();
このコードを実行すると、DogクラスのSpeakメソッドが呼び出されます。
4-2. sealed overrideは「これ以上のオーバーライド」を禁止する
sealed overrideは、オーバーライドしたメソッドを、さらに派生クラスでオーバーライドできないようにするための書き方です。
C#class Animal
{
public virtual void Speak()
{
Console.WriteLine("鳴きます");
}
}
class Dog : Animal
{
public sealed override void Speak()
{
Console.WriteLine("ワン");
}
}
この場合、DogクラスではAnimalのSpeakメソッドをオーバーライドしています。しかし、sealedが付いているため、Dogをさらに継承したクラスではSpeakをオーバーライドできません。
C#class Poodle : Dog
{
public override void Speak()
{
Console.WriteLine("キャン");
}
}
このコードはコンパイルエラーになります。
つまり、overrideは「上書きする」、sealed overrideは「上書きするが、ここで打ち止めにする」という意味です。
4-3. sealed overrideを使う条件
sealed overrideを使うには、次の条件を満たす必要があります。
まず、基底クラス側にvirtual、abstract、またはoverrideされたメンバーが必要です。
C#class BaseClass
{
public virtual void Execute()
{
}
}
次に、派生クラスでoverrideします。
C#class ChildClass : BaseClass
{
public sealed override void Execute()
{
}
}
sealedは、単独で通常のメソッドに付けることはできません。
C#class Sample
{
public sealed void Execute()
{
}
}
このコードはエラーになります。
正しくは、次のようにoverrideと一緒に使います。
C#class ChildClass : BaseClass
{
public sealed override void Execute()
{
}
}
4-4. sealed overrideのサンプルコード
次の例では、ログ出力処理を例にしてsealed overrideを使っています。
C#class Logger
{
public virtual void Write(string message)
{
Console.WriteLine(message);
}
}
class FileLogger : Logger
{
public sealed override void Write(string message)
{
Console.WriteLine($"File: {message}");
}
}
class CustomFileLogger : FileLogger
{
public override void Write(string message)
{
Console.WriteLine($"Custom: {message}");
}
}
このコードでは、FileLoggerのWriteメソッドがsealed overrideになっています。そのため、CustomFileLoggerでWriteを再度オーバーライドすることはできません。
修正する場合は、次のようにします。
C#class CustomFileLogger : FileLogger
{
public void WriteCustom(string message)
{
Console.WriteLine($"Custom: {message}");
}
}
または、そもそも再オーバーライドを許可したいのであれば、FileLogger側のsealedを外します。
C#class FileLogger : Logger
{
public override void Write(string message)
{
Console.WriteLine($"File: {message}");
}
}
4-5. sealedメソッド単体では使えない理由
C#では、通常のメソッドにsealedだけを付けることはできません。
C#class Sample
{
public sealed void Run()
{
}
}
このコードはエラーになります。
理由は、sealedメソッドの目的が「これ以上のオーバーライドを禁止すること」だからです。
通常のメソッドは、そもそもvirtualでなければオーバーライドできません。
C#class Sample
{
public void Run()
{
}
}
このRunメソッドはvirtualではないため、派生クラスでoverrideできません。つまり、すでにオーバーライド不可です。
そのため、通常のメソッドにsealedを付ける必要がありません。
sealedをメソッドに付ける意味があるのは、すでにオーバーライドされたメソッドを、さらに派生クラスでオーバーライドさせたくない場合だけです。
C#class BaseClass
{
public virtual void Run()
{
}
}
class ChildClass : BaseClass
{
public sealed override void Run()
{
}
}
5. sealed・virtual・override・abstract・newの違い
5-1. virtual:派生クラスで上書き可能にする
virtualは、派生クラスでメソッドやプロパティを上書きできるようにするキーワードです。
C#class Animal
{
public virtual void Speak()
{
Console.WriteLine("鳴きます");
}
}
このようにvirtualを付けると、派生クラスでoverrideできます。
C#class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("ニャー");
}
}
virtualは「必要であれば、派生クラスで動きを変えてよい」という意味になります。
5-2. override:virtualやabstractを上書きする
overrideは、基底クラスのvirtualやabstractメンバーを上書きするときに使います。
C#class Animal
{
public virtual void Speak()
{
Console.WriteLine("鳴きます");
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("ワン");
}
}
overrideを使うことで、基底クラスのメソッド呼び出しを、派生クラス側の実装に差し替えることができます。
また、abstractメソッドを実装するときにもoverrideを使います。
C#abstract class Animal
{
public abstract void Speak();
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("ワン");
}
}
5-3. sealed:継承や再オーバーライドを禁止する
sealedは、クラスに付ける場合と、overrideメンバーに付ける場合で意味が少し変わります。
クラスに付ける場合は、継承を禁止します。
C#sealed class User
{
}
メソッドやプロパティに付ける場合は、再オーバーライドを禁止します。
C#class BaseClass
{
public virtual void Run()
{
}
}
class ChildClass : BaseClass
{
public sealed override void Run()
{
}
}
つまり、sealedは「これ以上、派生方向に変更させない」ためのキーワードです。
5-4. abstract:継承と実装を強制する
abstractは、クラスやメソッドを抽象化するためのキーワードです。
abstract classは、そのままインスタンス化できません。派生クラスを作って使うことが前提になります。
C#abstract class Animal
{
public abstract void Speak();
}
abstractメソッドには本体を書きません。
C#public abstract void Speak();
派生クラスでは、必ず実装する必要があります。
C#class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("ワン");
}
}
abstractは「継承して実装してください」という意味です。一方、sealedは「継承しないでください」という意味です。
そのため、abstractとsealedは正反対の考え方を持っています。
5-5. new:オーバーライドではなく隠蔽する
newは、基底クラスのメンバーを「オーバーライド」するのではなく「隠蔽」するときに使います。
C#class BaseClass
{
public void Show()
{
Console.WriteLine("Base");
}
}
class ChildClass : BaseClass
{
public new void Show()
{
Console.WriteLine("Child");
}
}
この場合、ChildClassのShowは、BaseClassのShowを上書きしているわけではありません。同じ名前の別メソッドで隠しているだけです。
違いは、変数の型によって呼び出されるメソッドが変わる点です。
C#ChildClass child = new ChildClass();
child.Show(); // Child
BaseClass baseObj = child;
baseObj.Show(); // Base
一方、overrideの場合は、変数の型が基底クラスでも派生クラスのメソッドが呼び出されます。
C#class BaseClass
{
public virtual void Show()
{
Console.WriteLine("Base");
}
}
class ChildClass : BaseClass
{
public override void Show()
{
Console.WriteLine("Child");
}
}
C#BaseClass obj = new ChildClass();
obj.Show(); // Child
newとoverrideは似ていますが、動作は大きく違うので注意しましょう。
5-6. 修飾子の違いがわかる比較表
| キーワード | 主な意味 | 使う場所 | 初心者向けのイメージ |
|---|---|---|---|
virtual | 派生クラスで上書き可能にする | メソッド、プロパティなど | 上書きしてもよい |
override | 基底クラスのメンバーを上書きする | メソッド、プロパティなど | 実際に上書きする |
sealed | 継承や再オーバーライドを禁止する | クラス、overrideメンバー | ここで打ち止め |
abstract | 継承先で実装を強制する | クラス、メソッドなど | 必ず実装してね |
new | 基底クラスのメンバーを隠蔽する | メソッド、プロパティなど | 同名の別物として隠す |
初心者のうちは、次のように覚えるとわかりやすいです。
C#virtual = 上書きしてよい
override = 上書きする
sealed = これ以上上書きさせない
abstract = 派生先で必ず作らせる
new = 上書きではなく隠す
6. sealedを使う具体的なケース
6-1. 継承させる必要がないユーティリティクラス
ユーティリティ的な処理をまとめたクラスで、継承させる必要がない場合はsealedが使われることがあります。
C#sealed class FileNameFormatter
{
public string Format(string fileName)
{
return fileName.Trim().ToLower();
}
}
このクラスは、ファイル名を整形するだけのシンプルなクラスです。派生クラスを作って拡張する必要がないのであれば、sealedにしておくことで設計意図が明確になります。
ただし、完全なユーティリティクラスでインスタンス化も不要な場合は、static classを使う選択肢もあります。
C#static class FileNameFormatter
{
public static string Format(string fileName)
{
return fileName.Trim().ToLower();
}
}
sealedとstaticは似ている部分もありますが、目的は異なります。インスタンスを作って使うクラスならsealed class、インスタンス自体が不要ならstatic classを検討するとよいでしょう。
6-2. 値オブジェクトや不変オブジェクト
値オブジェクトや不変オブジェクトでは、sealedがよく使われます。
値オブジェクトとは、IDではなく値そのものに意味があるオブジェクトです。たとえば、金額、住所、メールアドレス、電話番号などが該当します。
C#sealed class PhoneNumber
{
public string Value { get; }
public PhoneNumber(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("電話番号が空です。");
}
Value = value;
}
}
このようなクラスでは、「作成後に状態が変わらない」「値の意味が変わらない」ことが重要です。
継承を許可すると、派生クラスによって別の状態や挙動が追加され、不変性や値としての意味が崩れることがあります。sealedを使うことで、クラスの性質を固定しやすくなります。
6-3. 認証・権限・検証ロジックを含むクラス
認証、権限、検証ロジックを含むクラスでは、意図しない継承を避けたい場合があります。
C#sealed class LoginValidator
{
public bool IsValid(string userName, string password)
{
if (string.IsNullOrWhiteSpace(userName))
{
return false;
}
if (string.IsNullOrWhiteSpace(password))
{
return false;
}
return password.Length >= 8;
}
}
このようなクラスが自由に継承されると、派生クラスで検証ルールが弱められる可能性があります。
もちろん、拡張可能な認証方式を設計したい場合は、インターフェースや抽象クラスを使うべきです。しかし、「この検証ルールは固定したい」という場合には、sealedが有効です。
6-4. フレームワークやライブラリの公開クラス
フレームワークやライブラリを作る場合、公開するクラスを継承可能にするかどうかは慎重に決める必要があります。
継承可能なクラスは、外部の開発者が派生クラスを作れるため柔軟です。しかし、その分、ライブラリ側の変更が難しくなります。
C#public sealed class ApiResponse
{
public int StatusCode { get; }
public string Body { get; }
public ApiResponse(int statusCode, string body)
{
StatusCode = statusCode;
Body = body;
}
}
このようにsealedにしておけば、利用者は継承ではなく、インスタンスとしてこのクラスを使うことになります。
ライブラリ設計では、「継承させるために設計していないクラス」はsealedにするという考え方があります。逆に、拡張ポイントとして提供したい場合は、abstractクラスやインターフェースを用意するのが一般的です。
6-5. Equals・GetHashCode・ToStringの挙動を固定したい場合
C#では、Equals、GetHashCode、ToStringなどのメソッドをオーバーライドできます。
C#sealed class ProductCode
{
public string Value { get; }
public ProductCode(string value)
{
Value = value;
}
public override bool Equals(object? obj)
{
return obj is ProductCode other && Value == other.Value;
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public override string ToString()
{
return Value;
}
}
このようなクラスでは、等価性の判断が重要です。
もし継承を許可すると、派生クラスでEqualsやGetHashCodeの挙動が変わり、コレクション内での比較や辞書のキーとしての動作に影響する可能性があります。
sealedにしておけば、等価性のルールを固定しやすくなります。
7. sealedを使うメリット・デメリット
7-1. メリット:設計が明確になる
sealedを使う最大のメリットは、設計が明確になることです。
クラスにsealedが付いていれば、そのクラスは継承して使うものではないとすぐにわかります。
C#sealed class UserId
{
public string Value { get; }
public UserId(string value)
{
Value = value;
}
}
このコードを見た開発者は、「UserIdは値として使うクラスであり、派生クラスを作ることは想定されていない」と理解できます。
継承可能なクラスなのか、単体で完結するクラスなのかが明確になるため、チーム開発でも意図が伝わりやすくなります。
7-2. メリット:予期しない拡張を防げる
sealedを付けると、他のクラスから継承できなくなります。
これにより、想定外の派生クラスが作られることを防げます。
C#sealed class DiscountCalculator
{
public decimal Calculate(decimal price)
{
return price * 0.9m;
}
}
割引計算のように、業務ルールとして処理内容を固定したい場合、継承によって処理が変わると問題になることがあります。
sealedを使えば、「この計算ロジックは勝手に変えないでください」という意思をコード上で表現できます。
7-3. メリット:保守性が高まる
sealedクラスは継承されないため、影響範囲を読みやすくなります。
継承可能なクラスを修正する場合、派生クラスへの影響を考える必要があります。しかし、sealedクラスであれば派生クラスが存在しないため、修正時に考慮すべき範囲が比較的狭くなります。
C#sealed class ReportFormatter
{
public string Format(string title, string body)
{
return $"{title}\n{body}";
}
}
このクラスを修正するとき、継承先のクラスが壊れる心配はありません。
そのため、内部実装の変更やリファクタリングをしやすくなります。
7-4. デメリット:拡張性が下がる
sealedのデメリットは、拡張性が下がることです。
一度sealedにすると、そのクラスを継承して機能を追加できません。
C#sealed class CsvExporter
{
public void Export()
{
Console.WriteLine("CSVを出力します");
}
}
後から「CSV出力をカスタマイズしたい」と思っても、継承による拡張はできません。
もちろん、継承以外の方法で拡張することはできます。たとえば、インターフェースを使ったり、別クラスに処理を委譲したりする方法があります。
C#interface IExporter
{
void Export();
}
拡張が必要になりそうなクラスでは、最初からsealedにするかどうか慎重に判断しましょう。
7-5. デメリット:テストやモック化が難しくなる場合がある
sealedクラスは継承できないため、テストでモックを作りにくくなる場合があります。
たとえば、次のようなクラスがあるとします。
C#sealed class PaymentGateway
{
public bool Pay(int amount)
{
return true;
}
}
このクラスを継承してテスト用の偽物クラスを作ることはできません。
C#class FakePaymentGateway : PaymentGateway
{
}
これはエラーになります。
テストしやすい設計にしたい場合は、インターフェースを使うのが一般的です。
C#interface IPaymentGateway
{
bool Pay(int amount);
}
sealed class PaymentGateway : IPaymentGateway
{
public bool Pay(int amount)
{
return true;
}
}
このようにしておけば、実装クラスはsealedにしつつ、テストではIPaymentGatewayを差し替えられます。
7-6. sealedを使うべきか判断するポイント
sealedを使うべきか迷ったときは、次のポイントで判断するとよいです。
| 判断ポイント | sealedが向いているか |
|---|---|
| 継承させる予定がない | 向いている |
| クラスの振る舞いを固定したい | 向いている |
| 値オブジェクトとして使う | 向いている |
| セキュリティや検証ロジックを含む | 向いている |
| 将来、派生クラスで拡張したい | 向いていない |
| テストで継承して差し替えたい | 注意が必要 |
| フレームワークの拡張ポイントにしたい | 向いていない |
基本的には、「継承を前提として設計していないならsealedを検討する」と考えるとよいでしょう。
ただし、何でもsealedにするのではなく、将来の拡張性やテストのしやすさも考慮することが大切です。
8. sealedを使うときの注意点
8-1. abstractとsealedは同時に使えない
abstractとsealedは、同じクラスに同時に付けることはできません。
C#abstract sealed class Sample
{
}
このコードはエラーになります。
理由は、abstractとsealedの意味が矛盾しているからです。
abstractは「継承して使ってください」という意味です。一方、sealedは「継承しないでください」という意味です。
つまり、次のように正反対の性質を持っています。
C#abstract class Animal
{
public abstract void Speak();
}
C#sealed class User
{
}
ただし、static classは内部的には継承できない特別なクラスとして扱われます。ユーティリティクラスを作りたい場合は、abstract sealedではなくstatic classを使います。
C#static class MathUtility
{
public static int Add(int a, int b)
{
return a + b;
}
}
8-2. structは暗黙的にsealed扱いになる
C#のstructは、クラスのように継承できません。
C#struct Point
{
public int X { get; }
public int Y { get; }
}
structは値型であり、他のstructやclassから継承することはできません。そのため、構造体は暗黙的にsealedのような性質を持っています。
次のように、structを継承することはできません。
C#struct Point3D : Point
{
}
このコードはエラーになります。
また、structに明示的にsealedを付けることもできません。
C#sealed struct Point
{
}
これもエラーです。
structはもともと継承できないため、sealedを付ける必要がありません。
8-3. 継承を前提にした設計ではsealedを避ける
クラスを拡張ポイントとして提供したい場合、sealedは避けるべきです。
たとえば、次のような基底クラスを用意して、派生クラスごとに処理を変えたい場合があります。
C#abstract class NotificationSender
{
public abstract void Send(string message);
}
class EmailSender : NotificationSender
{
public override void Send(string message)
{
Console.WriteLine($"メール送信: {message}");
}
}
class SmsSender : NotificationSender
{
public override void Send(string message)
{
Console.WriteLine($"SMS送信: {message}");
}
}
このような設計では、NotificationSenderをsealedにしてはいけません。
C#sealed class NotificationSender
{
}
これでは継承できなくなり、設計の目的と矛盾します。
継承を使って多態性を実現したい場合は、abstractやvirtualを使い、sealedは必要な箇所だけに限定しましょう。
8-4. 何でもsealedにすればよいわけではない
sealedにはメリットがありますが、すべてのクラスに付ければよいわけではありません。
sealedを付けると、将来的に継承による拡張ができなくなります。
特に、アプリケーションの中で拡張される可能性があるクラスや、テストで差し替えたいクラスにsealedを付けると、不便になる場合があります。
C#sealed class MailSender
{
public void Send(string message)
{
Console.WriteLine(message);
}
}
このクラスを後からテスト用に差し替えたい場合、継承できないことが問題になるかもしれません。
その場合は、インターフェースを用意する設計が有効です。
C#interface IMailSender
{
void Send(string message);
}
sealed class MailSender : IMailSender
{
public void Send(string message)
{
Console.WriteLine(message);
}
}
このように、実装クラスはsealedにしつつ、外部からはインターフェース経由で扱う設計にすると、保守性とテストしやすさを両立できます。
8-5. 公開APIでは将来の拡張性も考慮する
公開APIでsealedを使う場合は、将来の拡張性を考える必要があります。
一度sealedにしたクラスは、利用者が継承できません。将来的に「このクラスを継承して拡張したい」という要望が出る可能性がある場合は、慎重に判断しましょう。
ただし、逆に最初から継承可能にしてしまうと、後でsealedに変更するのは難しくなります。既に利用者が継承している可能性があるためです。
そのため、ライブラリ設計では次のように考えるとよいでしょう。
| 設計方針 | 選択肢 |
|---|---|
| 継承させる予定がない | sealedを検討する |
| 拡張ポイントとして使わせたい | abstractやvirtualを使う |
| 実装は固定しつつ差し替え可能にしたい | インターフェースを用意する |
| 将来の仕様変更に備えたい | 不要な継承は許可しない |
公開APIでは、sealedを付けるかどうかが利用者の設計にも影響します。単に好みで決めるのではなく、クラスの役割を明確にして判断することが重要です。
9. sealedのよくあるエラーと対処法
9-1. sealedクラスを継承しようとしている
もっともよくあるエラーは、sealedクラスを継承しようとするケースです。
C#sealed class BaseClass
{
}
class ChildClass : BaseClass
{
}
このコードはコンパイルエラーになります。
原因は、BaseClassがsealedで宣言されているためです。
対処法は、継承をやめることです。
C#class ChildClass
{
private readonly BaseClass _baseClass;
public ChildClass(BaseClass baseClass)
{
_baseClass = baseClass;
}
}
このように、継承ではなく、クラスを内部に持つ「合成」を使う方法があります。
自分で作ったクラスであれば、継承を許可したい場合に限ってsealedを外します。
C#class BaseClass
{
}
class ChildClass : BaseClass
{
}
9-2. sealed overrideされたメソッドをさらにoverrideしている
次によくあるのが、sealed overrideされたメソッドをさらにオーバーライドしようとするケースです。
C#class BaseClass
{
public virtual void Run()
{
}
}
class MiddleClass : BaseClass
{
public sealed override void Run()
{
}
}
class ChildClass : MiddleClass
{
public override void Run()
{
}
}
このコードはエラーになります。
MiddleClassでRunメソッドがsealed overrideされているため、ChildClassでは再度オーバーライドできません。
対処法としては、別名のメソッドを作るか、設計を見直します。
C#class ChildClass : MiddleClass
{
public void RunCustom()
{
}
}
自分がMiddleClassを管理している場合で、再オーバーライドを許可したいなら、sealedを外します。
C#class MiddleClass : BaseClass
{
public override void Run()
{
}
}
9-3. sealedとabstractを同時に指定している
sealedとabstractを同時に指定するとエラーになります。
C#abstract sealed class Sample
{
}
abstractは継承を前提にするキーワードで、sealedは継承を禁止するキーワードです。そのため、同時に使うことはできません。
ユーティリティクラスを作りたい場合は、static classを使いましょう。
C#static class Sample
{
public static void Run()
{
}
}
抽象クラスとして派生クラスに実装を強制したい場合は、abstractだけを使います。
C#abstract class Sample
{
public abstract void Run();
}
9-4. overrideなしでメソッドにsealedを指定している
通常のメソッドにsealedだけを付けるとエラーになります。
C#class Sample
{
public sealed void Run()
{
}
}
sealedメソッドは、overrideと一緒に使う必要があります。
正しい書き方は次のとおりです。
C#class BaseClass
{
public virtual void Run()
{
}
}
class ChildClass : BaseClass
{
public sealed override void Run()
{
}
}
通常のメソッドは、virtualでなければもともとオーバーライドできません。そのため、通常メソッドにsealedを付ける必要はありません。
C#class Sample
{
public void Run()
{
}
}
このRunメソッドは、派生クラスでoverrideできないため、sealedを付けなくても同じ目的を達成できます。
9-5. エラーメッセージから原因を見つける方法
sealedに関するエラーが出たときは、次の順番で確認すると原因を見つけやすくなります。
まず、継承しているクラスがsealedではないか確認します。
C#class Child : Parent
{
}
この場合、Parentの定義を見ます。
C#sealed class Parent
{
}
Parentがsealedなら、継承できないことが原因です。
次に、overrideしようとしているメソッドがsealed overrideされていないか確認します。
C#public override void Run()
{
}
基底クラス側に次のような定義があれば、再オーバーライドできません。
C#public sealed override void Run()
{
}
最後に、sealedを付けている場所が正しいか確認します。
C#public sealed void Run()
{
}
このようにoverrideなしでメソッドにsealedを付けている場合はエラーです。
エラーが出たら、「クラスに付けたsealedなのか」「メソッドに付けたsealed overrideなのか」を切り分けると理解しやすくなります。
10. sealedに関するよくある質問
10-1. sealedクラスはインスタンス化できますか?
はい、sealedクラスはインスタンス化できます。
C#sealed class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
}
class Program
{
static void Main()
{
User user = new User("Taro");
Console.WriteLine(user.Name);
}
}
sealedは「継承できない」という意味であり、「インスタンス化できない」という意味ではありません。
インスタンス化を禁止したい場合は、privateコンストラクターやstatic classなどを使います。
10-2. sealedクラスの中にvirtualメソッドは書けますか?
基本的に、sealedクラスの中で新しくvirtualメソッドを宣言する意味はありません。
C#sealed class Sample
{
public virtual void Run()
{
}
}
sealedクラスは継承できないため、派生クラスでRunをoverrideすることができません。そのため、virtualにしても実用上の意味がありません。
ただし、基底クラスから継承したvirtualメソッドをoverrideすることはできます。
C#class BaseClass
{
public virtual void Run()
{
}
}
sealed class ChildClass : BaseClass
{
public override void Run()
{
}
}
この場合、ChildClass自体がsealedなので、さらに派生クラスを作ることはできません。
10-3. sealedを付けるとパフォーマンスは上がりますか?
場合によっては、sealedがパフォーマンス最適化につながることがあります。
sealedクラスは継承されないため、実行環境がメソッド呼び出しを最適化しやすくなる場合があります。特に、仮想メソッド呼び出しに関して、これ以上派生クラスで上書きされないことがわかると、最適化の余地が生まれます。
ただし、sealedを付けたからといって、必ず体感できるほど速くなるわけではありません。
通常は、パフォーマンス目的だけでsealedを付けるのではなく、設計上の理由を優先して使うべきです。
C#sealed class PriceCalculator
{
public decimal Calculate(decimal price)
{
return price * 1.1m;
}
}
このように、継承させる必要がないクラスにsealedを付けた結果として、最適化される可能性がある、という程度に考えるとよいでしょう。
10-4. sealedとstaticの違いは何ですか?
sealedとstaticは、どちらも継承に関係するキーワードですが、目的が異なります。
sealed classは、インスタンス化できますが、継承できません。
C#sealed class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
}
C#User user = new User("Taro");
一方、static classはインスタンス化できず、継承もできません。
C#static class MathUtility
{
public static int Add(int a, int b)
{
return a + b;
}
}
C#int result = MathUtility.Add(1, 2);
違いを表にすると次のようになります。
| キーワード | インスタンス化 | 継承 | 主な用途 |
|---|---|---|---|
sealed class | できる | できない | 完成した通常クラス |
static class | できない | できない | 共通関数・ユーティリティ |
インスタンスごとに状態を持たせたいならsealed class、状態を持たず共通処理だけをまとめたいならstatic classが向いています。
10-5. sealedは初心者でも使うべきですか?
初心者でも、sealedの意味は理解しておくべきです。
特に、次のようなコードを読めるようになることが重要です。
C#sealed class User
{
}
C#public sealed override void Run()
{
}
ただし、初心者のうちは、すべてのクラスに積極的にsealedを付ける必要はありません。
まずは、次のように考えるとよいでしょう。
| 状況 | 判断 |
|---|---|
| 継承させる予定がない | sealedを検討する |
| 値オブジェクトとして使う | sealedが向いている |
| 拡張ポイントにしたい | sealedは避ける |
| テストで差し替えたい | インターフェースも検討する |
| 意味がよくわからない | 無理に付けない |
sealedは、クラス設計の意図を明確にするためのキーワードです。使いどころを理解していれば、読みやすく保守しやすいコードを書く助けになります。
まとめ
C#のsealedは、クラスの継承やメソッドの再オーバーライドを禁止するためのキーワードです。
クラスにsealedを付けると、そのクラスは継承できなくなります。
C#sealed class User
{
}
ただし、インスタンス化はできます。
C#User user = new User();
また、sealed overrideを使うと、基底クラスのvirtualやabstractメンバーをオーバーライドしつつ、それ以上の再オーバーライドを禁止できます。
C#class BaseClass
{
public virtual void Run()
{
}
}
class ChildClass : BaseClass
{
public sealed override void Run()
{
}
}
sealedを使う主な理由は、クラスの設計意図を守るため、予期しない継承によるバグを防ぐため、セキュリティや不変性を保つため、ライブラリやAPIの互換性を維持しやすくするためです。
一方で、sealedを付けると継承による拡張ができなくなるため、拡張性は下がります。テストやモック化が難しくなる場合もあるため、必要に応じてインターフェースを使う設計も検討しましょう。
virtualは「上書きしてよい」、overrideは「上書きする」、sealedは「これ以上上書きさせない」、abstractは「派生先で実装させる」、newは「上書きではなく隠す」と覚えると理解しやすくなります。
csharp sealedを学ぶうえで大切なのは、単に構文を覚えることではなく、「なぜ継承を禁止したいのか」を理解することです。継承を許可するべきクラスなのか、単体で完結させるべきクラスなのかを考えながら、適切な場面でsealedを使いましょう。

