rksoftware

Visual Studio とか C# とかが好きです

メンバーと継承の少しだけマニアックな話

C# 11 で static virtual メンバーというものが出てきました。この機能を確認していて、意外と皆さんメンバーと継承の挙動をちゃんと把握せずにいる方もいるのではないか、と思いまとめてみようかと思いました。

正直、継承の挙動をちゃんと把握し書ければならない状況、そうそうないのでちゃんと把握していなくても全く問題ないと思います。たぶん趣味の領域です。

■ abstract メンバー

abstract メンバー実装時の挙動を見てみましょう。

エラーのない状態

こんな感じです。

Console.WriteLine(Add(new CClass())); // 1

int Add(AAbstract @abstract) => @abstract.Add();

abstract class AAbstract { abstract public int Add(); }

class CClass : AAbstract { public override int Add() => 1; }

導出クラスで実装がない場合

導出クラスとは派生先とかサブクラスとか継承子とかとも言われる存在です。導出クラスに実装がない場合はエラーになります。

abstract class AAbstract { abstract public int Add(); }

class CClass : AAbstract { }  // エラー CS0534 'CClass' は継承抽象メンバー 'AAbstract.Add()' を実装しません
エラー CS0534 'CClass' は継承抽象メンバー 'AAbstract.Add()' を実装しません

abstract メンバーを default 実装しようとした場合

abstract メンバーは実装を持てません。

abstract class AAbstract { abstract public int Add() => 1; }  // エラー CS0500 'AAbstract.Add()' は abstract に指定されているため本体を宣言できません
エラー CS0500 'AAbstract.Add()' は abstract に指定されているため本体を宣言できません

多段階の継承がある場合

シンプルに実体クラスの実装が呼ばれます。

Console.WriteLine(Add(new CClass1())); // 1
Console.WriteLine(Add(new CClass2())); // 2

int Add(AAbstract @abstract) => @abstract.Add();

abstract class AAbstract { abstract public int Add(); }

class CClass1 : AAbstract { public override int Add() => 1; }
class CClass2 : CClass1 { public override int Add() => 2; }

abstract メンバーについてはこんな感じです。abstract メンバーを使うことは通常ないでしょうから、へーそうなんだー、と思っておいていただければと思います。

■ virtual メンバー

次に virtual メンバーの override 時の挙動を見てみましょう。

virtual メンバーに実装がない場合

当たり前ですが、abstract メンバーと違い virtual メンバーには実装が必要です。

class CClass { virtual public int Add(); }  // エラー CS0501 'CClass.Add()' は abstract、extern、または partial に指定されていないため、本体を宣言する必要があります
エラー CS0501 'CClass.Add()' は abstract、extern、または partial に指定されていないため、本体を宣言する必要があります

導出クラスに実装がない場合

導出クラスに実装はなくても OK です。

class CClass { virtual public int Add() => 1; }

class CClass1 : CClass { }

基底クラスの型で呼ばれる場合

基底クラスとは基本クラスとかスーパークラスとか継承親とかとも言われる存在です。シンプルに実体クラスの実装が呼ばれます。

Console.WriteLine(Add(new CClass())); // 1
Console.WriteLine(Add(new CClass1())); // 2

int Add(CClass @class) => @class.Add();

class CClass { virtual public int Add() => 1; }

class CClass1 : CClass { public override int Add() => 2; }

複数のクラスを基底にしようとした場合

複数のクラスを基底にすることはできません。

class CClass1 { virtual public int Add() => 1; }
class CClass2 { virtual public int Add() => 2; }

class CClass3 : CClass1, CClass2 { }  // エラー CS1721 クラス 'CClass3' は複数の基底クラス ('CClass1' と 'CClass2') を持つことができません
エラー CS1721 クラス 'CClass3' は複数の基底クラス ('CClass1' と 'CClass2') を持つことができません

virtual メンバーはこんな感じです。virtual メンバーは普段使うこともあるでしょうから、へーそうなんだー、とわかっていただければ良いかと思います。

■ interface の実装

最後に interface の実装時の挙動を見てみましょう。

実装クラスに実装がない場合

interface を実装するクラスに interface メンバーの実装がない場合はエラーになります。通常は。通常じゃないパターンは次の項目で説明します。

interface IInterface { int Add(); }

class CClass : IInterface { }  // エラー CS0535 'CClass' はインターフェイス メンバー 'IInterface.Add()' を実装しません
エラー CS0535 'CClass' はインターフェイス メンバー 'IInterface.Add()' を実装しません

インターフェイスのデフォルト実装

前項目で例外があると言いました、それがこの項目です。interface メンバーのデフォルト実装という機能があります。デフォルト実装をした次のコードは実行できます。

Console.WriteLine(Add(new CClass()));  // 1

int Add(IInterface @interface) => @interface.Add();

interface IInterface { int Add() => 1; }

class CClass : IInterface { }

複数のデフォルト実装

この辺りから少しマニアックになってきます。同名のデフォルト実装を持った複数の interface を実装した場合どうなるでしょうか? ちなみに class の場合は、複数のクラスを継承することはできないので考える必要がありません。
この場合、メソッドを呼び出した時の型の実装が呼ばれます。次の例では IInterface1 型の変数の Add メソッドを呼び出しているので IInterface1 型の実装が呼ばれます。

Console.WriteLine(Add(new CClass())); // 1

int Add(IInterface1 @interface) => @interface.Add();

interface IInterface1 { int Add() => 1; }
interface IInterface2 { int Add() => 2; }

class CClass : IInterface1, IInterface2 { }

クラスに実装があった場合

クラスに実装があっても同じく、interface の型の変数で呼ばれていてデフォルト実装もある場合は、デフォルト実装が呼ばれます。

Console.WriteLine(Add(new CClass())); // 1

int Add(IInterface1 @interface) => @interface.Add();

interface IInterface1 { int Add() => 1; }
interface IInterface2 { int Add() => 2; }

class CClass : IInterface1, IInterface2 { int Add() => 3; }

明示的なインターフェイスの実装

デフォルト実装は実装クラスからどうこうできないのでしょうか? この辺りからかなりマニアックになってきます。
明示的なインターフェイスの実装を行うと実装クラスの実装が呼ばれます。

Console.WriteLine(Add(new CClass())); // 3

int Add(IInterface1 @interface) => @interface.Add();

interface IInterface1 { int Add() => 1; }
interface IInterface2 { int Add() => 2; }

class CClass : IInterface1, IInterface2 { int IInterface1.Add() => 3; }

IInterface2 には影響なし

当然ですが、IInterface1 の明示的な実装をしても、IInterface2 のデフォルト実装には影響がありません。

Console.WriteLine(Add(new CClass())); // 2

int Add(IInterface1 @interface) => ((IInterface2)@interface).Add();

interface IInterface1 { int Add() => 1; }
interface IInterface2 { int Add() => 2; }

class CClass : IInterface1, IInterface2 { int IInterface1.Add() => 3; }

IInterface2 も明示的に実装すればクラスの実装が呼ばれます。

Console.WriteLine(Add(new CClass())); // 4

int Add(IInterface1 @interface) => ((IInterface2)@interface).Add();

interface IInterface1 { int Add() => 1; }
interface IInterface2 { int Add() => 2; }

class CClass : IInterface1, IInterface2 { int IInterface1.Add() => 3; int IInterface2.Add() => 4; }

明示的なインターフェイスの実装だけのクラスの型で呼ぶ場合

呼べません。

int Add(IInterface1 @interface) => ((CClass)@interface).Add();  // エラー CS1061 'CClass' に 'Add' の定義が含まれておらず、型 'CClass' の最初の引数を受け付けるアクセス可能な拡張メソッド 'Add' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください

interface IInterface1 { int Add() => 1; }
interface IInterface2 { int Add() => 2; }

class CClass : IInterface1, IInterface2 { int IInterface1.Add() => 3; int IInterface2.Add() => 4; }
エラー CS1061 'CClass' に 'Add' の定義が含まれておらず、型 'CClass' の最初の引数を受け付けるアクセス可能な拡張メソッド 'Add' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください

明示的なインターフェイスの実装でないクラスでの実装が必要です。実装すればクラスでの実装が呼ばれます。

Console.WriteLine(Add(new CClass())); // 5

int Add(IInterface1 @interface) => ((CClass)@interface).Add();

interface IInterface1 { int Add() => 1; }
interface IInterface2 { int Add() => 2; }

class CClass : IInterface1, IInterface2 { int IInterface1.Add() => 3; int IInterface2.Add() => 4; public int Add() => 5; }

片方にデフォルト実装がない場合

デフォルト実装のない interface のために通常のクラスでの実装が必要です。

interface IInterface1 { int Add() => 1; }
interface IInterface2 { int Add(); }

class CClass : IInterface1, IInterface2 { int IInterface1.Add() => 3; }  // エラー CS0535 'CClass' はインターフェイス メンバー 'IInterface2.Add()' を実装しません
エラー CS0535 'CClass' はインターフェイス メンバー 'IInterface2.Add()' を実装しません

実装するとこんな感じです。

Console.WriteLine(Add(new CClass())); // 5

int Add(IInterface1 @interface) => ((IInterface2)@interface).Add();

interface IInterface1 { int Add() => 1; }
interface IInterface2 { int Add(); }

class CClass : IInterface1, IInterface2 { int IInterface1.Add() => 3; public int Add() => 5; }

ちょっとマニアックでしたね

interface の挙動は最後ちょっとマニアックでしたね。しかもせっかくなので、へーそーなのかー、くらいに受け止めて覚えておいていただけたらと思います。