4. 子函数与嵌套函数
当一个 MATLAB 程序从“算一个公式”发展到“读数据、预处理、迭代求解、误差评估、绘图输出”时,最容易出现的问题是:所有代码堆在一个脚本里,变量名互相覆盖,某一步想单独测试却无从下手。子函数(subfunction)和嵌套函数(nested function)正是解决这类问题的两种函数组织方式。它们的共同点是都写在同一个函数文件内部,外部不可直接调用;区别在于作用域规则和变量共享机制完全不同,选错了会让程序要么难以调试,要么意外共享变量。
两种函数的定义位置与调用关系
子函数写在主函数(文件中第一个函数)的 end 之后,与主函数是并列关系。主函数和同一文件内的其他子函数都可以调用它,但命令窗口和其他文件看不到它。子函数拥有完全独立的局部工作区,只能通过输入、输出参数与外界交换数据。典型写法如下:
function result = solveTask(x, tol)
% 主函数:组织流程
x = preprocess(x);
[y, iter] = iterateCore(x, tol);
result = assess(y, iter);
end
function x = preprocess(x)
x = (x - mean(x)) / std(x); % 标准化
end
function [y, iter] = iterateCore(x, tol)
y = x; iter = 0;
while norm(y - x) > tol && iter < 500
x = y;
y = 0.5 * (x + 2 ./ x); % 示例迭代
iter = iter + 1;
end
end
function r = assess(y, iter)
r = struct('value', y, 'iter', iter, 'ok', iter < 500);
end
嵌套函数则定义在另一个函数体内部,必须位于父函数的 end 之前(若使用 end 结束函数)。它最关键的机制是共享父函数工作区:父函数中定义的变量,只要在嵌套函数里被赋值或读取,就自动成为两者共用的变量,不需要作为参数传递。这带来两个后果:一是省去了长参数列表,二是变量变化是“双向可见”的,父函数调用嵌套函数后能直接看到被修改的值。
function r = accumulateDemo(n)
total = 0; % 被嵌套函数共享
count = 0;
function addOne()
total = total + 1; % 直接修改父函数变量
count = count + 1;
end
for k = 1:n
addOne();
end
r = struct('total', total, 'count', count);
end
调用 accumulateDemo(5) 会得到 total = 5、count = 5。注意 total 和 count 并未出现在 addOne 的参数表中,却能被修改,这正是嵌套函数共享工作区的直接体现。
变量共享机制带来的实际差异
上图左侧说明子函数之间的数据流必须“显式”:主函数把 x 传给 preprocess,拿回处理后的 x,再传给 iterateCore。每个函数的局部变量在返回后即销毁,因此不会出现“某个中间变量被悄悄改掉”的问题,调试时可以在任意子函数内单独设断点、单独构造输入测试。代价是参数列表可能变长,尤其是多个函数都要用到同一批配置参数时。
右侧的嵌套函数则相反:total 和 count 定义在父函数中,addOne 内部对它们的赋值直接作用于父工作区。这种机制适合“一个小工具函数需要反复修改父函数的少数状态变量”的场景,例如迭代计数器、收敛标志、缓存数组。但必须警惕:如果嵌套函数里误写了一个与父函数同名的变量,实际修改的就是父函数的变量,而不是新建局部变量,这类错误往往不会报错,只会让结果偏离预期。判断方法是:在嵌套函数中只对确实需要共享的变量赋值,其余临时量使用明显不同的名字。
重构示例:把单文件长脚本拆成模块
假设原始脚本要完成“生成含噪信号 → 滑动平均滤波 → 计算信噪比 → 判断是否达标”,全部写在一个脚本里,变量 y、n、snr 在多处被覆盖。重构时把主流程放进 filterPipeline 主函数,把三个计算步骤写成子函数,形成清晰的数据链:
function report = filterPipeline(raw, win, snrTh)
clean = movingAverage(raw, win);
snrVal = calcSNR(raw, clean);
report = judge(snrVal, snrTh);
end
function y = movingAverage(x, w)
kernel = ones(1, w) / w;
y = conv(x, kernel, 'same');
end
function s = calcSNR(noisy, clean)
noise = noisy - clean;
s = 10 * log10(sum(clean.^2) / sum(noise.^2));
end
function r = judge(s, th)
r = struct('snr', s, 'pass', s >= th);
end
以示例参数说明:raw 为 1×200 的含噪序列,win = 5,snrTh = 15 dB。调用 report = filterPipeline(raw, 5, 15) 后,report.snr 给出计算得到的信噪比,report.pass 为逻辑判断结果。这里的数值属于教学示例设定,实际运行结果取决于 raw 的具体内容。重构后的好处是:想验证滤波是否正确,只需单独调用 movingAverage 并观察输出长度与边界效应;想调整判据,只改 judge 一处即可,主流程不受影响。
掌握这两种组织方式后,判断标准可以归结为一句话:数据流是否单向、是否需要独立测试。单向且需独立验证的步骤交给子函数;需要围绕少量共享状态协同工作的小逻辑,才考虑嵌套函数。按此原则重构,原本上百行的脚本就能变成主函数只负责编排、每个子函数只负责一件事的结构,后续修改和排错都会明显变快。