議論の余地のある.NETの新しい提案は、抽象インターフェイスを使った限定された多重継承である。この機能はJavaのデフォルトメソッドに触発されている。
デフォルトメソッドの目的は、公開された抽象インターフェイスを開発者が更新できるようにすることである。通常、これはJavaや.NETでは許可されていないため、破壊的な変更となる。しかしデフォルトメソッドでは、インターフェイスの作者は後方互換の問題を緩和できるオーバーライド可能な実装を提供できる。
この提案されたC#バージョンには以下のシンタックスが含まれている:
- メソッドのメソッドボディー(つまり"デフォルト"実装)
- プロパティアクセサ本体
- 静的メソッドとプロパティ
- プライベートメソッドとプロパティ(デフォルトのアクセスはpublic)
- メソッドとプロパティのオーバーライド
この提案ではインターフェイスにフィールドを持たせることはできないため、C++の多重継承で見つかったいくつかの問題を避ける限定的な形である(ただしフィールドはConditionalWeakTableと拡張プロパティパターンを使ってシミュレートできる)。
ユースケース: IEnumerble.Count
この機能のもっとも多い使い方は、IEnumerable<T>にCountプロパティを追加することだ。このアイディアは、Enumerable.Count拡張メソッドを使う代わりに開発者はCountを自由に、必要に応じてより効率的な方法でそれを上書きできる。
interface IEnumerable<T>
{
int Count()
{
int count = 0;
foreach (var x in this)
count++;
return count;
}
}
interface IList<T> ...
{
int Count { get; }
override int IEnumerable<T>.Count() => this.Count;
}
これをみてわかるように、IList<T>.Countプロパティが自動的にピックアップされるので、IList<T>の開発者はIEnumerable<T>.Count()メソッドのオーバライドについて心配する必要はない。
この提案の関心事のひとつは、インターフェイスを拡張することである。IEnumerableにCountを追加するのに、他の全てのIEnumerable拡張メソッドを追加しないのはなぜか?
Eirenarch氏は次のように書いた:
IEnumerableにCount()の追加を検討していることに心底驚いています。リセットと同じミスではないでしょうか?すべてのIEnumerablesをリセットして、それら全てを1度だけ安全にカウントすることはできません。これで私はIEnumerableのCount()を使うことはできなくなりました。Count()がenumerableを消費したり、非効率になるのを避けたいので、データベースLINQ呼び出しだけこれを使います。なぜさらなるCount()を推奨しているのでしょうか?
DavidArno氏は追加した:
ハハハ、この提案に対する本当に良い意見です。BCLチームはすでに様々なコレクション方で古いゴミを作り出しました。彼らがなにをしたかを知っていて、Barbara Liskovの置換原則を徹底的に破壊したものを目にできるとは考えていません。このような機能を持つアイディアは、さらなる大混乱を有無でしょう。本当に恐ろしい!
BCLミーティングでは:
"私たちはIEnumerable<T>にIEnumerable<T>という欠点を追加したいと考えています。なにかご意見はありますか?"
"これは簡単です。デフォルトインターフェイスメソッドで解決できます。単に(Tを頭にIEnumerable<T>を後ろにつけて) Cons() => throw new NotImplementedException();これだけです。IEnumerableの実装者は余暇にサポートを追加できます。"
"おめでとう。仕事は終わった。以上。今回のミーティングは終了です".
LINQは別チームが管理しているので、LINQの機能をIEnumerable<T>に移行する計画がないことに注意して欲しい。
この変更により拡張メソッドのレイヤーは破壊されることになる。Enumerable.Countは現在、mscorlibよりも2階層上のSystem.Coreにいる。LINQの一部または全てをmscorlibに取り込むと、アセンブリが必要以上に膨らむ可能性がある。
その他の批判は、拡張メソッドをオプションでオーバーライドできるデザインパターンがすでにあるため、これが必要ではないことである。
オーバーライド可能な拡張メソッドパターン(The Overridable Extension Method Pattern)
オーバーライド可能な拡張メソッドは、インターフェイスチェックに依存する。理想的にはチェックする必要があるインターフェイスはひとつだけだが、従来の理由からEnumerable.Countの例では2つのインターフェイスをチェックする必要がある:
public static int Count<TSource>(this IEnumerable<TSource> source) {
var collectionoft = source as ICollection<TSource>;
if (collectionoft != null) return collectionoft.Count;
var collection = source as ICollection;
if (collection != null) return collection.Count;
int count = 0;
using (var e = source.GetEnumerator()) {
while (e.MoveNext()) count++;
}
return count;
}
(クリアにするためにエラーハンドリングは省略)
このパターンの欠点はオプションのインターフェイスが広すぎる可能性があることである。たとえば、Enumerable.Countをオーバーライドしたいクラスは、ICollection<T>インターフェイス全体を実装する必要がある。読み取り専用クラスの場合は、(歴史的な理由からICollection<T>をチェックする代わりに大幅に小さなIReadOnlyCollection<T>で)書き込みに対して大量のNotSupported例外が必要になる。
デフォルトメソッドとクラスのPublic API
新しいメソッドを追加したときに下位互換の問題を避けるために、デフォルトメソッドはクラスのpublicインターフェイスからアクセスできなくなっている。たとえば、IEnumerable.Countを例にこのクラスについて考えてみよう:
class Countable : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator() {…}
}
IEnumerable.Countはオーバーライドされないので、このコードを書くことはできない:
var x = new Countable();
var y = x.Count();
代わりにそれをキャストする必要がある:
var y = ((IEnumerable<int>)x).Count();
クラスのpublic APIのインターフェイスメソッドを公開するために定型コードを追加する必要があるため、クラスのデフォルト実装を提供する有用性が制限される。
デフォルトメソッドでデフォルトメソッドをオーバーライドする
インターフェイスのデフォルトメソッドは他のインターフェイスのデフォルトメソッドでオーバーライドできる。私たちのIEnumerable.Countユースケースでこれを確認できる。
通常のメソッドと同様にoverrideキーワードを明示的に使用する必要がある。そうでなければ新しいメソッド他のメソッドと無関係として扱われてしまう。
またインターフェイスメソッドを“override abstract”としてマークすることもできる。通常abstractキーワードは不要で、すべてのabstractインターフェイスメソッドはデフォルトでabstractである。
拡張メソッド vs デフォルトプロパティによる解決順序
Zippec氏は新しく追加されたインターフェイスメソッドが、そのインターフェイスの拡張メソッドと同じ名前があった場合にどうなるのか疑問を上げた:
現在のAPIをデフォルトメソッドにアップグレードするにはどうしたらよいですか?私は拡張メソッドよりもオーバーロード開発が高い優先度を持つべきだと思っています。Count()を例に取ってみましょう。IEnumerableでそれを取得できるでしょうか?もしそうなら、LinqのIEnumerable.Count()を隠すことになるので、この機能でC#をリコンパイルすると呼び出されたコードは変更されますか?それで大丈夫ですか?私はIQueryableで問題があると思います。
もしこれが問題で、これを緩和するためにBCLのCountプロパティを取得したとすると、既存のカスタム拡張メソッドの意味を変える可能性があるため、既存のBCLインターフェイスのデフォルトメソッド(プロパティのみ)を使用できません。
まれにLINQのものをミラーするが、動作が異なる独自の拡張メソッドライブラリを作成する開発者もいる。他の拡張ライブラリへの切り替えるということは、インターフェイスのデフォルトメソッドに切り替えたときに機能が失われることになる。
ユースケース: INotifyPropertyChanged
新しい機能に対するその他の可能性として検討されているのが:
interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(args);
protected void OnPropertyChanged([CallerMemberName] string propertyName) => OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
protected void SetProperty<T>(ref T storage, T value, PropertyChangedEventArgs args)
{
if (!EqualityComparer<T>.Default.Equals(storage, value))
{
storage = value;
OnPropertyChanged(args);
}
}
protected void SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName) => SetProperty(ref storage, value, new PropertyChangedEventArgs(propertyName));
}
しかしながら、インターフェイスがイベントを発生さえる方法がないため、実際にはうまくいかない。インターフェイスはイベントハンドラーのリストをストアするために使うデリゲートではなく、AddとRemove機能のイベントを定義するだけである。
これは提案の一部ではなく変更される可能性がある。CLRはイベントを“発生させるアクセサメソッド”をストアするスロットを持っているが、主要な言語ではVBのみがこれを使っている。
提案のさらなるサポート
HaloFour氏は次のように書いた:
これはイデオロギーの議論に見えます。.NET 1.0がリリースされて以来、チームが対処できなかった既知の問題があります。標準的な解決策は長い間、知られてきました。彼らはしばしばAPIの混乱を生み出します。IFoo, IFoo2, IFoo3, IFooEx, IFooSpecial, IFooWithBar, etc., etc., etc. 拡張メソッドはこれらの問題解決に近づいていますが、拡張メソッドで明示的に識別/割当できる以外の機能が欠けています。
デフォルト実装はこれらの問題をうまく解決している。Javaチームは、Map#computeIfPresentのようなヘルパーメソッドを追加することで長期間存在した、いくつかのインターフェイスを締めくくることを可能にした。
この提案に対するその他の批判
HerpDerpImARedditor氏は次のように書いた:
これは慢性的なスパゲティコードを生み出すことになるでしょう。なにかを見落としていたら申し訳ないのですが、このパターンは実装レイヤーで解決できなかったなにを解決するのでしょうか?これはインターフェイスと具体的な実装との間に豪華な壁があるように見えます。IDEは実行時の実装がどこから作るのかを必死に特定するつもりでしょうか?IoCで動作するようには見えません。
私はクラシックASP/VBから直接.NETに来ましたが、.NETが大好きです。これは初めて私が賛成できない言語仕様です('dynamic'が来たときには少したじろぎましたが、そのユースケースを見ました)。他の人たちは、この機能の存在を無視すると言っていますが、後から来た他のコードが[この機能]を無視しないことを心配しています。
実際の判断が下される前に行動を起こさないといけないと思いました。
Canthros氏は次のように書いた:
これは残念です。
githubの議論は、最適化された実装を提供するためのタイプスニッフィングなど、LINQの様々な拡張メソッドの実装の混乱に対する不満に端を発しています。言語機能を一般化すれば、.NET Core実装者は多くの労力を節約できます。インターフェイスと抽象クラスの混沌とした境界で言語に負荷をかけて、下流の問題を非常に強く抱かせる機能です。
私はShapes提案によって大幅に緩和されているように感じていますが、本当にすべてについてを考える時間はありません。
Rate this Article
- Editor Review
- Chief Editor Action