# Dart教程 - 5 函数

函数也可以称之为功能,我们可以将一段实现特定功能的代码封装为一个函数。

为什么需要封装为函数呢?

举个栗子:

给定2整数,我现在想获取其中的最大值。

于是乎,哐哐敲代码:

void main() {
  int num1 = 24;
  int num2 = 12;

  int maxNum;											// 这里功能的代码不是很简洁,不要计较
  if (num1 > num2) {
    maxNum = num1;
  } else {
    maxNum = num2;
  }

  print("较大的数为:${maxNum}"); 	// maxNum得到最大的数
}
1
2
3
4
5
6
7
8
9
10
11
12
13

一个地方需要这个功能,我在一个地方写了这个功能的代码,100个、1000个地方需要这个功能,那就需要写1000遍。万一这个功能需要修改,修改1000个地方不得崩溃。

所以我们可以将这个功能封装为一个函数,想要使用到这个功能的时候,直接调用这个函数即可。

所以通过使用函数,提高代码的复用性,减少重复代码,提高开发效率。

我们之前已经使用了很多内置的函数,例如print()、tryParse()等,现在我们主要学习如何自定义函数。

# 5.1 函数的定义

函数定义的语法:

返回值类型 函数名(参数列表) {
  函数体
  return 返回值;
}
1
2
3
4
  • 返回值可以是任何类型或者是void;

  • 参数可以缺省;

  • return 返回值 也可以省略,不返回结果;

  • 函数必须先定义后调用。

  • 注意:函数内的return后面如果有语句,是不会被执行的。

函数调用的语法:

函数名(参数列表);
1

举个栗子:

下面定义一个函数,计算两个数的和,然后调用该函数。

int add() {							// 定义函数
  return 3 + 5;
}

void main() {
  int result = add();		// 调用函数
  print(result); 				// 输出8
}

1
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
}
1
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

上面的 {int? age, String? address} 就是可选参数,调用的时候通过名称来传参,对参数顺序没有要求

main 函数的调用中,第一个调用传递了nameageaddress三个参数;第二个调用只传递了nameage两个参数,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
}
1
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, "北京市");
}
1
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");
}
1
2
3
4
5

指定了默认参数,如果不传递参数,那么参数会使用指定的默认值。

位置可选参数,指定默认值:

void printUserInfo1(String name, [int age = 18, String address = "北京市"]) {
  print("姓名:$name");
  print("年龄:$age");
  print("地址:$address");
}
1
2
3
4
5

但是对于位置可选参数而言,在调用的时候,如果给一个位置可选参数传递值,那么在它前面的参数都要传递值,不能省略。

# 4 参数的类型

上面的函数的参数都是指定类型的,参数还可以不指定类型。

举个栗子:

int add(var a, var b) {
  return a + b;
}
1
2
3

var 也可以省略

int add(a, b) {
  return a + b;
}
1
2
3

函数参数的类型默认是动态的(dynamic),也就是说,如果没有明确指定参数类型,Dart 会自动推断参数的类型为 dynamic。这意味着在函数 add() 中,a,b 的类型将被推断为 dynamic,那么可以接收字符串、数字、布尔值等。这样的灵活性在某些情况下可能是有用的,但也会导致类型安全方面的隐患,特别是在函数内部可能会因类型不匹配而导致运行时错误。

举个栗子:

int add(a, b) {
  return a + b;
}

void main() {
  add(true, false);
}
1
2
3
4
5
6
7

在上面的代码中,我们可以给 a,b传递bool值,导致运行的时候报错:

# 5 函数的重载

什么是函数的重载?

函数的重载就是函数的方法名一样,但是参数不一样,注意不包括返回值。

举个栗子:

void sayHello() {
}
void sayHello(String name) {
}
1
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)); // 错误,
}
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
30

上面定义了一个函数,将两个数相加,然后将结果返回,并打印出来。

注意:函数内的return后面如果有语句,是不会被执行的。

函数的返回值需要定义变量来接收,不过,如果你只是调用,不接收结果,也是没有问题的:

int add(int a, int b) {
  // 定义函数
  return a + b;
}

void main() {
  add(1, 2); 				// 调用函数
}
1
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();
}
1
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
}
1
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函数内没找到,则继续向外寻找
}

1
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();
}
1
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");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

以 5 举例,求 5 的阶乘,调用了 factorial函数 ,则计算 5 乘以 4 的阶乘,然后求 4 的阶乘,重新调用了 factorial函数 ,然后计算 4 乘以 3 的阶乘,一次类推,一直得到1的阶乘,然后向上返回。

递归函数一定得有结束的条件,否则就会无限递归导致栈溢出错误。