Sealed クラス
CoddyのC#ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 22/70。
時には、継承チェーンを停止させたい場合があります。sealed キーワードは、クラスが継承されるのを防ぎ、その実装を固定(ロック)して、他のクラスがそれを拡張できないようにします。
public sealed class FinalDog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof!");
}
}
// これはコンパイルエラーを引き起こします:
// public class SuperDog : FinalDog { }なぜクラスをシールするのでしょうか?セキュリティと設計の整合性が一般的な理由です。クラスを慎重に設計し、継承によってその動作を誰にも変更されたくない場合、クラスをシールすることで、実装がそのままの状態に保たれることが保証されます。
クラス全体ではなく、個別のメソッドをシールすることもできます。これにより、クラス自体の継承は許可したまま、それ以上のオーバーライドを禁止することができます。
public class Dog : Animal
{
public sealed override void Speak()
{
Console.WriteLine("Woof!");
}
}
public class Puppy : Dog
{
// Speak() をオーバーライドできません - sealed 指定されています
// しかし、新しいメソッドを追加することは可能です
public void Whimper()
{
Console.WriteLine("Whimper...");
}
}メソッドにおける sealed は override と組み合わせる必要があることに注意してください。親クラスですでに virtual であったメソッドのみを seal することができます。これにより、継承階層内でのある程度のカスタマイズを許可し、その後特定のポイントでそれを固定するという、正確な制御が可能になります。
チャレンジ
簡単sealed を使用して重要な実装をロックダウンする方法を示す、決済処理システムを構築しましょう。特定の決済方法は拡張可能ですが、最も安全なものは変更を防ぐために封印(sealed)されるという階層構造を作成します。
コードを整理するために 4 つのファイルを作成します:
PaymentMethod.cs:Payments名前空間にPaymentMethodクラスを定義します。この基底クラスはNameプロパティ(string)と、それを設定するコンストラクタを持ちます。"Processing ${amount} via {Name}"を返すvirtualメソッドProcessPayment(decimal amount)を含めてください。CreditCardPayment.cs:PaymentMethodを継承するCreditCardPaymentクラスをPayments名前空間に定義します。CardNumberプロパティ(string)を追加します。コンストラクタは名前とカード番号を受け取り、baseを使用して名前を渡します。ProcessPaymentをsealedキーワードでオーバーライドし、これ以降の派生クラスがこの動作を変更できないようにします。このメソッドは"Charging ${amount} to card ending in {last 4 digits of CardNumber}"を返す必要があります。BankTransfer.cs:PaymentMethodを継承するBankTransferというsealedクラスをPayments名前空間に定義します。このクラス全体が封印されており、誰も継承することはできません。BankNameプロパティ(string)を追加します。コンストラクタは名前と銀行名を受け取ります。ProcessPaymentをオーバーライドして"Transferring ${amount} from {BankName}"を返すようにします。Program.cs: メインファイルで、入力値を使用して 3 つの決済タイプすべてのインスタンスを作成します。それらをPaymentMethod参照として保存し、それぞれでProcessPaymentを呼び出して、封印された実装によるポリモーフィズム(多態性)を確認します。
3 つの入力を受け取ります:
- クレジットカード番号(16桁)
- 銀行名
- 決済金額
各メソッドを通じて決済を処理した結果を出力してください:
{base PaymentMethod result}
{CreditCardPayment result}
{BankTransfer result}例えば、入力が 1234567890123456、First National、150.00 の場合、出力は以下のようになります:
Processing $150.00 via Generic
Charging $150.00 to card ending in 3456
Transferring $150.00 from First National基底の PaymentMethod インスタンスの名前には "Generic" を、クレジットカード決済には "Credit Card" を、銀行振込には "Bank Transfer" を使用してください。
CreditCardPayment の封印されたメソッドは、誰かがサブクラスを作成したとしても、クレジットカード決済の処理方法を変更できないことを保証します。封印されたクラス BankTransfer は継承自体を完全に防ぎます。これらはどちらも決済システムにおける重要なセキュリティパターンです!
チートシート
sealed キーワードは、クラスが継承されることや、メソッドが継承チェーンの先でさらにオーバーライドされることを防ぎます。
クラスの封印:
public sealed class FinalDog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof!");
}
}
// これはコンパイルエラーを引き起こします:
// public class SuperDog : FinalDog { }封印されたクラスは、セキュリティや設計の整合性のために使用され、継承を通じて実装が変更されないことを保証します。
メソッドの封印:
public class Dog : Animal
{
public sealed override void Speak()
{
Console.WriteLine("Woof!");
}
}
public class Puppy : Dog
{
// Speak() をオーバーライドできません - 封印されています
// しかし、新しいメソッドを追加することはできます
public void Whimper()
{
Console.WriteLine("Whimper...");
}
}メソッドにおける sealed キーワードは、override と組み合わせる必要があります。親クラスですでに virtual であったメソッドのみを封印できます。これにより、階層内の特定のポイントまではカスタマイズを許可し、その後はロックすることができます。
自分で試してみよう
using System;
using Payments;
class Program
{
public static void Main(string[] args)
{
// 入力を読み込む
string cardNumber = Console.ReadLine();
string bankName = Console.ReadLine();
decimal amount = Convert.ToDecimal(Console.ReadLine());
// TODO: 名前を "Generic" としたベースの PaymentMethod インスタンスを作成する
// それを PaymentMethod の参照として保存する
// TODO: 名前を "Credit Card" とし、カード番号を指定した CreditCardPayment インスタンスを作成する
// それを PaymentMethod の参照として保存する
// TODO: 名前を "Bank Transfer" とし、銀行名を指定した BankTransfer インスタンスを作成する
// それを PaymentMethod の参照として保存する
// TODO: それぞれに対して ProcessPayment を呼び出し、結果を出力する
// 各結果は個別の行に出力される必要がある
}
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。