多重継承禁止とインターフェース, Mix-in の許可

多重継承の問題と言語ごとの解決策に関する話を書いてみました。
ただ、 Ruby 好きのため、若干 Ruby よりの記述になっています。

多重継承の問題と Object クラス

多重継承

多重継承で問題になるのはメソッド名などが重複した場合です。
特にダイヤモンド継承(ひし形継承)と呼ばれる共通の祖先を持つクラスの継承では、 必ずメソッド名の重複は起こりますし、どっち経由の祖先のメソッドかわからなくなります。
ダイヤモンド継承
多重継承が許されている言語としては C++ や Python などがあります。
C++ では virtual な継承を使ったり、 Python では書いた順でメソッドの優先度を決めたり といった方法で共通祖先の継承をできるようしています。

しかし、どのメソッドを使うかの問題が解決しても、 共通のメンバー変数(属性)を別々のクラスのメソッドから入り乱れてアクセスすることになり、 不測の問題を引き起こしがちです。

ちなみに JavaScript はプロトタイプベース、 Google が開発した Go 言語では匿名フィールドといった感じで、これらは継承のシステム自体ちょっと違います。 ただ、多重継承っぽいこともできて、やれば似たような問題を引き起こします。

Object クラス

共通の祖先を持たないように多重継承すればいいかというと、 これもやりたくない事情があります。

C# や Ruby といった言語では 全てのクラスが共通の一つの基底クラスから派生しています。 この基底クラスはたいてい Object という名前になっています。
Object クラス
一つのクラスから派生することによって色々なメリットがあります。 その一つとしては ToString() のような全てのクラスが共通して持つメソッドを作れることです。
例えば、 C++ から C# では printf 系での出力位置の指定の仕方が変わりました。
    [C++] printf("%s 。 %d 時です\n", "こんにちは", 12);
    [C#]  Console.Write("{0}。 {1} 時です\n", "こんにちは", 12);
こういったことができるのも、整数など基本型を含めたすべてのクラスが文字列への変換関数を持っているからです。

一つのクラスから継承するメリットは大きいため、 この方式を捨てることは難しいでしょう。 そうなってくると必然的に多重継承はダイアモンド継承になってしまいます。


そこで、"多重継承は問題を起こす、だったら多重継承を禁止しよう" ということになります。
しかし、全面禁止にしたくない理由もあります。 今度はそういった事情を考えてみましょう。

インタフェース

ジェネリックプログラミング

C++ で vector などのコンテナークラスをソートするとします。
 std::vector<MyClass> ary;
 std::sort(ary.begin(), ary.end());
このコンテナークラスに格納するオブジェクトのクラス(MyClass)に < 演算子 が定義されてさえいれば、ソートは可能です。 < 演算子というと C++ 特有感が強いですが、strcmp() のような 比較して数値を返す compare() メソッドが実装されている必要があると考えてください。

C++ のテンプレートのように、型を気にせず、 そのオブジェクトがメソッドをもっているかだけが重要になってくるのが、 ジェネリックプログラミングです。
C++ ではテンプレートを使った時だけジェネリックですが、 Ruby, JavaScript といったスクリプト系の多くは言語全般がジェネリックで、 関数定義などで型指定する必要がありません。

ただし、型指定が悪いわけではありません。 静的な型チェックはコードの安全面で大きなメリットです。
以前 Google が JavaScript の後継として Dart という言語を出してきました。 この Dart では JavaScript の改良として型指定が導入されました。

Go 言語 や PHP では関数の引数定義で型指定するかどうかで、 簡単に型チェック、ジェネリックを切り替えらるようになっています。 こういったスタイルが一番使いやすいのではないかと思います。

メソッド定義の保障

前節の C++ の sort の話で、 compare 関数( < 演算子 )が定義されていない型を渡すとどうなるでしょうか?
これは STL に残っている問題の一つで、かなり意味不明なコンパイルエラーが発生します。
ジェネリックなスクリプト言語の場合は実行時に compare() が呼ばれた時点で エラーとなります。

"sort() に渡す型は compare() が定義されていなければならない" ということが、ジェネリックプログラミングではドキュメントに書かれていて、 それをプログラマーが守って使うことになります。
これは C++ で const 演算子を使わず、 IN だの OUT だのといったコメントだけで、 使い方を示しているようなものです。 もっとコードで明示的に示され、 コンパイル時などでちゃんとチェックしてくれる方がいいでしょう。 これを実現するのが、インターフェースです。

Comparable interface
インターフェースとは振る舞いだけが定義されたクラスです。
引数の型として Comparable を指定すると Comparable を継承したクラス以外を渡した時点でエラーとなり、 関数の入り口でのチェックが可能となります。 また、インターフェースを継承するクラスでは compare() の実装を強制されます。

インターフェースは必要なメソッドが定義されていることの保障として役に立ちます。
それでいて、属性やメソッドの実体を持たないクラスなので、 多重継承しても問題は発生しません。
そのため、 Java, C#, Dart, PHP といった型指定できる言語では、 多重継承は問題が起きるので禁止するけど、インターフェースは許可する というのが主流になってきています。

Mix-in

概念

インターフェース以外にも 多重継承しても問題を起こさず、役に立つものとして Mix-in があります。 Ruby では多重継承は禁止されてますが、 Mix-in は使えるようになっています。

多重継承の例に挙げた鯨の話に戻って考えてみます。 哺乳類と魚を継承して鯨というよりも、 "泳げる" という機能を共に持っているとみなします。こちらの方がより現実に近いのではないでしょうか。 こういったクラスに機能だけを追加することを Mix-in といいます。
Mix-in

インターフェースでは、属性もメソッドの実装も持ちませんでしたが、 Mix-in では、 メソッドの実装はありますが、継承される属性を持ちません。
これにより、多重継承時の問題を回避しています。

Comparable

もっと実際的な話にしましょう。 インターフェースで出てきた compare() ようなキーとなるメソッドがクラスに定義されているとします。 このクラスに Mix-in をすれば キーメソッドから導出できる様々なメソッドが使えるようになります。
例えば、 Ruby では compare() に当たる <=> 演算子を定義して、 Comparable を Mix-in すると ==, <, <= といった比較演算子が全て使えるようになります。
つまり、自分で作ったクラスに対して簡単に比較の機能を追加できるわけです。
Mix-in を使えば、 compare() を実装するというインターフェースと同じ労力で、 より優れたクラスが作れるということです。
[Mix-in]     if ( a < b) {
[Interface]  if ( a.compare(b) < 0) {
演算子のオーバーロード自体に批判的な意見もあります。 しかし、めちゃくちゃに使えばもちろんわかりにくくなりますが、 適切に使えば直感的にも読みやすく、書きやすいクラスを作ることができます。

Enumerable

Ruby でもう一つよく使われる Enumerable を見ていきましょう。 これはコンテナーに機能を追加する Mix-in です。

N 分木のような自作のコンテナークラスを作ったとします。
Enumerable のキー関数は要素一つ一つに順にアクセスする each() というメソッドです。 この each() メソッドを持っていれば、 検索(find, include?, min), フィルター(collect), ソート(sort), 変換(map) といったコンテナークラスで欲しいメソッドが使えるようになります。
つまり、自作のクラスに each() を実装するだけで、 組み込みクラスさながらの便利なクラスにすることができるということです。

ただ、機能的にはインターフェース系の言語でも近いことはできます。 例えば、 find() といった関数を用意して、 そこに each() のようなメソッドが定義されたクラスだけを渡せるようにしておく といった具合です。
しかし、書き方が変わってきます。
[Mix-in]       ntree.find(5)
[Interface]    find(ntree, 5)
インターフェースでは find() の引数にコンテナーを渡すのに対して、 Mix-in ではコンテナーに find() の機能を付加します。
Mix-in ではよりオブジェクト指向的に書けると言えるでしょう。 このため、"純粋"なオブジェクト指向言語である Ruby で採用されいるのだと思います。

まとめ

インターフェースと Mix-in は決して排他的なものではありません。 Mix-in は言語の機能というよりも多重継承の使い方の一つなので、 C++ のように多重継承が許可されていれば、 Mix-in はやろうと思えばできます。

多重継承は問題を起こしがちなので、 禁止しようというのもその通りだと思います。 インターフェースだけ許可し、多重継承は禁止するというのが、 今の言語の主流のような気がします。
しかしながら、インターフェースだけしか許可していないと Mix-in は作れません。 非常に残念な傾向にあると言えるのではないでしょうか。

一方、 Ruby では多重継承を禁止し、かつ Mix-in は使えます。 "やっぱり、 Ruby って最高" と結論付けたいところですが、 Ruby を使っていて、型指定やインターフェースの必要性を感じないというわけでもありません。

私としては多重継承は禁止し、インターフェースと Mix-in だけ許可 というのが理想的なんじゃないかと思います。
実際、最近では Dart はインターフェースに加えて Mix-in が後から追加されましたし、 C# にも LINQ という Minx-in 風の機能が追加されました。 今後はこういった流れが増えてきてほしいと思います。



スポンサーサイト



 

C# で gcc のようにファイルを指定してビルドする方法

以前、 Visual C++ で gcc のようにファイルを指定してビルドする方法を紹介しました。 今回はその C# 版です。

ファイルを指定したビルドというのは、 Visual Studio のソリューションファイル(sln)やプロジェクトファイル(csproj)を使わずにコマンドラインでコンパイルすることです。
ちょっとしたサンプルプログラムを試す場合などに便利です。
また、 define を使用した条件コンパイルやエントリーポイントを指定することによって、 クラスの動作確認のコードを実行するといったことにも使えます。

C# には gcc や cl.exe(Visual C++) に相当する csc.exe という C# コンパイラーがあります。 これを使ってビルドを行います。


なお、 プロジェクトファイルを使ってコマンドライン上でビルドする場合は MsBuild を使用します。こちらの使い方については以前の記事をご覧下さい。

環境設定

csc.exe は .NET フレームワークのフォルダー にあります。
これは以下のようなパスです。 最後のフォルダー名はフレームワークのバージョンによって変わります。
c:\Windows\Microsoft.NET\Framework\v4.0.30319
このフォルダーを環境変数 Path に追加します。

ビルド方法

ビルドする場合には次のようなコマンドを実行します。
csc ソースファイル [...]
 > csc hello.cs 
hello.cs :
using System;

namespace Hello
{
    class Program
    {
        static void Main()
        {
            Console.WriteLine("Hello World!");
        }
    }
}
実行すると "ソースファイルのベース名.exe"(hello.exe) がカレントフォルダーに作成されます。
複数のファイルを指定した場合にはエントリーポイント(Main メソッド) のあるソースファイルの名前が使われます。


ただし、ファイルを指定する際に一つ注意点があります。
それは csc.exe がパス区切りとして /(スラッシュ) を認識しない点です。 相対パスなどで別フォルダーのファイルを指定する場合には必ず \ を使って下さい。

コマンドオプション

よく使いそうなオプションをいくつか挙げておきます。
詳細は /? オプションのヘルプを見て下さい。
オプション 短い形式 説明
/help /? ヘルプを表示
/out:<ファイル> 出力ファイル名を指定
/target:exe /t:exe コンソール アプリケーションをビルド(既定)
/target:winexe /t:winexe GUI アプリケーションをビルド
/define:<シンボルリスト> /d 条件付きコンパイルシンボルを定義
/main:<型> /m エントリーポイントを含む型を指定 (他のエントリーポイントはすべて無視)
/warn:<n> /w 警告レベル (0-4) を設定
/codepage:<n> ソース ファイルを開くときに使用するコードページを指定
/utf8output UTF-8 エンコードでコンパイラーのメッセージを出力


 

C# インターフェース - IComparable, IComparable(T)

C# では覚えておくと役に立つインターフェースがいろいろとあります。 このうちの幾つかについて紹介していきたいと思います。
インターフェース 対象 使えるようになる機能
IComparable, IComparable<T> コンテナーに格納するクラス 格納したコンテナーのソート、検索など
IEnumerable コンテナークラス foreach
IEnumerable<T> LINQ の各種メソッド
IDisposable ファイルなどの後処理のタイミング管理が必要なクラス using

今回は IComparable, IComparable<T> インターフェースについての説明です。

用途

配列(Array)などのコンテナークラスでは数値を格納した場合に、 ソートや二分探索(ソート済みのデータから高速に検索)などができます。

IComparable または IComparable<T> インターフェースを継承しておけば、自作のクラスでもこれらの機能が使えるようになります。
また、 SortedList のような比較を必要とするコンテナーでは継承していないと格納することができません。

コンテナーに格納するようなクラスでは実装しておいた方がいいでしょう。

IComparable

まず IComparable から説明します。

実装するメソッド

IComparable インターフェースを継承した場合、CompareTo() メソッドを実装する必要があります。
int CompareTo(Object obj);
これは C 言語で言えば、文字列の strcmp() に相当する比較用メソッドです。
比較対象(obj)を受け取り、自身(this)と比べた結果を数値として返します。
比較 戻り値
obj < this -1 以下の整数
obj == this 0
this < obj 1 以上の整数

cs_icomparable.png

サンプル

サンプルとして点クラスを作成しました。 コンパイルする場合は以下のコマンドを実行します。
 > csc IComparableSample.cs Point_IComparable.cs
csc.exe の使用したコンパイル方法については以前の記事を見て下さい。
点クラス Point は x, y のプロパティを持たせています。
CompareTo() メソッドはまず x で比較し、 同じ場合に y で比較するという仕様にしました。

Point_IComparable.cs(抜粋) :
class Point : IComparable
{
    public int x  { get; set; }
    public int y  { get; set; }

    public Point(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    /// <summary>
    ///   比較メソッド
    /// </summary>
    public int CompareTo(Object obj)
    {
        if (obj == null) return 1;

        Point other = (Point)obj;
        if (other.x == x) {
            return y - other.y;
        }       
        return x - other.x;
    }
}
作成した Point クラスを使った例は IComparableSample.cs に記述しています。
ここで、BinarySearch() の例を後にしているのは、ソートしたデータでなければ検索できないためです。

IComparableSample.cs(抜粋) :
class Program
{
    static void Main()
    {
        // Point を格納したデータを準備
        Point [] ary = {
            new Point(1, 2),
            new Point(3, 5),
            new Point(1, 9),
            new Point(4, 1),
            new Point(3, 1)
        };          
        DumpArray("Original", ary);
            
        // データのソート
        Array.Sort(ary);
        DumpArray("Sorted", ary);

        // 二分探索
        Console.WriteLine("## Binary Search ##");
        var target = new Point(3, 1);
        int pos = Array.BinarySearch(ary, target);
        Console.WriteLine("{0} @ {1}", target, pos);
    }
}
実行結果 :
## Original ##
(1, 2) (3, 5) (1, 9) (4, 1) (3, 1) 

## Sorted ##
(1, 2) (1, 9) (3, 1) (3, 5) (4, 1) 

## Binary Search ##
(3, 1) @ 2

IComparable<T>

IComparable<T> は IComparable のジェネリック版です。 どちらを使っても同じように使うことができます。

わかりやすいように先に IComparable の説明を行いましたが、 実際に使うのは ジェネリック版の IComparable<T> が良いでしょう。
CompareTo() 実装時にキャストしなくて済みますし、若干処理が速いようです。 IComparable<T> の方がいいなら、なぜ IComparable があるのかというと、 IComparable が先にできていたためです。 ジェネリックは C# 2.0 から追加されています。

実装するメソッド

IComparable<T> の場合はジェネリックな CompareTo() を実装します。
int CompareTo(T other);
cs_icomparable_t.png

サンプル

点クラスを使用する側のコードは IComparable と同じ IComparableSample.cs を使用します。
 > csc IComparableSample.cs Point_IComparable_T.cs

Point_IComparable_T.cs(抜粋) :
class Point : IComparable<Point>
{
    public int CompareTo(Point other)
    {
        if (other == null) return 1;
             
        if (other.x == x) {
            return y - other.y;
        }       
        return x - other.x;
    }   
}
実行結果も同じです。

ジェネリックな Point

ついでに Point をジェネリックにしたサンプルも紹介します。

前のサンプルは Point の x, y の型は int 固定でしたが、 double などの任意の型を仕様できるようになります。
ただし、 CompareTo() で比較する必要があるので、 完全に任意ではなく、x, y の型は IComparable を継承しているなければなりません。

クラスのジェネリックと IComparable がジェネリックかどうかは関係ありません。 サンプルでは IComparable<T> で実装していますが、 IComparable でも同じように実装できます。


サンプルは今度は 1 つのファイルに記述しています。
 > csc GenericPointSample.cs 

GenericPointSample.cs(抜粋) :
class Point<Type> : IComparable<Point<Type> >
    where Type : IComparable
{
    public Type x  { get; set; }
    public Type y  { get; set; }

    public Point(Type x, Type y)
    {
        this.x = x;
        this.y = y;
    }

    public int CompareTo(Point<Type> other)
    {
        if (other == null) return 1;
            
        if (x.CompareTo(other.x) == 0) {
            return y.CompareTo(other.y);
        }       
        return x.CompareTo(other.x);
    }

} // Point

実行結果は他のサンプルと同じです。


 
このページをシェア
アクセスカウンター
アクセスランキング
[ジャンルランキング]
コンピュータ
23位
アクセスランキングを見る>>

[サブジャンルランキング]
プログラミング
7位
アクセスランキングを見る>>
カレンダー(アーカイブ)
プルダウン 降順 昇順 年別

05月 | 2023年06月 | 07月
- - - - 1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 -


はてな新着記事
はてな人気記事
ブロとも申請フォーム
プロフィール

yohshiy

Author:yohshiy
職業プログラマー。
仕事は主に C++ ですが、軽い言語マニアなので、色々使っています。

はてブ:yohshiy のブックマーク
Twitter:@yohshiy

サイト紹介
プログラミング好きのブログです。プログラミング関連の話題や公開ソフトの開発記などを雑多に書いてます。ただ、たまに英語やネット系の話になることも。