【C#】override した protected なメソッドを、あたかも public にアクセスできるように見せる方法
前提
ライブラリ等の開発において、例えば一般的な利用においては外部から隠蔽したいものがあるとする。ただし、継承したり拡張したりする上で(上級者向けに)外部からも操作できるようにもしておきたい、みたいな要望があるだろう。 (あんまりない。) それらを実現する方法を考えた結果たどり着いたものを備忘録として紹介する。
(極めてニッチで小技なので、役立つかはわからない。それと私が知らないだけで結構知られている事実かも?)
紹介する方法は提供側と利用側でアセンブリが異なるという条件付きで可能である。(この前提が最も大事。)internal
と拡張メソッドを融合させることで実現する。
少し昔(C# 8.0
)に発見したもの。
Unity のエディタ拡張で GraphView を用いてノードを繋いで会話シーンを作れる機能を作ろうとして、ノードやビュー自体の拡張性としてこういうことできればいいなと思ったのがきっかけ。
利用イメージ
Package
アセンブリとUser
アセンブリの二つのアセンブリあって、User
アセンブリがPackage
アセンブリに依存しているとする。
つまりPackage
アセンブリ内の機能をUser
アセンブリで使うことを想定する。
Package
が提供している機能に乗っけるためのBase
抽象クラスがあるとする。
namespace Package
{
public abstract class Base
{
protected abstract void SetValue(int value);
/* それ以外のさまざまな実装 */
}
}
User
アセンブリではBase
を継承したEntity
クラスを実装して、Package
のさまざまな機能を利用することを考える。
using Package;
namespace User
{
public class Entity : Base
{
int _value;
protected override void SetValue(int value)
{
_value = value;
}
}
}
さて、このときprotected
な関数SetValue
を外部から操作したいという時があるだろう。重要なのはEntity
を操作するのではなく、Base
一般に対して操作する機能を作りたい。
しかし、protected
な関数なのだから、当然外部に公開されているメソッドではない。
using Package;
namespace User
{
public class App()
{
public void Process(Base baseObj)
{
baseObj.SetValue(1); // エラーになる。SetValue() は protected なメソッドなので外部から使えない。
}
}
}
基本的には何か値を変える(しかもpublic
でないメソッド)はまあまあ危険な操作なはずで、これをやって欲しくないからpublic
で定義していないわけだ。
しかし、同時にこれはPackage
の機能拡張する上では不自由である。
これがもし、以下のようにたったPackage.Danger
ネームスペースを呼び出すだけで使えるようになったらどうだろう。
using Package;
+ using Package.Danger;
namespace User
{
public class App()
{
public void Process(Base baseObj)
{
baseObj.SetValue(1); // エラーが起きず操作可能!
}
}
}
これであれば通常利用時(Package
のみ)ではいじらせず、しかし、意図して利用しようとしているとき(Package.Danger
を使うとき)のみpublic
に使える。
protected
でoverride
したものが、あたかもpublic
に公開されているように見える(しかもネームスペースのインポート次第で!)この魔法のようなものをどうやって実現するのかを示したい。
実装
まずPackage.Base
クラスにちょっとした細工を施す。
namespace Package
{
public abstract class Base
{
protected abstract void SetValue(int value);
+ internal void _SetValue(int value)
+ {
+ SetValue(value);
+ }
}
}
SetValue
をinternal
に公開するラップメソッド_SetValue
を定義した。
ここで、Package.Danger
ネームスペースで以下の拡張メソッドを実装する。
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
に利用できているように見えることになる。
終わりに
一見不思議に見えるが、internal
とpublic
の違い、および拡張メソッドについて理解していれば納得できる仕組みだろう。
(ただ、internal
なものをpublic
で再公開できていいのか、という疑問はある。もしかしたら今回紹介した用途を実現する意図があるのかもしれない。知らんけど。)
もちろん、これはprotected
なメソッドと同名で拡張メソッドを定義しているから不思議に思えるのであって、別名であればそこまで違和感はないかもしれない。ただ、このようなことができるということを残しておきたいと思い記事にした次第である。
Discussion