Dependent Property and Its get Method and set Method of MATLAB Handle Class

Jan. 26, 2023

Dynamic Calculation Properties

在实际编程中,有一些属性的值是依赖于其他属性的,当其他属性发生变化时,该属性也将相应地发生变化,这类属性通常称为Dependent属性(非独立属性)。例如,二维坐标中的点$p(x,y)$到原点的距离$r$可以表示成:

\[r=\sqrt{x^2+y^2}\]

$r$的值依赖于$x$和$y$。

一开始学习面对对象编程时,可能会编写出下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
classdef Point2D < handle
    properties
        x
        y
        r
    end
    methods
        function obj = Point2D(x0, y0)
            obj.x = x0;
            obj.y = y0;
            obj.r = sqrt(obj.x^2+obj.y^2);
        end
    end
end

但是这里的r值仅仅在Constructor中进行了计算,如果对象的xy属性发生了变化,r值并不会随之改变。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
% === Script ===
clc, clear, close all

p = Point2D(1,1);
disp([p.x, p.y, p.r])

p.x = 2;
p.y = 2;
disp([p.x, p.y, p.r])

% === Command line ===
1.0000    1.0000    1.4142

2.0000    2.0000    1.4142

一种很简单的做法是增加一个更新r的方法,当x或者r属性发生了改变就调用该方法更新:

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
31
32
33
34
% === Point2D.m ===
classdef Point2D < handle
    properties
        x
        y
        r
    end
    methods
        function obj = Point2D(x0, y0)
            obj.x = x0;
            obj.y = y0;
            CalculateR(obj);
        end
        function CalculateR(obj)
            obj.r = sqrt(obj.x^2+obj.y^2);
        end
    end
end

% === Script ===
clc, clear, close all

p = Point2D(1,1);
disp([p.x, p.y, p.r])

p.x = 2;
p.y = 2;
p.CalculateR();
disp([p.x, p.y, p.r])

% === Command line ===
1.0000    1.0000    1.4142

2.0000    2.0000    2.8284

但是很明显,如果这样设计程序,每次xy属性发生改变都要调用CalculateR方法重新计算,这么做是很麻烦的,并且复杂一些得话,还需要编写监控xy属性发生改变的函数。

一种解决方式是使用Hanldle类的Dependent属性和get方法实现属性的动态更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
classdef Point2D < handle
    properties
        x
        y
    end
    properties(Dependent)
        r
    end
    methods
        function obj = Point2D(x0, y0)
            obj.x = x0;
            obj.y = y0;
        end
        function r = get.r(obj)
            r = sqrt(obj.x^2+obj.y^2);
            disp('get.r called');
        end
    end
end

在上面的用法中,对象内部并没有给Dependent属性分配物理的存储空间,而是每次访问Dependent属性时,它们才有一个get方法动态计算出来

我们可以简单地通过脚本文件测试一下这样定义的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
% === Script ===
p = Point2D(1,1);
disp([p.x, p.y, p.r])

p.x = 2;
p.y = 2;
disp([p.x, p.y, p.r])
disp([p.x, p.y, p.r])

p.x = 2;
p.y = 3;
disp([p.x, p.y])

% === Command line ===
get.r called
    1.0000    1.0000    1.4142

get.r called
    2.0000    2.0000    2.8284

get.r called
    2.0000    2.0000    2.8284

     2     3

可以看到:

(1)尽管xy属性值不发生变化,我们再次访问r属性时,r属性所对应的get方法也会被重新调用;

(2)虽然xy属性值发生了改变,但是如果我们不访问r属性,程序也不会调用r.get方法;但是,如果此时我们在工作空间中点开对象p

image-20230126143018838

可以看到实际上属性r的值已经重新进行了计算。

(3)用户每次查看对象p,程序都会调用r.get方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>> p
p = 
get.r called
  Point2D with properties:
    x: 2
    y: 3
    r: 3.6056

>> p
p = 
get.r called
  Point2D with properties:
    x: 2
    y: 3
    r: 3.6056


set and get of Handle Class

实际上,get方法是继承自handle类的一类方法,与此类似的还有set方法。set方法和get方法为对象属性的赋值对象属性的查询提供了一个中间层。

set Method

属性的set方法通常用来检测属性的赋值是否符合要求。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
classdef A < handle
    properties
        a
    end
    methods
        function set.a(obj,val)
            if val >= 0
                obj.a = val;
                disp('Right value!')
            else
                error('a must be positive')
            end
        end
    end
end

case 1:正确赋值

1
2
3
4
clc, clear, close all

Aobj = A();
Aobj.a = 10;

image-20230126144925398

case 2:错误赋值

1
2
3
4
clc, clear, close all

Aobj = A();
Aobj.a = -10;

程序报错:

image-20230126144943600

任何对属性a的赋值都会经过set.a的中间层(由MATLAB负责调用)。

属性的set方法还常用于GUI的编写中。例如:假如某对象的某属性值来自于GUI界面上的用户输入值,那么在将用户输入值赋给该属性之前,就可以使用set方法检查该输入值(包括数据类型、阈值等)是否符合要求。

注:如果想要对函数的输入参数做更全面的系统的检查,可以使用validateattributes函数或者inputParser类。

get Method

get方法提供对成员属性查询操作的一个中间层。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
% === A.m ===
classdef A < handle
    properties
        b = 10;
    end
    methods
        function val = get.b(obj)
            val = obj.b;
            disp('getter called')
        end
    end
end

% === Command line ===
>> obj = A();
>> obj.b
getter called
ans =
    10

在类的外部,对属性b的查询(Query)都将经过这个中间方法。并且,和上面一样,一旦调用A类对象就会调用该get方法:

1
2
3
4
5
>> obj
obj = 
getter called
  A with properties:
    b: 10

如果为两个属性都设置get方法,则两个get方法都会被调用:

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
% === A.m ===
classdef A < handle
    properties
        a = 10;
        b = 10;
    end
    methods
        function val = get.a(obj)
            val = obj.a;
            disp('getter a called')
        end
        function val = get.b(obj)
            val = obj.b;
            disp('getter b called')
        end
    end
end

% === Command line ===
>> obj = A;
>> obj
obj = 
getter a called
getter b called
  A with properties:
    a: 10
    b: 10

Backward Compatibility of Programs

set方法和get方法为Dependent属性的赋值与查询操作定义了中间层,因此技术人员可以利用这一特性实现程序的向后兼容。

例如,最开始开发者定义了一个Record类,并且其中的一个属性date用来记录Record对象的时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
% === Record.m ===
classdef Record < handle
    properties
        date
    end
    methods
        function obj = Record(date)
            obj.date = date;
        end
    end
end

% === Script ===
R = Record(date);
disp(R.date)

% === Command line ===
26-Jan-2023

假设现在用户已经广泛使用了这个Record类,而技术人员想要修改升级Record类,例如想要把Record类中的date名字改得更有意义一些,比如叫做timeStamp。此时就可以提供一些date属性的中间层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
classdef Record < handle
    properties
        timeStamp
    end
    properties(Dependent,Hidden)
        date
    end
    methods
        function obj = Record(timeStamp)
            obj.timeStamp = timeStamp;
        end
        function set.date(obj,val)
            obj.timeStamp = val;
        end
        function val = get.date(obj)
            val = obj.timeStamp;
        end
    end
end

面对此时的新版本,用户在外部初始化Record对象时,参数传递给timeStamp属性,并且推荐调用timeStamp属性查询记录日期:

1
2
3
4
% === Command line ===
>> R = Record(date);
>> disp(R.timeStamp)
26-Jan-2023

另外,由于将date设置成为了Hidden属性,timeStamp属性成为了唯一的public属性:

1
2
3
4
% === Command line ===
>> properties(R)
Properties for class Record:
    timeStamp

但尽管如此,老版本的程序同样可以运行:

1
2
3
4
% === Command line ===
>> R = Record(date);
>> disp(R.date)
26-Jan-2023

只是此时程序会经过get方法中间层转而访问timeStamp属性。这样,旧程序能够不加修改而继续使用,实现了软件的向后兼容。

另外还需要注意一点,类内部的赋值和查询也会调用set方法和get方法。例如,我们在新版的Constructor中仍然给date属性赋值(有别于上面的给timeStamp属性赋值),并且直接查询此时的date属性和timeStamp属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
classdef Record < handle
    properties
        timeStamp
    end
    properties(Dependent,Hidden)
        date
    end
    methods
        function obj = Record(date)
            obj.date = date;
            disp("date: "+obj.date)
            disp("timeStamp: "+obj.timeStamp)
        end
        function set.date(obj,val)
            obj.timeStamp = val;
            disp("set method called")
        end

        function val = get.date(obj)
            val = obj.timeStamp;
            disp("get method called")
        end
    end
end

结果为:

image-20230126155416730

可以看到,虽然我们在Constructor中给date属性赋值,但是程序仍然通过set方法同样地timeStamp属性进行赋值;程序在解析disp("date: "+obj.date)中的obj.date时调用get方法,转而查询timeStamp属性值。


 Dependent Property Features

在上文的Pont2D类的定义中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
classdef Point2D < handle
    properties
        x
        y
    end
    properties(Dependent)
        r
    end
    methods
        function obj = Point2D(x0, y0)
            obj.x = x0;
            obj.y = y0;
        end
        function r = get.r(obj)
            r = sqrt(obj.x^2+obj.y^2);
            disp('get.r called');
        end
    end
end

我们将变量r定义为了Dependent变量。实际上,这在我们上面所应用的场景中是不必要的。只要我们设置了r.get方法,就能够实现属性r的动态更新。例如:

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
% === Point2D ===
classdef Point2D < handle
    properties
        x
        y
        r
    end
    methods
        function obj = Point2D(x0, y0)
            obj.x = x0;
            obj.y = y0;
        end
        function r = get.r(obj)
            r = sqrt(obj.x^2+obj.y^2);
            disp('get.r called');
        end
    end
end

% === Command line ===
>> A = Point2D(1,1);
>> A.r
get.r called
ans =
    1.4142
>> A.x = 10;
>> A.r
get.r called
ans =
   10.0499

但是,当我们试图直接修改r的值(不合理的做法)时,两种定义的响应不同。

对于将r设置为Dependent属性的类(Class 1):

1
2
3
>> A = Point2D(1,1);
>> A.r = 10;
In class 'Point2D', no set method is defined for dependent property 'r'.  A dependent property needs a set method to assign its value.

对于将r设置为默认属性的类(Class 2):

1
2
3
4
5
6
7
8
9
>> A = Point2D(1,1);
>> A.r = 10;
>> A
A = 
get.r called
  Point2D with properties:
    x: 1
    y: 1
    r: 1.4142

可以看到,对于Class 1而言,程序会对“直接修改属性r”这一行为报错,报错信息提示我们:当我们没有为Dependent属性设置set方法时,程序无法直接给Dependent属性赋值;而对于Class 2,虽然语句A.r = 10;没有报错,但是也没有赋值,或者是赋值了,但是在我们调用的时候,程序又重新计算了r属性(我倾向于后者的情况)。

对比两种方法,我们会发现将r设置为Dependent属性更为合理,程序的报错信息会让我们去思考我们的程序设计是否有问题,以及是否完整地考虑了用户可能的行为。更进一步地,我们可以设置此时的set属性,向用户报出详细的报错信息。例如:

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
% === Point2D ===
classdef Point2D < handle
    properties
        x
        y
    end
    properties(Dependent)
        r
    end
    methods
        function obj = Point2D(x0, y0)
            obj.x = x0;
            obj.y = y0;
        end
        function r = get.r(obj)
            r = sqrt(obj.x^2+obj.y^2);
            disp('get.r called');
        end
        function set.r(~,~)
            error("The value of 'r' cannot be assigned directly")
        end
    end
end

% === Command line ===
>> A = Point2D(1,1);
>> A.r = 10;
Error using Point2D/set.r
The value of 'r' cannot be assigned directly

上文程序向后兼容的例子就根据需要“向后兼容”的需要定义了Dependent属性dateset方法。


References

[1] 徐潇. MATLAB 面向对象编程: 从入门到设计模式. 北京航空航天大学出版社, 2017.