🪄

【C#】override した protected なメソッドを、あたかも public にアクセスできるように見せる方法

2024/10/13に公開

前提

ライブラリ等の開発において、例えば一般的な利用においては外部から隠蔽したいものがあるとする。ただし、継承したり拡張したりする上で(上級者向けに)外部からも操作できるようにもしておきたい、みたいな要望があるだろう。 (あんまりない。) それらを実現する方法を考えた結果たどり着いたものを備忘録として紹介する。

(極めてニッチで小技なので、役立つかはわからない。それと私が知らないだけで結構知られている事実かも?)

紹介する方法は提供側と利用側でアセンブリが異なるという条件付きで可能である。(この前提が最も大事。)internalと拡張メソッドを融合させることで実現する。

少し昔(C# 8.0)に発見したもの。
Unity のエディタ拡張で GraphView を用いてノードを繋いで会話シーンを作れる機能を作ろうとして、ノードやビュー自体の拡張性としてこういうことできればいいなと思ったのがきっかけ。

利用イメージ

PackageアセンブリとUserアセンブリの二つのアセンブリあって、UserアセンブリがPackageアセンブリに依存しているとする。
つまりPackageアセンブリ内の機能をUserアセンブリで使うことを想定する。
Packageが提供している機能に乗っけるためのBase抽象クラスがあるとする。

Package/Base.cs
namespace Package
{
    public abstract class Base
    {
        protected abstract void SetValue(int value);

        /* それ以外のさまざまな実装 */
    }
}

UserアセンブリではBaseを継承したEntityクラスを実装して、Packageのさまざまな機能を利用することを考える。

User/Entity.cs
using Package;

namespace User
{
    public class Entity : Base
    {
        int _value;

        protected override void SetValue(int value)
        {
            _value = value;
        }
    }
}

さて、このときprotectedな関数SetValueを外部から操作したいという時があるだろう。重要なのはEntityを操作するのではなく、Base一般に対して操作する機能を作りたい。
しかし、protectedな関数なのだから、当然外部に公開されているメソッドではない。

User/App.cs
using Package;

namespace User
{
    public class App()
    {
        public void Process(Base baseObj)
        {
            baseObj.SetValue(1); // エラーになる。SetValue() は protected なメソッドなので外部から使えない。
        }
    }
}

基本的には何か値を変える(しかもpublicでないメソッド)はまあまあ危険な操作なはずで、これをやって欲しくないからpublicで定義していないわけだ。
しかし、同時にこれはPackageの機能拡張する上では不自由である。

これがもし、以下のようにたったPackage.Dangerネームスペースを呼び出すだけで使えるようになったらどうだろう。

User/App.cs
  using Package;
+ using Package.Danger;

  namespace User
  {
      public class App()
      {
          public void Process(Base baseObj)
          {
              baseObj.SetValue(1); // エラーが起きず操作可能!
          }
      }
  }

これであれば通常利用時(Packageのみ)ではいじらせず、しかし、意図して利用しようとしているとき(Package.Dangerを使うとき)のみpublicに使える。
protectedoverrideしたものが、あたかもpublicに公開されているように見える(しかもネームスペースのインポート次第で!)この魔法のようなものをどうやって実現するのかを示したい。

実装

まずPackage.Baseクラスにちょっとした細工を施す。

Package/Base.cs
  namespace Package
  {
      public abstract class Base
      {
          protected abstract void SetValue(int value);

+         internal void _SetValue(int value)
+         {
+             SetValue(value);
+         }
      }
  }

SetValueinternalに公開するラップメソッド_SetValueを定義した。

ここで、Package.Dangerネームスペースで以下の拡張メソッドを実装する。

Package/Danger/BaseDangerExtension.cs
namespace Package.Danger
{
    public static class BaseDangerExtension
    {
        // public な拡張メソッドなのでアセンブリ外でも利用できる。
        public static void SetValue(this Base self, int value)
        {
            // 同一アセンブリ内なので internal なメソッドにアクセスできる。
            self._SetValue(value);
        }
    }
}

すると、この拡張メソッドによりBaseクラスにはBase.SetValue(int value)の形で利用できるメソッドを作ることができる。
この利用ができるのはPackage.DangerにあるBaseDangerExtension.SetValueが利用可能なときであるから、using Package.Danger;したときのみ使えるメソッドである。
同じ関数名に設定しているので、ユーザーからするとBaseを継承したEntityクラスでprotected overrideしたメソッドが、あたかもpublicに利用できているように見えることになる。

終わりに

一見不思議に見えるが、internalpublicの違い、および拡張メソッドについて理解していれば納得できる仕組みだろう。
(ただ、internalなものをpublicで再公開できていいのか、という疑問はある。もしかしたら今回紹介した用途を実現する意図があるのかもしれない。知らんけど。)

もちろん、これはprotectedなメソッドと同名で拡張メソッドを定義しているから不思議に思えるのであって、別名であればそこまで違和感はないかもしれない。ただ、このようなことができるということを残しておきたいと思い記事にした次第である。

Discussion