.M 文件

MATLAB 的.m文件可以分成两类:

  • 脚本文件,不接受输入参数,它们处理工作区中的变量和数据。
  • 函数文件,可接受输入参数,并且可以有返回值,内部变量是函数的局部变量。

MATLAB 对于 .m 文件的文件名有一些特殊要求:

  • 文件名允许由字母、数字、下划线(_)和点(.)组成,注意不包括-和空格;
  • 文件名必须以字母开头;
  • 文件名区分大小写。

较新版本的MATLAB提供了后缀为.mlx的实时脚本/函数文件,大致就是对Jupyter Notebook的模仿,但是用起来并没有后者那么好用,各种操作不够自然。由于.mlx文件不是纯文本文件,如果我们需要在.m文件和.mlx文件之间转换,必须通过MATLAB专门提供的工具进行转换,vscode等编辑器也并不支持.mlx文件的显示。

脚本文件

载入脚本文件会依次执行所有命令,在重复执行大量命令时,可以整理为一个脚本进行执行。

对于当前目录下的myfile.m脚本文件,在命令行窗口可以输入脚本的名称来执行脚本(不含文件后缀), 执行结果会输出到命令行窗口。脚本文件可以访问当前工作区的所有变量,对变量的创建和修改也会留在当前工作区中。

例如,脚本文件

myfile.m
1
2
3
4
5
6
7
8
9
sum=0;n=0;
while sum<100
n=n+1;
sum=sum+n;
end

sum=sum-n;
n=n-1;
n,sum

执行结果

1
2
3
4
5
6
>> myfile
n =
13

sum =
91

除了直接使用脚本名称,还可以通过run命令实现

1
run('myscript.m');

为了提供代码的可读性,在脚本中更建议使用这种方式调用其它脚本。

脚本虽然可以通过名称直接调用,但是脚本名称本身并不是一个受保护的标识符,我们可以创建与之同名的变量,对于下文中的函数脚本名称同理。(毫无疑问,这是MATLAB在语法上的失败设计)

函数文件

一个典型的函数文件包括一个与文件名同名的主函数(主函数是全局函数,可以被外部调用)

func.m
1
2
3
4
5
6
function [output1, ...,outptn] = func(input1, ... , inputn)
% 注释说明部分

% 函数体代码部分

end

例如

rank.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function r = rank(A,tol)
% RANK Matrix rank.
% RANK(A) provides an estimate of the number of linearly
% independent rows or columns of a matrix A.
% RANK(A,tol) is the number of singular values of A
% that are larger than tol.
% RANK(A) uses the default tol = max(size(A)) * norm(A) * eps.

s = svd(A);

if nargin==1
tol = max(size(A)') * max(s) * eps;
end

r = sum(s > tol);
end

这里在函数定义行之后,可执行代码或空行之前的注释部分,视作函数文件的帮助语句,可以使用help rank查看 rank 函数的帮助。

注意:

  • 如果函数文件的名称和文件中提供的实际函数名不一致,那么 MATLAB 会发出警告,并且会以文件名为准,不可以通过函数名调用,只能通过文件名调用。
  • 只有一个函数的.M文件甚至不需要使用end标记来结束function语句;

局部函数

在函数文件和脚本文件的后面,还可以加上若干个局部函数,局部函数的名称不能和当前文件重名,但是局部函数只能当前文件中被调用,不能被外部调用。 对于局部函数来说,并不存在如C语言中在调用之前要添加函数声明的要求,即使函数定义在最后,在前面的语句中仍然可以直接使用。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function [x_max,x_min] = max_min_values(x)
x_max=func1(x);
x_min=func2(x);
end

function r=func1(x)
x1=sort(x, 'descend');
r=x1(1);
end

function r=func2(x)
x1=sort(x);
r=x1(1);
end

如果当前脚本中的局部函数与外部函数重名,那么局部函数的优先级更高。

在MATLAB 2024a 之前,如果一个脚本文件中希望定义局部函数,必须要放在脚本文件的最后,否则语法报错,但是最新版去掉了这种无意义的限制。

嵌套函数

除此之外,MATLAB还允许函数的嵌套定义,嵌套函数显然只允许在定义的函数内部被调用,定义无需出现在使用之前。 嵌套函数相比于局部函数最大的优势是,它可以直接访问外部函数中的变量(按照引用捕获,类似于全局变量),类似于无须声明的全局变量,例如

1
2
3
4
5
6
7
8
9
10
function outer_function
x = 3;
y = nested_function(x);
disp(y); % 12

function y = nested_function(z)
% 嵌套函数可以直接访问外部变量 x
y = z^2 + x;
end
end

嵌套函数可以直接访问(按照引用捕获)外部函数的变量,但是附带的代价是运行效率的降低。

嵌套函数 vs 局部函数:

  • 从追求效率和可读性的角度,更好的做法是使用局部函数替代嵌套函数,将所有需要的变量以函数参数的形式显式传递。
  • 从内存占用的角度,如果嵌套函数需要处理的是外部函数中的一个大型数组,那么使用嵌套函数相比于内部函数可以减少大型数组的拷贝,从而减少内存占用。

函数打包

虽然MATLAB支持局部函数和嵌套函数,但是仍然改变不了一个函数文件只能对外提供一个可用函数的事实,我们为了代码复用,不得不将很多小函数拆分为单独的文件,这种做法非常不合理。

我们的需求是将若干个小函数打包到一个文件中,并且希望这些函数都对外可见,有两种方案可以做到:

  • 返回局部函数的句柄组成的元胞数组;
  • 基于面向对象,将所有函数设置为一个类的静态函数。

第一种方案例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function funcs = mytools_auto()
funcs = {
@add_one;
@square;
@hello
};
end

function y = add_one(x)
y = x + 1;
end

function y = square(x)
y = x.^2;
end

function hello()
disp('Hello from mytools_auto!');
end

主函数还可以进一步简化,通过内置函数 localfunctions() 自动获取当前的所有局部函数的句柄所组成的元胞数组

1
2
3
function funcs = mytools_auto()
funcs = localfunctions();
end

第二种方案例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
classdef Tool
methods(Static)
function y = add_one(x)
y = x + 1;
end

function y = square(x)
y = x.^2;
end

function hello()
disp('Hello from Tool class!');
end
end
end

这两种方案各有优劣:

  • 前者的使用比较简洁,不需要加上额外的类名,但是需要使用句柄;
  • 后者虽然需要加上类名,但是由于不需要使用句柄,更利于代码提示,代码更容易维护。

补充

虽然直接使用命令/使用脚本/使用函数的方式在原理上是等效的,但是从优化角度考虑,这几种做法的效率是存在差异的, 通常的顺序是:命令 < 脚本 < 函数,也就是针对函数的优化是最好的。

函数基础

下面我们关注MATLAB中的函数语法,MATLAB是动态语言,无论是函数参数还是返回值都不存在类型匹配的问题,这即让我们写起来很方便,不需要考虑类型问题,也导致我们很容易出错。

不能直接通过控制台的输入来创建函数,通常需要创建并写入单独的函数文件。

函数的返回值

和Python不同,MATLAB函数不需要使用return语句来指定返回值,也不会自动使用最后一个表达式的值作为返回值,必须具体给返回变量赋值,例如

1
2
3
4
function result = hello()
result = 100;
return;
end

return语句通常不需要出现,它出现的作用是让函数执行提前终止,返回变量此时所存储的结果就是函数的返回值,例如

1
2
3
4
5
6
7
function result = hello()
result = 100;

return; % 提前返回,返回值 100

result = 200; % 返回值 200
end

函数也可以无返回值,例如

1
2
3
function displayMessage()
disp('Hello, this is a message from a MATLAB function!');
end

下面这种写法是等效的,也表明函数没有返回值

1
2
3
function [] = displayMessage()
disp('Hello, this is a message from a MATLAB function!');
end

函数还可以存在多个返回值,例如

1
2
3
4
5
function [a, b, c] = func()
a = 1;
b = 2;
c = 3;
end

必须使用对应个数的变量组成的数组来接纳函数的返回值,接收的变量个数通常要匹配返回值个数,例如

1
2
3
4
[a,b,c] = func()
% a = 1
% b = 2
% c = 3

如果接收变量个数多于提供的返回值个数,语法报错。 如果接收的变量个数少于返回值个数,则多余的返回值会丢弃,而且这种情况完全不报错!(MATLAB的语法真离谱)

1
2
a = func()
% a = 1

考虑一个极端情况:函数体内没有对返回变量赋值

1
2
3
function s = func()
return
end

此时函数处于一种薛定谔的状态:函数可以被正常调用,但是由于s没有被赋值(不会被赋值[]),如果我们在调用后抛弃返回值,没有任何影响,如果尝试使用返回值,则会导致错误,因为返回值是未定义的,不可以用于赋值或调用。(MATLAB的语法真离谱)

补充:在脚本文件中也可以使用return语句,它的含义为脚本提前结束。

考虑下面这种情况

1
2
3
4
5
6
7
8
9
10
func2(func1()) % 1

function func2(c, d)
disp(nargin)
end

function [a,b] = func1()
a = 1;
b = 2;
end

这里看起来是直接将func1()的返回值传递给了func2,但是实际上它只获取到了第一个返回值,因此只有一个参数被传递给func2,显示 1

无实参调用

对于不传递实参的函数调用,在定义和调用时都可以省略(),例如

1
2
3
4
5
6
7
8
function hello()
disp("hello,world!")
end

% or
function hello
disp("hello,world!")
end

在调用时下面的语句是完全等价的!!!

1
2
3
hello()

hello

这简直离了大谱,严重降低了程序的可读性:

  • hello可能是在显示一个变量hello的值;
  • hello也可能是在执行一个脚本文件hello.m
  • hello还可能是在执行一个函数但不传递任何参数hello()

不仅降低了程序的可读性,还给函数的传递人为造成了阻碍,必须额外提供一个函数句柄的语法。

对于无参数的函数调用,非常不建议省略括号。

持久性变量

在函数调用过程中会创建单独的作用域,除了使用global声明并使用全局变量之外,MATLAB还提供了持久性变量(使用persistent声明),相当于C语言中的局部静态变量,变量的生命周期与函数调用过程无关,例如

1
2
3
4
5
6
7
8
9
10
11
12
counter()   % 1
counter() % 2
counter() % 3

function count = counter()
persistent n
if isempty(n)
n = 0; % 初始化持久性变量
end
n = n + 1;
count = n;
end

注意这里MATLAB并不会像C++一样自动忽略掉持久化变量的重复初始化,因此需要额外的判断保护。

基于元胞数组打包

我们可以通过元胞数组给函数一次性提供多个参数,例如

1
2
3
4
5
6
7
8
9
10
% 将参数依次存入元胞数组
args = {1, 2, 3};

% 调用函数时展开元胞数组
output = func(args{:})
% output = func(1, 2, 3)

function result = func(a, b, c)
result = a + b + c;
end

对于提供多个返回值的函数,如果我们已知返回值个数,也可以使用元胞数组来接收,例如

1
2
3
4
5
6
7
8
output = cell(1,3);
[output{1:3}] = func();

function [a,b,c] = func()
a = 1;
b = 2;
c = 3;
end

这种打包方式可能会有一些效率上的损失,但是让程序更具有通用性,因为固定个数的参数或返回值会限制程序的灵活性。

函数的特殊变量

考虑到实际代码的简洁性和可维护性,下面列举的涉及参数和返回值的各种花里胡哨的操作都不建议使用。

nargin

函数的实参并不要求和函数定义时的形参严格对应:

  • 实参个数可以少于形参个数,此时后续的形参处于未定义状态:不能被调用,但是可以赋值后再使用;
  • 实参个数不能多于形参,否则会导致语法错误。

我们可以基于nargout在函数体内部获取函数调用方提供的实参个数,注意这并不是函数定义中的形参个数,而是由本次调用决定的,例如

1
2
3
4
5
6
7
8
9
func(1);            % Number of inputs: 1
func(1,2); % Number of inputs: 2
func(1,2,3); % Number of inputs: 3
func([1,2,3]); % Number of inputs: 1
func({1,2,3}); % Number of inputs: 1

function func(a,b,c,d,e,f)
disp(['Number of inputs: ', num2str(nargin)]);
end

通常利用nargin来判断实际传递的参数个数,并据此提供形参列表中最后几个参数的默认值;

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func(10,20,30)      % 60
func(10,20) % 33
func(10) % 15
func() % 6
func(1,1,1,1) % error

function result = func(a, b, c)
if nargin < 3
c = 3; % default value
end

if nargin < 2
b = 2; % default value
end

if nargin < 1
a = 1; % default value
end

result = a + b + c;
end

nargin还有第二种用法:在函数外部使用nargin函数可以获取函数定义中列出的形参个数,需要传递函数句柄,例如

1
2
3
4
5
nargin(@func) % 4

function [a,b,c] = func(a,b,c,d)
% ...
end

如果函数定义中的形参中出现varargin,则会将结果变成负数以提示。

nargout

我们可以基于nargout在函数体内部获取函数调用方请求的返回值个数,注意这并不是函数定义中的返回值个数,而是由本次调用决定的,例如

1
2
3
4
5
6
7
8
a = func();             % Number of outputs: 1
[a, b] = func(); % Number of outputs: 2
[a, b, c] = func(); % Number of outputs: 3

function [x, y, z] = func()
x = 1; y = 2; z = 3;
disp(['Number of outputs: ', num2str(nargout)]);
end

这种特殊机制是MATLAB在运行时专门提供的,对于一般的编程语言(例如Python和C/C++),在函数体内部是不可能获得调用请求的返回值个数的,除非将请求返回值个数作为参数输入。

在函数体内部可以根据nargout来提供合适的返回值,MATLAB的很多内置函数都利用了这种机制,来动态决定所需返回的内容,例如SVD分解 \(A = U S V^T\)

1
2
S = svd(A)
[U,S,V] = svd(A)

函数如果返回的值少于nargout个,这次调用会报错。 即使函数返回更多的值,仍然只有nargout个值是有效的,后几个则注定会被接收方丢弃,因此在函数体内部可以直接跳过。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a = func()          % 2
[a,b] = func() % 2 4
[a,b,c] = func() % 2 4 6

function [a, b, c] = func()
a = 2;

if nargout > 1
b = 4;
end
if nargout > 2
c = 6;
end
end

nargout还有第二种用法:在函数外部使用nargout函数可以获取函数定义中列出的返回值个数,需要传递函数句柄,例如

1
2
3
4
5
nargout(@func) % 3

function [a,b,c] = func(a,b,c,d)
% ...
end

如果函数定义中列出的返回值包括varargout,则会将结果变成负数以提示。

varargin

除了使用nargin来判断实参个数,MATLAB还提供了类似于Python的*args的特殊参数:varargin, 它会将所有多余参数打包为一个 \(1\times N\) 的元胞数组,如果没有多余参数则保持为空。 语法上要求varargin必须是最后一个参数。

例如

1
2
3
4
5
6
7
8
9
10
11
12
func()              % 0
func(1) % 1
func(1,2) % 3
func(1,2,3,4) % 10

function total = func(varargin)
total = 0;

for k = 1:length(varargin)
total = total + varargin{k};
end
end

再例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func(10)            % 0
func(10,1) % 10
func(10,1,2) % 30
func(10,1,2,3,4) % 100
func() % error

function total = func(x, varargin)
total = 0;

for k = 1:length(varargin)
total = total + varargin{k};
end

total = total * x;
end

varargout

varargin类似,MATLAB还提供了打包多个返回值的varargout,例如

1
2
3
4
5
6
7
8
9
10
func(0)
[x] = func(1) % x = 1
[x,y] = func(2) % x = 1, y = 2
[x,y,z] = func(3) % x = 1, y = 2, z = 3

function varargout = func(n)
for k = 1:n
varargout{k} = k;
end
end

键值对参数

在 R2021a 之前,MATLAB 只能以非常原始的方式提供可选的键值对参数:将键的名称作为字符串或字符数组传递,随后加上对应的值。例如

1
func(1, "PlotType", "log")

对于新版本的MATLAB,终于支持了类似 Python 等现代语言的键值对参数语法(但是网上很多教程没有更新这部分的知识),例如

1
func(1, PlotType="log")

对某些内置函数的使用可以得到简化,尤其是绘图相关的函数,例如

1
2
3
4
5
6
7
8
9
10
11
x = 1:10;
y1 = rand(1, 10);
y2 = rand(1, 10);
y3 = rand(1, 10);

% 分别为每条线设置不同的 LineStyle 和 Color
hold on; % 保持当前图形
plot(x, y1, 'LineWidth', 2, 'LineStyle', '--', 'Color', 'r');
plot(x, y2, 'LineWidth', 2, 'LineStyle', '-.', 'Color', 'b');
plot(x, y3, 'LineWidth', 2, 'LineStyle', ':', 'Color', 'g');
hold off; % 释放图形

可以改成下面的可读性更高的方式

1
2
3
4
5
6
7
8
9
10
11
x = 1:10;
y1 = rand(1, 10);
y2 = rand(1, 10);
y3 = rand(1, 10);

% 分别为每条线设置不同的 LineStyle 和 Color
hold on; % 保持当前图形
plot(x, y1, LineWidth=2, LineStyle='--', Color='r');
plot(x, y2, LineWidth=2, LineStyle='-.', Color='b');
plot(x, y3, LineWidth=2, LineStyle=':', Color='g');
hold off; % 释放图形

但是如果希望自定义函数也支持这样的键值对参数,配置则比较复杂,需要用到 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
demo();

demo(Len=1);
demo('Len', 1); % 两者等价

demo(Len=1, Verbose=true);
demo('Len', 2, 'Verbose', true); % 两者等价

function demo(varargin)
% 创建一个 inputParser 对象
p = inputParser;
% 配置参数以及对应的默认值
p.addParameter('Verbose', false, @islogical);
p.addParameter('Len', 10, @isnumeric);

% 解析输入参数varargin
p.parse(varargin{:});
% 获取解析后的参数
verbose = p.Results.Verbose;
len = p.Results.Len;

disp(['Len: ', num2str(len)]);
disp(['Verbose: ', num2str(verbose)]);
end

函数句柄

MATLAB 在语法设计上存在明显的坑:

  • 函数名称并不是受保护的标识符,我们可以直接创建同名变量
  • 无参数调用函数时,可以直接省略括号()

这导致 MATLAB 无法像 Python 一样把函数像普通变量一样直接赋值,而是需要单独设计函数句柄的语法,使用@可以获取函数句柄,例如对于下面这个简单函数

1
2
3
function result = square(x)
result = x^2;
end

使用@获取函数句柄并赋值给一个变量,通过这个变量就可以直接调用函数

1
2
3
f = @square;

y = f(5); % 25

但是和函数名不同,直接使用赋值函数句柄的变量f本身却不能调用函数,即使这个函数并不需要参数。

函数句柄一旦被获取,就可以和普通变量一样被正常传递,例如在函数参数中传递,起到回调函数的作用

1
2
3
4
5
6
7
8
function result = applyFunction(func, x)
result = func(x);
end

f = @sin;

y = applyFunction(f, pi/2); % 1
z = applyFunction(@sin, 0); % 0

特殊函数

为了避免在简单函数的使用时单独构造 .M 文件的麻烦,可以直接使用内联函数或匿名函数,更推荐使用匿名函数。

匿名函数

匿名函数就相当于 C++ 和 Python 中的 lambda 表达式,需要指定输入参数,无需指定返回值。

例如

1
2
3
4
>> h=@(x,y)x^2+y^2;
>> h(2,2)
ans =
8

可以在匿名函数的表达式中自动捕获使用工作区的变量,捕获的语义是在定义时按值捕获,此后外部的修改不会对其产生影响。如果输入参数与工作区变量重名,前者会覆盖后者。

例如

1
2
3
4
5
6
7
8
9
>> a=1;
>> f = @(s) s + 1;
>> f(1)
ans =
2
>> a=2;
>> f(1)
ans =
2

匿名函数赋值得到的是一个函数句柄,对于匿名无参函数的调用,是不允许省略括号的。

一种常见的需求是对现有函数进行封装,固定其中的一部分参数,保留剩下的部分参数待定,例如

1
2
3
4
5
6
7
function result = multiplyAdd(a, b, c)
result = a * b + c;
end

newFunc = @(b, c) multiplyAdd(1, b, c);

result = newFunc(3, 4);

这时newFunc就变成了一个只接收两个参数的匿名函数。

我们还可以创建多重匿名函数,例如

1
2
3
f = @(a,b,c)@(x) a*x^2+b*x+c;
f1 = f(1,2,3); % 相当于f1 = @(x) x^2+2*x+3
f1(3)

注意:匿名函数在定义时没有提供返回值,在机制上相当于统一视作varargout,因此不能依靠nargout获取匿名函数的实际返回值个数。

内联函数

提供几个内联函数的例子即可。

例如

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
>> f=inline("x+2")

f =

内联函数:
f(x) = x+2

>> f(3)

ans =

5

>> g=inline('x^2+y','x','y')

g =

内联函数:
g(x,y) = x^2+y

>> g(2,3)

ans =

7

其中可以使用x+2x^2+y之类的 MATLAB 表达式组成的字符串,MATLAB 会自动推断自变量(默认为 x),然后直接使用。

由于内联函数的定义是基于字符数组解析的,只能作为玩具和演示使用,在实际编程中并不建议使用。