为了得到健壮的代码,我们非常有必要对函数的参数进行检查,除了最基础的参数个数,还需要关注参数的类型和数据范围等,下面介绍几个MATLAB提供的用于参数检查的内置函数。

assert + isXXX

最简单的做法就是基于 assertisXXX 检查函数的形参是否满足要求,例如

1
2
3
4
5
6
7
8
9
10
11
function func(u, t, f, n, b, flag, params)

assert(isnumeric(u) && (isvector(u) || ismatrix(u)), 'u must be a numeric vector or matrix.');
assert(isscalar(t) && t >= 0, 't must be a non-negative scalar.');
assert(isa(f, 'function_handle'), 'f must be a function handle.');
assert(isnumeric(n) && isscalar(n) && n > 0 && mod(n,1) == 0, 'n must be a positive integer.');
assert(isa(b, 'Base'), 'b must be an object of class Base or its subclass.');
assert(islogical(flag) && isscalar(flag), 'flag must be a logical scalar (true or false).');
assert(isstruct(params) && all(isfield(params, {'a', 'b', 'c'})), 'parameters must be a struct with fields a, b, and c')
% ...
end

注:这里对结构体 params 的检查是要求至少含有指定字段 a, b, c,但是也允许含有更多字段, 如果需要严格检查,则必须通过 fieldnames 函数动态获取 params 的所有字段进行比较,无法在一个 assert 语句中完成。

下面是一些常用的 isXXX 函数。

关于维度和形状

函数 说明 正面示例 反面示例
isscalar(A) 是否为标量 (1×1) isscalar(5) isscalar([1 2])
isvector(A) 是否为(行或列)向量 isvector([1 2 3]) isvector([1 2; 3 4])
isrow(A) 是否为行向量 isrow([1 2 3]) isrow([1; 2; 3])
iscolumn(A) 是否为列向量 iscolumn([1; 2; 3]) iscolumn([1 2 3])
ismatrix(A) 是否为矩阵 ismatrix(rand(3,4)) ismatrix(rand(3,4,2))
issparse(A) 是否为稀疏矩阵 issparse(speye(3)) issparse(eye(3))
isempty(A) 是否为空 isempty([]) isempty([0])

注:在 MATLAB 中,空数组是指至少一个维度长度等于零的数组。

关于类型

函数 说明 正面示例 (true) 反面示例 (false)
isnumeric(A) 是否为数值类型 isnumeric(3.14) isnumeric('3.14')
islogical(A) 是否为逻辑类型 islogical(true) islogical(0)
ischar(A) 是否为字符数组 ischar('abc') ischar(["abc"])
isstring(A) 是否为字符串类型 isstring("abc") isstring('abc')
iscell(A) 是否为元胞数组 iscell({1,2}) iscell([1,2])
isstruct(A) 是否为结构体 isstruct(struct('a',1)) isstruct([1 2])
isobject(A) 是否为对象 isobject(MException) isobject(struct())
isa(A, 'class') 是否为指定类(或者派生类)对象 isa(3, 'double') isa('abc', 'double')

注:在MATLAB中的字符数组和字符串是不一样的,很多情况下的使用都存在细微区别。

关于数值属性

函数 说明 正面示例 (true) 反面示例 (false)
isfinite(A) 是否为有限数 isfinite(3.14) isfinite(Inf)
isinf(A) 是否为无穷大 isinf(Inf) isinf(3.14)
isnan(A) 是否为 NaN isnan(NaN) isnan(3.14)
isinteger(A) 是否为整数类 (int32 等) isinteger(int8(3)) isinteger(3)
isfloat(A) 是否为浮点数 isfloat(3.14) isfloat(int8(3))
isreal(A) 是否为实数 isreal(3) isreal(1+2i)

其它

函数 说明 正面示例 (true) 反面示例 (false)
isfile(name) 是否为文件 isfile('myfile.txt') isfile('myfolder')
isfolder(name) 是否为文件夹 isfolder('myfolder') isfolder('myfile.txt')
isfield(S, 'name') 结构体是否包含字段 isfield(struct('a',1), 'a') isfield(struct('a',1), 'b')
iscellstr(C) 是否为字符数组的元胞数组 iscellstr({'a','b'}) iscellstr({'a', 1})
isStringScalar(A) 是否为包含一个元素的字符串数组 isStringScalar("A") isStringScalar(["A","B"])

官方文档提供了一份完整的 isXXX 函数参考表:Use is* Functions to Detect State

千万不要误用isinteger函数,因为默认的绝大多数数据都是浮点数,只有专门使用int32int64等整数类数据类型创建的数据,才会返回true

1
2
3
4
5
6
7
8
>> isinteger(1)
ans =
logical
0
>> isinteger(int32(1))
ans =
logical
1

validateattributes

validateattributes函数可以为我们检查某个具体参数的类型以及是否满足某些要求,不满足要求会抛出错误。

假设我们有一个函数func1,它接受两个输入参数ab,我们要求:a是一个非空的标量,b是一个非空的向量。

1
2
3
4
5
6
7
function result = func1(a, b)
validateattributes(a, {'numeric'}, {'nonempty', 'scalar'});
validateattributes(b, {'numeric'}, {'nonempty', 'vector'});

result = a + sum(b);
disp(['Result: ', num2str(result)]);
end

validateattributes函数可以确保a是一个非空的标量,b是一个非空的向量。如果输入参数不符合这些要求,MATLAB 会抛出错误。

validateattributes函数的基本格式为

1
validateattributes(a, classes, attributes);

其中:

  • 第一个参数是要检查的函数参数
  • 第二个参数是要求的类型,通常包括:doublelogicalcharstruct
  • 第三个参数是要求参数满足的条件,可以是多个条件,例如{'nonempty', 'vector'},也可以留空{}

关于参数的类型,MATLAB提供了class函数来获取,常见的类型包括:

  • double 浮点数
  • numeric 数值类型(包括各种浮点数和各种整数)
  • char 字符
  • logical 布尔值
  • struct 结构体数组
  • cell 元胞数组
  • function_handle 函数句柄
  • class_name 自定义类型名称

关于参数满足的条件,常见的条件包括

  • 维度检查
    • 2d 二维数组;3d 三维数组
    • row 行向量;column 列向量;vector 行向量或列向量
    • scalar 标量
    • 'size',[d1,...dN] 指定维数信息的数组
    • 'numel',N 指定元素总个数为N的数组
    • 'nrows',N 指定行数为N的数组;'ncols',N 指定列数为N的数组
    • square 方阵(每一个维度都相等)
    • nonempty 要求数组的每一个维度都不为0
    • nonsparse 要求数组非稀疏
  • 大小范围检查
    • '>',N 所有值大于N
    • '>=',N 所有值大于等于N
    • ...
  • 其它检查
    • finite 数组中的元素不含有Inf
    • nonnan 数组中的元素不含有Nan
    • nonnegative 数组中的元素全部非负
    • nonzero 数组中的元素全部非零
    • decreasing 单调减
    • increasing 单调增

validatestring

对于字符串参数,MATLAB专门提供了检查工具validatestring,它可以限制字符串参数的所有合法取值,例如

1
2
3
4
5
6
7
8
9
10
11
12
function result = selectOption(option)
option = validatestring(option, {'Option1', 'Option2', 'Option3'});

switch option
case 'Option1'
result = 'You selected Option 1';
case 'Option2'
result = 'You selected Option 2';
case 'Option3'
result = 'You selected Option 3';
end
end

但是这种限制不是严格的,正如很多MATLAB内置函数一样,可以忽略大小写进行模糊匹配,也可以只匹配到合法选项的开头部分。 如果这个参数只能匹配到唯一的合法选项,就不会报错,反之则会报错。

成功的模糊匹配例如

1
2
3
4
5
6
>> validatestring('G',{'green','red'})
ans =
'green'
>> validatestring('bla',{'black','blue'})
ans =
'black'

下面的匹配则会报错

1
2
3
4
5
6
7
8
>> validatestring('u',{'black','blue'})
Expected input to match one of these values:
'black', 'blue'
The input, 'u', did not match any of the valid values.
>> validatestring('ue',{'black','blue'})
Expected input to match one of these values:
'black', 'blue'
The input, 'ue', did not match any of the valid values.

inputParser

inputParser是输入参数解析器类型,和C++中常见的命令行参数解析器非常类似,只不过在这里是对函数参数进行检查。

假设我们有一个函数func2,它接受两个必需的输入参数ab,一个可选的输入参数c,以及一个键值对参数Verbose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function result = func2(a, b, varargin)
% 创建一个 inputParser 对象
p = inputParser;
% 配置必需参数
p.addRequired('a', @isnumeric);
p.addRequired('b', @isnumeric);
% 配置可选参数(默认值1)
p.addOptional('c', 1, @isnumeric);
% 配置键值对参数(键值对的值默认取false)
p.addParameter('Verbose', false, @islogical);
% 解析输入参数
p.parse(a, b, varargin{:});
% 重新获取解析后的参数
a = p.Results.a;
b = p.Results.b;
c = p.Results.c;
verbose = p.Results.Verbose;

result = a + sum(b) + c;

if verbose
disp(['a: ', num2str(a)]);
disp(['b: ', num2str(b)]);
disp(['c: ', num2str(c)]);
disp(['Result: ', num2str(result)]);
end
end

在这个示例中,我们首先创建了一个inputParser对象 p,然后用它对参数进行检查:

  • 使用addRequired方法添加了必需参数ab,并指定它们必须是数值类型。(必需参数对顺序是敏感的,在调用时必须按照声明的顺序提供。
  • 使用addOptional方法添加了可选参数c,并指定默认值为 1。
  • 使用addParameter方法添加了键值对参数Verbose,指定键的名称为Verbose,值必须是布尔类型,默认为false
  • 使用parse方法解析输入参数。
  • p.Results重新获取解析后的参数。

使用例如

1
2
3
4
5
6
7
8
9
10
11
% 调用 func2 并传递必需参数 a 和 b
func2(1, [2, 3]);

% 调用 func2 并传递必需参数 a 和 b 以及可选参数 c
func2(1, [2, 3], 4);

% 调用 func2 并传递必需参数 a 和 b,以及名称-值对参数 'Verbose'
func2(1, [2, 3], 'Verbose', true);

% 调用 func2 并传递必需参数 a 和 b, 可选参数 c 和名称-值对参数 'Verbose'
func2(1, [2, 3], 4, 'Verbose', true);

在较新的版本中,还支持对可选参数以键值对的形式传递,例如下面两个语句是等价的

1
2
func2(1, [2, 3], 4, Verbose=true);
func2(1, [2, 3], 4, 'Verbose', true);

我们可以使用匿名函数的方式,将validateattributes函数也组合到inputParser中使用,用于判断参数的有效性,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function result = myFunction(a, b, varargin)
% 创建一个 inputParser 对象
p = inputParser;
% 设置输入参数保持顺序
p.KeepUnmatched = true;
% 必需参数
addRequired(p, 'a', @(x) validateattributes(x, {'numeric'}, {'scalar', 'nonnegative'}));
addRequired(p, 'b', @(x) validateattributes(x, {'numeric'}, {'vector'}));
% 可选参数
addOptional(p, 'c', 1, @(x) validateattributes(x, {'numeric'}, {'scalar', 'nonnegative'}));
% 名称-值对参数
addParameter(p, 'Verbose', false, @(x) validateattributes(x, {'logical'}, {'scalar'}));
% 解析输入参数
parse(p, a, b, varargin{:});
% 重新获取解析后的参数
a = p.Results.a;
b = p.Results.b;
c = p.Results.c;
verbose = p.Results.Verbose;

result = a + sum(b) + c;

if verbose
disp(['a: ', num2str(a)]);
disp(['b: ', num2str(b)]);
disp(['c: ', num2str(c)]);
disp(['Result: ', num2str(result)]);
end
end

对于 inputParser 对象,我们还可以设置它的一些属性来修改解析规则,例如:

  • FunctionName:默认为空。通常设置为 inputParser 所在函数的名字,这样可以在出错是给出是在哪个函数发生的;
  • CaseSensitive:默认为 false,即对键的大小写不敏感,这样在输入键值对参数是,键的大小写不会影响解析;
  • PartialMatching:默认为 true,即允许对键的部分匹配。例如如果定义了 window 参数,实际使用时只输入 w ,依然能够正确匹配;(如果存在歧义则会报错)
1
2
3
4
5
p = inputParser;
p.FunctionName = 'myFunc';
p.CaseSensitive = true;

...

arguments

在R2019b推出的arguments块可能是MATLAB目前最推荐的参数检查语法,参考官方文档。(但是我并不觉得它最好,而且对版本要求有点高)

arguments块是一段单独的语法块,通常写在函数的开头部分,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
function myFunction(a, b, c, option)
arguments
a (1,1) double {mustBeNumeric}
b string
c (1,1) double = 10
option char = "default"
end

disp(['a = ', num2str(a)]);
disp(['b = ', num2str(b)]);
disp(['c = ', num2str(c)]);
disp(['option = ', option]);
end

调用例如

1
2
3
4
5
6
myFunction(5, "hello");

% a = 5
% b = hello
% c = 10
% option = default

补充

对于参数检查有几个简单的原则:

  • 函数文件只有主函数对外提供接口,剩下的子函数仅在文件内部可以调用,因此只有主函数需要考虑参数检查;(对于类文件来说,同理我们只需要考虑公开方法的参数检查)
  • 对于某些对性能非常敏感的函数,可以仅在注释中说明,而不进行实际的参数检查。

除了参数检查,我们还可以为自定义函数提供自定义代码建议和自动填充功能,需要提供对应的 functionSignatures.json 文件,具体细节参考官方文档