本サイトからスピンオフした姉妹サイトです。破壊的な影響を他に及ぼすことなくインターフェースの機能を拡張するには、デフォルト実装に加えて拡張メソッドも使用できる。今回はこれら2つの方法がなぜ必要なのか、それぞれが得意としている分野について詳しく見る。→デフォルト実装と拡張メソッドC#以外の言語が持つ「インターフェースのデフォルト実装」に類似の機能、例えばJavaのデフォルトメソッドは、「既存のインターフェースに対して、利用側のコードを壊さずに、インターフェース機能を拡張するもの」と説明される。C#で「拡張」というと、すでに拡張メソッドという機能があって、同じ目的のものがすであるのにどうしてデフォルト実装が必要なのかと思うかもしれない。これら2つの機能は、確かに同じ目的に使える部分もあるが、それぞれにしかできないことも存在している。ひとくちに「インターフェースの機能拡張」といっても、実のところ、拡張の仕方には以下のような2種類のものがある。インターフェースの作者以外の人が、インターフェースとも、インターフェースを実装するクラスとも別のライブラリで機能を追加したいインターフェースの作者自身が、新バージョンを作るに当たって、インターフェースを実装する既存のコードを壊さないように機能を追加したい拡張メソッドは前者向け、デフォルト実装は後者向けの機能である。その他にも細かな違いはあるが、この「それぞれにしかできないこと」の価値は高くて、両方の機能が必要とされる。これが、今、C#にデフォルト実装を導入しようとしている動機となる。これらの「それぞれにしかできないこと」に関して、もう少し詳しく見てみよう。拡張メソッドの利点まずはC#利用者にとってなじみの深い拡張メソッドから見て行こう。x)というように、インスタンスメソッドと同じ書き方ができるようにする単純な機能である。非常に単純なため、妥協的で低機能と勘違いするかもしれないが、案外この実装方法が有利な点は多い。以下のような点は、デフォルト実装ではできず、拡張メソッドでだけできるものである。使わない選択(要らないときはusingしなければIntelliSenseを汚さない)これら3つの点について少し考えてみよう。第三者拡張インターフェース自体に手を入れてメンバーを追加するのは、インターフェースの作者自身が行う必要がある。これは結構大きなデメリットになる。何か欲しい機能があったときに、作者に提案を送って実装してもらい、それがリリースされるのを待つのは現実的ではない。例として、LINQ的なちょっとした便利機能を紹介しよう。リスト12は、よくある「インデックス付きのforeach」を拡張メソッドでやっている例である。7.0でタプルと分解構文が使えるようになったので、リスト12ではさっそくこれを使っている(NuGetからSystem.ValueTupleパッケージをインストールする必要がある)。いう新機能を活用したライブラリは、C#がリリースされてそれが世に広まってから、その次の世代でようやく標準で提供されたりする。標準提供されるのを待っていたら、いつそれを使えるようになるかは分からないものである。これに対して、拡張メソッドであれば、必要なときに自分自身でメンバーを追加できる。ちなみに、このコードは実のところ手抜きである。名前はこれでいいのかという検討もしていないし、例外処理もさぼっている。自分向けに書いたコードだからこそ許されている面もある。標準ライブラリとしてIEnumerable<T>自体にメンバーを追加するとなると、こういう手抜きはできないだろう。程よく手抜きができるのも、第三者が自由に機能追加できる拡張メソッドのいい点だろう。選択性インターフェースを使いたい全ての人が、そのインターフェースの拡張機能まで使いたいとは限らない。Object(=System.Linq.Enumerableクラス中の拡張メソッド)は不要という場面も多いだろう。インターフェースと、それに対する拡張機能は関心の範囲が異なることが多い。関心もないものが常に付属してくると正直迷惑だろう。大は小を兼ねる的に「あっても困らないだろう」と思うかもしれないが、使い勝手は確実に落ちる。無意味に機能を増やすと、本当に必要とするものを探しにくくなるのはそれなりのストレスである。System.Linq;ディレクティブの有無によって拡張メソッドを使うか使わないかを選択できる。図15に示すように、明示的にusingディレクティブを書いて使うことを選択したときにだけ、拡張メソッドがIntelliSenseの補完候補に現れる。必要なときに必要なだけ検索候補を得られるのも拡張メソッドのメリットである。特殊化C#のジェネリクスでは、型の特殊化は認めていない。要するに、あるジェネリックな型X<T>があるとき、X<int>のような型引数が特定の型に特殊化された場合にだけ、実装を別のものにするようなことはできない。インターフェースにデフォルト実装が入っても、この制限は引き継がれる。IEnumerable<T>の要素の和を計算するような場合を考えよう。LINQのSumメソッドがまさにその例だが、ジェネリックな型引数Tに対して加算演算子を使えないため、一般には和の計算ができない。intやdoubleなど、加算演算子を持っている型に対してだけ実装がある。インターフェースの特殊化はできないので、このような実装を行うには拡張メソッドが必要になる(拡張メソッドであれば、リスト13の要領で実装できる)。拡張メソッドにはできなくて、デフォルト実装でならばできることがある。前編(第13回)でも話した通り、インターフェースにデフォルト実装を認める最大の動機は、インターフェースにメンバーを追加しても破壊的変更を起こさないようにすることにある。インターフェースに対するメンバー追加が破壊的な変更となる問題について、一番悩まされているのは.NETの標準ライブラリである。標準ライブラリはそのプログラミング言語で最も広く使われるライブラリであり、当然、破壊的な変更は避けなければならない。.NETの開発チームは初期の段階からインターフェースの問題を認識していたため、.NETの標準ライブラリはインターフェースを避ける傾向にある。インターフェースを定義しておいてほしいような場面でも、代わりに抽象クラスが使われがちだ。.NETチームも、以下のような2つの意味で現状をつらいと思っているようである。インターフェースに後からメンバーを足せないことで、別のインターフェースを追加する必要があるなど、回りくどい実装になるのがつらい抽象クラスでは、多重継承ができなくてつらいかつてはインターフェースを避けていたような箇所でも、もうあきらめてインターフェースを使う場面も見られる。以下のコードを見てほしい。IVariableDeclarationStatementこれは標準ライブラリではなくC#コンパイラーのソースコードだが、そこには以下のようなコメントが添えられている。future.このインターフェースは実装上、関連するAPIによって差し押さえられている。われわれ(=C#コンパイラーチーム)は、これを将来的に変更する権利を予約する。これは、インターフェースに対して、破壊的変更になることが分かっている「メンバーの追加」を将来的に行うという予告である。つまるところ、このインターフェースを第三者が実装することは推奨していないし、それで破壊的変更になっても責任は取らないと言っている。コメントで注釈している辺り、言語機能の敗北である。したことからも、インターフェースへのメンバー追加ができるようになるデフォルト実装が強く求められていることがよく分かる。仮想呼び出し拡張メソッドで困るのは、仮想呼び出しができないことである。ある特定の派生型でだけ特別な処理をするということが難しい。EnumerableクラスのLastOrDefaultメソッドの実装はリスト14のようになっている。IList<T>のようにインデクサーが使えるものに対してMoveNextでの列挙をするのは完全に無駄なので、特別な分岐が存在する。LastOrDefaultの実装(抜粋)このような実装には2点ほど難点がある。IList<T>に対してしかこの最適化は働かない。IReadOnlyList<T>ではこの最適化が掛かっておらず、MoveNextが呼ばれる効率の悪いコードが実行されるas演算子を使った分岐が不格好これに対して、拡張メソッドが持つ「第三者が機能追加できる」「関心が分離される」という利点が失われるため一長一短だが、インターフェースのデフォルト実装機能を使えばリスト15のような書き方ができる。他のインターフェースでも、デフォルトの実装よりも効率的な実装が可能であれば、それぞれでoverrideすればよい。場合によっては、ほとんどデフォルトの実装しか使わないメソッドもあるだろう。中には歴史的経緯でそうせざるを得なかったものもある。ジェネリックなIEnumerable<T>インターフェース(System.Collections.Generic名前空間)は、もともとあったIEnumerableインターフェース(System.Collections名前空間)を継承しているが、IEnumerableの方のGetEnumeratorはほぼ全てのクラスでリスト16に示すような実装になるだろう。IEnumerable.GetEnumeratorの実装例ほぼ全てがリスト16と同じ実装になるのであれば、リスト17に示すように、インターフェース側にデフォルト実装を持たせるべきだろう。IEnumerable.GetEnumeratorにデフォルト実装を与える例まとめデフォルト実装はインターフェースの「拡張」に使われるものである。C#で拡張というとすでに拡張メソッドがあるわけだが、デフォルト実装と拡張メソッドではそれぞれにしかできないことがある。拡張メソッドには、第三者による拡張ができる、拡張機能の独立性・選択性がある、特殊化ができるなどの利点がある。その一方で、インターフェース自身へのメンバー追加や、仮想呼び出しについてはデフォルト実装でしかできない。このそれぞれにしかできない部分には大きな価値がある。拡張メソッドがあるC#にもデフォルト実装が必要とされているし、デフォルト実装が導入されたからといって拡張メソッドの立ち位置が変わることはない。1.0がプレビュー版だった頃からC#によるプログラミング入門を公開していて、C#とともに今年で15年になる。最近の自己紹介は「C#でググれ」。※以下では、本稿の前後を合わせて5回分(第13回~第17回)のみ表示しています。INDEX]を参照してください。C#への「インターフェースのデフォルト実装」の導入(前編)C#におけるインターフェースとは、ある型が持つべきメソッドを示す「契約」であり、実装は持てない。このことが大きな問題となりつつある。今回から全3回に分けて、C#がこの問題にどう対処しようとしているかを見ていく。C#への「インターフェースのデフォルト実装」の導入(中編)前回は一般論としてのインターフェースとその課題を見た。今回はC#にインターフェースのデフォルト実装を導入すると、どのようなコードが書けるようになるのか、導入するために必要な修正点などについて見ていく。C#への「インターフェースのデフォルト実装」の導入(後編)破壊的な影響を他に及ぼすことなくインターフェースの機能を拡張するには、デフォルト実装に加えて拡張メソッドも使用できる。今回はこれら2つの方法がなぜ必要なのか、それぞれが得意としている分野について詳しく見る。7.0で登場した新しいタプル(ValueTuple構造体)は、複数の値をひとまとめにして扱うのに便利なデータ構造だが、その実装は一般的な構造体のガイドラインに従っていない。なっているのか、技術的背景を追う。7.1がリリースされた。7.2/7.3に含まれる予定の機能、そこから見えてくるものについて考えてみよう。機械学習エンジニアを目指す人~初中級者のための技術情報サイトです。「機械学習&AI」「クラウド&ビッグデータ」「IoTデバイス」のネット記事情報を共有中。開発者のためのSlackチーム、作りました:「機械学習&AI」「クラウド&ビッグデータ」「IoTデバイス」のネット記事情報を共有中。2020最適なビューへの切り替えを推奨します:スマートフォン用表示に。