クラスメソッドのオーバーロードをしてみる。

ブツ

実行

NAME='CSharp.DotNet6.Override.Overload.20220926142101'
git clone https://github.com/ytyaru/$NAME
cd ./$NAME/src/MyApp
dotnet run

経緯

Electronでつぶやきを保存する21のときJavaScriptでオーバーロードできない問題があった。自信がなかったのでC#でオーバーロードできるか書いてみる。

ようするに同じメソッド名でありながら、引数がない場合とある場合の2種類のメソッドを作りたい。それぞれの実装は別にあり、異なるクラスで書きたい。

題材

今回は車を題材にしてみる。AT車とMT車でおなじ動作であるアクセルを踏むAccelerator()を実装する。ただしその内容は異なる。AT車はギアを自動できりかえる。MT車はギアを手動できりかえる。このときギアを引数として受けとるようにする。この引数を受けとるかどうかが両者のちがい。

種類 アクセルを踏む
AT Accelerator()
MT Accelerator(int gear)

プロジェクト作成

NAME=MyApp
dotnet new console -o $NAME -f net6.0
cd $NAME

MyApp.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>disable</Nullable><!-- enable, disable -->
  </PropertyGroup>

</Project>

<Nullable>disableにしておく。デフォルトはenableだったが、実行すると以下のような警告が出たので。

/tmp/work/MyApp/Car.cs(9,60): warning CS8602: null 参照の可能性があるものの逆参照です。 [/tmp/work/MyApp/MyApp.csproj]

ソースコード

IAccelerator.cs

interface IAccelerator {
    void Accelerator();
}

Accelerator()メソッドをもたせて共通化するためのインタフェース。

Car.cs

class Car : IAccelerator {
    private int gear;
    protected int Gear {
        get { return this.gear; }
        set { if (0<=value && value<=6) { this.gear = value; } }
    }
    public void Accelerator() {
        Console.WriteLine("{0}.{1}() {2}", GetType().Name, System.Reflection.MethodBase.GetCurrentMethod().Name, this.Gear);
    }
}

車クラス。Accelerator()を実装する。呼び出すとクラス名、メソッド名、現在のギアを表示する。

ギアは06の整数をセットできる。

ATCar.cs

class ATCar : Car {
    public new void Accelerator() {
        if (6 == this.Gear) { this.Gear = 0; }
        this.Gear++;
        base.Accelerator();
    }
}

AT車。Carを継承する。Accelerator()をオーバーライドする。newキーワードをつけることで親の同名メソッドでなくこのメソッドを呼び出すようにする。newを明示しないとビルド時に以下のような警告がでる。

/tmp/work/MyApp/ATCar.cs(2,17): warning CS0108: 'ATCar.Accelerator()' は継承されたメンバー 'Car.Accelerator()' を非表示にします。非表示にする場合は、キーワード new を使用してください。 [/tmp/work/MyApp/MyApp.csproj]

MTCar.cs

class MTCar : Car {
    public void Accelerator(int gear) {
        if (1 == Math.Abs(this.Gear - gear)) {
            Console.WriteLine("{0}.{1}({2}) {3}", 
                GetType().Name, 
                System.Reflection.MethodBase.GetCurrentMethod().Name, 
                gear, this.Gear);
            this.Gear = gear;
        }
        else {
            Console.WriteLine("{0}.{1}({2}) {3} {4}", 
                GetType().Name, 
                System.Reflection.MethodBase.GetCurrentMethod().Name, 
                gear, this.Gear, "エンスト!");
            this.Gear = 0;
        }
    }
}

MT車。Carを継承する。Accelerator()をオーバーライドではなくオーバーロードする。引数がちがう同名メソッドはオーバーロードになる。

オーバーロードの場合、同名だが別メソッドとして識別されるらしい。よってnewをつける必要はない。つまり、MTCarクラスはAccelerator()Accelerator(int)の2種類の異なるメソッドを保持していることになる。

私としてはMTの場合Accelerator()のほうは不要だった。Accelerator(int)だけ実装したかった。むしろAccelerator()が呼び出されたら「そんなメソッドないよ!」とコンパイラ先生に叱ってもらいたかった。

だったら継承だのインタフェースだのは使わずにAT、MTそれぞれ別クラスにすべきだったのかな? でもアクセルを踏むという共通のメソッドは欲しいし。うーん。

Program.cs

インスタンス生成

各クラスのインスタンスを生成する。

Car car = new Car();
ATCar at = new ATCar();
MTCar mt = new MTCar();

Car

まずはギアの実装がなにもないCarクラスでAccelerator()を呼ぶ。

car.Accelerator();
car.Accelerator();

実行結果は以下。ギアは0である。

Car.Accelerator() 0
Car.Accelerator() 0

AT

つぎにAT車のそれを呼ぶ。

at.Accelerator();
at.Accelerator();
at.Accelerator();
at.Accelerator();
at.Accelerator();
at.Accelerator();
at.Accelerator();

実行結果は以下。ギアは16である。最大値6までいったら1に戻る。このAT車、いちど走り出したら止まれない仕様である。おそろしい。

ATCar.Accelerator() 1
ATCar.Accelerator() 2
ATCar.Accelerator() 3
ATCar.Accelerator() 4
ATCar.Accelerator() 5
ATCar.Accelerator() 6
ATCar.Accelerator() 1

MT

MT車。まずは引数なしから。これが呼べてしまう。Carクラスのメソッドである。私の気持ちとして引数なしのときは「そんなメソッドはないよ!」と叱ってほしいので、この実装はベストじゃない。どうしたらいいんだろう。

mt.Accelerator();
mt.Accelerator();
Car.Accelerator() 0
Car.Accelerator() 0

つぎに本命、MT車でギアを渡す。

mt.Accelerator(1);
mt.Accelerator(2);
mt.Accelerator(3);
mt.Accelerator(2);
mt.Accelerator(4); // 2から4に一気にあげたからエンストする
mt.Accelerator(3); // 一度エンストするとギアは0にもどる。0との差が1でないからやはりエンストする
mt.Accelerator(1); // エンストから復帰するには1を渡すしかない
MTCar.Accelerator(1) 0
MTCar.Accelerator(2) 1
MTCar.Accelerator(3) 2
MTCar.Accelerator(2) 3
MTCar.Accelerator(4) 2 エンスト!
MTCar.Accelerator(3) 0 エンスト!
MTCar.Accelerator(1) 0

AT, MTインスタンス変数の型を親クラスCarに変えてみる。

Car at1 = new ATCar();
Car mt1 = new MTCar();
at1.Accelerator();
at1.Accelerator();
//mt1.Accelerator(1); // error CS1501: 引数 1 を指定するメソッド 'Accelerator' のオーバーロードはありません
//mt1.Accelerator(2); //  error CS1501: 引数 1 を指定するメソッド 'Accelerator' のオーバーロードはありません
ATCar.Accelerator() 0
ATCar.Accelerator() 0

AT, MTインスタンス変数の型をインタフェースIAcceleratorに変えてみる。

IAccelerator at2 = new ATCar();
IAccelerator mt2 = new MTCar();
at2.Accelerator();
at2.Accelerator();
//mt2.Accelerator(1); // error CS1501: 引数 1 を指定するメソッド 'Accelerator' のオーバーロードはありません
//mt2.Accelerator(2); // error CS1501: 引数 1 を指定するメソッド 'Accelerator' のオーバーロードはありません
ATCar.Accelerator() 0
ATCar.Accelerator() 0

私としては次のようなイメージだった。

IAccelerator car = generateCar(); // ATまたはMTを返す
car.Accelerator(1) // AT/MTどちらでもエラーなく動く

引数は必ずある想定。引数を受けとらないATのときは引数を無視する。

でも、そうはならなかった。原因は以下だと思う。

  • 引数の有無によってメソッドシグネチャが変わる
  • 引数ありのシグネチャを用意していない

これらを解決するにはIAcceleratorAccelerator()を引数なしとありの2パターンで通用する書き方をする必要がありそう。

方法 コード例
オプション引数 Accelerator(int gear = -1)
名前付き引数呼出 Accelerator(gear : 3)
可変長引数 Accelerator(params int[] gear)

次はこれで試してみよう。

所感

やはり私は基本的なことが理解できていないっぽい。

でもC#で思い通りに書けるようになってもJavaScriptで書けるようになるわけではない。迷走しているけど勉強と思ってやってみよう。