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 块支持的写法,但要求该参数确实声明了默认值。

函数输入输出参数设计:三条主线与调用组合 调用方 位置参数 / 名称-值 输出个数 [a,b] = f(...) 可能少传、多传、传错类型 入口校验层 arguments 块 类型 / 尺寸 / 默认值 validateattributes 补充 函数体 varargin 收集可变输入 nargout 判断输出需求 核心计算 主线一:输入验证 mustBePositive / mustBeMember 错误在入口暴露,而非计算中途 报错含参数序号与函数名 主线二:可变数量参数 varargin 元胞收集位置参数 nargout 决定是否计算昂贵结果 varargout 按需逐个赋值 主线三:默认值设置 arguments 中 = 默认值 传空 [] 触发默认(需已声明) 名称-值 opts 覆盖单项 调用组合示例(polyEvalStats) polyEvalStats([1 2 3]) → 默认采样点,仅返回 y [y,s] = polyEvalStats([1 2 3], 0:0.5:2) → 指定采样点,返回统计量 polyEvalStats([1 2 3], [], 'order', 3) → 空占位取默认 x,覆盖 order

六、设计时的几条实用判断

  • 必填与可选要分清:没有默认值的参数就是必填,调用方少传会立即报错,这比在函数体里写 if nargin < 2 更清晰。
  • 名称-值参数优于长位置参数列表:当可选参数超过两个时,用 opts 结构承载,调用处可读性显著提升,也便于后续扩展而不破坏旧调用。
  • 校验信息要可定位:validateattributes 的 argIndex 与 funcName 参数不要省,报错时能直接指出“第 2 个参数不合法”。
  • nargout 只影响“算不算”,不影响“对不对”:按需计算的分支里,未请求的输出不要赋值,避免无谓开销;但已请求的输出必须保证被赋值,否则调用方拿到空值。
  • 兼容性提醒:arguments 块需要 R2019b 及以上;若需支持更早版本,改用 varargin + validateattributes + inputParser 的组合实现同等效果。