1. 抽象メソッド
前回のレクチャーで基礎はやったよね:抽象クラスに触れて、抽象メソッドやプロパティが入れられるって知ったはず。これは「契約」みたいなもので、継承するクラスは必ず自分なりの実装を用意しなきゃいけないってこと。
じゃあ、もうちょい深掘りして、実践的なシナリオを見てみよう。このレクチャーでは、構文の細かいところや、多段階の階層で抽象化がどう動くか、abstractとvirtualをどう使い分けるかをハッキリさせるよ。
抽象メソッドって、昔の料理本にある謎レシピみたいなもん:「秘密の材料を加えよ」って書いてあるけど、何を入れるかは後の料理人次第!
抽象メソッドは必ず抽象クラスの中にある
普通(非抽象)のクラスで抽象メソッドを宣言しようとすると、コンパイラエラーになるよ。なぜかって?普通のクラスは直接インスタンス化できるから、未実装のメソッドを呼ぼうとしたらどうなる?IDEが「え?」って顔するだけ。
// これはコンパイルエラーになるよ:
public class WrongClass
{
public abstract void Oops(); // こういうのはダメ!
}
クラスに1つでも抽象メソッドがあったら、そのクラス自体もabstractでマークしなきゃダメ!
派生クラスでの抽象メソッドの実装
実装はoverrideキーワードでやるよ。抽象クラスを継承して、抽象メソッドを全部実装しないクラスも、やっぱり抽象クラスじゃないとダメ(じゃないとコンパイラが怒る)。
アプリのアイデアを続けよう:
前のレクチャーでShape(図形)クラスを作って、面積計算用の抽象メソッドを宣言したよね。じゃあ、今度は具体的な図形を作ってみよう:
public class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double CalculateArea()
{
return Width * Height;
}
}
public class Circle : Shape
{
public double Radius { get; }
public Circle(double radius)
{
Radius = radius;
}
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
なぜ抽象メソッドがそんなに大事なの?
抽象メソッドは「このクラスを継承するなら、どう動くか自分で決めてね!」っていう宣言。これでプログラムの設計がスッキリするし、うっかり実装忘れも防げるし、何よりポリモーフィズムをフル活用できる!
2. 抽象プロパティ(properties)
抽象プロパティって何?
抽象プロパティも抽象メソッドと同じく「契約」だよ、ただしプロパティ(property)用。C#ではプロパティはデータをカプセル化するメイン手段だから、抽象プロパティは「値の取得(や設定)は子孫に任せるよ」って意味。
例:
図形にNameプロパティを追加しよう。全ての子孫で必須だけど、何を返すかは各自で決めてOK。
public abstract class Shape
{
public abstract string Name { get; }
public abstract double CalculateArea();
}
これで、各子孫はこのプロパティを必ず実装しなきゃいけない:
public class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public override string Name => "長方形";
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double CalculateArea()
{
return Width * Height;
}
}
public class Circle : Shape
{
public double Radius { get; }
public override string Name => "円";
public Circle(double radius)
{
Radius = radius;
}
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
構文の特徴
抽象プロパティはインターフェースのプロパティに似てる:本体(実装)は無しで、getterだけか、setterも付けるかを指定できる。
public abstract class Creature
{
// 読み取り専用プロパティ
public abstract string Species { get; }
// 読み書きプロパティ
public abstract int Age { get; set; }
}
実装クラスで値の取得や(必要なら)設定のロジックを書けるよ。
なぜ抽象プロパティが必要?
抽象プロパティは、各子孫がどうやって値を取得・保存するか自分で決めたい時に便利。モデリングやビジネスモデル、ViewModelとか、プロパティのロジックが単なるフィールド返却より複雑な時によく使う。
例えば、ある図形は名前を計算で出すけど、別の図形は定数で返す、みたいな時も抽象プロパティでキレイに隠せる。
3. 図:抽象クラス・メソッド・プロパティの関係
次の図を見て、どう動くかイメージしよう:
┌─────────────┐
│ abstract │
│ Shape │
│-------------│
│ +Name: str │ <-- 抽象プロパティ
│ +Area(): dbl│ <-- 抽象メソッド
└─────┬───────┘
│
┌────▼────┐ ┌───────┐
│Rectangle│ │ Circle│
│........ │ .... │ ......│
│+Name │ │+Name │
│+Area() │ │+Area()│
└─────────┘ └───────┘
こうやって、Shapeクラスのどんな子孫でも同じインターフェースで扱えるけど、中身の実装は自由ってわけ。
抽象メソッド・プロパティのUML図:
┌────────────────────────────┐
│ abstract class │
│ Animal │
│────────────────────────────│
│+ Name: string {abstract} │
│+ MakeSound(): void {abstract}│
└─────────────┬──────────────┘
│
┌────────┴──────────┐
│ │
┌────────────┐ ┌─────────────┐
│ Cat │ │ Dog │
│────────────│ │─────────────│
│+ Name │ │+ Name │
│+ MakeSound()│ │+ MakeSound()│
└────────────┘ └─────────────┘
4. 実践シナリオとリアルプロジェクトでのメリット
抽象メソッドやプロパティは、クラス階層を進化させたい時の武器。大規模ビジネスアプリや多層ドメインモデル、プラグインシステム、UIフレームワークみたいな「全員守るべきルール」を作りたい時の基盤になる。
リアルなプロジェクトだと、こういう「クリーン」な設計があると、何ヶ月・何年後でも新しいエンティティや機能を安心して追加できる。古いコードも全部ちゃんと動くって分かるからね。
「なぜ?」とポリモーフィズムに戻る
ポリモーフィズムって魔法みたいだけど、実はただの契約:「このファミリーのどんなオブジェクトでも、あるメソッドを呼べば必ず正しい結果が返る」ってだけ。抽象メソッドやプロパティは、全階層で共通言語を強制する手段。でも「デフォルト実装」は無し。
このやり方はプラグインシステム(IDE、グラフィックエディタ、CRMとか)でよく使う。外部開発者が拡張を作る時、プラットフォームが要求する最低限の機能を必ず実装させるため。例えばファイル処理モジュールなら、基底クラスに抽象プロパティFileExtensionや抽象メソッドOpen()を持たせて、どんなプラグインでも自分のファイル型をちゃんと扱えるようにする。
抽象・virtual・普通のクラスメンバーの違い
| 特徴 | 普通のメソッド/プロパティ | virtual | abstract |
|---|---|---|---|
| 実装の有無 | あり | あり | なし |
| override必須か | いいえ | いいえ(上書き可) | はい(子孫で必須) |
| 直接呼び出し可? | はい | はい | いいえ |
| 普通のクラスでOK? | はい | はい | いいえ |
| sealed指定可? | いいえ | はい | いいえ |
5. 学習アプリでの使い方
じゃあ、新しい知識を使って、図形アプリに戻ってみよう。抽象プロパティとメソッドがどう一緒に動いて、柔軟で分かりやすいシステムを作るか見てみよう。
// 例えば、メインメソッドで
List<Shape> shapes = new List<Shape>
{
new Rectangle(4, 5),
new Circle(2.5)
};
foreach (Shape shape in shapes)
{
// 抽象プロパティNameとメソッドCalculateAreaのおかげで、
// 図形の具体的な型を気にしなくていい
Console.WriteLine($"{shape.Name}, 面積: {shape.CalculateArea():F2}");
}
結果:
長方形, 面積: 20.00
円, 面積: 19.63
美しいでしょ:コードは図形が何か知らなくても、必要なプロパティやメソッドを呼ぶだけでOK。.NET CLRがちゃんと正しい実装を呼んでくれる!
6. よくあるミスと実装の注意点
ミス1:親の抽象メソッドを実装し忘れ。
派生クラスで1つでも抽象メソッドやプロパティを実装しないまま、そのクラスがabstractじゃなかったら、コンパイラがビルドを止める。これはありがたい保護策:ロジックが「穴あき」のオブジェクト(例えばCalculateArea()無しの図形)は作れない。
ミス2:メソッドをabstractで宣言したのに、クラスが抽象じゃない。
これもコンパイルできない。クラスにabstractメソッドを追加したら、クラス自体も抽象にしなきゃダメ:
public abstract class Polygon : Shape
{
// CalculateArea()は実装しない、だからクラスはabstractのまま
}
ミス3:抽象メソッドをvirtualで実装しようとする。
抽象メソッドはoverrideで実装しなきゃダメ。もしさらに下の階層で上書きしたいなら、まずoverrideで実装してから、その子クラスでvirtualにできる:
public override double CalculateArea()
{
// デフォルト実装...
}
もっと下の子孫でこのメソッドを再度上書きできる。でももう一度abstractにはできないよ、だってもう実装があるから。
GO TO FULL VERSION