MATLAB面向对象学习笔记——2. 进阶
现在我们关注一些进阶的内容,这部分内容并不是标准的面向对象编程内容, 而是MATLAB的特殊机制所带来的,在面向对象编程中必须考虑的问题,更具体地说,是由MATLAB内存管理机制所带来的问题。 与Java的垃圾收集机制不同,在MATLAB的内存管理机制中,内存的释放发生在确定的时刻。
句柄类和全值类
在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等是一致的,即赋值和参数传递都蕴含着深复制的逻辑;
- 句柄类适合于资源管理和对大型数据的管理,句柄类的赋值和参数传递都蕴含着浅复制的逻辑。
- 对于资源管理,复制是不合理的;
- 对于大型数据,我们希望避免很多耗时的复制行为。
链式调用
我们来探究全值类和句柄类对于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
类对象的清除
我们可以使用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