2013/11/25

C#Tips5 クラスの設計

クラスはC言語から見れば構造体にメソッドを付けただけのように思えます。
実はそれだけでなく継承という便利な機構があり、C#ではクラスを継承するだけでなくインターフェースというものも継承できます。今回はこれらの使い分けに迫ります。

1. クラスの継承
クラスの継承とは、構文的にはあるクラスのメソッドやプロパティを引き継ぎ、それに付け加えて新たなメソッドやプロパティを定義することです。

継承は次のパターンがあるでしょう。

a. 抽象的なものごとを具体的にする。
b. 一般的なものに付け加えて特殊なものにする。

具体的に考えてみましょう。

a. TPIPクラスからTPIP1, 2, 3クラスを作る
b. ボタンクラスからトグル出来るボタンクラスを作る

両者の違いは、インスタンス化されるかされないかです。
a.では抽象的なもののクラスは具体的にできない、即ちインスタンス化されません。
一方でb.ではボタンクラスはそれ自身でも普通のボタンとして振る舞いますが、
それに付け加えてボタンを押すと内部のパラメータが変化するような特殊なボタンを作っています。

そして両者には文法的な違いもあります。

a.では抽象クラスという書き方をします。以下のTPIP1, 2クラスは抽象クラスであるTPIPを継承しています。こうすることで、それぞれのTPIP1, 2クラスは何かを送るときにsendという名前のメソッドで送信できます。こうすることで、TPIP_send(tpip1);としてもTPIP_send(tpip2);としてもどちらもTPIPということでsendメソッドを呼び出すことができます。

文法的には抽象クラスはabstractをクラスの前と、抽象メソッド(子クラスは必ず定義しなければならないメソッド)の前に入れます。また子クラスは抽象メソッドを定義する際にoverrideキーワードを入れて上書き定義したことを示します。

abstract class TPIP {
    abstract public void send();
}

class TPIP1 : TPIP {
    override public void send() {
        System.Console.WriteLine("in TPIP1.send()");
    }
}


class TPIP2 : TPIP {
    override public void send() {
        System.Console.WriteLine("in TPIP2.send()");
    }
}

class Program {
    public static void Main() {
        TPIP1 tpip1 = new TPIP1();
        TPIP2 tpip2 = new TPIP2();

        TPIP_send(tpip1);
        TPIP_send(tpip2);
    }

    static void TPIP_send(TPIP tpip) {
        tpip.send();
    }
}

さて、b.では元のクラスは抽象的ではなく具体的です。従って、普通のクラス定義に更に上書きするような形となります。普通のクラスでも、上書きすることが想定されるようなメソッドにはvirtualキーワードを入れます。
もしも上書きされなかった場合、そのままメソッドが呼び出されます。
上書きする場合はoverrideを入れます。

class Button {
    virtual public void onClick() {
        System.Console.WriteLine("クリック");
    }
}

class ToggleButton : Button {
    override public void onClick() {
        System.Console.WriteLine("トグルクリック");
    }
}

class Program {
    public static void Main() {
        Button button = new Button();
        ToggleButton toggle_button = new ToggleButton();

        button.onClick();
        toggle_button.onClick();
    }
}

b.でも両者に共通する処理を呼び出すことができます。それは即ちButtonクラスが持っているメソッドならToggleButtonクラスのものでも呼び出せると言うことです。

class Button {
    public void method() {
        System.Console.WriteLine("in method()");
    }
}

class ToggleButton : Button {
   
}

class Program {
    public static void Main() {
        Button button = new Button();
        ToggleButton toggle_button = new ToggleButton();

        button.method();
        toggle_button.method();
    }
}

a.のことを多態性とか、ポリモーフィズムとか言ったりします。a.もb.もオブジェクト指向では必須な考え方なのでどんどん利用していきましょう。個人的にはa.の方がb.よりも出くわす場面が多いと思います。

2. 依存関係
クラス単体では動作が完結しない場合があります。或いは、他のクラスを取り入れた方が便利なことがあります。クラスA内でクラスBを必要としたときクラスAはクラスBと依存関係にあると言います。
クラスを設計する際はこの依存関係は極力小さくした方がいいです。
但し、次のような例はいただけません。

class Book {
    private string title;

    public string Title {
        private set;
        get;
    }

    public Book(string title) {
        this.title = title;
    }
}

class Booklist {
    public void searchByTitle(string title) {
        // titleから検索する処理
    }
}

class Program {
    public static void Main() {
        Book book = new Book("abc");
        Booklist bl = new Booklist();
        bl.searchByTitle(book.Title);
    }
}

なぜならば、Booklistの検索を使うのに、Bookの内容(今回ではTitle)を知らないといけないからです。
これは依存関係を広めてしまうことになります。極力狭めようとすると次のように書けるでしょう。

class Book {
    private string title;

    public string Title {
        private set;
        get;
    }

    public Book(string title) {
        this.title = title;
    }
}

class Booklist {
    public void searchByTitle(Book book) {
        // titleから検索する処理
    }
}

class Program {
    public static void Main() {
        Book book = new Book("abc");
        Booklist bl = new Booklist();
        bl.searchByTitle(book); // Booklistの利用側はBookのことについて知らなくても良い
    }
}

さて、依存関係について大きく分けると、

a. クラスAはクラスBのデータを所有している(関連)
b. クラスAとクラスBは互いにc.の関係にある
c. クラスAはクラスBを引数などで受け取り、利用する(依存)

があります。a, b, cの順に依存関係が強いということになります。特にa.をhas-a関係と呼びます。

データを所有すると言うことは、プロパティとして持つと言うことです。
これはクラスAの中ではクラスBの操作もすることですから、
クラスAの制作者はクラスBのことを深く知らなければなりません。
特にクラスBのプロパティをクラスAで操作するなんて言うのは非常に依存関係が強い状態です。
クラス設計を見直した方がいいでしょう。

class A {
    B b = new B();

    public void m() {
        System.Console.WriteLine("in A.m()");
        b.m();
    }
}

class B {
    public void m() {
        System.Console.WriteLine("in B.m()");
    }
}

class Program {
    public static void Main() {
        A a = new A();
        a.m();
    }
}

一方でc.は、引数として受け取りそれを用いて処理します。
結局以下の例ではa.と同じだと言うことが分かると思います。
どう同じかというと、クラスBが変更となればクラスAも変更しなければならないと言うことです。
ただ幸い、クラスAの利用者は変更せずに済みそうです。

class A {
    public void m(B b) {
        System.Console.WriteLine("in A.m()");
        b.m();
    }
}

class B {
    public void m() {
        System.Console.WriteLine("in B.m()");
    }
}

class Program {
    public static void Main() {
        A a = new A();
        B b = new B();
        a.m(b);
    }
}

a.ではクラスBを外部に出さないという利点があります。
例えば本の管理では、本のデータを外部に出さないで、内部だけで保管する場合は有効です。
外部にデータを公開しないようなオブジェクトは、およそ、本のようなデータであることが多いと思います。
このときはclassではなくstructを使うといいでしょう。classとstructはC#では異なったものとして扱われます。

c.ではクラスAとクラスBを分離できるというメリットがあります。
両者に従属関係がないときはこのように分けることをお勧めします。

3. 依存関係の契約
先ほどのように依存関係があるとき、クラスAからクラスBを呼び出すようなことが発生します。
これにはクラスBはクラスAから呼び出されることを想定しなければなりません。
クラスBがクラスAから呼び出されるのに必要な機能のリストをC#ではinterfaceとして定義できます。
次の例では、AのインターフェースA_IFを定義し、クラスBはそれを継承して実装します。
こうすることで、クラスAは安心してクラスBのメソッドを呼び出すことができるのです。

class A {
    public void m(B b, int x) {
        System.Console.WriteLine("in A.m()");
        b.m(x);
    }
}

interface A_IF {
    void m(int x);
}

class B : A_IF {
    public void m(int x) {
        System.Console.WriteLine("in B.m(); {0}", x);
    }
}

class Program {
    public static void Main() {
        A a = new A();
        B b = new B();
        a.m(b, 100);
    }
}

逆にクラスAがクラスBのことをよく知っており、メソッドを呼び出すのもいとわない場合は素直にhas-a関係にした方がいいでしょう。極力2.cのようなコーディングは避けた方がいいです。interfaceを使いましょう。

4. 構造体とクラス
C#ではクラスと構造体に参照型かそうでないかの違いがあります。次の例の実行結果は、

class C {
    public int x;
}

struct S {
    public int x;
}

class Program {
    public static void Main() {
        C c = new C();
        S s = new S();

        c.x = 10;
        s.x = 20;

        f(c);
        g(s);

        System.Console.WriteLine("c.x={0}, s.x={1}", c.x, s.x);
    }

    static void f(C o) {
        o.x = 100;
    }

    static void g(S o) {
        o.x = 200;
    }
}

c.x=100, s.x=20
となります。

これはf(c)の内部で100と上書きされたのに対して、g(s)では上書きされなかったことを示します。
このようにクラスは参照型で、インスタンスを指し示すデータだけが引数として渡されるのに対して、構造体は値型で、インスタンス自体を引数として渡します。値型は他にもint, double, などの組み込み型に該当します。

メソッドのないレコードはこのように構造体を用いた方がいいらしいです。
> struct 型は、Point、Rectangle、Color などの軽量のオブジェクトを表すのに適しています。点を表すには、自動実装するプロパティを持つクラスを使用すると便利ですが、シナリオによっては構造体を使用する方がより効率的です。たとえば、1,000 個の Point オブジェクトから成る配列を宣言する場合は、各オブジェクトの参照用に新たにメモリが割り当てられます。この場合、構造体であれば処理上の負荷を抑えることができます。
即ち、クラスはポインタですが、このポインタのためのメモリが別途必要になるのに対して、structではその必要がない点で軽量だと言うことです。

構造体など値型に対して参照渡しをすることで内部の値を書き換えることができます。
それだけでなく、値のコピーによるオーバーヘッドを防げます。(構造体では有効)

struct S {
    public int x;
}

class Program {
    public static void Main() {
        S s = new S();

        s.x = 20;

        g(ref s);

        System.Console.WriteLine("s.x={0}", s.x);
    }

    static void g(ref S o) {
        o.x = 200;
    }
}

参照渡しにはrefキーワードを使用します。但し、呼び出し元と定義の引数の両方にrefを付ける必要があります。
参照渡しは、上の例で言うと、変数s = 変数oとなる機構です。つまり、メソッドg内でoを上書きするとsも上書きされてしまいます。この点は参照型(クラス)の値渡し(refをつけないで渡すこと)とは異なります。

個人的には、refで受け取ったらその変数自体を変えてはいけないと思います。
その変数のプロパティなどを変えるに留めるべきでしょう。
もしもその変数自体を変えたいことがあったら、outキーワードを使うべきです。

struct S {
    public int x;
}

class Program {
    public static void Main() {
        S s;

        g(out s);

        System.Console.WriteLine("s.x={0}", s.x);
    }

    static void g(out S o) {
        o = new S(); // o自体を変更している
        o.x = 200;
    }
}

C#では改良して欲しい点ですね。refでは上書きできないようになったらC++でいうconstのように便利になると思います。

ちなみにstructはnewしなくても宣言するだけで領域が確保されます。但し、値は未定です。

クラスと構造体でベンチマークしてみました。

class C {
    public int x;
    public int y;
}

class Program {
    const int LENGTH = 10000000;

    public static void Main() {
        System.DateTime dt = System.DateTime.Now;

        C[] c = new C[LENGTH];

        for(int i = 0; i < LENGTH; i++) {
            c[i] = new C();
            c[i].x = i;

            for(int j = 0; j < 100; j++) {
                c[i].y = c[i].x / 2;
                c[i].x += c[i].y + j;
            }
        }

        int sum = 0;

        for(int i = 0; i < LENGTH; i++) {
            sum += c[i].y;
        }

        System.Console.WriteLine(sum);
        System.Console.WriteLine(System.DateTime.Now - dt);
    }
}

構造体 : 00:00:16.1900226
クラス : 00:00:17.8294763
このテストでは対して差はないようです。このテストでは両者の差がほとんどなかったのですが、この実行時間の大部分を計算で消費しているようです。計算をなくして暮らすと構造体の違いだけに着目したところ、

struct C {
    public int x;
}

class Program {
    const int LENGTH = 100000000;

    public static void Main() {
        System.DateTime dt = System.DateTime.Now;

        C[] c = new C[LENGTH];

        for(int i = 0; i < LENGTH; i++) {
            c[i] = new C();
            c[i].x = i;
        }

        int sum = 0;

        for(int i = 0; i < LENGTH; i++) {
            sum += c[i].x;
        }

        System.Console.WriteLine(sum);
        System.Console.WriteLine(System.DateTime.Now - dt);
    }
}

構造体 : 00:00:01.1100015
クラス : 00:00:16.4400230

こんなに差が出ました。なおCPUはCore i3です。
ただ、1億回ループさせないとこれだけ差が出ないんですがね;
精神衛生上もstructを使っていった方がいいかも!

0 件のコメント:

コメントを投稿