.M 文件

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

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

除此之外,MATLAB也提供了.mlx格式的实时脚本(实时函数)文件,大致就是Jupyter Notebook的模仿,但是用起来并没有后者那么好用,各种操作不够自然。如果我们需要在.m文件和.mlx文件之间转换,直接改文件后缀的做法是不安全的,可能会导致格式错误,需要用MATLAB专门提供的工具进行转换。

脚本文件

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

对于当前目录下的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
7
function [output1, ...,outptn] = func(input1, ... , inputn)

% 注释说明部分

% 函数体代码部分

end

例如

rank.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 函数的帮助。

补充

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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中的函数语法,MATLAB是动态语言,无论是函数参数还是返回值都不存在类型匹配的问题,这即让我们写起来很方便,不需要考虑类型问题,也导致我们很容易出错。

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

函数的返回值

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

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

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

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

return; % 返回100

result = 200;
end

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

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

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

无实参调用

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

1
2
3
function hello()
disp("hello,world!")
end

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

1
2
3
hello()

hello

这简直离了大谱,严重降低了程序的可读性: hello既可能是在显示一个变量hello的值,也可能是在执行一个脚本文件hello.m,还可能是在执行一个函数但不传递任何参数hello()

持久性变量

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

1
2
3
4
5
6
7
8
function count = counter()
persistent n
if isempty(n)
n = 0; % 初始化持久性变量
end
n = n + 1;
count = n;
end

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

使用例如

1
2
3
4
5
6
>> counter()
ans =
1
>> counter()
ans =
2

函数进阶

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

MATLAB的函数语法其实非常的灵活,可以接收不同个数的输入,甚至根据用户提供的接收变量个数生成不同的返回值!

在函数调用过程中,MATLAB提供两个特殊函数:

  • nargin 表示输入参数个数,可以用于检验当前传入的参数个数是否合法;
  • nargout 表示输出参数个数,可以用于判断调用时请求个数以提供不同的返回值。

不同实参个数

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

  • 实参个数可以更少,这并不会导致语法错误,可以基于此加上nargin来判断并提供缺省参数的默认值;
  • 实参个数不能更多,这会导致语法错误。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function result = example(a, b, c)
if nargin < 1
a = 1;
end

if nargin < 2
b = 2;
end

if nargin < 3
c = 3;
end

result = a + b + c;
end

使用例如

1
2
3
4
5
6
7
8
9
example(10,20,30) % 60

example(10,20) % 33

example(10) % 15

example() % 6

example(1,1,1,1) % error

可变数量参数

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

例如

1
2
3
4
5
6
7
8
9
function total = sum_all(varargin)
% 初始化总和
total = 0;

% 遍历 varargin 中的每一个参数并计算总和
for k = 1:length(varargin)
total = total + varargin{k};
end
end

使用例如

1
2
3
4
>> sum_all(1,2)
3
>> sun_all(1,2,3,4)
10

不同输出个数

函数支持多个输出,例如

multi_output.m
1
2
3
4
5
6
function [a,b] = multi_output()

a=1;
b=2;

end

必须使用对应个数的变量组成的数组来接纳函数的返回值,

1
2
3
4
5
>> [x,y] = multi_output()
x =
1
y =
2

如果接收的变量个数过少,则返回值会部分丢失,而且这种情况还不报错!离谱!

1
2
3
>> z = multi_output()
z =
1

不同返回值请求

我们可以基于nargout来获取函数调用时请求的返回值个数,并据此提供合适的返回值,这时至少有两种写法:

  • 第一种是函数固定返回多个值,但是仅nargout数目的前几个值是有效的,后几个返回无效值(反正也会被接收方丢弃);
  • 第二种是使用元胞数组实现,返回值是一个元胞数组,数目由nargout决定,接收方自动解包。

第一种方式,例如

1
2
3
4
5
6
7
8
9
10
11
function [a, b, c] = dynamic_outputs(x)
a = x^2;
b = x^3;
c = x^4;
if nargout < 3
c = [];
end
if nargout < 2
b = [];
end
end

第二种方式,例如

1
2
3
4
5
6
7
8
9
10
11
function result = dynamic_outputs(x)
result{1} = x^2;

if nargout > 1
result{2} = x^3;
end

if nargout > 2
result{3} = x^4;
end
end

调用的例子为

1
2
3
4
5
6
7
8
% 返回一个值 4
r1 = dynamic_outputs(2);

% 返回两个值 [4,8]
[r1, r2] = dynamic_outputs(2);

% 返回三个值 [4,8,16]
[r1, r2, r3] = dynamic_outputs(2);

这种特殊机制是MATLAB在运行时专门提供的,对于一般的编程语言例如Python和C++,在函数内部是无法获得调用请求的返回值个数的,除非将请求返回值个数作为参数输入。 MATLAB有很多函数都利用了这种机制,来减少所需传递的参数,例如SVD分解 \(A = U S V^T\)

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

函数句柄

为了填补前面在语法设计上的坑:

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

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 文件的麻烦,可以直接使用内联函数或匿名函数,更推荐使用匿名函数。

内联函数

例如

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),然后直接使用。

匿名函数

匿名函数比内联函数更方便,可以指定输入参数的名称,还可以在表达式中捕获使用工作区的变量,并且效率更高。例如

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

匿名函数的本质就是一个函数句柄,对于匿名无参函数的调用,也是不允许省略括号的。

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

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)

匿名函数在概念上与其他语言中的lambda表达式是一样的,在实现上则是利用了MATLAB提供的函数句柄机制。

补充

为了提供代码的可读性,不建议使用脚本名称直接调用脚本,建议使用内置函数run

1
run('demo.m');

对于不含任何参数的函数调用,也不建议省略括号(),保留括号才能体现出函数调用的过程。