LoginSignup
50

More than 3 years have passed since last update.

MATLAB with オブジェクト指向 への乗り換えガイド

Last updated at Posted at 2020-12-19

はじめに

MATLABは、オブジェクト指向プログラミングに対応する構文(クラス、継承、多態性)があります。
しかし、C++,C#やJavaなどほかの言語と比べるとかなり特徴的な文法をしているため、使うのを躊躇してしまうことがあります。
そこで、今回はオブジェクト指向関係の文法を多言語との差異に注目してまとめようと思います。

クラスの構文

まずは、クラスの作り方です。
ほかの言語では参照型のクラスしか作れないことが多いです。
それに対して、MATLABでは値型のクラスと参照型のクラスを作り分けることができます。1

今回は他言語と近い参照型を先に説明し、そのあとに値型のクラスを説明します。

参照型クラス(ハンドルクラス)

まず、参照型のクラスを作ります。
参照型にするためには、handleクラスを継承する必要があります。
例えば、以下のようなrefClassを作れます。

refClass.m
classdef refClass < handle
    properties
        sum
    end

    methods
        function obj = refClass()
            % コンストラクタ
            obj.sum = 0;
        end

        function sum = add(obj, num)
            % これまで入力された値の総和を返す
            obj.sum = obj.sum + num;
            sum = obj.sum;
        end
    end
end

オブジェクトを作ってメソッドを実行してみると、きちんと総和が表示されています。

>> refObj = refClass();
>> refObj.add(10)
ans =
    10
>> refObj.add(200)
ans =
   210

ここで、addメソッドの引数がクラス定義と違うことに気づいたでしょうか。
実は、MATLABにおいて ドット演算子はシンタックスシュガー にすぎません。
つまり、 refObj.add(10)add(refObj, 10)等価な表現です。
この仕組みによって、メソッド内部ではobjという変数を他言語のthisキーワードのように使うことができます。(参照型の場合)

値型クラス(値クラス)

次に値クラスを作ってみます。
上で紹介したrefClasから、handleの継承のステートメントを取り除いて実行してみましょう。

valClass.m
classdef valClass
    properties
        sum
    end

    methods
        function obj = valClass()
            % コンストラクタ
            obj.sum = 0;
        end

        function sum = add(obj, num)
            % これまで入力された値の総和を返す?
            obj.sum = obj.sum + num;
            sum = obj.sum;
        end
    end
end

参照型と同様に実行してみるとなんとも不思議な結果となります。
入力した引数がそのまま返却されてしまいます。

>> valObj = valClass();
>> valObj.add(10)
ans =
    10
>> valObj.add(200)
ans =
   200

これは、値型のクラスではメソッド引数のobjコピーされるため発生します。
つまり、値型の場合はメソッド内でのobjと他言語でいうところのthisは異なるオブジェクトなのです。

したがって、値型のオブジェクトを使って先ほどと同じ処理を行うためには、以下のようにコピーしたオブジェクトを返すように書き換える必要があります。

valClass2.m
classdef valClass2
    properties
        sum
    end

    methods
        function obj = valClass2()
            % コンストラクタ
            obj.sum = 0;
        end

        function [obj, sum] = add(obj, num)
            % これまで入力された値の総和を返す
            obj.sum = obj.sum + num;
            sum = obj.sum;
        end
    end
end

呼び出すときも、返却されるオブジェクトをきちんと受け取ってやる必要があります。
すると、sumの値がきちんと増えていきます。

>> valObj = valClass2()
>> [valObj, sum] = valObj.add(10)
valObj = 
  valClass2 のプロパティ:
    sum: 10
sum =
    10
>> [valObj, sum] = valObj.add(10)
valObj = 
  valClass2 のプロパティ:
    sum: 20
sum =
    20

しかし、どうもややこしいと思うのが正直なところです。
C#ではclassとstructで使い分けもありますが、MATLABではstruct型も別に存在しています。
ですので、個人的には値型でクラスを作る必要はないと思っています。

アクセス制限

ここでは、プロパティやメソッドへのアクセス制限についてみていきます。
他言語と同様にpublicやprivate, protectedに対応しています。
また、それ以外にも代入のみprivateで読み取りはpublicなど柔軟に設定できます。

アクセス制限を付加するには、定義ブロックにAccess属性を追加します
プロパティの場合には、読み取りと書き込みで個別にアクセス制限できます。

AccessClas.m
classdef AccessClass < handle
    properties(Access = public)
        % どこからでも読み書き可能
        publicProperty
    end
    properties(Access = private)
        % このクラス内からのみ読み書き可能
        % クラス外からは見ることもできない
        privateProperty
    end
    properties(SetAccess = private, GetAccess = public)
        % このクラス内からは読み書き可能
        % クラス外からは読み込み可能だが、書き込みは不可
        readOnlyProperty
    end

    methods
        function obj = AccessClass()
            obj.setDefault();
        end
    end
    methods(Access = private)
        function setDefault(obj)
            obj.publicProperty   = 10;
            obj.privateProperty  = 20;
            obj.readOnlyProperty = 30;
        end
    end
end

オブジェクトを作って、外部からの様子を見てみましょう。
適切に設定されているようです。

>> acObj = AccessClass()
acObj = 
  AccessClass のプロパティ:
      publicProperty: 10
    readOnlyProperty: 30
>> acObj.readOnlyProperty = 10
AccessClass の読み取り専用プロパティ 'readOnlyProperty' を設定できません。
>> acObj.publicProperty = 100
acObj = 
  AccessClass のプロパティ:
      publicProperty: 100
    readOnlyProperty: 30
>> acObj.setDefault()
クラス 'AccessClass' のメソッド 'setDefault' にアクセスできません。

読み取りと書き込みで別のアクセス指定ができるのは面白いですね。
(うまく使う方法が思いつきませんが・・・)

継承と多態性

ここでは、クラスの継承関連の事項を見ていきます。
MATLABの特徴は以下の通りです。

  • 抽象メソッドをオーバーロードしない派生クラスは抽象クラスとなる
  • インターフェイスクラスは抽象クラスで代用する
  • 多重継承は可能
  • 継承時にハンドルクラスと値クラスを混在できない(値クラスを継承したハンドルクラスは作れない)

抽象クラス

先に抽象クラスの作り方を見てみます。
抽象クラスではclassdef (Abstract) ClassNameとすることで、作成できます。

AbstractClass.m
classdef (Abstract) AbstractClass < handle
    properties
        sum
    end

    methods
        out = add(obj,num)
        reset(obj)
        show(obj)
    end
end

一部のプロパティやメソッドのみ抽象化することもできます。
なお、この場合も、インスタンスを生成することはできません。

classdef AbstractMemberClass < handle
    properties
        concreteProperty    % 具象プロパティ
    end
    properties(Abstract = true)
        abstractProperty    % 抽象プロパティ
    end

    methods
        function obj = AbstractMemberClass()
            % コンストラクタ
            obj.concreteProperty = 0;
        end

        function concreteMethod(obj)
            % 具象メソッド
            obj.concreteProperty = 10;
        end
    end

    methods(Abstract = true) 
        abstractMethod(obj);    % 抽象メソッド
    end
end

継承

すでにhandleクラスの継承を使っていますが、ユーザー定義クラスでも継承可能です。
抽象クラスは継承してすべてのメソッドを実装することでインスタンス化できるようになります。
また、クラス名を&でつなげることで多重継承可能です2

classdef ConcreteClass < AbstractClass & handle
    methods
        function obj = ConcreteClass()
            obj.sum = 0;
        end

        function out = add(obj, num)
            obj.sum = obj.sum + num;
            out = obj.sum;
        end

        function reset(obj)
            obj.sum = 0;
        end

        function show(obj)
            disp("合計:" + obj.sum);
        end
    end
end

実行してみると、下記のようになります。

>> conObj = ConcreteClass();
>> conObj.add(100);
>> conObj.add(21);
>> conObj.show();
合計:121
>> conObj.reset();
>> conObj.show();
合計:0

メソッドオーバライドによる多様性

次は多様性を継承を使って実現してみます。
同じ抽象クラスAbstractClass を継承した具象クラスを作ってみましょう。

ConcreteClass2.m
classdef ConcreteClass2 < ConcreteClass & handle
    methods
        function obj = ConcreteClass2()
            obj.sum = "= 0";
        end

        function out = add(obj, num)
            obj.sum = obj.sum + " + " + num;
            out = obj.sum;
        end

        function reset(obj)
            obj.sum = "= 0";
        end

        function show(obj)
            disp(obj.sum);
        end
    end
end

先ほどのConcreteClassとコンストラクタ呼び出し以外、全く同じメソッドを呼んでみます。
すると、先ほどとは異なり文字列として総和が表示されます。

>> conObj = ConcreteClass2();
>> conObj.add(100);
>> conObj.add(21);
>> conObj.show();
= 0 + 100 + 21
>> conObj.reset();
>> conObj.show();
= 0

MATLABは変数宣言時に型が決まらないため、ラフに書いても動くのがいいですね。3

SetterとGetter

C#のプロパティと同じように、MATLABはGetterとSetterを言語仕様としてサポートしています。
SetterとGetter専用のメソッドをプロパティアクセスメソッドと呼び、set.propertyName(obj, inputArg)get.propertyName(obj)のように定義します。

例えば、以下のような入力の履歴のみを保持して、必要なときにだけ合計や平均を算出するクラスを作ることができます。

SetGetClass
classdef SetGetClass < handle
    properties(SetAccess = private)
        sum;
        ave;
        cnt;
    end
    properties(Access = private)
        inputHistory;
    end

    methods
        function obj = SetGetClass()
            obj.sum = 0;
            obj.inputHistory = [];
            obj.cnt = 0;
        end

        function add(obj,num)
            obj.inputHistory = [obj.inputHistory, num(:)];
        end

        function out = get.sum(obj)
            % sumが読み込まれたときに呼ばれる
            out = 0;
            for i = obj.inputHistory
                out = out + i;
            end
            obj.cnt = obj.cnt + 1;
        end

        function out = get.ave(obj)
            % aveが読み込まれたときに呼ばれる
            out = obj.sum/length(obj.inputHistory);
            obj.cnt = obj.cnt + 1;
        end
    end
end

プロパティが読み込まれたとき、関数が呼ばれて値が計算されているようです。

>> sgObj = SetGetClass();
>> sgObj.add(1);
>> sgObj.add(2);
>> sgObj.add(3);
>> sgObj.add(4);
>> sgObj.add(5);
>> disp("平均:" + sgObj.ave + " 合計:" + sgObj.sum);
平均:3 合計:15
>> disp("呼び出し回数:" + sgObj.cnt);
呼び出し回数:3

注意点として、プロパティアクセスメソッドはプロパティが読み込まれるごとに実行されるということです。
上記の例からも分かるように、この対象にはクラス内部からのプロパティへのアクセスも含まれます。
そのため、アクセスメソッド内での計算量が多いと値の取得に時間がかかり、プログラム全体のパフォーマンスの低下を招きます。

通常は、setter関数で値の範囲をチェックする程度の利用に留めるべきでしょう。

組み込み関数のオーバーロード

組み込み関数のオーバーロードによって多態性を実現することもできます。
ここでは、disp関数とplot関数をオーバーライドしてみます。

OverloadClass.m
classdef OverloadClass < handle
    properties(SetAccess = private)
        inputHistory;
        sum;
        ave;
    end

    methods
        function obj = OverloadClass()
            obj.inputHistory = [];
            obj.sum = 0;
            obj.ave = nan();
        end

        function add(obj, num)
            obj.inputHistory = [obj.inputHistory; num(:)];
            obj.sum = obj.sum + num;
            obj.ave = obj.sum/length(obj.inputHistory);
        end

        function disp(obj)
            fprintf("\t合計: " + obj.sum + "\n");
            if isnan(obj.ave)
                str = "\t平均: " + "NaN\n";
            else
                str = "\t平均: " + obj.ave + "\n";
            end
            fprintf(str);
        end

        function h = plot(obj)
            h = plot(obj.inputHistory);
        end
    end
end

インスタンスを作って実行してみると、オブジェクトに対する表示やグラフがカスタムできていることが分かります。
このように、オーバーロードを用いることで表示も好きなように変更することができます。

>> ovObj = OverloadClass()

ovObj = 

    合計: 0
    平均: NaN
>> ovObj.add(10);
>> ovObj.add(100);
>> ovObj.add(2);
>> ovObj.add(120);
>> disp(ovObj);
    合計: 232
    平均: 58
>> plot(ovObj);

Overload_fig.png

おわりに

今回はMATLABのオブジェクト指向関係の文法について、多言語との差異を中心にまとめました。
やはり、C++の系譜を継ぐ言語と比べると独特な文法が多いです。
しかし他の言語でできることは一通りできますので、MATLABにおいても オブジェクト指向プログラミングの恩恵4 を受けることはできます。

この記事で、スクリプトではなく クラスを使う選択肢もある ことが伝われば幸いです。
気になる点などございましたら、お気軽にコメントして頂ければと思います。

参考文献

  1. Objective-Oriented Matlab (@sshojiro さんの記事)
  2. クラス (公式ドキュメント)
  3. MATLAB と他のオブジェクト指向言語との比較 (公式ドキュメント)
  4. プロパティの属性 (公式ドキュメント)
  5. メソッドの属性 (公式ドキュメント)
  6. 抽象クラスとクラス メンバー (公式ドキュメント)
  7. サブクラス定義 (公式ドキュメント)
  8. プロパティ アクセス メソッド (公式ドキュメント)
  9. 関数 disp のオーバーロード (公式ドキュメント)

  1. ただし、そのことがあらぬ混乱を招いている気がしますが・・・。 

  2. なお、handleクラスを多重継承する必要は本来ないのですが、可読性のために記述しておくべきです。 

  3. 同じことをC++でやるなら、変数sumは派生クラスで宣言するかvoidポインタにせざるを得ないでしょう。 

  4. 特に大規模なプログラムを作るときは、オブジェクト指向で設計すると保守性が高く保つことが容易になりやすいです。 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50