はじめに
MATLABは、オブジェクト指向プログラミングに対応する構文(クラス、継承、多態性)があります。
しかし、C++,C#やJavaなどほかの言語と比べるとかなり特徴的な文法をしているため、使うのを躊躇してしまうことがあります。
そこで、今回はオブジェクト指向関係の文法を多言語との差異に注目してまとめようと思います。
クラスの構文
まずは、クラスの作り方です。
ほかの言語では参照型のクラスしか作れないことが多いです。
それに対して、MATLABでは値型のクラスと参照型のクラスを作り分けることができます。1
今回は他言語と近い参照型を先に説明し、そのあとに値型のクラスを説明します。
参照型クラス(ハンドルクラス)
まず、参照型のクラスを作ります。
参照型にするためには、handle
クラスを継承する必要があります。
例えば、以下のようなrefClassを作れます。
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の継承のステートメントを取り除いて実行してみましょう。
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
は異なるオブジェクトなのです。
したがって、値型のオブジェクトを使って先ほどと同じ処理を行うためには、以下のようにコピーしたオブジェクトを返すように書き換える必要があります。
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属性を追加します
プロパティの場合には、読み取りと書き込みで個別にアクセス制限できます。
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
とすることで、作成できます。
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
を継承した具象クラスを作ってみましょう。
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)
のように定義します。
例えば、以下のような入力の履歴のみを保持して、必要なときにだけ合計や平均を算出するクラスを作ることができます。
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
関数をオーバーライドしてみます。
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);
おわりに
今回はMATLABのオブジェクト指向関係の文法について、多言語との差異を中心にまとめました。
やはり、C++の系譜を継ぐ言語と比べると独特な文法が多いです。
しかし他の言語でできることは一通りできますので、MATLABにおいても オブジェクト指向プログラミングの恩恵4 を受けることはできます。
この記事で、スクリプトではなく クラスを使う選択肢もある ことが伝われば幸いです。
気になる点などございましたら、お気軽にコメントして頂ければと思います。
参考文献
- Objective-Oriented Matlab (@sshojiro さんの記事)
- クラス (公式ドキュメント)
- MATLAB と他のオブジェクト指向言語との比較 (公式ドキュメント)
- プロパティの属性 (公式ドキュメント)
- メソッドの属性 (公式ドキュメント)
- 抽象クラスとクラス メンバー (公式ドキュメント)
- サブクラス定義 (公式ドキュメント)
- プロパティ アクセス メソッド (公式ドキュメント)
- 関数 disp のオーバーロード (公式ドキュメント)