# Dart教程 - 5 函数
函数也可以称之为功能,我们可以将一段实现特定功能的代码封装为一个函数。
为什么需要封装为函数呢?
举个栗子:
给定2整数,我现在想获取其中的最大值。
于是乎,哐哐敲代码:
void main() {
int num1 = 24;
int num2 = 12;
int maxNum; // 这里功能的代码不是很简洁,不要计较
if (num1 > num2) {
maxNum = num1;
} else {
maxNum = num2;
}
print("较大的数为:${maxNum}"); // maxNum得到最大的数
}
2
3
4
5
6
7
8
9
10
11
12
13
一个地方需要这个功能,我在一个地方写了这个功能的代码,100个、1000个地方需要这个功能,那就需要写1000遍。万一这个功能需要修改,修改1000个地方不得崩溃。
所以我们可以将这个功能封装为一个函数,想要使用到这个功能的时候,直接调用这个函数即可。
所以通过使用函数,提高代码的复用性,减少重复代码,提高开发效率。
我们之前已经使用了很多内置的函数,例如print()、tryParse()等,现在我们主要学习如何自定义函数。
# 5.1 函数的定义
函数定义的语法:
返回值类型 函数名(参数列表) {
函数体
return 返回值;
}
2
3
4
返回值可以是任何类型或者是void;
参数可以缺省;
return 返回值
也可以省略,不返回结果;函数必须先定义后调用。
注意:函数内的return后面如果有语句,是不会被执行的。
函数调用的语法:
函数名(参数列表);
举个栗子:
下面定义一个函数,计算两个数的和,然后调用该函数。
int add() { // 定义函数
return 3 + 5;
}
void main() {
int result = add(); // 调用函数
print(result); // 输出8
}
2
3
4
5
6
7
8
9
上面定义的函数只能计算 3 + 5 的值,复用性太差,我们可以让函数接收两个参数,然后根据这两个参数的和。
# 5.2 函数的参数
函数的参数可以接收外面传递的值。
通过传递参数,我们可以改写上面计算两个数的和的函数:
int add(int a, int b) { // 定义函数
return a + b;
}
void main() {
int result = add(1, 2); // 调用函数
print(result); // 输出3
}
2
3
4
5
6
7
8
在调用的时候,传递了两个参数,1和2
,根据传入的顺序,1传递给了a,2传递给了b,调用函数的时候,需要根据函数定义时候的参数顺序来传递参数,这种方式叫位置传参。
通过传递不同的参数,可以计算不同值的和,复用性更强。
函数名后面的参数为形式参数(形参),函数的参数个数不限,使用逗号分隔;
调用函数的时候,传递的是实际参数(实参),表示函数执行的时候的参数值。
上面函数调用时候需要根据函数定义时候的顺序来传递参数。
上面的位置传参是必须参数,也就是在调用的时候,是必须传递值的。除了必须参数还有可选参数。
其中可选参数还包括 命名可选参数 和 位置可选参数。
# 1 命名可选参数
命名可选参数是用方括号{}
包裹的参数,它们放在参数列表的末尾。
举个栗子:
/**
* 定义方法,带有命名可选参数
*/
void printUserInfo(String name, {int? age, String? address}) { // 可选参数可能为空,所以使用?,也可以使用var来定义
print("姓名:$name");
if (age != null) {
print("年龄:$age");
}
if (address != null) {
print("地址:$address");
}
}
void main() {
// 命名可选参数,不用考虑参数的位置,根据名称传递
printUserInfo("张三", address: "北京市", age: 18);
printUserInfo("李四", age: 20);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上面的 {int? age, String? address}
就是可选参数,调用的时候通过名称来传参,对参数顺序没有要求。
在 main
函数的调用中,第一个调用传递了name
、age
和address
三个参数;第二个调用只传递了name
和age
两个参数,address
参数被省略了。
如果希望命名可选参数必须传递的话,可以在前面加上 required
关键字。
举个栗子:
在 address
参数前面添加 required
关键字,在调用的时候就必须传递 address
参数。
/**
* 定义方法,带有命名可选参数
*/
void printUserInfo(String name, {int? age, required String address}) {
print("姓名:$name");
if (age != null) {
print("年龄:$age");
}
print("地址:$address");
}
void main() {
// 命名可选参数,不用考虑参数的位置,根据名称传递
printUserInfo("张三", address: "北京市");
//printUserInfo("李四", age: 20); // 代码报错,因为没有传递address
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2 位置可选参数
位置可选参数是用方括号 []
包裹的参数,它们放在参数列表的末尾。
举个栗子:
/**
* 定义方法,带有位置可选参数
*/
void printUserInfo(String name, [int? age, String? address]) { // 可选参数可能为空,所以使用?,也可以使用var来定义
print("姓名:$name");
if (age != null) {
print("年龄:$age");
}
if (address != null) {
print("地址:$address");
}
}
void main() {
printUserInfo("张三", 18, "北京市");
// 位置可选参数,后面的参数可以不传递
printUserInfo("李四", 20);
// 位置可选参数是根据位置匹配的,所以在传递后面参数的时候,前面的参数必须传递值,哪怕传递null值
printUserInfo("张三", null, "北京市");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
上面的 [int? age, String? address]
就是位置可选参数,在调用的时候,如果给一个位置可选参数传递值,那么在它前面的参数都要传递值,不能省略。
# 3 默认参数
默认参数为形参提供默认值,那么在调用函数的时候,如果不传递参数,则使用默认参数值。
举个栗子:
命名可选参数,指定默认值:
void printUserInfo1(String name, {int age = 18, String address = "北京市"}) {
print("姓名:$name");
print("年龄:$age");
print("地址:$address");
}
2
3
4
5
指定了默认参数,如果不传递参数,那么参数会使用指定的默认值。
位置可选参数,指定默认值:
void printUserInfo1(String name, [int age = 18, String address = "北京市"]) {
print("姓名:$name");
print("年龄:$age");
print("地址:$address");
}
2
3
4
5
但是对于位置可选参数而言,在调用的时候,如果给一个位置可选参数传递值,那么在它前面的参数都要传递值,不能省略。
# 4 参数的类型
上面的函数的参数都是指定类型的,参数还可以不指定类型。
举个栗子:
int add(var a, var b) {
return a + b;
}
2
3
var 也可以省略
int add(a, b) {
return a + b;
}
2
3
函数参数的类型默认是动态的(dynamic
),也就是说,如果没有明确指定参数类型,Dart 会自动推断参数的类型为 dynamic
。这意味着在函数 add()
中,a,b
的类型将被推断为 dynamic
,那么可以接收字符串、数字、布尔值等。这样的灵活性在某些情况下可能是有用的,但也会导致类型安全方面的隐患,特别是在函数内部可能会因类型不匹配而导致运行时错误。
举个栗子:
int add(a, b) {
return a + b;
}
void main() {
add(true, false);
}
2
3
4
5
6
7
在上面的代码中,我们可以给 a,b
传递bool值,导致运行的时候报错:
# 5 函数的重载
什么是函数的重载?
函数的重载就是函数的方法名一样,但是参数不一样,注意不包括返回值。
举个栗子:
void sayHello() {
}
void sayHello(String name) {
}
2
3
4
上面的两个函数,方法名一样,但是参数不一样,这就是函数的重载。
但是需要注意:在dart中,不支持函数的重载,所以是不支持像上面那样定义相同名称的函数的。
瞎逼逼了半天,java中是支持的。
# 5.3 函数的返回值
函数的返回值类型可以在函数声明时指定,也可以根据函数体中的代码自动推断出来。
如果函数没有指定返回值,则默认返回 null
。
举个栗子:
int add(int a, int b) {
// 返回值的类型为整形
return a + b;
}
subtract(int a, int b) {
// 没有返回值类型,会自动推断出返回值类型
return a - b;
}
funcA(int a, int b) {
// 函数没有指定返回值,则返回null
print(a + b);
}
void funcB(int a, int b) {
// 函数没有返回值
print(a + b);
}
void main() {
int result = add(1, 2); // 调用函数
print(result); // 输出3
print(subtract(5, 3)); // 输出:5
print(funcA(2, 3)); // 输出:null
// print(funcB(2, 3)); // 错误,
}
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
30
上面定义了一个函数,将两个数相加,然后将结果返回,并打印出来。
注意:函数内的return后面如果有语句,是不会被执行的。
函数的返回值需要定义变量来接收,不过,如果你只是调用,不接收结果,也是没有问题的:
int add(int a, int b) {
// 定义函数
return a + b;
}
void main() {
add(1, 2); // 调用函数
}
2
3
4
5
6
7
8
# 5.4 函数的嵌套调用
函数的嵌套调用就是一个函数可以调用另外一个函数,另外一个函数还可以继续调用其他的函数,依此类推。
举个栗子:
下面定义了2个函数,funA() 和 funB(),并在main()函数中调用了funA() ,然后在funA()中调用了 funB()。
void funB() {
print("----b");
}
void funA() {
print("----a1");
funB();
print("----a2");
}
void main() {
funA();
}
2
3
4
5
6
7
8
9
10
11
12
13
执行结果:
----a1 ----b ----a2
我们会发现在 funA() 中调用 funB() 后,funB() 执行完成,重新回到了 funA() 继续执行。
# 5.5 变量的作用域
变量的作用域就是在哪里可以使用这个变量。不可能我们随便定义了一个变量,哪里都可以使用。
变量主要分类两类:局部变量和全局变量。
变量的作用域是根据代码的结构 {}
来决定作用域范围的,优先使用自己作用域中的变量,如果没有找到,则一层层向外查找。
# 1 局部变量
局部变量就是在函数内部定义的变量,这样变量只能在变量内部使用。
举个例子:
void funA() {
int num = 3;
print(num);
}
void main() {
// print(num); // 无法访问到funA()内定义的变量num
}
2
3
4
5
6
7
8
因为变量num在 funA() 函数内部定义,所以在main函数无法访问。
局部变量当函数调用完成,就被销毁释放了。
# 2 全局变量
全局变量就是在函数内、外都内调用的变量。
举个栗子:
int a = 1; // 定于全局变量a
int b = 2; // 定于全局变量b
void main() {
int a = 5;
{
int a = 10;
print(a); // 优先在所在的{}内查找变量
}
print(a); // 优先在所在的{}内查找变量,即main()函数内查找a
print(b); // 优先在所在的{}内查找变量,main函数内没找到,则继续向外寻找
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int a = 10;
只在它所在的 {}
内有效,{}
外是无法访问的。
执行结果:
10
5
2
# 5.6 函数的递归
什么是函数的递归?
函数的递归,就是在一个函数内部,又调用了这个函数自己,这个就是函数的递归。
举个例子:
void funA() {
print("----");
funA();
}
void main() {
funA();
}
2
3
4
5
6
7
8
上面的函数,执行 funA(),然后在 funA() 内部又调用了自己,那么会重新又调用了 funA() 函数,然后又调用了自己,这样就会一直无限调用,变成了无限循环调用,执行的时候很快就报Stack Overflow的错误,栈溢出。
所以函数的递归有时候是很危险的,很容易无限调用,造成栈溢出,程序崩溃。所以函数的递归调用一定要注意结束或跳出递归的条件。
例如我们写一个用递归求阶乘的函数:
int factorial(int num) {
// 求阶乘
if (num <= 1) {
return 1;
}
return num * factorial(num - 1);
}
void main() {
int num = 5;
int result = factorial(num);
print("$num的阶乘为:$result");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
以 5 举例,求 5 的阶乘,调用了 factorial函数
,则计算 5 乘以 4 的阶乘,然后求 4 的阶乘,重新调用了 factorial函数
,然后计算 4 乘以 3 的阶乘,一次类推,一直得到1的阶乘,然后向上返回。
递归函数一定得有结束的条件,否则就会无限递归导致栈溢出错误。