Sponsored Link
 
>>  Main > Programing > Primary > 0015

D言語入門 第15章 - クラス

#01 - クラスの定義と使用

今回は…

前回のオブジェクトとしての構造体の使い方を、より限定化したもの、それがclassです。 まずはクラスの書き方を見てみましょう。 今回は単純に前回の構造体をクラスで表現して使ってみます。

今回のミソ

  • 構造体の宣言 struct の代わりに class を使うとクラスを作ることができる
  • classで定義された型を、クラス、あるいはクラスオブジェクトと呼びます。
  • new ClassType として作られたデータをインスタンス、あるいはインスタンスオブジェクト、または単にオブジェクトと呼びます。
  • ClassType variable = new ClassType; とすることでクラスおよびインスタンスを利用することができる。

サンプルコード

import std.stdio;

// 「人」型を class で定義
class Parson
{
    
    // メンバ変数の定義
    
    // 名前
    char[] name;
    // 場所
    char[] location;
    
    // メンバ関数の定義
    // メンバ変数の定義は普通の関数の定義を構造体などの中に記述することで行われます。
    // メンバ関数の中では、同じクラス内のメンバの変数や関数がスコープに含まれるので、
    // name や location 、 WakeUp などにアクセスすることができます。
    
    // 起床
    void WakeUp()
    {
        writefln( "%sは現在%sにいます。", name, location );
        writefln( "%sは%sで目を覚ました。\n", name, location );
    }
    // 食事
    void Eat(char[] foodName)
    {
        writefln( "%sは現在%sにいます。", name, location );
        writefln( "%sは%sを食べた。少し元気になった。\n", name, foodName );
    }
    // 移動
    void Move(char[] locationName)
    {
        writefln( "%sは現在%sにいます。", name, location );
        writefln( "%sは%sから%sへ移動した。\n", name, location, locationName );
        location = locationName;
    }
    // 勉強
    void Study()
    {
        writefln( "%sは現在%sにいます。", name, location );
        writefln( "%sは勉強した。少し賢くなった。\n", name );
    }
    // 就寝
    void Sleep()
    {
        writefln( "%sは現在%sにいます。", name, location );
        writefln( "%sは%sで眠りに就いた。\n", name, location );
    }

}

// メインの流れ
int main(char[][] args)
{
    // new Parson でParsonクラスのインスタンスを作ることができる。
    // keiichi は ポインタ型の変数のようなものとなり、 new Parson を代入することで
    // 初めて使用することができる。
    Parson keiichi = new Parson;
    with (keiichi)
    {
        name = "前原圭一".dup;
        location = "自室".dup;
    }
    
    // メンバ関数の使い方。
    // 変数と同じように"."(ピリオド)でアクセスすることで使用することができます。
    keiichi.WakeUp();
    keiichi.Move( "食卓".dup );
    keiichi.Eat( "朝食".dup );
    keiichi.Move( "学校".dup );
    
    // メンバ関数にもwith文でアクセスすることができます。
    with (keiichi)
    {
        Study();
        Eat( "お昼の弁当".dup );
        Study();
        Move( "家".dup );
        Move( "食卓".dup );
        Eat( "夕食".dup );
        Move( "自室".dup );
        Study();
        Sleep();
    }
    return 0;
}

実行結果

前原圭一は現在自室にいます。
前原圭一は自室で目を覚ました。

前原圭一は現在自室にいます。
前原圭一は自室から食卓へ移動した。

前原圭一は現在食卓にいます。
前原圭一は朝食を食べた。少し元気になった。

前原圭一は現在食卓にいます。
前原圭一は食卓から学校へ移動した。

前原圭一は現在学校にいます。
前原圭一は勉強した。少し賢くなった。

前原圭一は現在学校にいます。
前原圭一はお昼の弁当を食べた。少し元気になった。

前原圭一は現在学校にいます。
前原圭一は勉強した。少し賢くなった。

前原圭一は現在学校にいます。
前原圭一は学校から家へ移動した。

前原圭一は現在家にいます。
前原圭一は家から食卓へ移動した。

前原圭一は現在食卓にいます。
前原圭一は夕食を食べた。少し元気になった。

前原圭一は現在食卓にいます。
前原圭一は食卓から自室へ移動した。

前原圭一は現在自室にいます。
前原圭一は勉強した。少し賢くなった。

前原圭一は現在自室にいます。
前原圭一は自室で眠りに就いた。

まとめ

前回のコードと見比べてみましょう。
前回のコードとの違いは、
  1. struct Parson としていたところがclass Parsonと書きなおされています。
  2. Parson keiichi;となっていたところがParson keiichi = new Parson;となっています。
1の方の違いは、「人」型が struct と class のどちらであるかという違いですね。
問題は2のほうです。
2のほうの意味は、型が「値型」なのか「参照型」なのかという違いがあります。
「値型」はintやreal、構造体で定義された型などがあります。
一方「参照型」はポインタに近い機能を持っています。というかポインタのようなものです。
ポインタですので、初めの状態ではなにも指示していないということになります。
なので、新しく new Parson として、指し示す者を作ってやらなければなりません。
new Parsonというものは、演算子の一つです。
メモリを獲得して、新しく扱うことのできるデータを作り出すことができます。
この作られたデータのことを、インスタンス、あるいはインスタンスオブジェクト、または単にオブジェクトと呼びます。

#02 - カプセル化

今回は…

カプセル化というものについて説明させていただきます。
考えてみてください。

Patchouli と Alice が共同開発しています。 ある日 Patchouli が Alice に クラス「Hoge」を作ってほしいと頼みました。
要求条件は以下のとおり。
  • クラス名は "Hoge"
  • クラスは名前を設定することのできるメンバ関数 void setName(char[] str); を持っている
  • クラスは名前を取得することのできるメンバ関数 char[] getName(); を持っている
  • クラスは設定された名前の文字数を数える int count(); を持っている
Aliceは次のようなクラスを考えました。 class Hoge
{
    int cnt;
    char[] name;
    void setName(char[] str)
    {
        cnt = str.length;
        name = str;
    }
    char[] getName()
    {
        return name;
    }
    int count()
    {
        return cnt;
    }
}
これを使った Patchouli は Alice の作ったコードの内容をよく見ないで次のようなコードを書きました。 void main(char[][] args)
{
    Hoge hoge = new Hoge;
    hoge.name = "aaaaa".dup;
    writefln(hoge.name, " : ", hoge.count());
    hoge.name = "bb".dup;
    writefln(hoge.name, " : ", hoge.count());
}
これを実行した Patchouli は言いました。
「思ってたのと違うわ」
Alice は このコードを見て言いました。
「名前は void setName(char[] str) の関数を使って設定しないと文字数がおかしくなるのよ」
それを聞いた Patchouli は言いました。
「紛らわしいメンバ変数を使わないでちょうだい。 コンパイルが通ったんだから正しいように見えるじゃない。」

このようなミスは割とよくおこるものです。
そんなわけで、先の例でのクラスの内部でしか使わない予定の name というメンバ変数は、クラス内部からしかアクセスできないようにしてやるとこのようなミスが減りますね。
これを行うのが、カプセル化です。
D言語では private 属性 と public 属性 がカプセル化を行う手助けになります。

今回のミソ

  • private アクセス指定子はクラス内部からのみアクセスが可能
  • public アクセス指定子はクラス外部からもアクセスが可能

サンプルコード

import std.stdio;

// クラス Hoge を定義
class Hoge
{
    // プライベート変数 cnt
    private int cnt;
    // プライベート変数 name
    private char[] name;
    // パブリック関数 void setName(char[] str)
    public void setName(char[] str)
    {
        // private 変数はクラス内からは参照が可能
        cnt = str.length;
        name = str;
    }
    // パブリック関数 int count()
    // デフォルトは パブリックなのです。
    int count()
    {
        return cnt;
    }
    // 以降全てパブリックしたい場合には public: とするといいです。
    // 同様に、残りはすべてプライベートとして宣言したいならば private: でもOKです。
public:
    // パブリック関数 char[] getName()
    char[] getName()
    {
        return name;
    }
}

int main(char[][] args)
{
    // Hoge のインスタンス hoge
    Hoge hoge = new Hoge;
    
    // hoge に名前 "aaaaa" を設定
    hoge.setName("aaaaa".dup);
    writefln(hoge.getName(), " : ", hoge.count());
    
    // hoge に名前 "bb" を設定
    hoge.setName("bb".dup);
    writefln(hoge.getName(), " : ", hoge.count());
    
    // private 変数は外部からアクセス不可能
    // ただし、同じファイル内の場合のみこの制限は無効になります。
    // なので、この場合は以下のコードもコンパイル可能です
    // しかしながら、クラスHogeが別のファイルで定義されていた場合はコンパイルエラーが生じます。
    /+
        hoge.name = "ccc".dup;
        writefln(hoge.name, " : ", hoge.cnt);
    +/

         return 0; }

実行結果

aaaaa : 5
bb : 2

まとめ

このように、 private や public でクラス内の変数/関数に対してアクセスを制限することを「カプセル化」といいます。
また、 privateおよびpublic というキーワードは「アクセス指定子」と呼んだりすることがあります。
基本的には、例のようにメンバ変数はすべて private で宣言し、それを設定および取得する関数を作るとよいとされています。

#03 - コンストラクタ

今回は…

ところで、先程の例のように初期設定をインスタンス生成後に行ってもよいのですが、必ず初期化作業が必要である場合、インスタンス生成時に初期化を行った方が効率的でわかりやすくなる場合があります。
今回は、インスタンス生成時に必ず呼び出される関数「コンストラクタ」についてお話します。

今回のミソ

  • this() としてコンストラクタを定義する。

サンプルコード

import std.stdio;

// クラス Hoge の定義
class Hoge
{
    int cnt;
    // コンストラクタの定義
    this()
    {
        // コンストラクタが呼び出されました
        writefln("constructor called!");
        // 期化されていなければならないメンバ変数 cnt の初期化を
        // インスタンス生成時に行う
        cnt = 0;
    }
    // メンバ関数 void func() の定義
    void func()
    {
        // 初期化されていなければならないメンバ変数 cnt の使用
        cnt++;
        // 関数が実行されたことを通知。
        // 呼び出された回数を表示する。
        writefln("function called! count : ", cnt);
    }
}

// メイン関数
int main(char[][] args)
{
    // インスタンスの作成(コンストラクタの呼び出し)
    Hoge hoge = new Hoge;
    // 1回目
    hoge.func();
    // 2回目
    hoge.func();
    // 3回目
    hoge.func();
    // 4回目
    hoge.func();
    // 5回目
    hoge.func();
    return 0;
}

実行結果

constructor called!
function called! count : 1
function called! count : 2
function called! count : 3
function called! count : 4
function called! count : 5

まとめ

クラスの初期化のための記述はコンストラクタに書くとスマートに行く場合があります。
もちろん、コンストラクタでは複雑な処理はしません。 単純にメンバ変数の初期化だけを記述するとよいでしょう。

#04 - 引数付きコンストラクタ

今回は…

コンストラクタには引数をつけることがあります。
引数によって初期化の方法を変える場合などに使います。

今回のミソ

  • this(TypeName VariableName ...) として引数付きコンストラクタを定義する。
  • ClassName VariableName = new ClassName(Value ...) として引数付きコンストラクタを利用する。

サンプルコード

import std.stdio;

// クラス Hoge の定義
class Hoge
{
    int cnt;
    // コンストラクタの定義
    this(int c)
    {
        // コンストラクタが呼び出されました。
        // 初期値を表示します
        writefln("constructor called! count default value : ", c);
        // 期化されていなければならないメンバ変数 cnt の初期化を
        // インスタンス生成時に行う
        cnt = c;
    }
    // メンバ関数 void func() の定義
    void func()
    {
        // 初期化されていなければならないメンバ変数 cnt の使用
        cnt++;
        // 関数が実行されたことを通知。
        // 呼び出された回数を表示する。
        writefln("function called! count : ", cnt);
    }
}

// メイン関数
int main(char[][] args)
{
    // インスタンスの作成(コンストラクタの呼び出し)
    // 初期値を 5 として指定する。
    // このように
    // new ClassName(value)
    // として引数付きコンストラクタを利用する
    Hoge hoge = new Hoge(5);
    // 1回目
    hoge.func();
    // 2回目
    hoge.func();
    // 3回目
    hoge.func();
    // 4回目
    hoge.func();
    // 5回目
    hoge.func();
    return 0;
}

実行結果

constructor called! count default value : 5
function called! count : 6
function called! count : 7
function called! count : 8
function called! count : 9
function called! count : 10

まとめ

このようにしてコンストラクタに引数を付けた場合は、必ずその引数を入力してやる必要があります。
また、引数付きコンストラクタの引数の数はいくつでも増やすことが可能です。
ただし、あまり多くの引数をつけたりすると面倒になるため、多くても5個程度がいいでしょう。
引数の入力を楽にしたい場合は関数2-#06引数のデフォルト値を参考にしてください。

#summary - まとめ

第15章のミソ

  • 構造体の宣言 struct の代わりに class を使うとクラスを作ることができる
  • classで定義された型を、クラス、あるいはクラスオブジェクトと呼びます。
  • new ClassType として作られたデータをインスタンス、あるいはインスタンスオブジェクト、または単にオブジェクトと呼びます。
  • ClassType variable = new ClassType; とすることでクラスおよびインスタンスを利用することができる。
  • private アクセス指定子はクラス内部からのみアクセスが可能
  • public アクセス指定子はクラス外部からもアクセスが可能
  • this() としてコンストラクタを定義する。
  • this(TypeName VariableName ...) として引数付きコンストラクタを定義する。
  • ClassName VariableName = new ClassName(Value ...) として引数付きコンストラクタを利用する。

宿題

次のようなmain関数で動作するように、class Hogeを定義してください。
int main(char[][] args)
{
    // Hogeのインスタンスを 10 で初期化
    writefln("コンストラクタを呼び出します。");
    Hoge hoge = new Hoge(10);
    writefln("表示します");
    hoge.disp();
    writefln("カウントアップします");
    hoge.countUp();
    writefln("表示します");
    hoge.disp();
    writefln("カウントダウンします");
    hoge.countDown();
    writefln("表示します");
    hoge.disp();
    return 0;
}
出力結果
コンストラクタを呼び出します。
コンストラクタを呼び出しました。値 : 10
表示します
値は 10 です
カウントアップします
表示します
値は 11 です
カウントダウンします
表示します
値は 10 です

コメント

この章では class の宣言、利用、カプセル化、コンストラクタについてお話ししました。
次回はオブジェクト指向から少し離れて関数について掘り下げていってみます。
そんなわけで、宿題できたら、あるいは予想ついたら次にいきましょう~

ページトップ / 目次 / 次へ
Copyright©SHOO All rights reserved. / LastModified : 2007/12/12(水)[04:54:25]