継承(クラスの継承)
継承にはクラス、Windowsフォーム、WEBフォームと大きく分けて3種類あります。今回はクラスの継承のお話しです。継承の中でも一番基礎的な部分となります。
VB6出身の方だと聞きなれない言葉「継承」ですが、簡単に言えば「コピーを使いまわす」といったところです。但しコピーと違う点として、以下が考えられます。
継承は・・・
一部書き換えが可能
すべてが継承されるわけではない
継承元(基底と言います)を呼び出せる
継承される元のクラスを「基底クラス」、「スーパークラス」などと言ったりします。私のブログでは「基底クラス」に統一します。
継承したクラスを「派生クラス」、「サブクラス」などと言います。こちらも私のブログでは「派生クラス」で統一です。
早速やってみよう
基底クラス(BaseClass)を以下のようにします。
public class BaseClass { public int test1 = 1; protected int test2 = 2; private int test3 = 3; public string Prop { get; private set; } = string.Empty; public void Piyo(string value) { Console.WriteLine(value); } }
Public Class BaseClass Public test1 As Integer = 1 Protected test2 As Integer = 2 Private test3 As Integer = 3 Private prop As String = String.Empty Public Property Prop1() As String Get Return prop End Get Private Set(value As String) prop = value End Set End Property Public Sub Piyo(ByVal value As String) Console.WriteLine(value) End Sub End Class
ここで注目すべきはアクセス修飾子です。別クラスからアクセスを許可する場合(public、internal)と派生クラスからアクセスを許可する場合(protected)、全てのアクセスを許可しない場合(private)と、役割に応じて隠ぺいしましょう。
sasaki816.hatenablog.com
基底クラスを継承して派生クラス(SubClass)を作ります。なお、継承は一つのクラスで一つしかできません(多重継承の禁止)。
C#の場合は派生クラス名に続いて「:」で区切って継承する基底クラスを宣言します。
public class SubClass : BaseClass { public void PiyoSub() { Console.WriteLine(test1 * 10); Console.WriteLine(test2 * 10); //Console.WriteLine(test3); ←これはコンパイルエラー Piyo("hoge"); } }
VB.NETの場合は、派生クラス名の次の行でInheritsステートメントで基底クラスを宣言します。
Public Class SubClass Inherits BaseClass Public Sub PiyoSub() Console.WriteLine(test1 * 10) Console.WriteLine(test2 * 10) 'Console.WriteLine(test3) ←これはコンパイルエラー Piyo("hoge") End Sub End Class
変数test1とtest2はアクセス修飾子がpublicとprotectedなので派生クラスからアクセス可能です。しかし、test3はprivateなので派生クラスでもアクセスすることは出来ません。
これで基底クラスと派生クラスが出来ましたので、別クラスから派生クラスを呼び出してみましょう。
SubClass sc = new SubClass(); sc.Piyo("fuga"); sc.PiyoSub(); Console.WriteLine(sc.test1); //Console.WriteLine(sc.test2); ←これはコンパイルエラー //Console.WriteLine(sc.test3); ←これはコンパイルエラー
Dim sc As New SubClass sc.Piyo("fuga") sc.PiyoSub() Console.WriteLine(sc.test1) 'Console.WriteLine(sc.test2) ←これはコンパイルエラー 'Console.WriteLine(sc.test3) ←これはコンパイルエラー
fuga 10 20 hoge 1
2行目はSubClassにないBaseClassのメソッドですが、基底クラスとして継承していますので、派生クラスのインスタンスからも呼び出せるということになります。
また、変数test2とtest3はpublicではないためアクセスすることが出来ません。
定数表現はconst?readonly?
シンボリック定数などの定数を宣言する場合、二通りの方法があります。
- static readonly(VB.NETはReadOnly)
- const
どちらも定数表現としてよく使われますが、明確な違いは以下の通りです。
項目 | static readonly (ReadOnly) |
const |
宣言 | クラスメンバーのみ | どこでもOK |
速度 | やや遅い | 速い |
switch(Select Case) | × | ○ |
書き換え | コンストラクタ内であれば書き換え可能 | 宣言時のみ初期化可能 (初期値を入れないとコンパイルエラーになる) |
静的か動的か | staticが無くても可 | 強制的にstaticとなる |
コンパイル | 変数と同等扱い | 値が埋め込まれる |
インスタンス | ○ | × |
どっちを使えば?の話ですが、正直どちらでもやりたいことは実現可能ですが、私は基本的には以下のように分けています。
- 基本はstatic readonlyを使用する
- 長期的に不変な情報で、且つ変更される予定が無い 場合にのみconstを使用する
遅延バインディング
バインディング(型を結びつけること)には事前バインディングと遅延バインディングの2種類あります。これもC#とVB.NETで微妙に動作(というかお作法)が違います。
はじめに事前バインディングと遅延バインディングの違いを簡単に説明します。読んで字のごとくなのですが、事前バインディングは宣言時に型を指定して使用する方法、遅延バインディングは実行時に初めてObject型変数に対して代入される型が決定する方法です。
'事前バインディング Dim obj As DateTime '←ここで型が決定 obj = DateTime.Today Console.WriteLine(obj.Month)
'遅延バインディング Dim obj As Object obj = DateTime.Today '←ここで型が決定 Console.WriteLine(obj.Month)
C#ではそもそもこのような書き方ができません。
遅延バインディングはかつてVB6、VBAなどで当たり前のように使われてきました。遅延バインディングはある意味負の遺産と言ってもよいでしょう。また、レガシー機能というだけでなく、実際速度の低下やバグの温床(ランタイムエラー)などになりやすいので、VB.NET環境下においては推奨された手法ではありません。しかし、VB.NETには「Option Strict Off」という機能があります。この場合はほぼVB6スタイルでコードを書くことができますので、遅延バインディングで書いてもエラーは起きません。
sasaki816.hatenablog.com
しかし、「Option Strict On」の状態だと、暗黙の型変換は使用出来なくなる為、当然遅延バインディングも使用出来なくなります。
そもそも論として、遅延バインディングはよほどの事(アンマネージリソースの呼び出しとか)が無い限り使用すべきではないのですが、状況によっては使わざるを得ない場合もあるのです。
例えばVB6のレガシーコードスタイルが強いVB.NETのコードを引き継いで使用したりする場合です。こういった場合、一部を除外しようとすると余りにも影響が大きい事があります。まぁ「影響が大きい=面倒」ということから遅延バインディングで書いているんでしょうけど。
以下は遅延バインディングを使用した場合の回避方法です。手法はたくさんありますが、代表的なものを3つ(+1つおまけ)だけ紹介します。
キャストする
通常は匿名メソッド、ラムダ式を利用した場合によく使う手法ですが、遅延バインディングの回避としても有用です。但し例外が起こりやすいので、忘れずに例外処理を入れましょう。また、キャストする前に型が正しいかどうかの確認を入れるとよいです。
object obj; obj = DateTime.Today; if (obj is DateTime) { Console.WriteLine(((DateTime)obj).Month); }
Dim obj As Object obj = DateTime.Today If TypeOf obj Is System.DateTime Then Console.WriteLine(CType(obj, DateTime).Month) End If
リフレクションを使う
リフレクションを使ってプロパティを取得、実行します。回避方法としてはこの方法が一番安全です。
object obj; obj = DateTime.Today; System.Reflection.PropertyInfo pro = obj.GetType().GetProperty("Month"); if (obj != null) { Console.WriteLine(pro.GetValue(obj).ToString()); }
Dim obj As Object obj = DateTime.Today Dim pro As System.Reflection.PropertyInfo = obj.GetType().GetProperty("Month") If Not pro Is Nothing Then Console.WriteLine(pro.GetValue(obj).ToString()) End If
.NET FrameworkとC#、VB.NETのバージョン対応表
どうしても.NET FrameworkとC#、VB.NETのバージョン一覧をド忘れます。
完全に自分用です。すぐ閲覧できるようにまとめました。
.NET Framework | C# | VB.NET | 主な追加機能 |
2.0 3.0 |
C# 2.0 | VB 8 | 匿名メソッド(C#) staticクラス(C#) ジェネリック null 許容型 yield デリゲート |
3.5 | C# 3.0 | VB 9 | オブジェクト/コレクション初期化子 ラムダ式 拡張メソッド 匿名型 自動プロパティ 型推論 LINQ ASP.NET AJAX |
4.0 | C# 4.0 | VB 10 | dynamic型(C#) オプション/名前付き引数 省略可能パラメーター F# |
4.5 | C# 5.0 | VB 11 | Async/Await |
4.6 | C# 6.0 | VB 14 | null条件演算子 読取専用の自動プロパティ 自動プロパティ初期化子 |
4.7 | C# 7.0 | VB 15 | out変数(C#) Tuple型 |
VB.NETでブロック文を作成したい
C#には構造文を作る区切り文字「{ }」があります。
{ string hoge = "piyo"; Console.WriteLine(hoge); } { int hoge = 123; Console.WriteLine(hoge); }
こんな感じで局所的に変数を作ったり、処理を分けたりするのにすごく便利なのです。お馴染みのforやIfなどの構造文同様で変数スコープはその内側だけになり、string型の変数「hoge」とint型の変数「hoge」は別物となります。
しかしVB.NETには同じ機能がありません。が、似たようなことは出来ます。
要は既存の機能を流用して無理やり構造化してしまえばよいわけです。
If True Then Dim hoge As String = "piyo" Console.WriteLine(hoge) End If If True Then Dim hoge As Integer = 123 Console.WriteLine(hoge) End If
又は、
With Nothing Dim hoge As String = "piyo" Console.WriteLine(hoge) End With With Nothing Dim hoge As Integer = 123 Console.WriteLine(hoge) End With
if文を利用した場合だと、本当のif文と混同してしまうので、私は後者の書き方を使っています。
リファクタリングの判断
私がコードをチェックする時、「これは駄目なコードだ」とリファクタリングの対象とする点がいくつかあります。
今回はそれをまとめて紹介しますので、共感できる部分は是非修正してみて下さい。逆に共感できない部分は、何故それが駄目なのかを考えるのも面白いかもしれません。
変数・プロパティ・メソッドが多すぎる
一つのクラスに多くの状態保持(変数やプロパティ)や振る舞い(メソッド)が多いということは、クラスの背負う責務が多すぎる場合がほとんどです。こういった場合は、責務を抽出して分散させる(別クラスにする)ことで解決することができます。
名前が変
メソッドや変数の名前が体をなしていない場合があります。これは非常にコードを見づらくします。例えば、
int i; int j
などです。全く意味のない変数名は局所なら問題ありませんが、メソッド全体に影響するような場合は正しい名前を付けるべきです。
また、略名や接頭語(プリフィックス)も度々見られますが、これも好ましくありません。
string kbn; //恐らく区分のこと? string strA; //string型変数全てにstrをつける
一つのメソッドが長すぎる
非常に長いメソッドも、責務を持ちすぎている可能性があります。別メソッドにしたり、リファクタリングすることで改善することがほとんどです。状況にもよりますが私の経験上一つのメソッドはおおよそ2~30行程度(例外処理、空白行除く)でまとまらないと、どこかに設計上問題があると思います。
隠蔽されていない
要は公開しすぎ(public)ということです。
例えばプロパティはgetterとsetterでアクセス範囲を限定できますので、外部から設定されるべきものなのか、読み取りが必要なのか等の吟味が必要です。
メソッドにしても外部から全く呼ばれることがないにも関わらずpublicやprotectedで宣言されていたりします。これではクラス設計とは意図しない不具合を起こしかねません。
オブジェクト指向において、この隠蔽は非常に重要な概念です。
振る舞いが別クラスと重複している
これもよくある現象ですが、似たような処理を引数や戻り値の違いでいくつも別クラスにする、というコードを見かけます。必要であればオーバーロードするか、継承してオーバーライドする、デリゲートする等を検討しましょう。とにかく長い、多いは「害悪」です。
メソッド引数が多すぎる
基本的にメソッドの引数はオペランドのみにしましょう。
sasaki816.hatenablog.com
静的クラスであれば、ある程度オーバーロードで逃げる必要がありますが、通常のクラスであればメソッド設計が誤っている可能性があります。機能を抽出して、プロパティにすべきか、変数にすべきか、引数にすべきかをもう一度検討する必要があります。
条件分岐 if、switch(Select Case)が多すぎる
条件分岐は無いにこしたことはありませんが、どうしても分岐する場合は必要最低限とします。状況によっては三項演算子等も上手に織り交ぜながら記述しましょう。
入れ子についても同様で、入れ子した分だけ可読性が悪くなります。
メソッド内変数が多すぎ
一回しか使用しない変数や、使っていない変数(たまにある)、スコープを間違えた変数等も可読性ばかりかバグの温床になってしまいます。変数として持つべきかどうかの適正な判断が必要です。なお、C#やVB.NETは構造化プログラミングが可能ですので、出来るだけ処理を最小限にまとめ、変数スコープもその中で使うようにするべきです。
コメントの応酬
コメントが多すぎるコードも良く見ますが、これも無駄です。というかコメントで説明しなければならないような複雑な処理は別クラス化して責務を分担させましょう。ただコメントがなさ過ぎるのも問題なので、その辺りのさじ加減は必要です。
また、コードをコメントアウトして新旧保存している場合もたまに見かけますが、これも無駄ですのですぐに止めましょう。変更履歴を記録するなどもってのほかです。こういったコードと関係ない部分は別ファイルや別の媒体に保管すべきです。
似ている処理、同じ処理
似ている、同じ処理が色々散らばっている状況です。これは一か所にまとめてクラス化するなり関数化するなりしましょう。
クラスを跨いでいる場合は共通クラスとして定義するか、継承、デリゲートを利用する等を検討しましょう。
処理が分散しすぎ
一つのメソッド内で自作関数が多すぎる状況を言います。関数自体は処理の分散化といった点を考えれば至極当然の結果なわけですが、一度しか使われていない関数や、そもそも関数自体が1~2行と外だしする必要が本当にあるのか?という場合もあります。
.NET Frameworkの機能を使っていない
.NET Frameworkの機能(クラスやインターフェース)があるにも関わらず、それを使用せず自作関数を作ってしまっている場合です。リファレンスなどを有効活用して、まずは.NET Frameworkの機能で実現可能かどうかを調べる「正しい癖」を身につけるべきです。
既存クラス機能では実装レベルで問題(足りない)がある場合はこの限りではありませんが、私の経験上はそれはかなり限られた状況です。