C#でクラスを作ろう(8)/クラスの継承
C言語やC++言語などを学んではいるけどクラスをあまり作ったことが無い、という方を対象にしています。
このシリーズでは、C#でクラスを作るための基本的な構文を解説しています。C++やJavaなどと共通している概念も多いですが、サンプルコードは基本的にC#で解説します。ところどころ、C++特有の概念を解説することもあります。
クラスとは、構造体にメソッドが定義できるものであると解説しました。また、クラスには情報隠蔽をする機能が備わっていて、クラスを外側から使う人はクラスの内側の内部実装を詳しく知る必要が無く、関心事を外側と内側で分けられるということを解説しました。
しかし、クラスの有用な点はそれだけではありません。クラスには継承という概念があり、これこそがクラスの最も特徴的な性質と言えるでしょう。
クラスを継承する
あるクラスを親クラスとして、親クラスの全ての機能を受け継ぎつつ新たなフィールド・メソッド・プロパティなどを追加する機能のことを、継承といいます。
//親クラス
class Person
{
//人物を表わすクラス
public string Name { get; set; }
public string Address { get; set; }
public DateTime Birthday { get; set; }
public void EatOniku()
{
Console.WriteLine("おにくもぐもぐ");
}
}
//子クラス
class Student : Person
{
//小学生を表わすクラス。人物を表わす親クラスPersonを継承している
public string SchoolName { get; set; }
public int Grade { get; set; }
public void GradeUp()
{
if (this.Grade < 6)
this.Grade++;
else
Console.WriteLine("卒業おめでとう!");
}
}
//使い方
Student student = new Student();
//親クラスのプロパティやメソッドにアクセスできる
student.Name = "○山×郎";
student.Address = "△△市☆☆町";
student.Birthday = new Date(2012, 5, 13);
student.EatOniku();
//子クラスのプロパティやメソッドにもアクセスできる
student.SchoolName = "☆☆小学校";
student.Grade = 1;
student.GradeUp();
このように、Studentクラスを"class Student : Person"とすることで、Personクラスから継承させることができます。子クラスのStudentは、親クラスのPersonのプロパティやメソッドを全て扱うことができます。
親クラスには他に「基本クラス」「基底クラス」「スーパークラス」などの呼び方があります。子クラスにも「継承クラス」「派生クラス」「サブクラス」などの呼び方があり、いずれも同じ意味です。
クラスを継承すると、親クラスの機能を全て引き継いだ上に、さらに他の機能を追加することができます。クラスの継承は、このように親クラスの機能を拡張するときに使われる手法です。
他の拡張方法との違い
単純に機能を拡張するだけなら、次の2つの方法が考えられます。それぞれ、クラスの継承とどう違うのか、見ていきましょう。
プロパティやメソッドを追加する
元のPersonクラスに単純にプロパティやメソッドを追加する方法です。
class StudentPerson
{
public string Name { get; set; }
public string Address { get; set; }
public DateTime Birthday { get; set; }
public void EatOniku()
{
Console.WriteLine("おにくもぐもぐ");
}
public string SchoolName { get; set; }
public int Grade { get; set; }
public void GradeUp()
{
if (this.Grade < 6)
this.Grade++;
else
Console.WriteLine("卒業おめでとう!");
}
}
このようにして、「小学生の人物」を表わすStudentPersonクラスを作れば、機能を拡張したことになります。
しかしこれではもう、「小学生ではないただの人物」を表わす元のPersonクラスを使うことはできなくなります。逆に元のPersonクラスも残した場合は同じコードが重複して登場することになり、保守性が著しく低下します(修正時、一方だけ修正して、もう一方を修正し忘れるなど)。
さらに、親クラスが他人の作ったものでソースコードが存在しない場合(コンパイル済みのバイナリしか存在しない場合)、このような方法は不可能になります。
親クラスに相当する機能をメンバに持たせる
//親クラス
class Person
{
//略...
}
//親クラスに相当する機能をメンバに持ったクラス
class Student
{
//親クラス相当分
public Person InnerPerson { get; set; } = new Person();
//Studentクラスの独自部分
public string SchoolName { get; set; }
public int Grade { get; set; }
public void GradeUp()
{
if (this.Grade < 6)
this.Grade++;
else
Console.WriteLine("卒業おめでとう!");
}
}
//使い方
Student student = new Student();
//親クラス相当分の機能へのアクセス
student.InnerPerson.Name = "○山×郎";
student.InnerPerson.Address = "△△市☆☆町";
student.InnerPerson.Birthday = new Date(2012, 5, 13);
student.InnerPerson.EatOniku();
//studentクラス独自部分へのアクセス
student.SchoolName = "☆☆小学校";
student.Grade = 1;
student.GradeUp();
このようにすれば、同じコードを重複させることなく、親クラスの機能を拡張することができます。このStudentクラスの中のInnerPersonをprivateなフィールドにし、さらに
class Student
{
private Person _innerPerson = new InnerPerson();
public string Name
{
get { return _innerPerson.Name; }
set { _innerPerson.Name = value; }
}
public string Address
{
get { return _innerPerson.Address; }
set { _innerPerson.Address = value; }
}
public DateTime Birthday
{
get { return _innerPerson.Birthday; }
set { _innerPerson.Birthday = value; }
}
public void EatOniku()
{
_innerPerson.Oniku();
}
//以下略...
}
とすれば、継承したときと同じような構文で外側から内部の親クラスにアクセスできます。このように、内部に親クラスをメンバとして持たせるやり方を合成(または包含)、親クラスのプロパティやメソッドへのアクセスを再定義することを委譲といいます。
継承を使うか合成を使うか
先の節の2つ目の例で、継承の他にクラスを拡張するやり方として、合成という方法を紹介しました。では、継承を使うか合成を使うかの線引きはどこにあるのでしょうか?
一般的には、次のようなガイドラインに沿うと良いとされています。
【継承】
「子クラス is a 親クラス」のように、「is a」の関係であるとき。つまり、Student(小学生)はPerson(人物)でもあるので、「is a」の関係になっている。
【合成】
「親クラス has a 子クラス」のように、「has a」の関係であるとき。例えばランドセルを表わすBugクラスがあり、「ランドセルを持った小学生」という新しいクラスを作りたいとき、Student(小学生)はBug(ランドセル)を持っているので、「has a」の関係になっている。ランドセルという物体は小学生ではないので、「ランドセル is a 小学生」ではない。
従って、例のStudentクラスの場合は「is a」の関係なので、継承を使うほうが適切です。
クラスの継承を使うと、親クラスと子クラスの間にある種の「関係」が生じることになります。先の章で説明するようなポリモーフィズムが複雑に絡み合った状況だと、親クラスに変更を加えたら子クラスにどのような影響があるだろうか、子クラスに変更を加えるためには親クラスの設計も変更しなければならないだろうか、と、関心事が余計に多く増えてしまいます。
そこで近年は、クラスの継承をできるだけ避け、委譲(前述)のためのコードがそれほど大きくならないようであれば、「is a」の関係であっても合成を使うという流儀もあります。
子クラスのコンストラクタ
継承された子クラスにもコンストラクタを定義することができます。インスタンスを作成した際は、親クラスのコンストラクタ→子クラスのコンストラクタの順に呼び出されます。
//親クラス
class Person
{
//中略...
//親クラスのコンストラクタ
public Person(string name)
{
this.Name = name;
}
}
//子クラス
class Student : Person
{
//中略...
//子クラスのコンストラクタ
public Student(string name, string schoolName}
: base(name)
{
this.SchoolName = schoolName;
}
}
//使い方
Student student = new Student("○山×郎", "△△市☆☆町");
子クラスのコンストラクタの定義のところで、先に呼び出される親クラスのコンストラクタへの引数をbase初期化子によって" : base(name)"のように指定します。
親クラスがコンストラクタを持たないとき(デフォルトコンストラクタしかないとき)や、引数無しのコンストラクタが定義されているときは、base初期化子による記法は必ずしも必要ではありません。
protectedアクセス修飾子
C#でクラスを作ろう(3)/アクセス修飾子で、プロパティやメソッドに対するアクセス制限を規定するためのpublicアクセス修飾子、privateアクセス修飾子を紹介しました。
クラスの継承が絡むとき、publicとprivateの中間にあたるprotectedアクセス修飾子を指定することができます。
//親クラス
class Parent
{
private string PrivateMember { get; set; }
protected string ProtectedMember { get; set; }
public string PublicMember { get; set; }
}
//子クラス
class Child : Parent
{
public void SomeMethod()
{
PrivateMember = "あいうえお"; //※エラー! アクセスできない
ProtectedMember = "あいうえお"; //アクセスできる
}
}
//外側から使うとき
Child child = new Child();
child.PrivateMember = "あいうえお"; //※エラー! アクセスできない
child.ProtectedMember = "あいうえお"; //※エラー! アクセスできない
child.PublicMember = "あいうえお"; //アクセスできる
protected修飾子が付けられたメンバーは基本的にはprivateと同じように振る舞います。つまり、クラスの外側からアクセスすることはできません。
ただし例外的に、継承された子クラスの中からは、protectedメンバーにアクセスすることができます。
まずは継承してみよう
どのような場合にクラスを継承すべきかという問題は、しばしば宗教論争に発展します。特に、継承という仕組みの仕様が言語ごとに微妙に違うため、C++では継承すべきだけどC#では継承すべきでない、みたいな事例もあったりします。
言語に拠らない「オブジェクト指向とは何ぞや」という学術的な視点からは、継承は極めて限定的な場面でしか使ってはいけないという意見さえあります。
いろいろな考え方はありますが、まずはとにかくクラスの継承をしてみて、その雰囲気を掴んでみましょう。学術的な意見に捉われず、まずは手を動かして実感してみて、時には他の人の意見も取り入れながら、クラスの継承というものを自分の知識の中に取り込んでみてください。
ディスカッション
コメント一覧
お世話になります。C#初心者です。
一つのクラスに沢山のプロパティがあるのですが、これをグループごとにまとめられないかと思い、検索していたところ、こちらのサイトにたどり着きました。
第8回の記事のクラスの合成によって、下記 Test2 のようにプロパティをグループに分ける事はできるのですが、クラス間でデータを渡すにはどのようにしたらいいのか、という所で詰まってしまいました。
Test2 において、_category1_a, _category1_b, _category2_c, _category2_d を各プロパティに返すためにはどのようにすればいいのか、ご教授いただければ幸いです。
よろしくお願い致します。
// Test1: 全てのプロパティを一つのクラスにまとめるやり方
static void Main()
{
Test1 test1 = new Test1(100);
test1.DoSomething(1, 2, 3, 4);
Console.WriteLine(test1.Category1_A.ToString());
Console.WriteLine(test1.Category1_B.ToString());
Console.WriteLine(test1.Category2_C.ToString());
Console.WriteLine(test1.Category2_D.ToString());
}
public class Test1
{
private int _data;
private int _category1_a;
private int _category1_b;
private int _category2_c;
private int _category2_d;
public Test1(int data)
{
this._data = data;
}
public void DoSomething(int a, int b, int c, int d)
{
// 何らかの処理を行い、_category1_a, _category1_b, _category2_c, _category2_d に結果を格納
_category1_a = _data + a;
_category1_b = _data + b;
_category2_c = _data * c;
_category2_d = _data * d;
}
public int Category1_A { get { return _category1_a; } }
public int Category1_B { get { return _category1_b; } }
public int Category2_C { get { return _category2_c; } }
public int Category2_D { get { return _category2_d; } }
}
// Test2: クラスの合成により、プロパティをカテゴリーに分けるやり方
static void Main()
{
Test2 test2 = new Test2(100);
test2.DoSomething(1, 2, 3, 4);
Console.WriteLine(test2.Category1.A.ToString());
Console.WriteLine(test2.Category1.B.ToString());
Console.WriteLine(test2.Category2.C.ToString());
Console.WriteLine(test2.Category2.D.ToString());
}
public class Test2
{
private int _data;
private int _category1_a;
private int _category1_b;
private int _category2_c;
private int _category2_d;
public Test2(int data)
{
this._data = data;
}
public Category1 Category1 { get; } = new Category1();
public Category2 Category2 { get; } = new Category2();
public void DoSomething(int a, int b, int c, int d)
{
// 何らかの処理を行い、_category1_a, _category1_b, _category2_c, _category2_d に結果を格納
_category1_a = _data + a;
_category1_b = _data + b;
_category2_c = _data * c;
_category2_d = _data * d;
}
}
public class Category1
{
public int A { get { return 1; } } // _category1_a を返したい
public int B { get { return 2; } } // _category1_b を返したい
}
public class Category2
{
public int C { get { return 3; } } // _category2_c を返したい
public int D { get { return 4; } } // _category2_d を返したい
}
public static void test2()
{
Test2 test2 = new Test2(100);
test2.DoSomething(10, 20, 30, 40);
Console.WriteLine(test2.Category1.A.ToString());
Console.WriteLine(test2.Category1.B.ToString());
Console.WriteLine(test2.Category2.C.ToString());
Console.WriteLine(test2.Category2.D.ToString());
}
返信が遅れて申し訳ありません。
ずっとログインしていませんでした。
_category1_aや_category1_bが、他のTest1の要素(_category2_aなど)と
深く関わっているようなら、Test1のままで十分だと思います。
もし、_category1_aや_category1_bがCategory1の中だけで完結するような
独立したフィールドであれば、これらをTest2ではなくCategory1の中に
置いてしまってもいいかもしれません。その場合は、
class Test2
{
Category1 Category1 { get; } = new Category1();
Category2 Category2 { get; } = new Category2();
void DoSomething(int a, int b, int c, int d)
{
this.Category1.A = _data + a;
this.Category1.B = _data + b;
this.Category2.C = _data * c;
this.Category2.D = _data * d;
}
}
class Category1
{
private int _a;
private int _b;
public int A { get { return _a; } set { _a = value; } }
public int B { get { return _b; } set { _b = value; } }
}
//Category2も同様
という形が最適かと思います。
ありがとうございます。こちらこそ返信遅くなりすみません。
Test1ではプロパティ (Category1_A, Category1_B, Category2_C, Category2_D) を読み取り専用にできますが、Test2やご紹介いただいた方法でプロパティ (A, B, C, D) を読み取り専用にするにはどのようにすればよいでしょうか?
A,B,C,Dを単に読み取り専用にするだけなら、
set { _a = value; }
などを書かなければいいだけですが、これだとTest2.DoSomething内で
this.Category1.A = _data + a;
等ができなくなってしまいます。そこで、
class Category1
{
public void DoSomething(int data, int a, int b)
{
_a = data + a;
_b = data + b;
}
}
class Category2
{
public void DoSomething(int data, int c, int d)
{
_c = data * c;
_d = data * d;
}
}
のようにCategory1のDoSomething、Category2のDoSomethingを用意し、
class Test2
{
public void DoSomething(int a, int b, int c, int d)
{
this.Category1.DoSomething(_data, a, b);
this.Category2.DoSomething(_data, c, d);
}
}
大元のTest2のDoSomethingではこのようにする形になるかと思います。
できました!
どうもありがとうございます!大変助かりました。
C#に常に触れている訳ではないので、時間が経つと忘れてしまって、思い出すのに苦労しますが、また何かありましたら質問させていただくかも知れませんが、その時はよろしくお願い致します。
お役に立てたのなら、何よりです~。
いつも返信が遅くてすみません。
ちゃんとコメントのメール通知が来るように設定しておきます…m(_ _)m