C#でクラスを作ろう(12)/インターフェース
C言語やC++言語などを学んではいるけどクラスをあまり作ったことが無い、という方を対象にしています。
このシリーズでは、C#でクラスを作るための基本的な構文を解説しています。C++やJavaなどと共通している概念も多いですが、サンプルコードは基本的にC#で解説します。ところどころ、C++特有の概念を解説することもあります。
「C#でクラスを作ろう(10)/仮想メソッド」の抽象メソッドの章で、抽象的なクラスに抽象的なメソッドを定義する手法を紹介しました。
インターフェースは、それをもっと抽象的にした概念です。
インターフェースの定義
インターフェースは、抽象メソッドと抽象プロパティのみを持つ抽象クラスのようなものです。その2つを書き並べてみます。
//インターフェース
interface ICryable
{
string CryString { get; set; }
void Cry();
}
//抽象メソッドのみを持つ抽象クラス
abstract class Cryable
{
public abstract string CryString { get; set; }
public abstract void Cry();
}
インターフェースは、キーワード“Interface"を使って定義します。インターフェース名に特に決まりはありませんが、先頭に大文字の「I」を付けるのがC#での慣習になっています。
インターフェースの中には、実装を持たない抽象的なメソッドとプロパティのみを定義することができます。定義の方法は抽象メソッドと似ていますが、publicやprivateなどのアクセス修飾子を付けることはできず、常にpublicになります。
※インターフェース内で定義しているプロパティの定義構文は自動実装プロパティの定義構文と同じですが、インターフェースの場合は全く実体を持たない抽象的なものです。
インターフェースの(暗黙的な)実装
インターフェースは、クラスで「実装」することによって、具体的な実体を持つようになります。その構文もまた、抽象クラスを継承するときと似ています。
class Cat : ICryable
{
public string CryString { get; set; }
public void Cry
{
Sound.Play("cat.mp3");
}
}
class Dog : ICryable
{
public string CryString { get; set; }
public void Cry
{
Sound.Play("dog.mp3");
}
}
//使い方
ICryable[] items = new ICryable[2];
items[0] = new Cat();
items[0].CryString = "にゃー";
items[1] = new Dog();
items[1].CryString = "わんわん";
for (int i = 0; i < 2; i++)
items[i].Cry();
いかがでしょうか。ほとんど仮想メソッドのオーバーライドと同じです。ただし、overrideのようなキーワードは必要ありません。単純に同名のメソッド(Cry)を定義すれば、インターフェースメソッドを実装したことになります。
※プロパティ(CryString)はここで初めて実装されます。"string CryString { get; set; }"という構文は全く同じですが、インターフェースに書かれていたものは「抽象的な定義」で、クラスで書かれたものは「バッキングフィールドを備えた具体的なプロパティの自動実装」です。
使い方も似ています。インターフェース型にアップキャストして、インターフェース型のオブジェクトとして使うことができます。
クラスの継承と似たような使い方になることから、インターフェースの場合でも「CatクラスはICryableインターフェースを継承している」というふうに表現します。
インターフェースを継承したクラスは、全てのインターフェースメソッドを実装しなくてはいけません。実装漏れがある場合にはコンパイルエラーになります。
インターフェースの明示的な実装
先ほどのように、インターフェースメソッドと同名のメソッドを単純に定義して実装する方法を、暗黙的な実装といいます。「暗黙的な」という言葉が無く単に「実装」と表現される場合は、この暗黙的な実装を指している場合が多いです。
一方、インターフェースを明示的に実装する方法があります。
class Horse : ICryable
{
string ICryable.CryString { get; set; }
void ICryable.Cry()
{
Sound.Play("horse.mp3");
}
}
インターフェースメソッドやプロパティにインターフェース名とドット"ICryable."を付けると、明示的な実装になります。明示的な実装にはpublicなどのアクセス修飾子を付けることはできません。
暗黙的な実装と明示的な実装では、挙動に違いがあります。その例を以下に示します。
//暗黙的な実装の場合
Cat cat = new Cat();
cat.Cry(); //実行可能
ICryable icat = (ICryable)cat;
icat.Cry(); //実行可能
//明示的な実装の場合
Horse horse = new Horse();
horse.Cry(); //※コンパイルエラー!
ICryable ihorse (ICryable)horse;
ihorse.Cry(); //実行可能
暗黙的な実装の場合、メソッドCryはCatクラスで定義されたメソッドであり、かつ同時に、ICryableインターフェースの実装でもあります。従って、CatクラスからでもICryableインターフェースからでも呼び出すことができます。
一方、明示的な実装の場合、メソッドCryはHorseクラスに特化されたICryableインターフェースの実装でしかなく、Horseクラスのpublicなメソッドではありません。従ってHorseクラスから直接呼び出すことはできず、必ずICryableインターフェースにアップキャストする必要があります。
基本的には、暗黙的な実装(単純に同名のメソッドを定義する方法)を使っておけば問題ありません。明示的な実装は、インターフェースメソッドの動作を厳しく制限するときなどにのみ、使います。
インターフェースの継承
インターフェースからインターフェースへ継承することもできます。
interface IParent
{
void ParentSomeMethod();
}
interface IChild : IParent
{
void ChildSomeMethod();
}
class RealClass : IChild
{
public void ParentSomeMethod() { /*何か実装*/ }
public void ChildSomeMethod() { /*何か実装*/ }
}
IChildインターフェースはIParentインターフェースを継承しています。この時点で、IChildインターフェースは親のParentSomeMethodと自分自身のChildSomeMethodという2つのメソッドを定義していることになります。
IChildインターフェースを実装する具体的なRealClassでは、インターフェースで定義されているParentSomeMethodとChildSomeMethodの2つを両方実装しなければいけません。
インターフェースの使いどころ
インターフェースの文法の解説は以上です。ここまで読み進めた方の中には、次のような疑問を抱いた方もおられるかもしれません。
インターフェースって、抽象クラスに抽象メソッドを定義するのと同じことじゃん。何が面白いの?
次回に解説する多重継承を除けば、全くおっしゃる通りです。しかも、抽象メソッドの他に具体的な(実体のある)メソッドも併せ持つことが出来る抽象クラスと違って、インターフェースは具体的なメソッドを一切持つことができません。制限が強くなっただけで、使いどころがよくわからない…。
この疑問に対する答えは、言語仕様的なものというよりは、思想的なところにあります。
インターフェースというのは、「こういうメソッドがありますよ」という規約だけを定めたものである、と考えるとしっくりくるかもしれません。
先の例のICryableインターフェースは、「鳴くことができる」というインターフェースであり、「鳴く」というメソッドがあるということだけを定めています。このインターフェースを実装するクラスは、動物クラスであることもあれば、まるで鳴き声のように聞こえる楽器のクラスであることもあります。もしかしたらボーカロイドかもしれません。
クラスの継承が「is-a」の関係であったことを思い出してください。クラスの継承の場合、親クラスは「何か」であり、子クラスもまたその「何か」であることが求められます。
しかしインターフェースの場合は、「何か」ですらありません。インターフェースを実装するクラスに対して「『何か』でありなさい」と求めることは無く、単に「このメソッドを持ちなさい」と規定しているだけにすぎません。
つまりインターフェースは、広くいろいろなクラスに対して、単にどんなメソッドを持つかということだけを定める規約を作る場合に、使用します。
インターフェースが使われている具体例
実は既に、インターフェースが有効に使われている例を過去の章で紹介しています。そのときはインターフェースメソッドとしては紹介しませんでしたが、アンマネージリソースの破棄のところで紹介したDisposeメソッドがこれにあたります。
実は、Disposeメソッドは、IDisposableインターフェースのメソッドとして定義されています。
interface IDisposable
{
void Dispose();
}
IDisposableインターフェースを実装するクラスはたくさんあります。ファイル関連のクラス、ウィンドウ関連のクラス、TCP/IP通信関連のクラスなどなど、いずれも破棄可能な(Disposableな)クラスにIDisposableクラスが継承されています。
IDisposableクラスは、「破棄を実行するDisposeというメソッドを持つ者に、私の名前『IDisposable』という規約の保持者であるという称号を与えよう」と宣言している、というわけです。
アプリケーション全体で、ファイルやウィンドウやTCP/IP通信など、ありとあらゆる「破棄可能な」クラスが生成されます。それらを一ヶ所にまとめておき、アプリケーション終了時に全部一斉に破棄する、なんてことも可能になります。
List<IDisposable> objList; //ここに、ファイルやウィンドウなどがごちゃ混ぜにたくさん入ってる
//アプリケーション終了時
foreach (IDisposable obj in objList)
obj.Dispose();
そしてさらにIDisposableインターフェースが有用な点は、usingという一種のシンタックスシュガー(糖衣構文)が使えるというところにあります。
using (FileStream fs = new FileStream("abc.txt"))
{
//ファイルを使う...
}
//上の構文は、以下の構文とほぼ等価です
try
{
FileStream fs = new FileStream("abc.txt");
}
finally
{
fs.Dispose();
}
usingによって、確実にオブジェクトが破棄されることが約束されます。そしてそれは、usingのカッコ()の中で宣言されたオブジェクトがIDisposableインターフェースを継承していることによって担保されています。
C#8.0でのインターフェースの制限緩和
C#の言語バージョン「C#8.0」では、インターフェースには抽象メソッドと抽象プロパティしか持てないという制限が大幅に緩和されました。
C#8.0のインターフェースには、メソッドに実装を持つことができ、静的(static)なメソッドを持つこともできます。各メソッドにpublicやprivateなどのアクセス修飾子を指定することもできます。
…ってそれ、もうほとんど抽象クラスやん。
そのあたりのことは、こちらの記事に詳しく書かれています。
確かに、インターフェースにデフォルト実装を持ちたくなるときはあります。とりあえず何もしない空の実装をデフォルトで定義しておけば、何か特別な動作をさせたいときはクラス側で新たに実装し、その必要が無いときは無駄にコードを追加しなくてもいい。
いや、だからそれ、ほとんど抽象クラスやん。
インターフェースの制限の緩和については、意見がわかれるところです。この制限の緩和を歓迎すべきなのか、そんなことしたいならインターフェースじゃなくて抽象クラスにすればいいとか、そもそも万物はインターフェースであるべきとか…。
あまり深入りすると宗教論争に発展するので、どの考え方が正しいとかじゃなくて、どの考え方も認めるという寛容なスタンスでいいと思います。
ディスカッション
コメント一覧
まだ、コメントがありません