这一章在干嘛?

前 10 章我们写的都是过程式代码:一个函数接一个函数地执行。这一章引入另一种组织代码的思想——面向对象编程(Object-Oriented Programming, OOP)。MATLAB 的切入点非常聪明:它不先讲抽象理论,而是让你从图形系统入手——你画过的每一张 plot,其实都是一个「对象树」上的对象,每个对象都有属性(数据)和方法(函数)。学完本章,你既能精确控制一张图的每个零件(改线宽、改颜色、改坐标轴),也能写出自己的 classdef 类。

11.1 OOP 心智模型:类、对象、属性、方法

11.2 图形对象层次结构与句柄

11.3 get / set 与点语法:读写图形属性

11.4 用 classdef 定义自己的类

11.5 重载:让运算符和 disp 听你的

11.6 继承与 handle 类:引用 vs 拷贝

11.7 属性与方法的高级控制

11.8 本章速查表与常见坑

11.9 通关标准与自测

11.1 OOP 心智模型:类、对象、属性、方法

编程语言大致分两派:过程式(procedural)和面向对象(object-oriented)。过程式程序由一堆函数组成,每个函数完成一个任务——我们前 10 章写的全是这种。面向对象程序则把数据和操作数据的函数打包在一起,形成一个「类」。

先建立一套术语,后面所有小节都在反复用它们:

术语英文一句话解释
class一个「图纸」:定义了数据(属性)+ 操作数据的函数(方法)
对象object按图纸造出来的「实物」,也叫类的实例(instance)
实例化instantiation从类创建对象的过程
属性properties类里的数据,类似「捆绑在对象身上的变量」
方法methods类里的函数,负责读写属性
构造函数constructor与类同名的特殊方法,创建对象时自动调用,负责初始化属性
继承inheritance子类(subclass)自动拥有父类(superclass)的全部属性和方法,还可以加自己的

如果你写过 C,可以这样建立第一印象:一个类有点像 struct + 一组操作该 struct 的函数打成的包。C 里你要写 rect.lenrect_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(文本):

MATLAB 图形对象的层次结构:Root(屏幕) → Figure Window → Axes → Graphics Primitives,上层是 Parent,下层是 Children

也就是说:屏幕上放着 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 结构体:长得像,不是一回事

ffstruct 都能用点号,于是容易混:

  • fFigure 对象的句柄,类型是 matlab.ui.Figure
  • fstructget(f) 返回的结构体,类型是 struct
>> class(f)
ans =
matlab.ui.Figure
>> class(fstruct)
ans =
struct

f.Namefstruct.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;

把 sin 曲线的 LineWidth 从默认 0.5 改成 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 输出里那些 eqne 就是 ==~= 的函数形态——所以两个句柄可以直接比相等:

>> 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 窗口一点 × 关掉,hlf 全部失效,再对它们 set 就报错。调试绘图属性时,先把窗口留着,改完满意了再关。

11.4 用 classdef 定义自己的类

MATLAB 不只有内置类,也允许你自定义类。类定义用 classdef 关键字,组织成块(block)结构:

一个类定义文件以 classdef 关键字开头,内部由 properties 块和 methods 块组成

基本骨架如下,保存在与类同名的 .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: 33

11.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

设计决策点:「一个矩形小于另一个矩形」是什么意思? 这里定义为「面积更小」——但这完全是你的自由,按周长比、按长比都行。运算符语义由应用场景决定,这正是类强大的地方。

「重载」这个词值得多咀嚼一下。同一批函数名(plusltdisp……)早已被 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 的构造函数初始化继承来的 lenwidth这条调用必须放在访问任何对象属性之前。子类同名方法会覆盖(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.00

11.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
end

handle 父类自动送来了 ==<> 等比较运算符的重载,于是:

>> HRecA = HandleRect(2, 7.5);
>> HRecB = HRecA;        % 引用,不是拷贝!
>> HRecA == HRecB
ans =
  1

现在改 HRecA.lenHRecB 跟着变:

>> 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 指向另一个值相同的盒子:

句柄类赋值不拷贝数据: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
end

timestwo 正常工作——但它返回的是一个新对象,原对象不动。调用前基区工作空间里只有 ha、hb 两个对象:

timestwo 调用前的基区:ha(x:12)、hb(x:7)

>> va = valClass(3);
>> vb = valClass(5);
>> vmult2 = timestwo(vb)
vmult2 =
  valClass with properties:
    x: 10
>> vb
vb =
  valClass with properties:
    x: 5        % vb 还是 5

值类的 timestwo:函数结束后基区工作空间里有 va(x:3)、vb(x:5) 和新对象 vmult2(x:10),三者互相独立

timesthree 是个彻底的无效函数:函数内部确实把传入的 obj 的 x 乘了 3,但那是基区对象的一份拷贝,函数一结束这份拷贝就销毁了,什么都没留下来:

>> timesthree(vb)
>> vb
vb =
  valClass with properties:
    x: 5        % 纹丝不动

句柄类 hanClass(同样的方法,只是 classdef hanClass < handle)就完全不同了。timestwo 执行期间,函数工作空间里的 inobj 通过虚线与基区的 hb 指向同一个对象——传的是引用:

句柄类的 timestwo 执行中:基区的 ha、hb 与函数区的 inobj、outobj。注意 inobj 与 hb 指向同一对象(虚线表示引用)

句柄类 timestwo 结束后:基区有 ha(x:12)、hb(x:7)、hmult2(x:14),hb 未被修改

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

timesthree 调用前基区状态:ha(x:12)、hb(x:7)

timesthree 开始时:函数区的 obj 与基区的 hb 指向同一对象(x:7)

obj.x = obj.x * 3 执行后:由于 obj 就是 hb,基区里的值直接变成 21

timesthree 结束后:函数工作空间销毁,但基区 hb 的 x 已经永久变成 21

>> 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 =
    40

11.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 窗口关了还 setInvalid or deleted object 报错改完属性再关窗口
struct 字段名拼错不报错struct.feild = v 静默新增错误字段用类替代 struct,拼错直接抛错
值类写「就地修改」函数函数跑完属性没变要么返回新对象,要么把类改成句柄类
子类构造函数先摸属性报错(父类部分未初始化)obj@Superclass(...) 永远放第一行

11.9 通关标准与自测

本章通关标准:

  1. 能默画图形对象层次树(root → figure → axes → line),并用 gcf/gca/hl.Parent 在命令窗口里真实地走一遍这棵树,用点语法改掉一条线的颜色和线宽。
  2. 能不看书写出 SimpleClass:含一个带默认值的属性、一个用 nargin 判断参数的构造函数、一个普通方法,并解释「构造函数在什么时候被自动调用」。
  3. 能用一句话讲清值类与句柄类的区别(拷贝 vs 引用),并预测 b = a; b.prop = 1 之后 a.prop 的值——对两种类分别预测。