这一章在干嘛?
前 10 章我们写的都是过程式代码:一个函数接一个函数地执行。这一章引入另一种组织代码的思想——面向对象编程(Object-Oriented Programming, OOP)。MATLAB 的切入点非常聪明:它不先讲抽象理论,而是让你从图形系统入手——你画过的每一张 plot,其实都是一个「对象树」上的对象,每个对象都有属性(数据)和方法(函数)。学完本章,你既能精确控制一张图的每个零件(改线宽、改颜色、改坐标轴),也能写出自己的 classdef 类。
11.1 OOP 心智模型:类、对象、属性、方法
编程语言大致分两派:过程式(procedural)和面向对象(object-oriented)。过程式程序由一堆函数组成,每个函数完成一个任务——我们前 10 章写的全是这种。面向对象程序则把数据和操作数据的函数打包在一起,形成一个「类」。
先建立一套术语,后面所有小节都在反复用它们:
| 术语 | 英文 | 一句话解释 |
|---|---|---|
| 类 | class | 一个「图纸」:定义了数据(属性)+ 操作数据的函数(方法) |
| 对象 | object | 按图纸造出来的「实物」,也叫类的实例(instance) |
| 实例化 | instantiation | 从类创建对象的过程 |
| 属性 | properties | 类里的数据,类似「捆绑在对象身上的变量」 |
| 方法 | methods | 类里的函数,负责读写属性 |
| 构造函数 | constructor | 与类同名的特殊方法,创建对象时自动调用,负责初始化属性 |
| 继承 | inheritance | 子类(subclass)自动拥有父类(superclass)的全部属性和方法,还可以加自己的 |
如果你写过 C,可以这样建立第一印象:一个类有点像 struct + 一组操作该 struct 的函数打成的包。C 里你要写 rect.len、rect_area(&rect),数据和函数是分开的;OOP 里它们长在一起——rect.area()。但注意差异:C 的 struct 只有数据没有「行为」,而且 OOP 还多出继承、访问控制、运算符重载这些 struct 玩不转的能力。
拿一个具体场景对比两套范式:前 10 章你写「矩形」大概是 r.len = 3; r.width = 5; 然后到处调用 r.len * r.width——数据和运算散落在脚本各处,谁都能把 r.width 改成 -5(负宽度,物理上荒谬但 MATLAB 不吭声)。换成类之后,「矩形」自己带着长宽两个属性、面积方法和「宽度不能为负」的规矩,外界想犯规都犯不了。这就是 OOP 的核心收益:把「数据 + 规则」封装成一个自我保护的整体,复杂程序里出错面会小很多。
为什么 MATLAB 要引入 OOP?
你前 10 章不用 OOP 也活得很好,为什么现在非要学?两个理由:① MATLAB 的图形系统本身就是用对象搭的——不搞懂对象,你就只能停留在「plot 出来看运气」的水平,没法精确控制图上的任何零件;② 当你的数据有「固定搭配的一组值 + 一组固定操作」时(比如一个矩形有长、宽、面积计算),用类封装比散装变量 + 函数安全得多——后面 11.7 节的属性校验会看到,类可以让非法数据根本写不进去。
MATLAB 里的类分两大类:值类(value class)和句柄类(handle class),核心区别在 11.6 节展开。这里先记住一句话:MATLAB 用句柄类实现了绘图中的图形对象——这就是为什么学 OOP 要从画图学起。
11.2 图形对象层次结构与句柄
MATLAB 的每张图都不是「一张图」,而是一棵对象树。树的根是整个计算机屏幕(root 对象,用 groot 函数引用),往下是 Figure 窗口,再往下是坐标轴(Axes),最底层是构成图形的基本积木——图形基元(graphics primitives),比如 line(线)、text(文本):

也就是说:屏幕上放着 Figure 窗口,Figure 里装着 Axes(用来定向图形的坐标系),Axes 里装着 line、text 这些绘图积木。树上的每个节点都是对象,每个对象都有属性。**句柄(handle)**就是一个指向某个对象的变量——拿到句柄,你就能读写这个对象的所有属性。
为什么要设计成这样?回忆你以前画图的经历:plot 完之后想改线宽,只能重新 plot 一遍;想让两条线一粗一细,只能在调用参数里塞风格字符串,可选项全靠背。句柄彻底改变了游戏规则——图不再是一次性的输出,而是一组可以被随时寻访、单独修改的活对象。曲线太细?hl.LineWidth = 4。坐标轴范围不对?ax.XLim = [...]。窗口标题想写工程名?f.Name = '实验数据'。每个零件都可以在画完之后无限次精修,这就是「精确控制一张图的每个零件」的底气所在。
11.2.1 创建 Figure 对象,查看属性
直接调用 figure 会弹出一个空白窗口并返回它的句柄:
>> f = figure
f =
Figure (1) with properties:
Number: 1
Name: ''
Color: [0.9400 0.9400 0.9400]
Position: [440 378 560 420]
Units: 'pixels'
Show all properties默认只显示几个常用属性:
Number:窗口编号 1;Name:窗口标题,默认空字符串;Color:背景色,用 RGB 三元组表示(三个 0~1 的数,0=该颜色分量亮度为 0,1=最亮;[0.94 0.94 0.94]是非常浅的灰,全 0 是黑、全 1 是白);Position:[左 下 宽 高]四个数——窗口左下角距屏幕左边 440 像素、距底边 378 像素,宽 560、高 420;Units:位置的单位,这里是像素。
想看全部属性,点输出里的 “Show all properties” 链接,或者把句柄传给 get 函数:
>> get(f)
Alphamap: [1x64 double]
BeingDeleted: 'off'
Color: [0.9400 0.9400 0.9400]
Colormap: [64x3 double]
Children: [0x0 GraphicsPlaceholder]
Parent: [1x1 Root]
Position: [440 378 560 420]
...属性有几十个,现在看不懂没关系!但有两个结构性属性必须注意:
Parent:这个对象的父节点是谁(figure 的 Parent 是 Root);Children:这个对象有哪些子节点(空窗口的 Children 为空)。
这对 Parent/Children 就是把整棵对象树串起来的「血缘关系」。get 返回的东西还能存进一个结构体里用点号访问:
>> fstruct = get(f);
>> fstruct.Color
ans =
0.9400 0.9400 0.9400只要 Figure 窗口不关,句柄 f 一直有效;窗口一关,句柄就失效了——这是后面会反复出现的坑。
11.2.2 对象句柄 vs 结构体:长得像,不是一回事
f 和 fstruct 都能用点号,于是容易混:
f是 Figure 对象的句柄,类型是matlab.ui.Figure;fstruct是get(f)返回的结构体,类型是struct。
>> class(f)
ans =
matlab.ui.Figure
>> class(fstruct)
ans =
structf.Name 和 fstruct.Name 此刻值相同,但改 f.Name 不会同步改 fstruct.Name——后者只是查询那一刻的快照拷贝。一个是活的遥控器,一张是拍下来的说明书照片。
常见坑:把对象句柄当结构体快照
改了
f.Color之后还去读旧的fstruct.Color,发现「没生效」——不是 bug,是结构体只保存了get(f)调用那一刻的值。要最新值,永远从句柄上读。
11.2.3 plot 也返回句柄
不止窗口,每次 plot 画的线本身也是一个对象,plot 函数会把它的句柄返回给你:
>> x = -2*pi: 1/5 : 2*pi;
>> y = sin(x);
>> hl = plot(x, y)
hl =
Line with properties:
Color: [0 0.4470 0.7410]
LineStyle: '-'
LineWidth: 0.5000
Marker: 'none'
MarkerSize: 6
MarkerFaceColor: 'none'
XData: [1x63 double]
YData: [1x63 double]
Show all properties这条 sin 曲线是用 line 图形基元画的。用 hl.Parent 可以看到它的父节点是一个 Axes 对象:
>> axhan = hl.Parent
axhan =
Axes with properties:
XLim: [-8 8]
YLim: [-1 1]
...想改 x 轴范围为 ?两种写法都行:
>> axhan.XLim = [-10 10] % 点语法
>> set(axhan, 'XLim', [-10 10]) % set 函数Color 属性用 RGB 向量定量描述颜色,几个常用值:[1 0 0] 红、[0 1 0] 绿、[0 0 1] 蓝、[1 1 1] 白、[0 0 0] 黑、[0.5 0.5 0.5] 中灰。把线加粗后颜色看得更清楚——改 LineWidth 即可:
>> hl.LineWidth = 4;
Tab 键补全属性名
记不清属性名?输入
hl.Li然后按 Tab,编辑器会列出所有以 Li 开头的属性(LineStyle、LineWidth……)。这比查文档快得多。
为什么本章要花这么大篇幅「查看」属性?
因为学会查看 = 学会一切。MATLAB 图形属性多达几十个,没有人背得下来;实际工作流永远是「句柄往
get里一丢(或点 Show all properties)→ 扫一眼属性名 → 猜一个改改看 → 不对再查文档」。这个探索循环跑顺了,你面对任何图形需求(透明度、虚线样式、刻度标签、字体……)都有能力自助。教材让读者「不必担心看不懂这些属性」,正是这个意思——属性表是查的,不是背的。
11.2.4 gcf / gca 与 Children 链
除了自己存句柄,MATLAB 还内置了两个「现查」函数:gcf(get current figure,当前 Figure 的句柄)和 gca(get current axes,当前坐标轴的句柄)。「当前」指的是最近被创建/点击的那个。看这个例子:
>> x = -pi: 0.1: pi;
>> f1 = figure(1);
>> p1 = plot(x, sin(x));
>> f2 = figure(2);
>> p2 = plot(x, cos(x), 'Color', [0 1 1]);
>> curfig = gcf % 当前图是 2 号
curfig =
Figure (2) with properties:
Number: 2
...当前 Figure 的 Children 存着坐标轴,而 gca 返回的就是它:
>> curfig.Children == gca
ans =
1再往下钻一层,Axes 的 Children 就是那条 cos 曲线的 Line 对象:
>> curfig.Children.Children
ans =
Line with properties:
Color: [0 1 1]
...层次结构一目了然:Figure → Axes → Line。句柄让你能顺着这棵树走到任何一个零件,单独修改它。
C 程序员的类比
句柄类似 C 里的指针:它「指向」一个对象。但有三点不同:① 句柄是强类型的(
matlab.ui.Figure的句柄不能随便当别的用);② 用==比较两个句柄比较的是「是否指向同一个对象」(handle 类重载了eq/ne),不是比指针数值;③ 对象有生命周期管理——Figure 窗口关掉,MATLAB 自动清理对象,不存在悬空野指针导致段错误的问题。
11.3 get / set 与点语法:读写图形属性
汇总一下,读写一个对象属性现在有三套写法:
% 读 Color
c1 = f.Color; % ① 点语法(R2014b 起,推荐)
c2 = get(f, 'Color'); % ② get 函数取单个属性
c3 = get(f).Color; % ③ get 全部 → 结构体 → 取字段
% 写 Color
f.Color = [0.5 0.5 0.5];
set(f, 'Color', [0.5 0.5 0.5]);教材明确推荐点语法:因为 R2014b 之后句柄本身就是对象,点语法是访问对象属性的标准姿势,而且支持 Tab 补全、赋值时还会回显新值(set 不回显)。get/set 并没有被淘汰——get(f) 一把梭列出全部属性、批量设置的场景仍然好用。
除了 properties/get,还有两个内置函数专门用来「透视」一个类:
>> properties(f) % 只列属性名(不列值,也不按字母排序)
Properties for class matlab.ui.Figure:
Position
Units
Renderer
...
>> methods(f) % 列出这个对象能调用的所有方法
Methods for class matlab.ui.Figure:
Figure clo double horzcat reset
addlistener details eq isprop set
...methods 输出里那些 eq、ne 就是 == 和 ~= 的函数形态——所以两个句柄可以直接比相等:
>> f = figure(1); g = figure(2);
>> f == g % 两个不同窗口 → false
ans =
0
>> h = f; f == h % 同一个对象的两个句柄 → true
ans =
1
>> f.Color == g.Color % 逐元素比 RGB,三个 1(颜色相同)
ans =
1 1 1常见坑:窗口关了,句柄就死了
所有属性修改(无论点语法还是 set)都要求对象还活着。Figure 窗口一点 × 关掉,
hl、f全部失效,再对它们 set 就报错。调试绘图属性时,先把窗口留着,改完满意了再关。
11.4 用 classdef 定义自己的类
MATLAB 不只有内置类,也允许你自定义类。类定义用 classdef 关键字,组织成块(block)结构:

基本骨架如下,保存在与类同名的 .m 文件里:
% MyClassName.m
classdef MyClassName
properties
prop1
prop2
end
methods
% 构造函数:与类同名
function obj = MyClassName(val1, val2)
obj.prop1 = val1;
obj.prop2 = val2;
end
% 其他操作属性的方法
end
end几条硬规则:
- 类定义文件必须与类同名(
SimpleClass.m里定义classdef SimpleClass); - 构造函数与类同名,只返回一个输出参数——初始化好的对象;
- 创建对象时构造函数自动被调用;不写构造函数,MATLAB 会替你生成一个,属性取 properties 块里的默认值(没给默认值就是空向量);
- 构造函数最好用
nargin检查输入参数个数,允许不带参数也能创建对象。
11.4.1 最小可用的类
% SimpleClass.m
classdef SimpleClass
properties
x % 没有默认值 → 空向量
y = 33; % 默认值 33
end
methods
function obj = SimpleClass(val1, val2)
if nargin == 2
obj.x = val1;
obj.y = val2;
end
end
end
end实例化就是「拿类名当函数调用」——这一步自动触发构造函数:
>> myobject = SimpleClass % 无参 → 用默认值
myobject =
SimpleClass with properties:
x: []
y: 33
>> newobject = SimpleClass(4, 22) % 传参 → 覆盖默认值
newobject =
SimpleClass with properties:
x: 4
y: 22属性用点语法读写:
>> myobject.x = 11
myobject =
SimpleClass with properties:
x: 11
y: 3311.4.2 用 varargin 写通用的构造函数,再加一个普通方法
% SimpleClassii.m
classdef SimpleClassii
properties
x
y = 33;
end
methods
function obj = SimpleClassii(varargin)
if nargin == 0
obj.x = 0;
elseif nargin == 1
obj.x = varargin{1};
else
obj.x = varargin{1};
obj.y = varargin{2};
end
end
% 普通方法:把输入值与两个属性相加
function outarg = ordMethod(obj, arg1)
outarg = obj.x + obj.y + arg1;
end
end
end>> objA = SimpleClassii % x:0, y:33
>> objB = SimpleClassii(4, 9) % x:4, y:9
>> ob = SimpleClassii(1, -6, 7, 200) % 多余参数被忽略:x:1, y:-6普通方法(ordinary method)的调用有两种等价形式:
>> resultA = ordMethod(objA, 5) % 显式把对象当第一个参数传
resultA =
38 % 0 + 33 + 5
>> resultB = objB.ordMethod(11) % 点语法:对象隐式传入
resultB =
24 % 4 + 9 + 11注意不管怎么调用,函数头都是两个形参——点语法只是把点号前面的对象偷偷塞进了第一个参数位。这与 C++/Python 的 this/self 是同一个思想,只不过 MATLAB 要求你在签名里明写出来。
类方法内部还能访问全局函数吗?
当然。方法体里可以调用任何 MATLAB 内置函数,只是「属于这个类」的方法要写在 methods 块里。这也是类与 struct 的本质差别:struct 只是数据容器,类是数据 + 行为 + 权限控制的统一体。
11.5 重载:让运算符和 disp 听你的
MATLAB 默认给所有类自动提供赋值运算符(逐属性拷贝的成员赋值),但其他运算符(<、+、==……)默认对自定义类没有定义,用了就报错。好消息是:这些运算符的含义由你决定。所有运算符都有函数形态,比如 a + b 等价于 plus(a, b)、a < b 等价于 lt(a, b)。在类里定义同名函数就叫重载(overloading)——给已有的函数名追加一个针对新类型的定义,MATLAB 根据参数类型自动选择用哪个。
11.5.1 Rectangle 类:构造函数 + 普通方法 + 两个重载
% Rectangle.m
classdef Rectangle
properties
len = 0;
width = 0;
end
methods
function obj = Rectangle(l, w)
if nargin == 2
obj.len = l;
obj.width = w;
end
end
% 普通方法:算面积
function outarg = rectarea(obj)
outarg = obj.len * obj.width;
end
% 重载 disp:自定义对象怎么显示
function disp(obj)
fprintf('The rectangle has length %.2f', obj.len)
fprintf(' and width %.2f\n', obj.width)
end
% 重载 lt:< 号的函数形态
function out = lt(obja, objb)
out = rectarea(obja) < rectarea(objb);
end
end
end设计决策点:「一个矩形小于另一个矩形」是什么意思? 这里定义为「面积更小」——但这完全是你的自由,按周长比、按长比都行。运算符语义由应用场景决定,这正是类强大的地方。
「重载」这个词值得多咀嚼一下。同一批函数名(plus、lt、disp……)早已被 MATLAB 内置类型占用,你在类里重新定义它们,并不会破坏内置功能——对 double 求 a + b 走内置加法,对 Rectangle 求 r1 + r2 走你的定义。MATLAB 根据参数的类型自动分发,这叫多态:同一个「名字」,对不同类型有不同行为。好处是用户代码可以写得很自然——sort 一组 Rectangle 对象时,只要你重载了 lt,排序函数根本不需要知道矩形是什么,它只会问「这个小于那个吗」。
用起来:
>> rect1 = Rectangle(3, 5)
rect1 =
The rectangle has length 3.00 and width 5.00 % 重载的 disp 生效了!
>> rect2 = rect1; % 自动成员赋值
>> rect2.width = 11
rect2 =
The rectangle has length 3.00 and width 11.00
>> rect1 < rect2 % 面积 15 < 面积 33
ans =
1
>> rect1 > rect2 % gt 没定义 → 报错
Undefined operator '>' for input arguments of type 'Rectangle'.只要不加分号,赋值语句就会自动调用你重载的 disp 来显示——所以重载 disp 等于给这个类定制了「官方输出格式」。
常见坑:重载函数的类型假设
rect1 < 20会报错Undefined function 'rectarea' for input arguments of type 'double'——因为lt假设两个参数都是 Rectangle。要支持混合类型,得在函数里用isa(inp1, 'Rectangle')检查类型:不是对象就把数字包装成Rectangle(inp, 1)(宽为 1,面积恰等于该数),再比较。健壮的重载函数应该做完整的类型分派。
11.6 继承与 handle 类:引用 vs 拷贝
11.6.1 子类:Box 继承 Rectangle
继承(inheritance)让子类自动拥有父类的全部属性和方法,再加自己的东西。语法是在 classdef 行用 < 跟父类名(这里的 < 不是小于号!):
% Box.m
classdef Box < Rectangle
properties
height = 0;
end
methods
function obj = Box(l, w, h)
if nargin < 3
l = 0; w = 0; h = 0;
end
obj@Rectangle(l, w) % 先调父类构造函数!
obj.height = h;
end
function out = calcvol(obj)
out = obj.len * obj.width * obj.height;
end
end
end关键语法 obj@Rectangle(l, w):调用父类 Rectangle 的构造函数初始化继承来的 len、width。这条调用必须放在访问任何对象属性之前。子类同名方法会覆盖(override)父类版本——比如 Box 里重写 disp,显示时就不会再走 Rectangle 的 disp:
>> mybox = Box(2, 5, 8)
mybox =
The rectangle has length 2.00 and width 5.00 % 没重写 disp 时只显示长宽
>> mybox.height
ans =
8在 Box 里重写 disp(打印长宽高)之后:
>> mybox = Box(2, 5, 8)
mybox =
The box has a length of 2.00, width 5.00
and height 8.0011.6.2 值类 vs 句柄类:本章最重要的区别
MATLAB 类分两种,行为天差地别:
- 值类(value class,默认):赋值 = 完整拷贝。改一个不影响另一个。内置数值类型(如 double)都是值类。
- 句柄类(handle class):
classdef MyClass < handle。赋值 = 多一个引用,所有变量指向同一份数据。plot 对象就是句柄类。
值类和句柄类用生活类比最清楚:值类像复印文件——你把原件复印一份给别人,别人在复印件上乱涂,原件毫发无损;句柄类像共享在线文档——你把链接发给别人,别人改的和你看到的是同一份文档。选哪种取决于语义:数值、日期、矩形这类「值」天然该用值类(改副本不该惊动原件);而图形窗口、文件连接、传感器节点这类「实体」天然该用句柄类——你就是需要「多处引用同一个东西,谁改都算数」。MATLAB 把 plot 对象设计成句柄类正是这个道理:一个窗口可能被脚本、回调、用户鼠标同时「盯着」,必须大家看到同一个。
先用 double 感受值类:
>> num = 33;
>> value = num;
>> value = value + 4
value =
37
>> num % 完全不受影响
num =
33再感受句柄类——plot 句柄赋给另一个变量后,两个变量控制的是同一条线:
>> x = 0: 0.1: pi;
>> plothan = plot(x, sin(x));
>> handleb = plothan;
>> set(handleb, 'LineWidth', 3) % 用哪个句柄改都一样,改的是同一条线用自定义类做实验更直观。把 Rectangle 改成句柄类(< handle):
classdef HandleRect < handle
properties
len = 0;
width = 0;
end
methods
function obj = HandleRect(l, w)
if nargin == 2
obj.len = l;
obj.width = w;
end
end
function outarg = rectarea(obj)
outarg = obj.len * obj.width;
end
function disp(obj)
fprintf('The rectangle has length %.2f', obj.len)
fprintf(' and width %.2f\n', obj.width)
end
end
endhandle 父类自动送来了 ==、<、> 等比较运算符的重载,于是:
>> HRecA = HandleRect(2, 7.5);
>> HRecB = HRecA; % 引用,不是拷贝!
>> HRecA == HRecB
ans =
1现在改 HRecA.len,HRecB 跟着变:
>> HRecA.len = 6;
>> HRecA
HRecA =
The rectangle has length 6.00 and width 7.50
>> HRecB
HRecB =
The rectangle has length 6.00 and width 7.50 % 同一个对象!但调用构造函数创建的新对象永远是一个全新对象,即使属性值恰好相同也不相等:
>> HRecC = HandleRect(2, 7.5); % 属性值一样,但它是独立对象
>> HRecA == HRecC
ans =
0三个对象的关系画出来是这样——HRecA 和 HRecB 的箭头指向同一个盒子,HRecC 指向另一个值相同的盒子:

11.6.3 值类 / 句柄类传参的对比实验
这一组实验最能暴露两类行为差异。两个类各有一个 double 属性 x 和四个方法:构造函数、add(返回新对象)、timestwo(返回新对象)、timesthree(就地改属性但不返回任何东西)。
值类 valClass:
% valClass.m
classdef valClass
properties
x = 0;
end
methods
function obj = valClass(in)
if nargin == 1
obj.x = in;
end
end
function out = add(obja, objb)
out = valClass(obja.x + objb.x);
end
function outobj = timestwo(inobj)
outobj = valClass(inobj.x * 2);
end
function timesthree(obj)
obj.x = obj.x * 3; % 注意:无返回值,改了个寂寞
end
end
endtimestwo 正常工作——但它返回的是一个新对象,原对象不动。调用前基区工作空间里只有 ha、hb 两个对象:

>> va = valClass(3);
>> vb = valClass(5);
>> vmult2 = timestwo(vb)
vmult2 =
valClass with properties:
x: 10
>> vb
vb =
valClass with properties:
x: 5 % vb 还是 5
而 timesthree 是个彻底的无效函数:函数内部确实把传入的 obj 的 x 乘了 3,但那是基区对象的一份拷贝,函数一结束这份拷贝就销毁了,什么都没留下来:
>> timesthree(vb)
>> vb
vb =
valClass with properties:
x: 5 % 纹丝不动句柄类 hanClass(同样的方法,只是 classdef hanClass < handle)就完全不同了。timestwo 执行期间,函数工作空间里的 inobj 通过虚线与基区的 hb 指向同一个对象——传的是引用:


而 timesthree 这次真的干了活——因为 obj 和 hb 是同一个对象。先看它调用前的基区状态:




>> ha = hanClass(12);
>> hb = hanClass(7);
>> timesthree(hb)
>> hb
hb =
hanClass with properties:
x: 21 % 没有返回值,属性却被改了!结论一句话:句柄类对象传进函数后,函数可以就地修改它的属性而不返回任何东西;值类做不到。写 timesthree(vb) = 赋值 这类代码时,值类会报 Too many output arguments(函数没定义输出),两类都一样。
常见坑:混淆值类和句柄类
这是教材列出的本章头号陷阱。两个典型翻车现场:① 以为
b = a拷贝了一份句柄对象,改 b 却发现 a 也变了;② 给值类写了个「就地修改」函数还疑惑为什么不起作用。拿不准时,先看 classdef 第一行有没有< handle。
和 C 对比着记
值类 ≈ C 的结构体按值传参(函数拿到拷贝);句柄类 ≈ C 传指针(函数能改原数据)。但 C 的指针可以对任意地址做算术、可能悬空,MATLAB 句柄不能做算术、生命周期由系统管理——安全得多,代价是灵活性低。
11.7 属性与方法的高级控制
11.7.1 属性的访问属性(attributes)
属性和方法都能挂属性控制(attributes),写在块首的圆括号里。对属性最常用的是访问控制,分三档:
public(默认):任何地方都能读写;private:只有本类的方法能访问;protected:本类及其子类的方法能访问。
读写还可分开控制:GetAccess(读)、SetAccess(写)、Access(读写一起)。下面这个类,num 完全公开,word 允许读但禁止外部写:
% valAttributes.m
classdef valAttributes
properties
num = 0;
end
properties (SetAccess = protected)
word = 'hello';
end
methods
function obj = valAttributes(n, w)
if nargin == 2
obj.num = n;
obj.word = w;
end
end
end
end>> valA = valAttributes(3, 'hi');
>> valA.num = 14 % public:随便写
>> valA.word % GetAccess 默认 public:能读
ans =
hi
>> valA.word = 'ciao' % SetAccess = protected:不能写!
You cannot set the read-only property 'word' of valAttributes.这个能力极其有用:把属性设为外部只读,再通过类方法修改它,就能在方法里集中做合法性检查——非法数据从根源上进不了系统。
工程视角再强调一次它的价值:假设你写一个温度监控类,温度属性任何代码都能改,那某处一个手滑赋值 t.temp = -999 就会让后续所有报警判断失效,而且没人知道是谁改的。加了 SetAccess = protected 之后,温度只能走 set.temp 这一条通道,校验逻辑写一次就管住所有入口,出了问题排查范围也锁定在类内部——这就是「封装」二字在真实工程里的含义。
11.7.2 set.PropertyName:给属性装上门卫
MATLAB 有一类特殊方法 get.PropertyName / set.PropertyName,不能直接调用,而是在任何代码读/写该属性时自动触发。最实用的场景:重写 set.grade,拦截所有对 grade 属性的赋值并校验范围:
% valSet.m
classdef valSet
properties
grade = 0;
end
methods
function obj = valSet(in)
if nargin == 1
obj.grade = in; % 这一句也会触发 set.grade!
end
end
function obj = set.grade(obj, val)
if val >= 0 && val <= 100
obj.grade = val;
else
error('Grade not in range')
end
end
end
end>> valobj = valSet(98) % 构造函数内部赋值也被拦截校验
valobj =
valSet with properties:
grade: 98
>> badobj = valSet(105)
Error using valSet/set.grade
Grade not in range
>> valobj.grade = -5 % 直接赋值同样拦下
Error using valSet/set.grade
Grade not in range句柄类的版本几乎一样,唯一区别:句柄类的 set.grade 不需要返回对象(签名是 function set.grade(obj, val))——因为它改的就是原对象本身。对比一下 struct 的世界:struct.feild = value 拼错字段名不会报错,只会默默新增一个错误字段;而类对象上写错属性名会直接抛错——类从机制上帮你挡住了一整类 typo bug。
11.7.3 方法属性与静态方法
方法块同样支持属性:Access = private/protected 控制谁能调用;Abstract(抽象方法,无实现);Hidden(不在 methods 列表显示);Sealed(禁止子类重定义)。特别的是 Static(静态方法):它不属于任何对象,用类名直接调用,适合放「跟具体对象无关、但概念上属于这个类」的计算,比如单位换算:
classdef StatClass
methods (Static)
function out = statex(in)
out = in * 10;
end
end
end
>> StatClass.statex(4) % 用类名调,不是对象名
ans =
4011.7.4 析构函数、事件与监听器
- 析构函数(destructor):与构造函数相对,在对象销毁时自动调用,通过重载
delete实现(要求恰好一个对象输入参数、无输出)。用途是「打扫战场」,比如类打开过文件,析构时确保文件被正确关闭。MATLAB 里它是可选的。 - 事件与监听器:句柄类(仅句柄类!)可以有
events块,实现事件驱动编程(event-driven programming)。三个角色:事件(event,一个动作,如属性被改、鼠标被点击)、监听器(listener,侦测事件并响应)、回调(callback,监听器触发后执行的函数)。配套两个方法:notify(事件发生时通知监听器)和addlistener(给对象挂监听器)。GUI 编程全靠这套机制——你按一个按钮(事件),触发一个回调函数执行。本系列后续 GUI 章节会实际用到。
本章的知识地图
① 图形系统是理解 OOP 的入口:root → figure → axes → line 的对象树 + 句柄;② classdef 自定义类:properties + methods + 构造函数;③ 重载让
disp、<这些「名字」为你的类服务;④ 值类拷贝 / 句柄类引用——fprintf 之前先想清楚你手里是遥控器还是照片;⑤ 访问控制 + set.PropertyName = 数据保险箱。
11.8 本章速查表与常见坑
11.8.1 函数与命令速查
| 函数/命令 | 作用 | 关键提醒 |
|---|---|---|
groot | 返回 root 对象(整个屏幕)的句柄 | 树的根;想知道屏幕尺寸就 groot.ScreenSize |
figure / figure(n) | 创建 Figure 窗口并返回句柄 | 窗口关闭即句柄失效 |
gcf | 当前 Figure 的句柄 | ”get current figure” |
gca | 当前坐标轴的句柄 | ”get current axes” |
get(h) / get(h,'Prop') | 列出全部属性 / 读单个属性 | get(h) 返回结构体快照 |
set(h,'Prop',v) | 写属性 | 不回显新值 |
h.Prop | 点语法读写属性(R2014b 起推荐) | 支持 Tab 补全,赋值回显 |
h.Parent / h.Children | 沿对象树上下爬 | 可连续点号:h.Children.Children |
properties(obj) | 列出类的属性名 | 只列名字不列值 |
methods(obj) | 列出对象可调用的方法 | 包括从 handle 继承的 |
class(obj) | 返回对象的内置类名 | 如 matlab.ui.Figure |
classdef A < B | 定义 A 为 B 的子类 | 这里的 < 不是小于号 |
obj@Superclass(args) | 在子类构造函数里调父类构造函数 | 必须先于其他属性访问执行 |
nargin / varargin | 构造函数判参个数 / 接任意参数 | 构造函数要能无参创建 |
isa(x, 'ClassName') | 判断 x 是不是某类(或其子类) | 重载函数做类型分派用 |
notify / addlistener | 事件通知 / 挂监听器 | 仅句柄类可用 |
11.8.2 常见坑清单
| 坑 | 症状 | 修法 |
|---|---|---|
| 混淆值类与句柄类 | b = a; b.prop = 1 后 a 意外变了(句柄类),或改了个寂寞(值类) | 先看 classdef 首行有无 < handle |
| 忘了构造函数自动调用 | 以为「只是定义」,实际对象已按默认值初始化 | 建对象 = 调构造函数,写类时想好无参情形 |
| Figure 窗口关了还 set | Invalid or deleted object 报错 | 改完属性再关窗口 |
| struct 字段名拼错不报错 | struct.feild = v 静默新增错误字段 | 用类替代 struct,拼错直接抛错 |
| 值类写「就地修改」函数 | 函数跑完属性没变 | 要么返回新对象,要么把类改成句柄类 |
| 子类构造函数先摸属性 | 报错(父类部分未初始化) | obj@Superclass(...) 永远放第一行 |
11.9 通关标准与自测
本章通关标准:
- 能默画图形对象层次树(root → figure → axes → line),并用
gcf/gca/hl.Parent在命令窗口里真实地走一遍这棵树,用点语法改掉一条线的颜色和线宽。- 能不看书写出
SimpleClass:含一个带默认值的属性、一个用nargin判断参数的构造函数、一个普通方法,并解释「构造函数在什么时候被自动调用」。- 能用一句话讲清值类与句柄类的区别(拷贝 vs 引用),并预测
b = a; b.prop = 1之后a.prop的值——对两种类分别预测。
1.
f = figure之后执行fs = get(f); f.Name = 'T'; fs.Name显示什么?为什么?显示空字符串
''。因为fs是get(f)调用那一刻所有属性值的结构体快照,之后f.Name的修改不会回写到fs里。f是活的对象句柄,fs是死的数据拷贝——二者类型也不同(matlab.ui.Figurevsstruct)。
2.
Rectangle类只重载了lt,为什么rect1 > rect2会报错?因为
>的函数形态gt没有在类里定义。MATLAB 的运算符对自定义类不会自动生效,每个用到的运算符都得单独重载(lt对应<,gt对应>,eq对应==……)。报错信息Undefined operator '>' for input arguments of type 'Rectangle'就是提示你补一个 gt 方法。句柄类比较幸运:继承自内置 handle 父类时自动获得全套比较运算符。
3.
HRecB = HRecA(句柄类)之后,如何创建一个「属性相同但独立」的新对象?调用构造函数:
HRecC = HandleRect(HRecA.len, HRecA.width)。赋值语句只会增加一个指向同一对象的引用;只有构造函数才创建新对象。HRecA == HRecC会返回 0——==对句柄类比较的是「是否同一个对象」,而不是属性是否相同。
4. 值类的
timesthree(obj)(就地改属性、无返回值)为什么是无效代码?句柄类为什么就有效?值类传参传的是拷贝:函数内部改的
obj是基区对象的一份复制品,函数返回后复制品销毁,基区对象原样不动(MATLAB 还会对此发出警告)。句柄类传的是引用:函数里的obj与基区对象是同一个,修改直接落在原对象上,函数结束后修改保留。这个对比是理解两类类差异的最短路径。
5. 怎样让
obj.grade = 150这种非法赋值在赋值那一刻就报错,而不是事后检查?在类里重写
set.grade(obj, val)访问方法,在里面校验val >= 0 && val <= 100,不合法就error(...)。这个方法会在任何对 grade 的赋值时自动触发——包括构造函数内部的赋值。这比在每个使用处手写检查可靠得多:门卫只有一个,谁都绕不过去。