流程控制

在划分控制结构所对应的代码块时,编程语言通常采用如下三种做法:

  • 基于大括号 {}:例如 C/C++,Java
  • 基于 end 标记:例如 FORTRAN,MATLAB
  • 基于缩进:Python

MATLAB 受到 FORTRAN 的影响很大,也采用基于 end 的代码块标记,并不使用大括号 {} 来划分代码结构。

if 条件语句

提供例子即可

1
2
3
if x>1
x=1;
end
1
2
3
4
5
if x>1
y=x;
else
y=1;
end
1
2
3
4
5
6
7
if x>10
y=x;
elseif x>0
y=1;
else
y=0
end

在 if 语句中通常需要使用与或非运算(不建议用 &|

1
2
3
if cond1 && cond2
% ...
end
1
2
3
if cond1 || cond2
% ...
end
1
2
3
if ~cond
% ...
end

switch 条件语句

MATLAB 支持基本的 switch 语句,我们可以判断表达式的值以进入不同的分支,不需要在分支结束使用 break,因为不会自动进入下一个分支,兜底的默认分支名为 otherwise

1
2
3
4
5
6
7
8
switch x
case 1
z=1
case 2
z=2
otherwise
z=3
end

MATLAB 并不要求 case 后面的结果是常量。
对于更复杂的 switch 情况,我们可以使用下面的 “反转” 技巧,将判断表达式取为 true,在 case 中加入不同的布尔表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
score = 85;

switch true
case (score >= 90)
disp('Grade: A');
case (score >= 80)
disp('Grade: B');
case (score >= 70)
disp('Grade: C');
case (score >= 60)
disp('Grade: D');
otherwise
disp('Grade: F');
end

for 循环语句

非常不建议基于 for 循环(尤其是多重 for 循环)来遍历矩阵进行计算,因为这种做法的效率非常低下,不能照搬使用 C/C++ 时直接写 for 循环的习惯,使用 MATLAB 提供的向量化运算是更好的选择。

for 循环语句的标准格式如下

1
2
3
for index = startValue:step:endValue
% 循环体代码
end

和其他编程语言通常采用的左闭右开区间不同,这里的索引区间实际上是闭区间

例如

1
2
3
4
for i = 1:2:11
disp(i);
end
% 1 3 5 7 9 11

对于步长为 1 的情况,可以省略步长

1
2
3
4
for i = 1:10
disp(i);
end
% 1 2 3 4 5 6 7 8 9 10

可以用双重循环来遍历一个矩阵

1
2
3
4
5
6
B = [1, 2;3, 4];
for i = 1:size(B, 1)
for j = 1:size(B, 2)
disp(B(i, j));
end
end

和其他语言一样,在循环中支持如下选项:

  • break 跳出当前循环;
  • continue 跳转进入下一次循环。

注意:

  • 关于循环指标:由于 i,j 默认是虚数单位,如果将其作为循环变量,虽然在语法上不会报错,但是这会修改它们的值,后续不能将其用作虚数单位。(可以使用 i=1i, j=1j 来恢复)
  • 如果不使用向量化的语法,那么大规模的 for 循环(尤其是多层循环)很可能就是可行计算程序的性能瓶颈,需要特别注意循环体内部的语句细节,这里不做讨论。

for 遍历语句

我们可以用下面的语法方便地遍历行向量中的每一项

1
2
3
4
A = [1 2 3];
for i=A
disp(i)
end

对于矩阵,遍历语句每次会获取一列

1
2
3
4
5
6
A = [1 2 3
4 5 6];

for i=A
disp(i)
end

输出

1
2
3
4
5
6
7
8
1
4

2
5

3
6

while 循环语句

while 循环没什么好说的,和其他语言没什么区别,例如

1
2
3
4
5
w=0;u=0;

while u<10
w=w+u;u=u+1;
end

同样也支持 breakcontinue 语句。

补充

上述结构可以随意嵌套,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
for c = 1:ncols
for r = 1:nrows

if r == c
A(r,c) = 2;
elseif abs(r-c) == 1
A(r,c) = -1;
else
A(r,c) = 0;
end

end
end

在单条语句的简单情况下,也可以使用单行形式,例如

1
2
3
4
5
6
7
8
9
10
% if 条件, 语句; end
if a > 0, disp('a is positive'); end
if ~isempty(TitleStr), title(TitleStr); end


% if 条件, 语句1; else 语句2; end
if x > 0, disp('Positive'); else disp('Non-positive'); end

% for 变量 = 范围, 语句; end
for i = 1:5, fprintf('%d ', i); end

虽然 MATLAB 将字符数组和字符串,字符串数组进行了区分,但是出于兼容性考虑,在下面的各种输入输出方式中无论是使用字符数组还是字符串都是一样的效果。

异常捕获(错误处理)

MATLAB 使用 try ... catch ... end 结构进行异常捕获,用于在运行时错误发生时接管控制流程,而不是直接终止程序。

基本用法

最基本的异常捕获形式如下:

1
2
3
4
5
6
try
A = rand(3);
B = A(5,1);
catch
disp('An error occurred.');
end

一旦 try 块中发生运行时错误,MATLAB 会立即跳转到 catch 块,try 中剩余的语句不会再执行。

捕获异常

通常我们需要获取错误的具体信息,此时可以在 catch 后接收一个异常对象(通常命名为 ME,类型为 MException):

ME 中常用的字段包括:

  • ME.message:错误信息(字符串)
  • ME.identifier:错误标识符(如 MATLAB:badsubscript
  • ME.stack:调用栈信息(结构体数组)

例如

1
2
3
4
5
6
7
8
9
try
A = rand(3);
B = A(5,1);
catch ME
fprintf('Error identifier: %s\n', ME.identifier);
fprintf('Error message: %s\n', ME.message);
fprintf('Error stack:\n');
disp(ME.stack);
end

输出如下

1
2
3
4
5
6
Error identifier: MATLAB:badsubscript
Error message: Index in position 1 exceeds array bounds. Index must not exceed 3.
Error stack:
file: '/path/to/test.m'
name: 'test'
line: 3

产生异常

除了捕获已有的运行时错误,MATLAB 也允许用户主动产生异常,用于在检测到非法状态或无效输入时中断程序执行。

最常见的方式是使用 error 函数:

1
2
3
if n < 0
error('n must be non-negative.');
end

一旦调用 error,代表当前操作已经无法继续,MATLAB 会立即终止当前执行流程,并抛出一个异常。

对于不严重的问题,可以发出一个警告: warning('Result may be inaccurate.');

error 支持与 fprintf 类似的格式化字符串:

1
2
3
if k > N
error('Index %d exceeds maximum value %d.', k, N);
end

推荐在函数或库代码中使用错误标识符(identifier),便于上层代码区分错误类型:

1
2
3
if ~isscalar(n)
error('MyToolbox:InvalidInput', 'Input n must be a scalar.');
end

错误标识符通常采用 <package>:<category> 的格式,避免与 MATLAB 内置错误冲突。

assert 是一种比 if error 更轻量级的做法,主要用于快速检查前置条件,不满足时自动抛出异常:

1
2
assert(n > 0, 'n must be positive.');
assert(isnumeric(x), 'Input must be numeric, not %s.', class(x));

对于更复杂的错误构造,可以显式创建 MException 对象并抛出:

1
2
ME = MException('MyToolbox:OutOfRange', 'Value %.3f is out of range.', x);
throw(ME);

如果希望保留当前调用栈信息,通常直接使用 error 即可;MException 通常在对异常进行加工或传递时使用。

异常处理

在捕获异常并获取信息之后,需要对其进行相应处理。最简单的处理是重新抛出异常:

1
2
3
4
5
6
try
risky_function();
catch ME
disp('Error detected, cleaning up...');
rethrow(ME);
end

这种做法看起来什么也没做,但是仍然是有意义的,因为在捕获错误和重新抛出之间可以加入一些清理或记录。

rethrowthrow 也是不一样的:

  • rethrow(ME) 会保留原始的调用栈(推荐)
  • throw(ME) 会从当前行重新抛出,调用栈会发生变化,视作从当前位置触发的异常

可以根据错误标识符对不同类型的错误进行区分处理:

1
2
3
4
5
6
7
8
9
10
try
load('nonexistent.mat');
catch ME
switch ME.identifier
case 'MATLAB:load:couldNotReadFile'
warning('File not found.');
otherwise
rethrow(ME);
end
end

可以对底层错误进行包装,并添加新的错误信息,以提供更完整的错误信息。

例如

1
2
3
4
5
6
7
8
9
risky_function();

function risky_function()
try
load('nonexistent.mat');
catch ME
rethrow(ME);
end
end

此时输出信息为

1
2
3
4
5
6
Error using load
Unable to find file or directory 'nonexistent.mat'.
Error in test>risky_function (line 5)
load('nonexistent.mat');
Error in test (line 1)
risky_function();

在调用时加上一层错误包装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try
risky_function();
catch ME
newME = MException('MyToolbox:Failure', 'Task failed: %s', ME.message);
newME = addCause(newME, ME);
throw(newME);
end

function risky_function()
try
load('nonexistent.mat');
catch ME
rethrow(ME);
end
end

此时输出信息为

1
2
3
4
5
Error using test
Task failed: Unable to find file or directory 'nonexistent.mat'.
Caused by:
Error using load
Unable to find file or directory 'nonexistent.mat'.

但是这里也改变了原本的调用栈信息。(对于库函数,可以用来隐藏底层细节)

onCleanup

在异常发生时使用 try/catch 会导致正常的工作流程中断,直接跳出当前上下文,此时 MATLAB 不会自动进行清理工作。对于文件、锁、路径修改等资源,可以使用 onCleanup 来保证退出时执行清理逻辑:

1
2
3
4
5
fid = fopen('data.txt','w');
c = onCleanup(@() fclose(fid));

fprintf(fid, 'Hello\n');
error('Something went wrong'); % fclose 仍然会被调用

onCleanup 对象一旦离开作用域(包括异常中断),其绑定的函数就会被执行。

异常处理与调试

需要说明的是,代码调试并不是独立在异常处理机制之外的特殊机制,
异常处理流程会对代码调试造成不利的影响。

在开启 dbstop if error 的情况下,
try 语句中发生的错误不会触发 dbstop if error
即使在 catch 中调用 rethrow(ME),如果上层没有额外的异常捕获处),调试器也只会在重新抛出异常的位置停下,而无法回到最初出错的那一行。

在调试阶段,如果需要精确定位错误,最好临时移除 try/catch,或者复刻一个专门用于调试,无异常处理机制的执行流。

显示变量的值

disp 函数可以用来显示一个变量的值,如果这个变量是字符串的话,也可以达到输出信息的效果,但是会自动添加一个回车。

例如

1
2
disp('hello,world!');
% hello,world!

这里的 disp(X) 语句加不加 ; 都是一样的。

disp 函数只接受一个参数,我们可以将字符数组拼接起来进行显示

1
2
disp(['hello', ',', 'world']);
% hello,world

在很多默认行为中都会调用 disp 函数,例如一个普通的赋值语句如果不以 ; 结尾,可能会对赋值结果调用 disp 函数以展示它的值。
对于自定义类型也可以通过定义 disp 方法来达到自定义输出效果的目的。

格式化字符串

MATLAB 支持和 C 语言几乎一样的字符串格式化函数,包括 fprintfsprintf

fprintf 格式化输出到控制台或文件中,返回值只是输出的字节数,没什么用,例如

1
2
3
4
5
fprintf('Hello, %s!\n', 'World'); % 缺省时输出到控制台,不会自动换行,需要加上 \n 回车
Hello, World!

fileID = fopen('exp.txt','w'); % 打开文件
fprintf(fileID,'Hello, %s!\n', 'World'); % 写入文件中

fileID 是获取的文件句柄(其实就是一个大于 2 的整数),1 代表标准输出流,2 代表标准错误输出流。

sprintf 格式化输出到一个字符串,返回值就是格式化得到的字符串,例如

1
2
3
4
5
6
name = 'Alice';
age = 30;
height = 5.5;
str = sprintf('Name: %s, Age: %d, Height: %.1f feet', name, age, height);

disp(str);

除此之外,下面几个函数也是支持格式化字符串并输出的,不需要额外生成 message(得益于 MATLAB 对不定参数的支持,不需要像 C 语言那么麻烦)

1
2
3
4
5
6
7
8
9
10
11
n = 7;

assert(isa(c,'double'),'Product is type %s, not double.',class(c))

if ~ischar(n)
warning('Input must be a character vector, not a %s',class(n))
end

if ~ischar(n)
error('Error. \nInput must be a char, not a %s.',class(n))
end

标准文件读写

在 MATLAB 中,文件打开模式与 C 语言非常类似。(底层的操作系统提供的接口就是这样的,以这种方式可以尽量保持完整的接口)

使用 fopen 函数打开文件,除了文件名之外,还可以指定不同的模式来控制文件的读写行为,常见的文件打开模式包括:

  • r:只读模式。文件必须存在,否则会出错。
  • w:写入模式。如果文件存在,将覆盖文件;如果文件不存在,将创建新文件。
  • a:追加模式。如果文件存在,数据将写入文件末尾;如果文件不存在,将创建新文件。
  • r+:读写模式。文件必须存在,否则会出错。
  • w+:读写模式。如果文件存在,将覆盖文件;如果文件不存在,将创建新文件。
  • a+:读写模式。如果文件存在,数据将写入文件末尾;如果文件不存在,将创建新文件。

此外,还可以指定文本模式或二进制模式:

  • t:文本模式(默认)。
  • b:二进制模式。

fopen 函数返回的是文件 ID,后续对这个文件的操作都需要传入文件 ID,包括最后的关闭文件 fclose

下面列举几个常见的文件操作:

逐行读取文本并展示

1
2
3
4
5
6
7
8
fileID = fopen('example.txt', 'r');

while ~feof(fileID)
line = fgetl(fileID);
disp(line);
end

fclose(fileID);

逐行写入文本

1
2
3
4
5
6
fileID = fopen('output.txt', 'w');

fprintf(fileID, 'This is a test.\n');
fprintf(fileID, 'The value of pi is approximately %.4f\n', pi);

fclose(fileID);

将数组写入到文件中(每一行只写入一个元素)

1
2
3
fid = fopen('output.txt', 'w');
printf(fid, '%.12f\n', v);
fclose(fid);

写入二进制文件

1
2
3
4
5
6
7
fileID = fopen('binaryoutput.bin', 'wb');

data = [1.1, 2.2, 3.3];

fwrite(fileID, data, 'double'); % 以double格式写入

fclose(fileID);

读取二进制文件,这里重新读取出来的 data 变成列向量了。

1
2
3
4
5
6
7
fileID = fopen('binaryoutput.bin', 'rb');

data = fread(fileID, 'double'); % 以double格式读取

disp(data);

fclose(fileID);

在写入文件前确保目录存在

1
2
3
4
5
[filePath, ~, ~] = fileparts(fileName);
if ~isempty(filePath) && ~exist(filePath,'dir')
mkdir(filePath);
end
fileID = fopen(fileName, 'w');

MAT 文件读写

MATLAB 专门提供 saveload 函数,用于保存和加载 .mat 文件,这是一种 MATLAB 特有的二进制文件格式,可以完整存储工作空间中的变量,包括变量的类型、大小和其他元数据。

保存变量

使用 save 函数可以直接把当前工作区的所有变量保存到 MAT 文件中

1
save('all_data.mat')

我们也可以将指定的部分变量保存到 MAT 文件中

1
2
3
p = rand(1,10);
q = ones(10);
save('pqfile.mat','p','q')

可以以追加形式把数据添加到 MAT 文件中

1
2
3
D = ones(2);

save('mydata.mat', 'D', '-append')

注意:

  • 如果文件已经存在,默认会直接覆盖原始文件,除非加上 -append 选项;
  • load 命令其实需要一个格式选项,默认是 -mat,也支持 -ascii 等其他格式,但是对文本格式的操作不够灵活,建议使用 fprintf;对格式的判断与文件后缀名无关。
  • 有些特殊类型的变量无法保存到 MAT 文件中。

可以使用 whos 命令查看 MAT 文件当前存储的变量列表,得到的是一个结构体数组

1
2
fileInfo = whos('-file', 'mydata.mat');
disp(fileInfo);

可以加上 -struct 选项解包一个结构体数组,将其中的所有字段作为单独变量存储到 MAT 文件中

1
2
3
4
s1.a = 12.7;
s1.b = {'abc',[4 5; 6 7]};
s1.c = 'Hello!';
save('newstruct.mat','-struct','s1')

可以加上 MAT 文件的格式版本,默认 7.0 版本,但是只有 7.3 版本可以支持超过 2GB 的数据

1
2
3
A = rand(5);
B = magic(10);
save('example.mat','A','B','-v7.3')

关于 7.3 版本的补充说明:

  • 与之前版本不同,7.3 版本的 MAT 文件使用基于 HDF5 的格式,该格式要求使用一些存储空间开销来描述文件内容。
  • 底层 HDF5 格式使得 7.3 版本支持 MAT 文件内容的部分加载和更新,这对于那些超大的数据文件非常实用。
  • 对于元胞数组、结构体数组或可以存储异构数据类型的其他容器,7.3 版本的 MAT 文件有时比版本 7 的 MAT 文件要大。

可以使用 -nocompression选 项,这个选项会阻止存储过程中的压缩,优点是存储过程更快,缺点是 MAT 文件变大

1
2
3
A = rand(5);
B = magic(10);
save('example.mat','A','B','-v7.3','-nocompression')

一个常见的需求是,MAT 文件中已经存储了我们关注的若干变量,我们希望使用工作区中的同名变量更新 MAT 文件中的数据,但是不引入工作区中的其他变量

1
2
existingVars = whos('-file','mydata.mat');
save('mydata.mat',existingVars.name)

加载变量

使用 load 函数可以直接把 MAT 文件中的所有变量加载到工作区中

1
load('mydata.mat');

也可以指定加载 MAT 文件中的部分变量到工作区中(这里甚至支持通过正则匹配加载满足的部分变量)

1
load('mydata.mat', 'var1', 'var2', ...);

注意:

  • 如果当前工作区的变量和 MAT 文件中的变量重复,那么 MAT 文件中的变量值就会直接覆盖它!
  • 在加载MAT文件时我们可能需要传递文件格式,默认格式是 -mat,也支持 -ascii 等选项,格式的判断与文件后缀无关;
  • save 对于非默认的存储版本需要手动指定,但是 load 加载时不需要,会自动识别处理

我们可以使用变量接受 load 的返回值,这样会把所有变量加载到一个结构体数组中,作为结构体数组的字段,而不是直接暴露在工作区中

1
2
3
4
data = load('mydata.mat');

disp(data.A);
disp(data.B);

补充

除了 loadsave,还可以使用 matfile 访问和更改 MAT 文件,而不必将文件加载到内存中,相比前两个接口,它可以对 MAT 文件进行更灵活的操作,但是部分高效操作只支持 7.3 版本。

loadsave 相关函数接口看起来非常奇怪,几乎所有参数都是字符数组或字符串形式,这其实是为了与命令式语法兼容。

可以完全用命令式的语句完成变量的保存和加载

1
2
3
4
5
6
save all_data.mat
save pqfile.mat p q
save mydata.mat D -append

load mydata.mat
load mydata.mat var1 var2