MATLAB 面向对象学习笔记
随着代码越来越复杂,我实在是无法忍受修改完全面向过程的混乱程序了,急需引入面向对象的语法进行重构。
笔记主要参考的是《MATLAB面向对象编程——从入门到设计模式》(徐潇,李远),书中使用的估计是2015左右的版本。
MATLAB的面向对象语法从整体上看,既不像C++和java那样严格,也不像Python那样过于灵活,而是具有自身的特点。
虽然面向对象机制不可避免地会带来一些运算效率的损失,但是我认为这是值得的,只是需要避免在涉及大量计算的性能瓶颈中使用,对于一些辅助的部分,使用面向对象所带来的代码简化还是非常舒服的。
简单示例
从最简单的一个自定义类型开始 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
26classdef point2d
properties
x
y
end
methods
function obj = point2d(x0,y0)
if nargin == 0
obj.x = 0;
obj.y = 0;
elseif nargin == 2
obj.x = x0;
obj.y = y0;
else
error("unsupported input arguments")
end
end
function obj = normalize(obj)
r = sqrt(obj.x^2+obj.y^2);
obj.x = obj.x/r;
obj.y = obj.y/r;
end
end
end
这里我们定义的类型称为值类型(全值类),与之相对的是句柄类型,在写法上需要对第一行进行改动(< handle
代表继承自
handle
类型) 1
2
3classdef point2d < handle
% ...
end
两种自定义类型的区别主要体现在内存管理中的深拷贝或浅拷贝行为,具体讨论见下文。
MATLAB对于类的方法,在写法上与Python类似,两者在方法中不会省略对象参数,Python习惯使用
self
表示对象参数,而MATLAB习惯上使用obj
表示对象参数。
属性
属性默认值
MATLAB可以给类的属性提供默认值 1
2
3
4
5
6
7
8classdef point2d
properties
x = cos(pi/12);
y = sin(pi/12);
end
...
end
默认值甚至不需要是常量,可以是任何表达式,但是注意为属性生成默认值的时机是在类被MATLAB加载时,而不是每一个对象创建时,例如
1
2
3
4
5
6
7classdef demo
properties
time_stamp = date;
end
...
end
常量属性
MATLAB支持给类的属性标记为常量(只读),即不允许对其进行修改(无论是在类的内部还是外部),尝试修改会报错。
1
2
3
4
5
6
7classdef demo
properties(Constant)
R = pi/180;
end
...
end
对常量属性的赋值发生在类的定义加载时。
如果不给常量属性提供默认值的话,那么它就是[]
,也就是空的double矩阵。
常量属性实际上是和类绑定的,而不是和对象绑定的。
我们可以通过类名访问,也可以通过对象名访问,但是无论如何都不能进行修改
1
2
3
4
5
6disp(demo.R)
s = demo();
disp(s.R)
s.R = 100; % 报错
我又想吐槽MATLAB了,所有的语法错误都必须在运行时才会暴露出来,对于C++为代表的编译型语言,我们可以让更多的错误在编译期提前暴露,对于MATLAB则无能为力。
隐藏属性
在默认情况下,通过disp
可以查看自定义类型对象的所有属性,但是我们有时希望隐藏部分实现细节,可以使用Hidden
实现
1
2
3
4
5
6
7
8classdef demo
properties
x
end
properties(Hidden)
y
end
end
此时调用disp
会只显示属性x
,不会显示被隐藏的属性y
1
2
3
4
5
6>> s = demo();
>> s
s =
demo with properties:
x: []
>>
Hidden
只是让用户无法直接地看到这个属性,与属性对应的访问权限无关,如果我们已经知道隐藏属性的名称,仍然可以正常地访问。
方法
构造方法
与类同名的方法称为构造方法,构造方法是唯一的创建自定义类型对象的方式,只能有一个返回值以返回当前对象。
MATLAB只允许创建一个构造方法,但是我们可以使用nargin
判断参数个数,并据此执行不同的创建行为,例如
1
2
3
4
5
6
7
8
9
10
11function obj = point2d(x0,y0)
if nargin == 0
obj.x = 0;
obj.y = 0;
elseif nargin == 2
obj.x = x0;
obj.y = y0;
else
error("unsupported input arguments")
end
end
创建一个变量例如 1
2
3
4s1 = point2d(1,2);
s2 = point2d();
s3 = point2d;
在无参数构造时,仍然可以省略括号()
,但是非常不建议使用,因为这种做法的可读性太低了。
如果我们没有提供构造方法,那么MATLAB会给我们自动提供一个无参数的默认构造方法,使我们可以直接创建对象,除此之外没有具体的行为。 对于自动生成的默认构造方法,我们也无法向其传递任何参数。
一旦我们提供了构造方法,MATLAB就不会为我们提供默认构造方法。如果我们提供的构造方法不支持无参数调用(即没有处理nargin=0
的情形),那么这个类型就不支持默认构造了。由于某些涉及自定义类型的命令(例如methods
)会尝试无参数创建一个临时对象,在这种情况下使用可能会报错,因此最好支持无参数构造方式。
与构造方法对应的当然是析构方法,但是MATLAB作为一个有强大运行时和内存管理机制的语言,对析构方法的处理要比构造方法更加复杂,留在后文中讨论。
方法的调用
除了上面提到的构造方法,其他方法的第一个参数习惯上都是对象自身,并且使用obj
表示,因为obj
参数是显式存在的,在处理nargin
时需要注意。
对于类的方法我们可以通过对象来调用,此时对象会自动作为第一个参数obj
被传入,其他参数再依次传入,例如
1
2
3
4
5
6
7
8
9
10
11classdef demo
properties
x = 100;
end
methods
function z = compute(obj,y)
z = obj.x + y;
end
end
end
调用例如 1
2s = demo();
s.compute(10); % 110
我们还可以使用与之等价的普通函数调用方式 1
compute(s,20); % 120
注意:这里和Python不同,以普通函数方式调用时,我们不需要也不允许在前面加上类名,MATLAB有自己内部的分派调度器去判定需要调用哪一个普通函数或者哪一个类型的方法,即使它们是同名的。通常MATLAB会优先选择调用用户自定义类型对应的方法,而不是MATLAB的内置函数。
重写disp方法
对于内置的数据类型,MATLAB经常会使用disp
查看变量信息,它对于自定义类型也是支持的,但是默认信息有时过于繁琐,我们可以定制自定义类型调用disp
函数的行为,例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16classdef point2d
properties
x = 0
y = 0
end
properties(Constant)
R = pi/180;
end
methods
function disp(obj)
fprintf("(%f,%f)",obj.x,obj.y);
end
end
end
在默认情况下调用disp
的输出为 1
2
3
4
5
6
7>> s = point2d();
>> disp(s)
point2d with properties:
x: 0
y: 0
R: 0.0175
在重写了disp
之后的输出为 1
2>> disp(s)
(0.000000,0.000000)
set和get方法
对于自定义类型的属性,在默认情况下我们可以直接对其进行读写,但是一个很常见的需求是在读写属性时进行控制,MATLAB对句柄类型提供了特殊的set
和get
方法作为隐藏的中间层:如果我们对某个属性定义了对应的set
和get
方法,那么在我们读写某个属性时,实际上是执行了对应的set
和get
方法。
例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21classdef demo
properties
x = 100;
end
methods
function obj = set.x(obj,x1)
if x1 >=0
obj.x = x1;
else
error("x1 must be positive")
end
end
function x1 = get.x(obj)
x1 = obj.x;
disp("call get.x")
end
end
end
对属性x
的赋值尝试会通过set
方法检查:只有正值才能成功赋值,负值会报错
1
2
3>> s = demo();
>> s.x = 200;
>> s.x = -10; % 报错
对属性x
的读取会通过get
方法进行
1
2
3
4
5>> s = demo();
>> s.x
call get.x
ans =
100
注意:
- 对于全值类,
set
方法要求返回obj
自身(即set
方法的首个参数),否则语法报错,对句柄类则不需要返回值; - 在
set
方法内部对属性的赋值不会调用set
方法;(否则会陷入循环) - 在复制整个对象时不会调用
set
方法; - 在给属性提供默认值时不会调用
set
方法; - 在
load
过程中,会调用set
方法; - 如果赋的新值和原有值相同,(通常情况下)也不会调用
set
方法;
考虑这样的情景:一个属性实时依赖于对其他属性的计算,我们可以定制这个属性的get
方法来对外界伪装这个属性的存在,但是在内部不为其分配内存空间,将这个属性标记为非独立属性(Dependent
)可以做到这一点,例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14classdef demo
properties
x
y
end
properties(Dependent)
r
end
methods
function r = get.r(obj)
r = sqrt(obj.x^2+obj.y^2);
end
end
end
这样的非独立属性必然是只读的,我们无法对其进行修改,因此提供get
方法就足够了。
重载运算符
和disp
方法类似,MATLAB支持自定义类型对运算符的重载,例如plus
对应加法(但是考虑到代码的可读性,并不建议使用)
1 | classdef point2d |
1 | >> point2d(1,0) + point2d(0,2) |
Static方法
与C++一样,MATLAB也提供了静态方法,静态方法不需要obj
参数,既可以通过对象调用,也可以直接通过类名调用。例如
1
2
3
4
5
6
7classdef demo
methods(Static)
function hello()
disp("hello,world!")
end
end
end
使用例如 1
2
3
4>> demo.hello()
hello,world!
>> s = demo();s.hello()
hello,world!
静态方法不绑定任何的对象,不能访问类对象的普通属性,但是可以访问类的常量属性。
MATLAB 没有提供类的静态属性,可以使用其它方法替代实现,见下文。
类的继承
MATLAB 使用 <
表示继承关系,例如基类demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19classdef demo
properties
Value
end
methods
function obj = demo(val)
if nargin > 0
obj.Value = val;
else
obj.Value = 0;
end
end
function displayValue(obj)
disp(['Value: ', num2str(obj.Value)]);
end
end
end
继承类demo2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21classdef demo2 < demo
properties
ExtraValue
end
methods
function obj = demo2(val, extraVal)
obj = obj@demo(val); % 调用基类构造方法
if nargin > 1
obj.ExtraValue = extraVal;
else
obj.ExtraValue = 0;
end
end
function displayValue(obj)
displayValue@demo(obj); % 调用基类的同名函数
disp(['Extra Value: ',num2str(obj.ExtraValue)]);
end
end
end
使用例如 1
2
3
4>> s = demo2(10,100);
>> s.displayValue()
Value: 10
Extra Value: 100
这里需要注意的是:
- 在
demo2
的构造方法中,首先显式调用了基类的构造方法并获取返回的对象:obj = obj@demo(val);
- 在
demo2
的普通方法中,显式调用了基类的同名方法:displayValue@demo(obj);
派生类中可以直接使用基类中的(非private
权限的)方法和属性,但是有上面两种特殊情况:
- 基类的构造方法只能如上面所示在构造方法中用特殊语法调用。
- 如果派生类重写了基类的同名方法,那么对派生类而言,只有在同名方法内部可以用上面的特殊语法访问到基类的版本,在其他情况下只能访问到派生类的版本。
已知MATLAB是允许多继承的,但是我不清楚它是如何解决菱形继承问题的,也不会在代码中使用多继承,因此直接忽略。
可以使用Sealed
关键词(封闭,密封)来禁止一个类被继承,例如
1
2
3classdef (Sealed) demo
...
end
对于基类中的方法,也可以使用Sealed
关键词来禁止它被派生类重写,例如
1
2
3
4
5classdef demo
methods(Sealed)
...
end
end
类的权限管理
和C++类似,MATLAB也提供了public
,protected
,private
三类权限说明,在默认情况下所有的属性和方法都是public
权限。
我们主要关注属性的权限,方法的权限与之类似,并且更简单。
直接从例子开始 1
2
3
4
5
6
7
8
9
10
11classdef demo
properties
x1
end
properties(Access = protected)
x2
end
properties(Access = privated)
x3
end
end
这里的三个属性分别具有不同的访问权限:
x1
具有public
权限,通过内部方法和外部都可以直接访问;x2
具有protected
权限,外部无法访问,通过内部方法可以直接访问,通过子类的方法也可以访问;x3
具有private
权限,只能通过内部方法访问。
MATLAB对属性还提供了更精细的访问权限,我们可以将其拆分为读权限和写权限,例如
1 | classdef demo |
这里:
x1
的写权限是private
,而读权限仍然是默认的public
;x2
则分别指定了写权限和读权限。
C++的友函数/友类会一次性开放所有的权限,而MATLAB提供的类似机制则更加精细:我们可以指定某个属性对某个类的权限,例如
1
2
3
4
5classdef demo
properties(SetAccess = {?demomanager})
x1
end
end
这里的{?demomanager}
表示特别允许demomanager
类的成员函数对其访问,在其他情况下都相当于private
。
类的文件组织
和C++一样,如果类的方法过于复杂,我们可以在类的定义文件中仅仅包括它的方法声明(不含function
关键字),在单独的文件中实现具体的方法。
在这种情况下,MATLAB对文件和文件夹的命名有额外的要求:(以名为demo
的类为例)
- 文件夹名称
@demo
- 类的定义放在与类同名的文件中,例如
demo.m
- 类的方法放在与方法同名的文件中,例如
hello.m
,由于构造方法与类同名,不允许被单独拆分出去(实际也做不到)
事实上,所有放在
@demo
文件夹中的函数文件都会被视作demo
类的方法,即使它在demo
类的定义中没有声明(此时方法的各种权限修饰等都采用默认行为),但是不建议这种做法,因为降低了代码的可读性。
完整示例如下 1
2
3
4
5
6
7
8
9
10
11% @demo/demo.m
classdef demo
methods
str = hello(obj);
end
end
% @demo/hello.m
function str = hello(obj)
str = "hello, from demo";
end
注意:
- 和脚本文件、函数文件一样,我们也可以在类文件的
classdef ... end
之后定义局部函数,在类的方法中可以使用这些局部函数,但是外部无法调用。 - MATLAB规定:某些特殊方法是不能拆分到单独的文件中的,例如构造方法,析构方法,
set
和get
方法,还有static
方法。
抽象方法和抽象类
和C++类似,MATLAB也提供了抽象方法和抽象类:
- 不提供实现,只提供接口的方法称为抽象方法;
- 含有抽象方法的类称为抽象类。
抽象类不能用于实例化对象,自抽象类继承时,必须实现所有的抽象方法,才能得到可以实例化的派生类。
抽象基类例如 1
2
3
4
5classdef demo
methods(Abstract)
hello(obj)
end
end
继承自抽象基类的派生类例如 1
2
3
4
5
6
7classdef demo2 < demo
methods
function hello(obj)
disp("hello,world!")
end
end
end
枚举类型
MATLAB也支持枚举类型,例如 1
2
3
4
5
6
7classdef ColorType
enumeration
RED
GREEN
BLUE
end
end
使用例如 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
26classdef Demo
properties
color
end
methods
function set.color(obj,color)
if isa(color,'ColorType')
obj.color = color;
else
error('Invalid Color');
end
end
function val = get.color(obj)
switch obj.color
case ColorType.RED
val = 10;
case ColorType.GREEN
val = 11;
case ColorType.BLUE
val = 12;
end
end
end
end
这里通过isa
在写入时保证参数合法性,在读出时基于switch
遍历所有合法情形,自动转换为整数进行输出。
类的静态属性模拟
MATLAB只提供类的静态方法,却不提供类的静态属性,这看起来很奇怪,可能是历史兼容性的原因: 通过类名访问静态属性并赋值的语句会和直接创建结构体数组的语法相冲突,为了保持对结构体数组语法的兼容性,只能舍弃静态属性。
作为静态属性的替代,我们可以使用函数中的持久性变量来实现,例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17classdef Demo
methods (Static)
function data = data_accessor(new_data)
persistent static_data
if isempty(static_data)
static_data = new_data;
end
if nargin > 0
static_data = newLevel;
else
data = static_data;
end
end
end
end
这个静态方法的功能如下:
- 如果当前
static_data
没有被赋值,那么必须提供参数,用于赋值; - 其它情况下根据参数个数判断:
- 提供参数,则进行赋值;
- 没有提供参数,则读取当前值并返回。
这里返回值可能发生一次拷贝,也可能不会拷贝,这取决于
static_data
实际存储的是值类型还是句柄类型的数据,无法进一步确定。
进阶内容
句柄类和全值类
在MATLAB中实际上有两种类型:
- 继承自
handle
基类的类型,可以称为handle类、句柄类或引用类; - 其他情况下的默认类型,可以称为Value类、值类型或全值类。
下面以两个自定义类型作为对比 1
2
3
4
5
6
7
8
9
10
11classdef demo1
properties
data
end
end
classdef demo2 < handle
properties
data
end
end
在创建对象时两者都需要开辟内存来存储数据属性,但是对象和属性的关系是不一样的:
- 对于句柄类,对象和属性之间相当于指针指向的关系;
- 对于全值类,对象和属性之间是彻底的包含关系。
例如下面的赋值语句会触发对象的拷贝行为 1
2
3>> s1 = demo1(); s2 = demo2();
>> s1new = s1;
>> s2new = s2;
具体行为却不同,我们依次进行讨论。
对于全值类的对象赋值,在逻辑上会进行彻底的深拷贝, 但是MATLAB实际上使用了懒拷贝机制:只在必要时进行拷贝。 这里的深拷贝行为会暂时搁置,直到需要对数据属性修改时才会触发。
对于句柄类的对象的赋值只是浅拷贝:只复制了一个空壳子,新对象和旧对象实际上指向的是同一个数据属性。这带来了如下的效果:
- 对象赋值后的得到的两个变量,它们的属性实际上指向的是同一块内存空间,因此对数据的修改是相互影响的;
- 对于存储数据属性的内存,MATLAB实际上单独维护了它的引用计数,赋值操作只会增加和减少某块内存的引用计数,如果引用计数减少为0,自动释放内存。
在赋值的两个句柄类对象之间,数据属性实际是同一份,对数据属性修改会相互影响,可以用下面的代码验证
1
2
3
4
5
6
7
8>> s2 = demo2();
>> s2new = s2;
>> s2.data = 100;
>> s2new.data
100
>> s2new.data = 1;
>> s2.data
1
对于函数/方法中的参数传递,全值类和句柄类的区别就相当于C++中的按值传递和按引用传递,例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21classdef demo1
properties
data = 0;
end
methods
function update(obj,data)
obj.data = data;
end
end
end
classdef demo2 < handle
properties
data = 0;
end
methods
function update(obj,data)
obj.data = data;
end
end
end
对于全值类的测试如下,可以发现方法中修改的obj
是全值类对象的副本,并没有影响到对象自身
1
2
3
4
5
6
7
8
9>> s1 = demo1()
s1 =
demo1 with properties:
data: 0
>> s1.update(100)
>> s1
s1 =
demo1 with properties:
data: 0
对于句柄类的测试如下,可以发现方法中修改的obj
是句柄类对象的引用,影响到对象自身
1
2
3
4
5
6
7
8
9>> s2 = demo2()
s2 =
demo1 with properties:
data: 0
>> s2.update(100)
>> s2
s2 =
demo1 with properties:
data: 100
如果用句柄类对象作为类属性的默认值,会导致所有的对象实际上共享了这个属性,这在语法上的可读性非常低,建议不要使用这种方式实现类的静态属性。
关于全值类和句柄类小结:
- 全值类适合于简单的数据,它的行为和内置类型例如matrix等是一致的,即赋值和参数传递都蕴含着深复制的逻辑;
- 句柄类适合于资源管理和对大型数据的管理,句柄类的赋值和参数传递都蕴含着浅复制的逻辑。
- 对于资源管理,复制是不合理的;
- 对于大型数据,我们希望避免很多耗时的复制行为。
包括结构体,元胞等在内的所有内置基本类型都是全值类型,在使用中进行的复制行为都是深复制,可以放心使用。 对绘图对象的处理是日常使用中最可能接触到的句柄类。
在某些情景下,我们确实需要一个通用的句柄类对象,作为函数参数来携带一些返回值,从而不改变原有的函数返回值格式,
此时可以使用下面这种极简的句柄类,虽然很简单,但是足够使用。
1
2
3
4
5classdef DataHandle < handle
properties(Access=public)
data = struct()
end
end
链式调用
我们来探究全值类和句柄类对于C++风格链式调用的支持。
首先考虑一个全值类 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19classdef demo1
properties
n
end
methods
function obj = demo1(n)
obj.n = n;
end
function obj = add(obj,m)
obj.n = obj.n + m;
end
function show(obj)
fprintf('n=%d\n',obj.n);
end
end
end
对于这个全值类的正常使用如下 1
2
3
4
5a = demo1(1);
a.show(); % n=1
a.add(2);
a.show(); % n=1
这和全值类的逻辑是相符的,即修改不会影响原件。
下面的这些语句是合法的,因为每一个方法都返回了创建或修改后的副本
1
2demo1(3).show(); % n=3
demo1(4).add(1).show(); % n=5
然后考虑一个句柄类 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23classdef demo2 < handle
properties
n
end
methods
function obj = demo2(n)
obj.n = n;
end
function add_no_return(obj,m)
obj.n = obj.n + m;
end
function obj = add_return(obj,m)
obj.n = obj.n + m;
end
function show(obj)
fprintf('n=%d\n',obj.n);
end
end
end
这里我们提供了两个版本的加法方法,它们都可以达到对原件修改的目的,区别在于有没有返回值。
对于这个句柄类的正常使用如下 1
2
3
4
5
6
7
8a = demo2(1);
a.show(); % n=1
a.add_no_return(2);
a.show(); % n=3
a.add_return(3);
a.show(); % n=6
这和句柄类的逻辑是相符的,即修改会影响原件。
下面的这些语句是合法的 1
2
3
4
5
6
7
8
9
10
11
12
13demo2(3).show(); % n=3
demo2(4).add_return(1).show(); % n=5
a = demo2(5).add_return(1);
a.show(); % n=6
b = a.add_return(1);
a.show(); % n=7
b.show(); % n=7
b.add_no_return(1);
a.show(); % n=8
b.show(); % n=8
但是下面的语句却是非法的,因为add_no_return
没有返回值
1
2
3
4demo2(10).add_no_return(1).show(); % error
tmp = demo2(20);
tmp.add_no_return(1).show(); % error
单例模式
MATLAB的面向对象当然也支持实现不同的设计模式,这里我们只关注最常见的设计模式——单例模式。
实现方法比较简单:使用句柄类 + 静态方法 + 持久化变量,例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17classdef SingletonHandle < handle
methods (Access = private)
function obj = SingletonHandle()
disp('SingletonHandle created.');
end
end
methods (Static)
function instance = get_instance()
persistent u;
if isempty(u) || ~isvalid(u)
u = SingletonHandle();
end
instance = u;
end
end
end
此时只有第一次调用会创建对象,再次调用得到的还是同一个对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14>> SingletonHandle.get_instance()
SingletonHandle created.
ans =
SingletonHandle with no properties.
>> a=SingletonHandle.get_instance()
a =
SingletonHandle with no properties.
>> b=SingletonHandle.get_instance()
b =
SingletonHandle with no properties.
>> a==b
ans =
logical
1
类的生命周期
类定义的更新
我们考虑这样一个问题:对于一个自定义类型,如果我们已经使用它的定义创建了若干变量,它们仍然存在于工作区中,但是我又修改了类的定义,那么原定义下的对象会受到影响吗?原本的对象可能保存了有价值的数据,而类定义的更新很可能会损坏数据。
MATLAB对于类的定义更新所采取的策略是:出现下面的情况之一时,才会主动更新旧定义下的现有对象
- 使用类的新定义创建新对象;
- 在命令行中查看旧对象;
- 访问类的常量属性或
static
方法;(因为这些对当前类的所有对象是共享的) - ...
如果新定义存在语法错误,或者与旧定义存在无法调和的冲突,那么旧定义下的对象可能被损坏。
现在我们关注一些进阶的内容,这部分内容并不是标准的面向对象编程内容,而是MATLAB的特殊机制所带来的,在面向对象编程中必须考虑的问题。 更具体地说,是由MATLAB内存管理机制所带来的问题。与Java的垃圾收集机制不同,在MATLAB的内存管理机制中,内存的释放发生在确定的时刻。
类对象的清除
我们可以使用clear
命令清除当前作用域中的某个变量或所有变量(使得对应的标识符恢复到未定义状态),例如
1
2clear a;
clear; % 清除全部变量
在函数调用等具有独立作用域的过程中,离开作用域也会隐式调用clear
清理所有的局部变量。
那么,clear
命令作用于一个自定义类型对象会发生什么?
- 对于全值类的对象,
clear
会直接清理它以及它的所有数据属性,并释放对应的内存空间 - 对于句柄类的对象,
clear
会清理对象自身,但是对于对象的数据属性,只会让其引用计数减一。只有数据的引用计数归零,才会释放相应的内存。
需要注意的是:
clear
命令无视类型访问权限的约束,对于私有属性的操作并没有什么区别;- 如果清理的数据属性是
handle
类型,MATLAB会在引用计数减为0时,自动调用它的delete
方法,然后才会释放内存,见下文。
类对象的析构
我们在前面已经介绍了类的构造方法,但是析构方法的介绍涉及到MATLAB的内存管理机制,不得不拖到后面。
handle
基类为我们提供了delete
方法(析构方法),delete
方法的效果是:无视数据属性的引用计数,强制释放其内存。(与懒拷贝类似,这里只是逻辑上的立刻释放,MATLAB有独立的内存管理机制,不需要用户干涉)
我们可以手动调用delete
方法,但这是一个很危险的行为,它会导致所有正在引用这个数据属性的其他对象失效,但是并不会影响变量的标识符,对于失效对象的访问会触发错误。MATLAB提供的内置函数isvalid
可以用于判断句柄类对象是否保持在有效状态。
delete
方法会被MATLAB在合适的时机自动调用,除非显式进行手动调用,一般情况下对delete
的自动调用不会导致无效对象的产生。
对于管理资源的句柄类,我们可以重写继承自handle
基类的delete
方法,来自定义它的析构行为,例如资源管理
1
2
3
4
5
6
7
8
9
10classdef demo < handle
properties
fID = fopen('tmp.txt');
end
methods
function delete(obj)
fclose(obj.fID)
end
end
end
与之不同的是,全值类不支持自定义析构,即使我们定义了delete
方法,它与其他方法也没什么区别,并且只能手动触发。
类对象的保存和加载
我们可以直接用save
和load
命令作用于自定义类型的对象,这没有任何问题,但是我们还是需要研究一下其中的细节。
save
函数将自定义类型的对象保存到MAT文件时,会保存如下信息:
- 变量名称
- 自定义类型名称
- 自定义类型的所有属性的默认值(如果MAT文件中有多个同一类型对象,那么默认值只会保留一份)
- 当前对象的所有属性值
不会保存如下信息:
- 类的完整定义
- 对象的常量属性
- 方法中的持久性变量
注意:
- 由于MAT文件不会保存自定义类型的定义,在重新加载时必须在搜索路径中可以找到这个类的定义文件;
- 对于句柄类,在保存时建议检查其有效性,对于无效的对象可以进行正常的保存和加载,但是加载得到的仍然是一个无效对象,不含数据。
在加载时我们需要尽量保持类的定义与保存时的定义一致,如果类的新定义和保存时的旧定义不一致,MATLAB会采用如下的措施进行转换,可能丢失数据
- 如果移除了一个旧属性,则加载时会丢弃对应的值;
- 如果添加了一个新属性,则加载时会对该属性的值取为新定义中的默认值。
除了默认的保存和加载行为,我们还可以通过定义特殊的saveobj
方法和loadobj
方法来自定义行为,
save
和load
方法在发现自定义类型提供了对应的saveobj
方法和loadobj
方法时,会在过程中调用它们。
saveobj
方法负责实现的功能是将对象转换为一个简单的结构体数组,save
命令会从结构体数组开始,负责将数据以及其他信息存储到文件中。
例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14classdef demo
properties
x
y
z
end
methods
function s = saveobj(obj)
s.x = obj.x;
s.y = obj.y;
s.z = obj.z;
end
end
end
与此同时,我们还需要提供配套的loadobj
方法,它的过程是反过来的:接收一个结构体数组,生成一个自定义类型的对象。需要注意的是,loadobj
方法必须标记为Static
静态方法,因为对象还没创建呢。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25classdef demo
properties
x
y
z
end
methods
function s = saveobj(obj)
s.x = obj.x;
s.y = obj.y;
s.z = obj.z;
end
end
methods(Static)
function obj = loadobj(s)
if isstruct(s)
obj = demo();
obj.x = s.x;
obj.y = s.y;
obj.z = s.z;
end
end
end
end
这部分内容很有价值:在非常耗时的科学计算中,有必要及时地保存中间结果,例如执行了一定步数后自动保存,在遇到错误或异常中断后,下次可以直接从中间结果开始继续计算。
有时候,一个自定义类型有很多数据属性,但是其中只有很少的一部分是值得保存的,其他部分只是计算的中间产物或辅助变量,如果全部保存下来不仅占用空间过大,而且耗时。
这些不需要被保存到MAT文件中的属性,我们可以将其标记为瞬态属性(Transient
),MATLAB在保存过程中会将其自动忽略,在加载后对应的值为空。(这比定制saveobj
和loadobj
更加简便)
例如 1
2
3
4
5
6
7
8
9
10
11
12classdef demo
properties
x
y
z
end
properties(Transient)
x1
y1
z1
end
end
补充
与通常的面向对象编程语言不同,MATLAB的面向对象编程还有一些特色内容,至少包括:
- 自定义类型对象的数组化操作,包括两种思路:
- 数组的每一个元素都是一个自定义类型的对象;
- 让自定义类型的属性变成数组,并且让方法兼容数组的相关操作;
- 事件与响应机制,主要是为MATLAB的GUI编程服务的。
第一个内容其实就是 Structure of Arrays vs Array of Structures 这个问题在MATLAB中的体现,两种做法的性能优劣需要根据具体的需求来分析。
目前这些都用不到,因此暂时忽略。