.Netのインターフェイスについて少し(1つのインタビューに基づいて)

先週の月曜日、私は幸運なことに、ある国際企業のシニア.Net開発者のインタビューを受けました。 インタビュー中に、.Netに関連するいくつかの質問があるテストを受けるように頼まれました。 特に、質問の1つでは、いくつかのステートメントの評価(true / false)を行う必要がありました。



.Netでは、int []などの要素の配列はデフォルトでIListを実装します。これにより、foreachステートメントでコレクションとして使用できます。





この質問にすばやく否定的に答え、マージンを個別に追加します。 foreachにはIListではなくIEnumerableの実装が必要であるため、次の質問に進みました。 しかし、帰り道で、私は質問に苦しめられました:配列はまだこのインターフェースを実装していますか?



IListについて、このインターフェイスがIEnumerable、インデクサー、コレクション内の要素の数を含むCountプロパティ、およびIsFixedCollection()などのめったに使用されないプロパティを提供したことを漠然と思い出しました。 配列にはサイズのLengthプロパティがあり、IEnumerableのCountはLINQの拡張メソッドであり、このメソッドがクラスに実装されている場合は不可能です。 したがって、配列がIListインターフェイスを実装できなかったことが判明しましたが、漠然とした気持ちに悩まされました。 したがって、インタビュー後の夕方、私は少し調査することにしました。





クラスSystem.Array



Reflector.Netはインストールされていないため、整数配列によって実装されるインターフェイスを調べるための短いプログラムをC#で作成しました。



var v = new int[] { 1, 2, 3 }; var t = v.GetType(); var i = t.GetInterfaces(); foreach(var tp in i) Console.WriteLine(tp.Name);
      
      







コンソールウィンドウから受信したインターフェイスの完全なリストは次のとおりです。



 ICloneable IList ICollection IEnumerable IStructuralComparable IStructuralEquatable IList`1 ICollection`1 IEnumerable`1 IReadOnlyList`1 IReadOnlyCollection`1
      
      







したがって、 .Net配列は、IListインターフェイスとその汎用バージョンのIList <>をまだ実装しています



より完全な情報を取得するために、System.Arrayクラスの図を作成しました。







私のミスはすぐに私を襲いました:CountはIListのプロパティではなく、継承チェーンの以前のインターフェイスであるICollectionです。 ただし、このインターフェイスの他のプロパティIsFixedSizeおよびIsReadOnlyが実装されていたにもかかわらず、配列自体にはIListインターフェイスの他の多くのプロパティほどのプロパティはまだありませんでした。 これはどのように可能ですか?



C#でインターフェイスを実装できるだけでなく、

暗黙的ですが、明示的にも。 私は教科書からこの可能性について知っていました。それはそのような実装の例を示しました。 基本クラスに既にインターフェイスメソッドと同じ名前のメソッドが含まれている場合。 ReSharperでもこのような機会を見ました。 ただし、これまでのところ、自分のプロジェクトに明示的にインターフェイスを実装する必要性に直接対処する必要はありませんでした。



インターフェイスの明示的実装と暗黙的実装の比較



これら2種類のインターフェイスの実装を比較してみましょう。



基準

暗黙的な実装

明示的な実装

基本的な構文

 interface ITest { void DoTest(); } public class ImplicitTest : ITest { public void DoTest() { } }
      
      





 interface ITest { void DoTest(); } public class ExplicitTest : ITest { void ITest.DoTest() { } }
      
      







可視性

暗黙的な実装は常にパブリックであるため、メソッドとプロパティに直接アクセスできます。

 var imp = new ImplicitTest(); imp.DoTest();
      
      





明示的な実装は常に閉じられています(プライベート)。

実装にアクセスするには、クラスのインスタンスをインターフェイスにキャストする必要があります(インターフェイスにアップキャスト)。

 var exp = new ExplicitTest(); ((ITest)exp).DoTest();
      
      





多態性

暗黙的なインターフェイスの実装は仮想にすることができます。これにより、子孫クラスでこの実装を書き換えることができます。

明示的な実装は常に静的です。 新しい子孫クラスでオーバーライドまたはオーバーライドすることはできません。 ご注意 1

抽象クラスと実装

暗黙の実装は抽象的であり、子孫クラスでのみ実装できます。

明示的な実装は抽象的ではないかもしれませんが、クラス自体は他の抽象メソッドを持ち、抽象的であるかもしれません。 ご注意 2



注:

ご注意 1 - mayorovpがコメントで正しく観察しているように、子孫クラスのインターフェイスの明示的な実装を繰り返して実装をオーバーライドできます(記事の最初のコメントを参照)。



ご注意 2- ブログの1つは、クラス自体を抽象化できないことを示しました。 おそらくこれは、以前のバージョンのコンパイラのいくつかに当てはまりました。私の実験では、抽象クラスで明示的にインターフェイスを簡単に実装できました。



なぜインターフェイスの明示的な実装が必要なのですか?



MSDNによると、クラスによって実装された複数のインターフェイスに同じシグネチャを持つメソッドがある場合、明示的なインターフェイス実装が必要です。 この問題は、一般的に英語圏では「死の致命的なダイヤモンド」という恐ろしい名前で知られており、ロシア語では「ダイヤモンドの問題」と訳されています。 そのような状況の例を次に示します。



 /* Listing 1 */ interface IJogger { void Run(); } interface ISkier { void Run(); } public class Athlete: ISkier, IJogger { public void Run() { Console.WriteLine("Am I an Athlete, Skier or Jogger?"); } }
      
      







ちなみに、この例はC#の正しいコードです。つまり、(正しく)コンパイルされて起動されますが、Run()メソッドはクラス自体のメソッドであり、すでに2つのインターフェイスの実装です。 したがって、異なるインターフェイスとクラス自体に対して1つの実装を使用できます。 次のコードでこれを確認できます。



 /* Listing 2 */ var sp = new Athlete(); sp.Run(); (sp as ISkier).Run(); (sp as IJogger).Run();
      
      







このコードの結果は「アスリート、スキーヤー、ジョガーですか?」となり、コンソールに3回表示されます。



ここで、3つのケースすべてを分離するために、インターフェイスの明示的な実装を使用できます。



 /* Listing 3 */ public class Sportsman { public virtual void Run() { Console.WriteLine("I am a Sportsman"); } } public class Athlete: Sportsman, ISkier, IJogger { public override void Run() { Console.WriteLine("I am an Athlete"); } void ISkier.Run() { Console.WriteLine("I am a Skier"); } void IJogger.Run() { Console.WriteLine("I am a Jogger"); } }
      
      







この場合、リスト2のコードを実行すると、コンソールに「私はアスリート」「私はスキーヤー」「私はジョガー」という 3行が表示されます。



さまざまなインターフェイス実装の長所と短所



実装の可視性とカスタム実装


上記のように、暗黙の実装は通常のクラスメソッドと構文的に違いはありません(このメソッドが祖先クラスで既に定義されている場合、この構文ではメソッドは子孫で非表示になり、コードは問題なくコンパイルされますcメソッドの非表示に関するコンパイラ警告。)。 さらに、1つのインターフェイスの個々のメソッドの選択的な実装は、明示的および暗黙的に可能です。



 /* Listing 4 */ public class Code { public void Run() { Console.WriteLine("I am a class method"); } } interface ICommand { void Run(); void Execute(); } public class CodeCommand : Code, ICommand { // implicit interface method implementation // => public implementation // implicit base class method hiding (warning here) public void Run() { base.Run(); } // explicit interface method implementation // => private implementation void ICommand.Execute() {} }
      
      







これにより、個別のインターフェイスメソッドの実装をネイティブクラスメソッドとして使用でき、対応するインターフェイスにキャストした後にのみプライベートで表示されるメソッドの明示的な実装とは対照的に、たとえばIntelliSenseを介して使用できます。



一方、メソッドのプライベート実装の可能性により、完全に実装しながら多くのインターフェイスメソッドを隠すことができます。 .Netの配列を使用した最初の例に戻ると、配列は、たとえばICollectionインターフェイスのCountプロパティの実装を隠し、このプロパティをLengthという名前で公開していることがわかります(これはおそらくC ++ STLおよびJavaとの互換性を維持するための試みです)。 したがって、実装されたインターフェイスの特定のメソッドを非表示にし、他のメソッドを非表示(=公開)にすることはできません。



ただし、これらのインターフェイスのメソッドもプロパティもIntelliSenseで表示されないため、多くの場合、クラスによって実装されるインターフェイスを「暗黙的に」推測することは完全に不可能であるという問題が発生します(System.Arrayの例もここに示します)。 このような実装を検出する唯一の方法は、たとえばVisual Studioのオブジェクトブラウザーを使用して、リフレクションを使用することです。



インターフェイスリファクタリング


インターフェイスの暗黙的な(パブリック)実装は、パブリッククラスメソッドの実装と変わらないため、インターフェイスをリファクタリングし、パブリックメソッドを削除する場合(たとえば、上記のICommandインターフェイスからRun()およびExecute()メソッドを1つのRun( ))すべての暗黙的な実装では、オープンアクセスメソッドが残ります。このパブリックメソッドは、システムの他のコンポーネントですでに異なる依存関係を持っている可能性があるため、リファクタリング後でもサポートする必要がありそうです。 この結果、以前のインターフェイスメソッドの特定の(および異なるクラス、おそらく異なる)実装間で依存関係が既に存在するため、「実装ではなくインターフェイスに対して」というプログラミング原則に違反します。



 /* Listing 5 */ interface IFingers { void Thumb(); void IndexFinger(); // an obsolete interface method // void MiddleFinger(); } public class HumanPalm : IFingers { public void Thumb() {} public void IndexFinger() {} // here is a "dangling" public method public void MiddleFinger() {} } public class AntropoidHand : IFingers { void IFingers.Thumb() {} void IFingers.IndexFinger() {} // here the compiler error void IFingers.MiddleFinger() {} }
      
      







インターフェイスのプライベート実装の場合、より存在しないメソッドの明示的な実装を持つすべてのクラスは単にコンパイルを停止しますが、不要になった実装を削除した後(または新しいメソッドにリファクタリングした後)、インターフェイスにバインドされていない「余分な」パブリックメソッドはありません。 もちろん、インターフェイス自体への依存関係をリファクタリングする必要があるかもしれませんが、少なくとも「実装ではなくインターフェイスへのプログラム」という原則に違反することはありません。



プロパティに関しては、暗黙的に実装されたインターフェイスプロパティ(プロパティ)を使用すると、外部およびクラス自体から直接アクセサーメソッド(ゲッターとセッター)を介してアクセスできます。これにより、不要な効果(たとえば、初期化中の不要なデータ検証)プロパティ)。



 /* Listing 6 */ interface IProperty { int Amount { get; set; } } public class ClassWithProperty : IProperty { // implicit implementation, public public int Amount { get; set; } public ClassWithProperty() { // internal invocation of the public setter Amount = 1000; } } public class ClassWithExplicitProperty : IProperty { // explicit implementation, private int IProperty.Amount { get; set; } public ClassWithExplicitProperty() { // internal invocation isn't possible // compiler error here Amount = 1000; } }
      
      







インターフェイスプロパティの明示的な実装では、これらのプロパティはプライベートのままであり、アクセスのために「長い」道を進み、初期化が行われる追加のプライベートフィールドを宣言する必要があります。 その結果、プロパティアクセスメソッドが外部アクセスにのみ使用される場合、これによりコードが簡潔になります。



ローカル変数とクラスフィールドの明示的な型指定の使用


インターフェイスの明示的な実装の場合、クラスのインスタンスではなく、インターフェイスのインスタンスで作業していることを明示的に示す必要があります。 したがって、たとえば、型推論を使用して、サービスワードvarを使用してC#でローカル変数を宣言することはできなくなります。 代わりに、ローカル変数を宣言するとき、およびメソッドのシグネチャとクラスのフィールドで、インターフェイスのタイプを示す明示的な宣言を使用する必要があります。



したがって、一方では、コードの柔軟性をいくらか低くします(たとえば、ReSharperは、可能であれば常にvarを使用した宣言の使用を推奨します)が、システムが大きくなりサイズが大きくなるにつれて、特定の実装へのバインドに関連する潜在的な問題を回避します。コード。 この点は多くの物議をかもしているように思えるかもしれませんが、プロジェクトに取り組んでいる人や世界のさまざまな地域でさえ、明示的なタイピングを使用するとコードの可読性が向上し、メンテナンスのコストが削減されるため、非常に便利です。



関連ソース
記事の準備では、多くのネットワークソース、特にブログ( [1][2][3]および[4] )からの情報、およびStackOverflowからの[5]および[6]の質問からの情報が使用されました。 CodeProjectおよびJeffrey Richterの「 C#経由のCLR 」の13.5章。

少額のボーナス:埋め戻すための2つの質問(興味をそそる人向け)
これらの質問は、インターフェイスの明示的な実装のトピックに直接関連していませんが、ここで誰かに興味があるかもしれないように思えます:

1.リスト2の属性が別の行である場合

 (sp as Sportsman).Run();
      
      





コンソールには何が表示されますか?



2.リスト3の最小限の変更(あるキーワードを別のキーワードに置き換える)を使用して、コンソールへの最初の質問で「私はスポーツマンです」というフレーズ取得するにはどうすればよいですか?




All Articles