2. 函数输入输出参数设计
在上一节中,我们已经把一段计算逻辑收进函数文件,解决了“代码复用”的问题。但真正把函数交给别人(或几个月后的自己)使用时,问题往往不在算法本身,而在接口:调用者传了字符串怎么办?少传一个参数怎么办?想支持“只传两个参数时自动取默认步长”又该怎么写?本节围绕输入参数验证、可变数量参数和默认值三条主线,设计一个能处理多种输入组合的数值计算函数。
一、为什么参数设计要单独拿出来讲
MATLAB 的函数调用是弱约束的:调用 f(x, y) 时,函数体里若不检查,x 是字符串、是矩阵、是空数组都能“进来”,错误往往在几行之后才以莫名其妙的形式爆发,报错位置离真正的原因很远。参数设计的核心目标,是把错误挡在函数入口,并让合法输入的组合尽可能灵活。前者靠 arguments 块与 validateattributes,后者靠默认值、varargin 与 nargout。
二、arguments 块:把类型、尺寸、默认值写进签名
从 R2019b 起,MATLAB 推荐用 arguments 块声明输入。它位于函数签名之后、函数体之前,可以一次性写清“这个参数是什么类型、什么尺寸、默认值多少、是否必填”。例如一个计算矩形区域统计量的函数:
function [area, perimeter] = rectStats(len, wid, opts)
% 计算矩形面积与周长
arguments
len (1,1) double {mustBePositive}
wid (1,1) double {mustBePositive} = 1
opts.units (1,:) char {mustBeMember(opts.units, ...
{'m','cm','mm'})} = 'm'
opts.roundTo (1,1) double {mustBeInteger, mustBeNonnegative} = 2
end
area = len * wid;
perimeter = 2 * (len + wid);
end
这里 len 是必填的正标量;wid 有默认值 1,调用 rectStats(5) 时自动取 1;opts 是一个“名称—值”结构,调用者写 rectStats(5, 3, 'units', 'cm', 'roundTo', 1) 即可覆盖默认项。注意 opts.units 的校验用了 mustBeMember,把非法单位直接拦下,而不是等到后面做单位换算时才出错。
arguments 块的好处是声明式、可读性高,且校验发生在函数体执行之前。但它也有边界:当参数个数本身不固定、或需要兼容旧版 MATLAB 时,就要借助 validateattributes 与 varargin。
三、validateattributes:命令式校验的补充手段
validateattributes(A, classes, attributes, argIndex, funcName) 适合在函数体内部对某个变量做细粒度检查,尤其是需要给出“第几个参数、哪个函数”的完整报错信息时。例如要求输入是二维、非空、元素为有限实数:
validateattributes(A, {'double','single'}, ...
{'2d','nonempty','finite','real'}, 1, 'mySolver');
它与 arguments 块并不冲突:简单场景用 arguments 更清爽;需要动态判断(比如根据前一个参数决定后一个参数的合法范围)时,在函数体内用 validateattributes 更灵活。实践中常见的做法是“入口用 arguments 做基础约束,复杂业务规则在体内补充校验”。
四、varargin / nargout:应对数量不定的输入输出
当函数需要接受任意多个位置参数时,用 varargin 收集为元胞数组;当输出个数可变时,用 nargout 判断调用者实际要了几个输出,从而决定是否计算开销大的结果。下面这个函数把“求和 + 可选统计量”组合在一起:
function varargout = sumStats(varargin)
% 至少一个数值输入;输出按需返回
if nargin < 1
error('sumStats:NotEnoughInputs', '至少需要一个输入向量');
end
data = varargin{1};
validateattributes(data, {'double'}, {'vector','nonempty','finite'}, 1, 'sumStats');
total = sum(data);
if nargout >= 1, varargout{1} = total; end
if nargout >= 2, varargout{2} = mean(data); end
if nargout >= 3, varargout{3} = max(data) - min(data); end
end
调用 s = sumStats(v) 只算总和;[s, m] = sumStats(v) 才计算均值。这种“按需计算”在数据量大时能省下可观时间。需要注意:varargout 必须逐个赋值,且 nargout 在函数内部反映的是调用方请求的输出个数,不是函数定义里写了几个。
五、把三者组合成一个可用的计算函数
下面给出一个综合示例 polyEvalStats:输入一个系数向量与若干采样点,输出多项式取值及其统计量。它同时用到默认值、类型校验和可变输出。
function [y, stats] = polyEvalStats(coef, x, opts)
arguments
coef (1,:) double {mustBeNonempty}
x (1,:) double {mustBeNonempty} = linspace(0, 1, 11)
opts.order (1,1) double {mustBeInteger, mustBePositive} = numel(coef)
end
if opts.order ~= numel(coef)
warning('polyEvalStats:OrderMismatch', ...
'系数个数(%d)与指定阶数(%d)不一致,按系数实际长度计算', ...
numel(coef), opts.order);
end
y = polyval(coef, x);
if nargout > 1
stats.min = min(y);
stats.max = max(y);
stats.mean = mean(y);
end
end
调用组合可以这样验证:polyEvalStats([1 2 3]) 使用默认采样点;polyEvalStats([1 2 3], 0:0.5:2) 指定采样点;[y, s] = polyEvalStats([1 2 3], [], 'order', 3) 显式传空占位以使用默认 x,同时取回统计量。注意“传空数组触发默认值”是 arguments 块支持的写法,但要求该参数确实声明了默认值。
六、设计时的几条实用判断
- 必填与可选要分清:没有默认值的参数就是必填,调用方少传会立即报错,这比在函数体里写
if nargin < 2更清晰。 - 名称-值参数优于长位置参数列表:当可选参数超过两个时,用
opts结构承载,调用处可读性显著提升,也便于后续扩展而不破坏旧调用。 - 校验信息要可定位:
validateattributes的argIndex与funcName参数不要省,报错时能直接指出“第 2 个参数不合法”。 - nargout 只影响“算不算”,不影响“对不对”:按需计算的分支里,未请求的输出不要赋值,避免无谓开销;但已请求的输出必须保证被赋值,否则调用方拿到空值。
- 兼容性提醒:
arguments块需要 R2019b 及以上;若需支持更早版本,改用varargin+validateattributes+inputParser的组合实现同等效果。