やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

C#8.0のインタフェース新機能を試してみる

 インタフェースなのに実装済みメソッドが追加できた。publicでなくprotected staticな。

成果物

前回

 公式ドキュメントのコードがエラーになって動作確認できなかった。よって自前で動作確認コードを書いてみる。

確認項目

  • C#8.0インタフェース新機能(仕様変更)
    • メンバ変数やメソッドを実装できるようになった
      • static protectedメソッドで規定処理を定義できる(新機能)
        • インタフェースメソッドをオーバーライドすれば独自実装できる(今まで通り)

BOT作成インタフェース

 テキストを返すだけのインタフェースを作る。

Mastodonでつぶやくインタフェース

 Mastodon)はTwitterに似たサービス用サーバを自前で作れるソフトウェア。

 発言することはtootと呼ぶ。1回あたり500字までのテキストを入力できる。

守備範囲

  • 実際にtootはしない。単に文字列を返すだけ
  • 500字以内であるかチェックも実装しない

プロジェクト作成

dotnet new console -o CS8_Interface
cd CS8_Interface

コード

Program.cs

    class Program
    {
        static void Main(string[] args)
        {
//            Console.WriteLine(IBot::DefaultToot()); // protectedなので参照不可(error CS0432)
//            Console.WriteLine(IBot::Toot()); // staticでないため::で参照不可(error CS0432)
//            Console.WriteLine(IBot.Toot()); // newせねば.で参照不可(error CS0120)
//            Console.WriteLine(new IBot().Toot()); // interfaceはnewできない(error CS0144)
//            Console.WriteLine(new DefaultBot().Toot()); // (error CS1061)
            Console.WriteLine(new FixedBot().Toot());
            Console.WriteLine(new DateTimeBot().Toot());
            Console.WriteLine(new GreetingBot().Toot());
        }
    }

IBot.cs

 本題。C#8.0からインタフェースでもメンバ変数やメソッドを定義できるようになった。

 そこで、インタフェース用メソッドと、そのデフォルト処理を実装してみる。

public interface IBot
{
        public string Toot() => IBot.DefaultToot();
        protected static string DefaultToot() => "デフォルトのtootです。";
}

 staticにすることで1つだけ生成する。protectedにすることで子のみ参照できる。

 でもこれ、ふつうのクラスと何が違うの? 以下、これを継承したクラスを実装する。

DefaultBot.cs

public class DefaultBot : IBot {}

 コンパイルは成功する。ただしProgram.csで呼び出すとエラーになる。

Program.cs

Console.WriteLine(new DefaultBot().Toot()); // (error CS1061)
Program.cs(13,48): error CS1061: 'DefaultBot''Toot' の定義が含まれておらず、型 'DefaultBot' の最初の引数を受け付けるアクセス可能な拡張メソッド 'Toot' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください。 [/tmp/work/CSharp.Interface.8.0.20191026095218/src/CS8_Interface/CS8_Interface.csproj]

 なぜ? IBotインタフェースは継承している。そこにはTootメソッドを実装してある。publicなので公開されているから参照できるはずなのに……。インタフェースで実装されたメソッドはオーバーライドせねば参照できないのか?

 前回の既定の実装を拡張するをみてみると、継承先で呼出している。どうやら以下のように呼出せねばならないようだ。

public class DefaultBot : IBot
{
    public string Toot() => IBot.DefaultToot();
}

 でも、継承先で単にデフォルト動作をしたいだけの場合は面倒ではないか? いちいち実装せねばならないとか……。仕様を知らずとも未実装箇所はデフォルト定義を継承するようにして欲しい……。

 たとえばインタフェースメソッドが5つあるとき、4つはデフォルトで1つだけ独自実装したいときもあると思う。そういう場合、オーバーライドしたメソッドだけ独自処理になってくれると楽で助かる。いちいちデフォルト実装を書かねばならないとかダルすぎる。

 でも、それってインタフェースの役割としてどうなんだ? 「実装を強いる」のがインタフェースのキモだったはず。なら、先述のような要件のときはどう実装すべきなの? public staticにする? それともクラスを作って継承させる?

FixedBot.cs

public class FixedBot : IBot
{
    public string Toot() => "これは固定tootです。";
}

DateTimeBot.cs

public class DateTimeBot : IBot
{
    public string Toot() => $"これは{DateTime.Now:yyyy-MM-dd HH:mm:ss}時点におけるtootです。";
}

GreetingBot.cs

public class GreetingBot : IBot
{
    public string Toot() => DateTime.Now.Hour switch 
    {
        int n when (3 < n && n < 10) => "おはようございます。",
        int n when (9 < n && n < 17) => "こんにちは。",
        int _ => "こんばんは。",
    };
}

実行結果

デフォルトのtootです。
これは固定tootです。
これは2019-10-26 12:26:10時点におけるtootです。
こんにちは。

所感

インタフェースの使い方

 インタフェースの使い方が相応しくない気がする。以下のようにすべきでは?

interface IBot {
    public string Generate();
}

 継承したクラスが実装すべき。

class FixedBot : IBot {
    public string Generate() => "これは固定tootです。";
}

 これが本来のインタフェースだと思う。IBotを継承していながらGenerateメソッドを実装していなかったらコンパイルエラーになるはず。

 でも、これだと「C#8.0ではインタフェースにメンバ変数やメソッドが追加できるようになった」ことが確認できない。ぐぬぬ。いい例が思いつかない。

範囲における分岐

 以下みたいに簡単に023の範囲で分岐できたら嬉しかったんだけども。そういう構文はないらしい。

switch DateTime.Now.Hour,  {
    4..9 => "おはよう";
    10..17 => "こんにちは";
    0..23 => "こんばんは";
    _ => throw new Exception("不正な時間です。0〜23の整数値のみ有効です。");
}
  • もし0..23の範囲外なら例外。これは省略可
  • もし0..23以内で4..9,10..17以外ならdefault

 以下参考。

switch式

 C#8.0新機能であるswitch式。rust言語でいうmatch式。既存のswitch文と混同するからmatch式にしてくれたらよかったのに。

public string Toot() => DateTime.Now.Hour switch 
{
    int n when (3 < n && n < 10) => "おはようございます。",
    int n when (9 < n && n < 17) => "こんにちは。",
    int _ => "こんばんは。",
};
  • メソッドをラムダ式=>で定義しているため、末尾に;が必要である
  • switch式はパターン => 戻り値,で羅列し、全パターン網羅するもの

 パターンの記法が冗長。

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux