# Dart教程 - 7 面向对象

面向对象,首先涉及到的两个概念就是:对象

什么是类?

类就是对现实事物的抽象设计。

例如设计学生的类,可能包括属性:学号,姓名、年龄、性别等。

设计狗的类,可能包括属性:名字、年龄、品种。

类表示的是对一个事物的抽象,不是具体的某个事物。

什么是对象?

对象就是一个具体的实例,这个实例是从类派生出来的。

我们将类的属性赋值,就产生了一个个实例,也就是对象。

例如通过学生的类,我们赋值:学号=001,姓名=张三,年龄=16,性别=男,班级=三年二班,产生了就是一个名字叫张三的具体的学生,这样我们通过给类可以派生出一个个不同属性值的对象,李四、王五、赵六...。

# 7.1 类和对象

# 1 类的定义

类中可以定义属性和行为,属性也就是变量,表示这个类有哪些数据信息,行为也叫方法,表示这个类能干什么。

例如,对于学生类而言,学号、姓名、年级就是属性,学习这个行为,可以定义为方法。

那么我们可以定义以下学生类:

定义类使用class关键字,后面跟类名。

class Student {
  String sid = "001";
  String name = "Doubi";
  int age = 12;

  void study() {
    print("我是${name}, 我在学习");
  }
}
1
2
3
4
5
6
7
8
9

上面的类定义了三个成员变量(sid、name、age),并赋值了。还定义了一个成员方法 study,定义成员方法和之前定义函数是一样的。

# 2 类的使用

上面我们定义好类了,现在可以使用类来创建对象了。

/**
 * 定义类
 */
class Student {
  String sid = "001";
  String name = "Doubi";
  int age = 12;

  void study() {
    print("我是${name}, 我$age岁了, 我在学习");
  }
}

void main() {
  Student stu1 = new Student();
  print(stu1.name); // 访问成员变量
  print(stu1.age);
  stu1.study();

  Student stu2 = Student();
  stu2.name = "ShaBi";
  stu2.age = 13;
  stu2.study();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

通过 new 类名() 可以创建一个对象,new 可以省略。

创建对象后,我们可以通过 对象.属性 来访问变量,也可以通过 对象.属性=值 来给属性赋值。

使用 对象.方法() 可以调用方法。

执行结果:

Doubi 12 我是Doubi, 我12岁了, 我在学习 我是ShaBi, 我13岁了, 我在学习

面向对象编程就是先设计类,然后通过类创建对象,由对象做具体的工作。

面向过程编程关心的是实现功能的步骤,而面向对象编程更关心由谁来实现功能。

# 3 默认构造函数

我们上面在创建类的时候,是给属性直接赋值的String name = "Doubi";。但是在实际的使用中,一般是使用不同的属性值创建不同的对象,而且可以在创建对象的时候对属性进行初始化,这里就需要用到构造函数。

构造函数会在创建对象的时候执行,通过传递的属性值,给属性进行初始化。

举个栗子:

构造函数的名称和类型是一致的。

/**
 * 定义类
 */
class Student {
  late String sid;
  String? name;
  late int age;

  // 构造函数
  Student(String sid, String name, int age) {
    this.sid = sid;			// this.sid访问的是属性,sid访问的是形参
    this.name = name;
    this.age = age;
  }

  void study() {
    print("我是${name}, 我$age岁了, 我在学习");
  }
}

void main() {
  Student stu1 = new Student("001", "Doubi", 12);
  stu1.study();

  Student stu2 = Student("002", "Shabi", 13);
  stu2.study();
}
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

执行结果:

我是Doubi, 我12岁了, 我在学习 我是Shabi, 我13岁了, 我在学习

在上面的代码中,因为定义为 Stringint 都是非空类型的,所以定义的时候就必须进行初始化,否则报错,所以为了可以在构造函数中再进行初始化,必须添加 late 关键字,表示延迟初始化。当然也可以定义为可空类型。

当不写构造函数的时候,类是由一个无参的隐式构造函数,如果写了显式的构造函数,则隐式的构造函数失效。

就像下面这样:

class Student {
  late String sid;
  String? name;
  late int age;

  // 无参构造函数
  Student() {}
}
1
2
3
4
5
6
7
8

# 4 this的作用

在上面的代码中用到了 this,因为上面我们写构造函数的时候,形参和类的属性名相同(当然也可以不同),导致在构造函数中,使用 sid/name/age 无法知道访问的是类的属性还是形参,使用this,就表示访问的是属性 。

其实 this 表示的是调用当前方法的对象。如何理解?

举个栗子:

下面的代码,我们定义了学生类,创建了两个学生的对象。

/**
 * 定义类
 */
class Student {
  late String sid;
  String? name;
  late int age;

  // 构造函数
  Student(String sid, String name, int age) {
    this.sid = sid;
    this.name = name;
    this.age = age;
  }

  void study() {
    print("我是${this.name}, 我${this.age}岁了, 我在学习");
  }
}

void main() {
  Student stu1 = new Student("001", "Doubi", 12);
  stu1.study();

  Student stu2 = Student("002", "Shabi", 13);
  stu2.study();
}
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

执行结果:

我是Doubi, 我12岁了, 我在学习 我是Shabi, 我13岁了, 我在学习

当我们使用stu1调用study()方法的时候,this 就是指张三这个对象,那么this.name 的值就是张三;当我们使用stu2调用study()方法的时候,this就是指李四这个对象,那么 this.name 的值就是李四。

# 5 构造函数的简写

默认构造函数还可以使用如下简化的书写方式:

/**
 * 定义类
 */
class Student {
  late String sid;
  String? name;
  late int age;

  // 构造函数简写
  Student(this.sid, this.name, this.age);

  void study() {
    print("我是${this.name}, 我${this.age}岁了, 我在学习");
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 6 命名构造函数

上面的构造函数是默认构造函数,dart中不能像java中进行构造函数的重载(就是方法名一样,参数个数或类型不不同),如果要实现根据不同的参数创建对象,需要使用命名构造函数。

/**
 * 定义类
 */
class Student {
  late String sid;
  String? name;
  late int age;

  // 默认构造方法
  Student(this.sid, this.name, this.age);

  // 定义命名构造函数,类名后的名称是自定义的
  Student.initByNameAndAge(this.name, this.age);

  // 新的构造方法
  Student.fromMap(Map<String, Object> map) {
    this.name = map['name'] as String;
    this.age = map['age'] as int;
  }

  void study() {
    print("我是${this.name}, 我${this.age}岁了, 我在学习");
  }
}

void main() {
  // 通过默认构造函数创建对象
  Student stu1 = Student("001", "Doubi", 12);
  stu1.study();

  // 通过命名构造函数创建对象
  Student stu2 = Student.initByNameAndAge("ShaBi", 13);
  stu2.sid = "002";
  stu2.study();

  // 通过命名构造函数创建对象
  Student stu3 = Student.fromMap({"name": "ErBi", "age": 14});
  stu3.study();
}
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
31
32
33
34
35
36
37
38
39

定义和使用命名构造函数时,需要在类名后面加上构造函数的名称,名称是自定义的,但是名称必须与类名不同,否则会被解析为默认构造函数。

通过命名构造函数就可以通过不同的参数来创建对象了。

执行结果:

我是Doubi, 我12岁了, 我在学习 我是ShaBi, 我13岁了, 我在学习 我是ErBi, 我14岁了, 我在学习

# 7 初始化列表

初始化列表会在构造函数方法体执行之前执行,多个初始化列表表达式之间使用 , 分割。

初始化列表常用于设置final修饰的属性的值,也可以使用assert进行判断参数,进行形参的校验。

举个栗子:

class Person {
  String name;
  int age;

  Person(String name, int age)
      : name = name.toUpperCase(), // 使用表达式计算初始值
        age = age + 1 {
    // 初始化列表会在构造函数体之前执行
    print("name:${this.name}, age:${this.age}");
  }

  Person.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        age = json['age']; // 使用初始化列表初始化成员变量
}

void main() {
  Person person = Person("Doubi", 12);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在上面的代码中,会先执行构造函数 () 后面的初始化列表表达式,表达式可以是多个语句,使用 , 分隔,执行完初始化列表才会执行构造函数的函数体。在初始化name时,我们使用了一个表达式name.toUpperCase()来将name转换为大写字母。在初始化age时,我们使用了一个表达式age + 1来将age的值加1。

在Dart中,final修饰的属性只能在声明时或者在构造函数的初始化列表中进行赋值。一旦final属性被赋值,就不能再被修改。所以我们可以使用初始化列表对final修饰的属性进行赋值。

class Person {
  final String name;
  final int age = 12; 						 // 声明时赋值final属性

  Person(String name, int age)
      : name = name.toUpperCase(); // 使用初始化列表赋值final属性
}
1
2
3
4
5
6
7

除了初始化成员变量,初始化列表还可以用于对构造函数的参数进行校验。

举个栗子:

Main.dart

class Person {
  final String name;
  final int age;

  Person(String name, int age)
      : assert(age >= 0), // 对age参数进行校验
        name = name.toUpperCase(),
        age = age + 1;
}

void main() {
  Person person = Person("ShaBi", -10);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

在上面的示例中,我们使用了assert语句来对构造函数的age参数进行校验。如果age小于0,则会抛出一个异常。

assert叫做断言,assert(age >= 0) ,就是肯定 age >= 0 一定是对的,否则程序报错,终止执行。

需要注意的是,assert语句只在开发模式下生效,在生产环境中会被自动忽略。因此,它主要用于开发和调试阶段,不应该依赖于assert语句来处理正常的错误情况。

默认在vs code中运行上面的程序,assert断言是不会生效的,可以在运行的时候指定参数或使用命令行运行:

dart --enable-asserts Main.dart
1

运行会报错,提示:Failed assertion: line 6 pos 16: 'age >= 0': is not true.

# 8 重定向构造方法

在某些情况下, 我们希望在一个构造方法中去调用另外一个构造方法,这个时候可以使用重定向构造方法

class Person {
  String name;
  int age;

  Person(this.name, this.age);
  
  Person.fromName(String name) : this(name, 0);
  
  Person.defaultValue() : this.fromName("逗比");
}
1
2
3
4
5
6
7
8
9
10

在上面的代码中,我们在命名构造函数中Person.fromName 后面通过 this(name, 0) 调用默认构造函数。

在命名构造函数 Person.defaultValue() 后面通过 this.fromName("逗比") 调用了另外一个命名构造函数。

构造函数之间可以相互调用,但是注意相互调用,形成一个环。

注意:是在冒号后面使用this调用。

# 9 常量构造方法

默认情况下,我们在创建对象时,即使传入相同的参数,创建出来的也不是同一个对象。

举个栗子:

class Person {
  String name;
  int age;

  Person(this.name, this.age);
}

main(List<String> args) {
  var p1 = Person('逗比', 12);
  var p2 = Person('逗比', 12);
  print(identical(p1, p2)); // 输出: false
}
1
2
3
4
5
6
7
8
9
10
11
12

上面通过 identical(对象1, 对象2) 函数可以比较两个对象是否是同一个对象,它是基于对象的内存地址来判断两个对象是否相同,因此只有当两个引用指向内存中的同一个对象时,identical()函数才会返回 true

但是, 如果将构造方法前加const 进行修饰,那么可以保证相同的参数,创建出来的对象是相同的,这样的构造方法就称之为常量构造方法

举个栗子:

class Person {
  final String name;
  final int age;

  const Person(this.name, this.age);
}

main(List<String> args) {
  var p1 = const Person('逗比', 12);
  var p2 = const Person('逗比', 12);
  const p3 = Person('逗比', 12);
  print(identical(p1, p2)); // 输出: true
  print(identical(p2, p3)); // 输出: true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

必须注意:

  • 拥有常量构造方法的类中,所有的成员变量必须是final修饰的;
  • 为了可以通过常量构造方法,创建出相同的对象,不再使用 new 关键字,而是使用const关键字,注意const不能省略,否则创建的是不同的对象,除非是将结果赋值给const修饰的标识符时,const可以省略
  • 多个对象的属性值必须是一样的,创建出来的对象才可能是同一个对象。

在Flutter中经常会用到常量构造函数,const不仅仅节省创建组件的内存开销,还可以在重新构建组件的时候,不重新构建const组件,从而提高性能。

# 10 私有构造函数

通过在成员变量和方法前面添加下划线 _ 可以将变量和方法声明为私有的,同样的在构造函数前面添加下划线 _ 可以将构造函数声明为私有的,即只能在类内部访问,外部无法直接实例化。

举个栗子:

class Person {
  String name;
  int age;

  // 私有构造函数
  Person._(this.name, this.age);

  // 私有命名构造函数
  Person._initByNameAndAge(this.name, this.age);
}
1
2
3
4
5
6
7
8
9
10

注意:只有在文件外部调用才算私有,在当前文件中是可以调用的。

构造函数为什么要私有化呢,如果构造函数私有化不就无法创建对象了?

一般情况下我们不会将命名构造函数私有化,这个真没什么必要,但是有时候我们需要将默认构造函数私有化,来禁止外部通过默认构造函数创建对象,想创建对象必须通过指定的命名构造函数或工厂构造函数来创建对象。

# 11 工厂构造函数

工厂构造函数(Factory Constructors)是一种特殊类型的构造函数,用于创建对象。与普通构造函数不同,工厂构造函数可以返回一个已存在的对象或者通过其他方式创建对象。

举个栗子:

class Person {
  late String name;
  late int age;

  Person(this.name, this.age);

  // 命名构造函数
  Person.fromJson_1(Map<String, dynamic> json) {
    this.name = json['name'];
    this.age = json['age'];
  }

  // 工厂构造函数
  factory Person.fromJson_2(Map<String, dynamic> json) {
    return Person(json['name'], json['age']);
  }

  void displayInfo() {
    print('Name: $name, Age: $age');
  }
}

void main() {
  Map<String, dynamic> json = {'name': 'Alice', 'age': 25};

  Person person1 = Person.fromJson_1(json);
  person1.displayInfo();

  // 使用工厂构造函数创建对象
  Person person2 = Person.fromJson_2(json);
  person2.displayInfo();
}
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
31
32

通过上面的代码,可以看到,工厂构造函数和命名构造函数很相似,但是他们有很大的区别:

  • 工厂构造函数使用 factory 关键字声明,后跟构造函数名称。
  • 命名构造函数用于通过不同的方式或参数来实例化对象,但始终返回当前类的实例;工厂构造函数可以返回一个已存在的对象实例,也可以返回其他类的实例,甚至可以返回 null
  • 命名构造函数通常用于为类提供多种不同的实例化方式,以满足不同的需求;工厂构造函数常用于实现复杂的对象创建逻辑,例如对象池、缓存、单例模式等。
  • 命名构造函数通过使用类名和构造函数名称的组合调用,可以使用 new 来创建;工厂构造函数通过类名直接调用,类似于静态方法的调用方式。

下面举一个工厂构造函数的应用,使用工厂构造函数创建一个单例模式。

什么是单例模式?

单例模式是一种设计模式,它的目的是确保一个类只有一个实例对象,并提供一个获取该实例的方法。也就是说通过一个类只能创建一个实例对象。

看一下下面的代码:

Singleton.dart

class Singleton {
  static late final Singleton _instance = Singleton._();

  // 私有的构造函数
  Singleton._();

  factory Singleton() {
    return _instance;
  }

  void displayMessage() {
    print('This is a singleton instance.');
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

首先将构造函数私有化 Singleton._();,这样外部的代码就无法调用类的构造函数创建对象了。

然后提供一个工厂构造函数给外部调用,外部只能通过该工厂构造函数获取当前类的对象 _instance

当前类的对象 _instance 在类第一次执行的时候的时候就创建了,而且是一个静态的对象,只有一份。

所以每次通过工厂构造函数获取到的类实例,都是同一个。

Main.dart

void main() {
  Singleton instance1 = Singleton();
  Singleton instance2 = Singleton();

  print(identical(instance1, instance2)); // 输出:true
}
1
2
3
4
5
6

# 12 静态变量

上面我们定义的属性和方法,是实例变量和实例方法,也叫成员变量和成员方法。

实例变量对于每个实例而言,是独立的数据,每个对象之间相互不会影响。创建一个对象,就会开辟独立的内存空间保存对象的实例变量数据。但是无论创建多少对象,实例方法只有一份,所有对象共享,通过this,来确定是哪个对象调用了实例方法。

在类中还可以定义各个对象共享的数据,也就是静态变量。

打个比方,我们定义了一个Student类,然后通过Student类来创建对象,我们想知道一共创建了多少个Student对象,应该如何操作呢?

我们可以通过定义静态变量来实现:

静态变量前面通过 static 关键字来修饰。

/**
 * 定义类
 */
class Student {
  static int stuCount = 0;		// 定义一个静态变量,用于记录创建的对象个数
  late String sid;
  String? name;
  late int age;

  // 构造函数
  Student(String sid, String name, int age) {
    stuCount++;								// 创建对象会调用构造函数,调用一次就+1
    //Student.stuCount++;		
    
    this.sid = sid; 
    this.name = name;
    this.age = age;
  }
}

void main() {
  Student stu1 = Student("001", "Doubi", 12);
  Student stu2 = Student("002", "Shabi", 13);
  Student stu3 = Student("002", "ErBi", 14);

  print(Student.stuCount);		// 输出: 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

在上面的代码中,我们定义了一个类,然后在类中定义了一个 stuCount 静态变量。当创建对象的时候,会调用构造函数,我们在构造函数中将stuCount++,这样就可以记录调用构造函数的次数。

静态变量是属于类的,而不是属于类的实例。静态变量可以通过 类名.静态变量 来赋值和访问。

# 13 静态方法

除了静态变量,还有静态方法。静态变量也是属于类的,而不是属于类的实例。

静态方法通过 static 关键字来修饰。

/**
 * 定义类
 */
class Student {
  static int stuCount = 0;
  late String sid;
  String? name;
  late int age;

  // 构造函数
  Student(String sid, String name, int age) {
    Student.stuCount++;
    this.sid = sid; // this.sid访问的是属性,sid访问的是形参
    this.name = name;
    this.age = age;
  }

  // 静态方法
  static void getStuCount() {
    print("一共创建了${Student.stuCount}个学生");
  }
}

void main() {
  Student("001", "Doubi", 12);
  Student("002", "Shabi", 13);
  Student("002", "ErBi", 14);

  Student.getStuCount();		// 输出: 一共创建了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

静态方法不能访问非静态变量,因为静态方法是通过 类.静态方法() 调用的,不是通过对象实例来调用的,而非静态变量是属于类的实例的。所以如果 类.静态方法() 中调用的非静态变量没法确定是哪个实例的。但是在非静态方法中是可以访问静态变量的,也是通过 类名.静态变量 来赋值和访问。

静态方法一般用来定义一些工具类。

# 14 内置方法

介绍一下类中toString()方法,toString() 方法是 Dart 中的一个内置方法,用于返回表示对象的字符串表示形式。当我们打印对象的时候,就会将对象转换为字符串进行输出,则会调用toString() 方法。默认情况下,toString() 方法会返回对象的类型。

因为所有的类都是继承自Object类的,所以我们在类中不写 toString() 方法,默认是调用了Object类的toString() 方法。

举个栗子:

class Person {
  String name;
  int age;

  Person(this.name, this.age);
}

void main() {
  var person = Person('John', 25);
  print(person);  // 输出:Instance of 'Person'
}
1
2
3
4
5
6
7
8
9
10
11

打印person对象,会输出:Instance of 'Person'。打印的信息没什么用,因为它没有提供对象的具体信息。

因此,通常建议在自定义类中重写 toString()方法,以便更好地控制对象的字符串表示形式。

举个栗子:

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  
  String toString() {								// 重写toString()方法
    return 'Person{name: $name, age: $age}';
  }
}

void main() {
  var person = Person('DouBi', 12);
  print(person);  // 输出:Person{name: John, age: 25}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

打印对象,会调用 toString() 方法,打印 toString() 方法返回的内容。

方法上的 @override 是一个注解,表示这个方法是重写父类的方法,可以省略,但建议加上。

# 15 级联操作

级联操作允许你在一个对象上连续调用多个方法或访问多个属性,而无需重复引用该对象。这种技术可以使代码更简洁、易读,并且可以在单个表达式中对同一个对象进行多个操作。

级联操作使用连续的 .. 运算符来实现。

举个栗子:

class Person {
  String name;
  int age;

  void setName(String name) {
    this.name = name;
  }

  void setAge(int age) {
    this.age = age;
  }

  void printInfo() {
    print('Name: $name, Age: $age');
  }
}

void main() {
  var person = Person()
    ..setName('John')
    ..setAge(30)
    ..printInfo();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

在上面的示例中,我们定义了一个Person类,该类具有nameage属性以及setName()setAge()printInfo()方法。在main()函数中,我们创建了一个Person对象,并使用级联操作对该对象进行了连续的操作。通过使用..运算符,我们可以连续调用setName()setAge()printInfo()方法,而无需每次都重复引用person对象。

级联操作使得代码更加简洁,特别是在需要对同一个对象执行多个操作时非常有用。它在Dart中广泛用于构建复杂的表达式和操作链。

# 7.2 封装

面向对象编程,是一种编程思想,简单的理解就是:先有模板,也就是类,根据模板去创建实例,也就是对象,然后使用对象来完成功能开发。

我们经常说面向过程编程和面向对象编程,面向过程编程关注的是实现功能的步骤,面向对象编程更关注的是谁来实现功能。

面向对象编程有3大特性:

  • 封装
  • 继承
  • 多态

下面依次开始讲起。

在前面我们创建类,在类中定义了属性和方法,通过属性和方法来对现实世界的事物进行抽象的描述。

一个事物有很多的属性和方法,但是并不是所有属性和方法都需要开放出来。例如我们定义了一个手机类,我们可以使用手机打电话、拍照等,但是我们并不关心手机电压,驱动信息,也不关心内存的分配,CPU的调度等,虽然这些都属于手机的属性和行为。

我们可以将用户不关心的属性和方法封装并隐藏起来,只给类内部的方法调用,例如上网会用到4G模块,但是不是由用户来使用4G模块,而是由手机上网的功能开调用4G模块,只开放用户直接使用的信息和功能。

那么回过头来看,什么是封装?

它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息。如果要访问这些信息,需要通过该类所提供的方法来实现对内部信息的操作和访问。

# 1 私有属性和方法

那么怎么将不想暴露的变量和方法隐藏起来呢,就需要用到私有成员变量和私有成员方法。

定义私有成员的方式:

  • 定义私有成员变量:变量名以 _开头,1个下划线开头
  • 定义私有成员方法:方法名以 _开头,1个下划线开头

举个栗子:

class Phone {
  late String producer;
  late int _voltage;

  Phone() {
    this.producer = "华为"; 	 // 手机品牌
    this._voltage = 12; 			// 电压
  }

  void call() {
    print("打电话");
    print("手机品牌:${this.producer}");
    print("手机电压:${this._voltage}");
  }

  // 定义一个私有方法
  void _get_run_voltage() {
    print("当前电压:${this._voltage}");
  }
}

void main() {
  Phone phone = Phone();
  phone.call();

  phone.producer = "小米";
  phone._voltage = 24;
  phone.call();
  phone._get_run_voltage();
}
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

上面定义了私有属性 _voltage 和私有方法 _get_run_voltage,理论上私有属性和私有方法只能在类内部的方法中调用,不能通过对象来调用。但是上面为什么可以在 main 函数中通过 phone._voltage、phone._get_run_voltage() 的方式,使用对象来调用私有属性和方法呢?

这是因为main函数和Phone类是写在一个文件中的,如果要实现私有属性和方法,需要将定义和调用写在不同的文件中。

现在重新创建一个 Phone.dart 文件,将 Phone 类的定义放在 Phone.dart 文件中:

Phone.dart

class Phone {
  late String producer;
  late int _voltage;

  Phone() {
    this.producer = "华为"; // 手机品牌
    this._voltage = 12; // 电压
  }

  void call() {
    print("打电话");
    print("手机品牌:${this.producer}");
    print("手机电压:${this._voltage}");
  }

  // 定义一个私有方法
  void _get_run_voltage() {
    print("当前电压:${this._voltage}");
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

我们可以将项目中的文件通过文件夹来划分层级,使项目结构更为清晰,例如我们将 Phone.dart 放在domain文件夹中。

我们将 main函数 写在了 Main.dart 文件中,那么如何在 Main.dart 中使用 Phone 类呢?

首先需要在 Main.dart 文件中引入 Phone 类:

import 'domain/Phone.dart';

void main() {
  Phone phone = Phone();
  phone.call();

  phone.producer = "小米";
  // phone._voltage = 24;  // 报错,无法访问私有变量
  phone.call();
  //phone._get_run_voltage();  // 报错,无法访问私有方法
}
1
2
3
4
5
6
7
8
9
10
11

这样,Phone 中的私有变量和属性就无法在类外被访问了。

如果不想暴露的属性和方法可以定义为私有成员。私有属性和私有方法只能在类内部的方法中调用,不能通过对象来调用。

执行结果:

打电话 手机品牌:华为 手机电压:12 打电话 手机品牌:小米 手机电压:12

# 2 getter和setter

我们前面说到,封装是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息。如果要访问这些信息,需要通过该类所提供的方法来实现对内部信息的操作和访问。

在面向对象编程思想中,我们一般会将所有的属性都设置为私有的,然后为每个属性提供两个对应的方法,分别用来获取和设置对应的属性,这两个方法称为getter和setter方法,在java中就是这么做的。

举个栗子:

Phone.dart

class Phone {
  late String _producer;
  late int _voltage;

  Phone(this._producer, this._voltage);

  get producer {							// 定义producer的getter方法,用于获取_producer私有属性
    return this._producer;
  }

  set producer(producer) {		// 定义producer的setter方法,用于给_producer私有属性赋值
    this._producer = producer;
  }

  get voltage {
    return this._voltage;
  }

  set voltage(voltage) {
    if (voltage <= 36) {
      this._voltage = voltage;
    } else {
      throw Exception("参数错误"); // 限制传入的参数值,进行报错处理,异常处理后面再学习
    }
  }
}
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

通过 get和set 关键字定义getter和setter,用于访问私有属性和给私有属性赋值。

当调用voltage的setter的时候,如果voltage大于36,则会主动抛出错误,程序停止运行,异常处理后面的章节再学习。

Main.dart

import 'domain/Phone.dart';

void main() {
  Phone phone = Phone("华为", 12);
  print(phone.producer); // 通过getter方法获取变量,并不是直接访问变量
  print(phone.voltage);

  phone.producer = "小米"; // 通过setter方法设置变量,并不是直接给变量赋值
  phone.voltage = 24;
  print(phone.producer);
  print(phone.voltage);
}
1
2
3
4
5
6
7
8
9
10
11
12

需要注意上面 phone.producerphone.voltage 是通过属性的方式调用,但是实际上是调用了getter和setter,并不是直接访问变量和给变量赋值。

这样做的话,严格控制了属性获取和设置的入口,如果通过 对象.属性 来修改,代码很多的时候完全会不知道在哪里修改了属性导致出现了问题。另外我们可以在setter方法中对属性的设置进行限制。

执行结果:

华为 12 小米 24

# 7.3 继承

在现实世界,有麻雀和鸽子,它们都属于鸟类,麻雀、鸽子和鸟类的关系是父类和子类的关系。

麻雀和鸽子都会飞,我们可以在麻雀类中定义一个飞的方法,在鸽子类中定义一个飞的方法,但是这两个飞的方法是一样的,都是用翅膀飞。我们可以在鸟类中定义一个飞的方法,让麻雀和鸽子类都继承这个鸟类,那么它们就拥有了飞的方法,就不用再定义了。

# 1 继承的语法

在Dart中,使用extends关键字实现继承。

class 子类名 extends 父类名 {
  	类内容
}
1
2
3

举个栗子:

麻雀继承鸟类,鸽子继承鸟类:

class Bird {
  // 定义一个鸟类
  late int age; // 鸟都有年龄

  void fly() {
    // 定义了一个飞的方法
    print("我${this.age}岁了,我会飞");
  }

  void tweet() {
    // 定义了一个叫的方法
    print("我会叫");
  }
}

class Sparrow extends Bird {
  // 定义一个麻雀类,继承自鸟类
}

class Pigeon extends Bird {
  // 定义一个鸽子类,继承自鸟类
}

void main() {
  Sparrow sparrow = Sparrow(); // 创建一个麻雀对象
  sparrow.age = 1;
  sparrow.fly();
  sparrow.tweet();

  Pigeon pigeon = Pigeon(); // 创建一个鸽子对象
  pigeon.age = 2;
  pigeon.fly();
  pigeon.tweet();
}
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
31
32
33
34

在上面的代码中,我们先定义了一个鸟类,然后在鸟类中定义了 age属性 ,并定义了两个方法 fly方法tweet方法

然后定义类麻雀类,继承鸟类,那么就拥有了鸟类的属性和方法。鸽子类也同样。

执行结果:

我1岁了,我会飞 我会叫 我2岁了,我会飞 我会叫

注意,继承不能继承父类私有的成员变量和方法。

如果父类的成员变量和属性不想被子类继承,可以设置为私有成员。

一个类如果没有写明继承哪个类,那么默认都是集成自Object类,所以最终的结果就是Object类是所有类的父类!

# 2 复写父类方法

所以继承父类就拥有了父类非私有的成员变量和方法,如果父类不是我想要的方法,我还可以进行覆盖。

例如,鸟类提供了叫的方法,但是我麻雀有自己的叫法,我要啾啾叫,那么我们可以重写父类的方法,实现自己的个性化。

class Bird {
  // 定义一个鸟类
  late int age; // 鸟都有年龄

  void fly() {
    // 定义了一个飞的方法
    print("我${this.age}岁了,我会飞");
  }

  void tweet() {
    // 定义了一个叫的方法
    print("我会叫");
  }
}

class Sparrow extends Bird {
  // 定义一个麻雀类,继承自鸟类
  void tweet() {
    print("我会啾啾叫");
  }
}

class Pigeon extends Bird {
  // 定义一个鸽子类,继承自鸟类
}

void main() {
  Sparrow sparrow = Sparrow(); // 创建一个麻雀对象
  sparrow.age = 1;
  sparrow.fly();
  sparrow.tweet();

  Pigeon pigeon = Pigeon(); // 创建一个鸽子对象
  pigeon.age = 2;
  pigeon.fly();
  pigeon.tweet();
}
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
31
32
33
34
35
36
37

执行结果:

我1岁了,我会飞 我会啾啾叫 我2岁了,我会飞 我会叫

那么如果我在父类的fly()方法中调用tweet()方法,然后子类方法调用fly()方法,那么会如何执行呢?

class Bird {
  void fly() {
    // 定义了一个飞的方法
    print("我会飞");

    tweet();
  }

  void tweet() {
    // 定义了一个叫的方法
    print("我会叫");
  }
}

class Sparrow extends Bird {
  void tweet() {
    print("我会啾啾叫");
  }
}

void main() {
  Sparrow sparrow = Sparrow(); // 创建一个麻雀对象
  sparrow.fly();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

sparrow.fly() 调用的是继承自父类的方法,在 fly() 方法中调用了 tweet() 方法,那么调用的是父类的tweet() 方法还是子类的tweet() 方法呢?

是调用子类的方法。

执行结果:

我会飞 我会啾啾叫

# 3 调用父类方法

我们在重写父类方法的时候,应该尽量做到在父类方法的基础上进行扩展。

所以很有可能需要调用父类被重写的方法,然后在父类原来的方法上进行扩展。

那么如果我们想要在子类中调用父类被重写的属性和方法呢?

使用 super 关键字来调用父类的属性或方法。

举个栗子:

class Bird {
  // 定义一个鸟类
  int age = 1; // 鸟都有年龄

  void fly() {
    // 定义了一个飞的方法
    print("我${this.age}岁了,我会飞");
  }

  void tweet() {
    // 定义了一个叫的方法
    print("我会叫");
  }
}

class Pigeon extends Bird {
  late int age;
  // 定义一个鸽子类,继承自鸟类
  void tweet() {
    // 重写了父类叫的方法
    print("我会咕咕叫");
  }

  void test() {
    print("age:${super.age}"); // 调用父类的属性
    super.tweet(); // 调用父类的方法

    print("age:${this.age}");
    this.tweet();
  }
}

void main() {
  Pigeon pigeon = Pigeon(); // 创建一个鸽子对象
  pigeon.age = 10;
  pigeon.test();
}
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
31
32
33
34
35
36
37

上面子类中定义了一个和父类中相同名称的属性age,如果要在子类中调用父类的属性 ,可以通过 super.父类属性 来调用,同样,如果要在子类中调用父类的方法,可以通过 super.父类方法() 来调用。

执行结果:

age:1 我会叫 age:10 我会咕咕叫

# 4 构造函数的继承

子类继承父类,是没办法直接继承构造函数的。

但是子类的构造函数是会隐式的调用的无参构造函数。

举个栗子:

class Bird {
  late int age;

  Bird() {
    print("执行了 Bird 构造函数");
  }
}

class Pigeon extends Bird {
  Pigeon() {
    print("执行了 Pigeon 构造函数");
  }

  Pigeon.fromName(String name) {
    print("执行了 fromName 明明构造函数");
  }
}

void main() {
  Pigeon(); // 创建对象,没有传入参数
  Pigeon.fromName("DouBi");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

执行结果:

执行了 Bird 构造函数 执行了 Pigeon 构造函数 执行了 Bird 构造函数 执行了 fromName 明明构造函数

通过结果可以看到,子类的构造函数会先调用父类的无参构造函数。

如果父类定义了有参的构造函数,那么就没有无参的构造函数了,那么就会报错。

举个栗子:

class Bird {
  late int age;

  Bird(this.age); // 定义了有参的构造函数
}

class Pigeon extends Bird {}
1
2
3
4
5
6
7

上面的代码是会报错的,因为Pigeon类没有写构造函数,使用的是无参构造函数,执行的时候会调用父类Bird的无参构造函数,但是Bird类没有无参构造函数,所以会报错。

所以我们在写Pigeon类的构造函数的时候,要显式的调用父类的构造函数。

调用父类的构造函数需要在构造函数的初始化列表中使用super()来调用:

class Bird {
  late int age;

  Bird(this.age); // 定义了有参的构造函数
}

class Pigeon extends Bird {
  Pigeon(int age) : super(age);		// 使用super调用父类的构造函数
}
1
2
3
4
5
6
7
8
9

我们还可以调用父类的命名构造函数:

class Bird {
  late int age;

  Bird(this.age);

  Bird.fromMap(Map<String, Object> map) : this.age = map['age'] as int;
}

class Pigeon extends Bird {
  Pigeon(int age) : super(age);		// 使用super调用父类的构造函数

  // 调用父类的命名构造函数
  Pigeon.fromMap(Map<String, Object> map) : super.fromMap(map) {
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 5 类型判断

我们可以判断某个对象的类型,以及是否是某个对象的实例。

首先有如下代码:

class Bird {
  // 鸟类
}

class Sparrow extends Bird {
  // 麻雀类,继承自鸟类
}

class Pigeon extends Bird {
  // 鸽子类,继承自鸟类
}
1
2
3
4
5
6
7
8
9
10
11

# runtimeType属性

dart中,可以使用 runtimeType 判断某个对象是否是某个类型,注意:子类的对象不是父类的对象的类型。

举个栗子,基于上面的代码:

void main() {
  Pigeon pigeon = Pigeon();
  Sparrow sparrow = Sparrow();

  print(pigeon.runtimeType); // 输出: Pigeon
  print(sparrow.runtimeType); // 输出: Sparrow

  print(pigeon.runtimeType == Pigeon); // true,鸽子对象的类型是鸽子
  print(pigeon.runtimeType == Bird); // false,鸽子对象的类型不是鸟
  print(sparrow.runtimeType == Sparrow); // true,麻雀对象的类型是麻雀
}
1
2
3
4
5
6
7
8
9
10
11

# is 关键字

is 关键字可以判断某个对象是否是某个类型的实例,子类对象也是父类对象的实例。

举个例子,基于上面的代码:

void main() {
  Pigeon pigeon = Pigeon();

  print(pigeon is Pigeon); // true,鸽子对象是鸽子类型的实例
  print(pigeon is Bird); // true,鸽子对象是鸟类的实例
  print(pigeon is Sparrow); // false,鸽子对象不是麻雀类的实例
}
1
2
3
4
5
6
7

# 6 Mixins

我现在是一只鸡,我也想继承鸟类,但是我是家禽,我还想继承家禽类。

那么就需要用到多继承。

在C++、Python中是支持多继承的,但是在Dart中是不支持多继承的。

在Dart中,新增了一种复用代码的机制Mixins,中文意思是混入,允许你在类中重用其他类的代码,而无需继承这些类。它提供了一种灵活的方式来组合和共享功能,以实现代码重用和组合。

在Dart中,有两种方式可以定义Mixin:使用关键字mixin定义Mixin和使用 class 定义一个类。

举个栗子:

下面使用 class 定义了一个家禽类

class Bird {
  // 定义一个鸟类
  late int age; // 鸟都有年龄

  void fly() {
    // 定义了一个飞的方法
    print("我${this.age}岁了,我会飞");
  }
}

class Poultry {
  // 定义一个家禽类
  late int number; // 家禽需要编号

  void eat() {
    // 定义了一个吃的方法
    print("我吃饭啦");
  }
}

class Chicken extends Bird with Poultry {
  // 定义一个鸡类,继承自鸟类和家禽类

  void fly() {
    print("我不会飞");
  }
}

void main() {
  Chicken chicken = Chicken(); // 创建一个鸡对象
  chicken.age = 1;
  chicken.number = 9527;
  chicken.fly();
  chicken.eat();
}
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
31
32
33
34
35

然后我们在 Chicken 类上继承了Bird 类,然后使用 with 关键字将 Poultry 家禽类混入到Chicken 类中。

这样就 Chicken 类就可以访问和使用Poultry 家禽类中的属性和方法。

上面定义 Poultry 家禽类,还可以使用关键字mixin来定义:

mixin Poultry {
  // 定义一个家禽类
  late int number; // 家禽需要编号

  void eat() {
    // 定义了一个吃的方法
    print("我吃饭啦");
  }
}
1
2
3
4
5
6
7
8
9

那么使用 class 关键字和使用 mixin 关键字定义混入有什么区别呢?

使用mixin关键字定义Mixin:Mixin类不能直接被实例化,而是通过混入到其他类中来复用其方法和属性。这意味着Mixin类不能有构造函数,并且不能作为独立的类来使用。使用mixin关键字定义Mixin可以更明确地表达其用途(就是用来被混入的),而使用class关键字定义Mixin则与普通类的定义方式更一致。另外,mixin类只能继承自Object,不能继承其他类。

在使用的时候,一个类是可以混入多个类的。如果混入的多个类中存在相同的属性或方法,那么哪一个类中的方法生效呢?

当一个类混入多个类并且这些混入类中存在相同属性或方法时,方法解析的顺序遵循"最后声明的优先"原则。也就是说,最后一个混入的类中的方法将覆盖之前混入的类中的属性和方法。

举个栗子:

mixin A {
  int value = 1;
  void foo() {
    print('A foo');
  }
}

mixin B {
  int value = 2;
  void foo() {
    print('B foo');
  }
}

class C with A, B {
  void test() {
    foo(); // 解析为 B 中的 foo 方法
  }

  void testValue() {
    print(value);
  }
}

void main() {
  var c = C();
  c.test(); // 输出: B foo
  c.testValue(); // 输出: 2
}
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

在上面的示例中,我们定义了两个Mixin类:AB,它们都包含一个名为foo的方法。然后,我们创建了一个名为C的类,使用with关键字将AB混入到C中。

C类中的test方法中调用了foo方法。由于B是最后一个混入的类,所以方法解析的顺序从右到左,即先在B中查找同名方法。因此,最终调用的是B中的foo方法,输出结果为"B foo"。

同样的,打印value属性,打印的是后混入的B类中的属性。

# 7.4 多态

# 1 什么是多态

多态就是多种状态,同一个类型的父类型对象,因为指向的是不同的子对象,而表现出的不同的状态。

所以多态是建立在继承的基础之上的。

举个栗子:

class Bird {
  void tweet() {}
}

class Sparrow extends Bird {
  void tweet() {
    print("我会啾啾叫");
  }
}

class Pigeon extends Bird {
  void tweet() {
    print("我会咕咕叫");
  }
}

void main() {
  Bird bird1 = Sparrow(); // 创建一个麻雀对象
  Bird bird2 = Pigeon(); // 创建一个鸽子对象

  bird1.tweet();
  bird2.tweet();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

上面麻雀类和鸽子类都继承自鸟类,然后创建了一个麻雀对象和鸽子对象,都赋给了鸟类,通过两个对象分别调用tweet()方法。

执行结果:

我会啾啾叫 我会咕咕叫

虽然两个都是鸟类的变量,执行的都是tweet()方法,但是因为是不同的子类对象,却得到不同的结果。

以父类做定义声明,以子类做实际的工作,用于获取同一个行为的不同状态,这就是多态。

那也没看出多态有什么用啊。

多态的作用:

  • 提高代码的维护性
  • 提高代码的扩展性
  • 把不同的子类对象当做父类来看待,可以屏蔽不同子类对象之间的差异,写出通用的代码,以适应需求的不断变化。

# 2 多态的使用

说了那么多,有点虚,举个栗子:

实现一个功能:学生做交通工具去某个地方,交通工具可能是汽车、飞机。

先定一个汽车类:

传入一个目的地,就可以开车去了。

class Car {
  void run(String destination) {
    print("开车去->${destination}");
  }
}
1
2
3
4
5

再定义一个飞机类:

class Plane {
  void fly(String destination) {
    print("飞去->${destination}");
  }
}
1
2
3
4
5

然后定义一个学生类:

class Student {
  void go_to(var vehicle, String destination) {	// 传入交通工具
    if (vehicle.runtimeType == Car) {			// 判断交通工具的类型,然后调用交通工具的方法
      vehicle.run(destination);
    } else if (vehicle.runtimeType == Plane) {
      vehicle.fly(destination);
    }
  }
}
1
2
3
4
5
6
7
8
9

学生类有一个go_to()方法,接收交通工具和目的地,然后在方法中判断交通工具的类型,然后调用交通工具的方法。

调用代码:

void main(List<String> args) {
  Student stu = Student();
  Car car = Car();
  Plane plane = Plane();
  stu.go_to(car, "北京");
  stu.go_to(plane, "新疆");
}
1
2
3
4
5
6
7

执行结果:

开车去->北京 飞去->新疆

上面的代码可以实现功能,但是不易于扩展,如果我们现在增加一个交通工具火车,则还需要修改Student类的go_to()方法,针对新的交通工具来处理,因为学生类和汽车、飞机类直接存在依赖关系,耦合性高。

这样是违反了设计原则中的开闭原则,对扩展是开放的,对修改是封闭的,也就是允许在不改变它代码的前提下变更它的行为。

所以上面的代码扩展性就比较差了,那么怎么来优化代码,降低代码的耦合性呢?

这就需要用到多态了。

首先定义一个父类Vehicle(交通工具类),并定义一个transport()方法,都是交通工具,都是运输功能嘛。

然后让Car类和Plane类都继承这个父类,因为不同的子类运输方式不一样,所以子类需要重写父类的方法,实现自己的功能。

class Vehicle {
  void transport(String destination) {}
}

class Car extends Vehicle {
  void transport(String destination) {
    print("开车去->${destination}");
  }
}

class Plane extends Vehicle {
  void transport(String destination) {
    print("飞去->${destination}");
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

然后修改学生类:

class Student {
  void go_to(Vehicle vehicle, String destination) {
    vehicle.transport(destination);
  }
}
1
2
3
4
5

学生类只需要调用交通工具的运输功能就可以了。

调用的代码不变:

void main(List<String> args) {
  Student stu = Student();
  Car car = Car();
  Plane plane = Plane();
  stu.go_to(car, "北京");
  stu.go_to(plane, "新疆");
}
1
2
3
4
5
6
7

执行结果:

开车去->北京 飞去->新疆

上面的代码使用了多态,学生类与各个交通工具子类已经不直接产生关系,遵从了设置原则中的依赖倒置原则(程序依赖于抽象接口,不要依赖于具体实现)。

此时如果新增一个火车的交通工具,不用再修改学生类的代码,代码耦合性大大降低。

# 3 抽象类

什么是抽象类?

含有抽象方法的类成为抽象类。

那什么是抽象方法?

抽象方法就是没有方法体,方法体为空的方法。

上面的 Vehicle 类中的 transport 方法,没有方法体,我们可以将它定义为一个抽象方法,将 Vehicle 定义为抽象类 。

class 前面加上 abstract 关键字,就是定义了抽象类。

abstract class Vehicle {
  void transport(destination);
}
1
2
3

抽象类中也可以包含非抽象方法,子类继承抽象类需要实现(重写)抽象类中所有的抽象方法。

抽象类有什么作用呢?

抽象类是不能被实例化的,也就是不能用来创建对象。一般抽象类都是作为父类使用的,父类用来确定有哪些方法,相当于用来确定设计的标准,用于对子类的约束。

子类用来实现父类的标准,负责具体的实现,配合多态使用,获得不同的工作状态。

# 4 接口

如果一个抽象类中的方法都是抽象方法,我们可以将这个抽象类定义成接口。

在Java中,定义接口使用 interface 关键字,和定义抽象类是不同的。但是在Dart中,并没有专门的关键字来定义接口。在Dart 中的接口就是通过类来实现的。所以一个抽象类是接口还是抽象来要看怎么使用,通过 extends 来继承就是抽象类,通过 implements 来实现就是接口

举个栗子:

// 定义一个抽象类
abstract class Bird {
  void tweet();
}

// 定义一个接口
abstract class Animal {
  void breathe();
}

// 定义一个接口
abstract class Helpful {
  void help();
}

class Sparrow extends Bird implements Animal, Helpful {
  
  void breathe() {
    print("我要呼吸");
  }

  
  void tweet() {
    print("我会啾啾叫");
  }

  
  void help() {
    print("有益的");
  }
}

void main() {
  Animal animal = Sparrow();
  animal.breathe();
}
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
31
32
33
34
35
36

上面我们定义了一个抽象类 Bird ,和两个接口 AnimalHelpful

然后定义了一个 Sparrow 类作为子类继承 Bird 类,同时实现了 AnimalHelpful两个接口。此时才看出抽象类和接口的不同,也就是说接口的定义是隐式的。

一个类只能有一个父类,但是可以有多个接口。一个类需要实现抽象类和接口中所有的抽象方法,否则编译会报错。

接口中只能定义常量和抽象方法,所以抽象类的作用主要是提供约束和规范。

结合前面多态的使用去理解,前面多态的使用中,使用的是父类来完成的,我们也可以使用接口来完成。

代码:

abstract class Vehicle {				// 定义交通工具的接口,所有的交通工具都需要实现该接口
  void transport(String destination);
}

class Car implements Vehicle {    // 需要实现交通工具接口
  void transport(String destination) {
    print("开车去->${destination}");
  }
}

class Plane implements Vehicle {	// 需要实现交通工具接口
  void transport(String destination) {
    print("飞去->${destination}");
  }
}

class Student {
  void go_to(Vehicle vehicle, String destination) {		// 传入的参数是交通工具,交通工具都必须有transport()方法
    vehicle.transport(destination);
  }
}

void main(List<String> args) {
  Student stu = Student();
  Car car = Car();
  Plane plane = Plane();
  stu.go_to(car, "北京");
  stu.go_to(plane, "新疆");
}
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

# 7.5 类与类的关系

类与类有以下几种关系:

泛化

子类与父类的关系,B类泛化A类,B类是A类的一种。

场景:B类继承A类。

关联

部分与整体的关系,A与B关联,B是A的一部分。

场景:在A类中包含B类型的属性。

依赖

合作关系,A类依赖B类,A的某些功能需要靠B类实现。

场景:B类型作为A类中方法的参数,并不是A的成员。

# 7.6 数据与JSON的转换

# 1 JSON 简介

什么是JSON?

JSON就是特定格式的字符串。我们可以将数据按照这个格式进行封装,然后可以在不同的语言和系统之间进行传送和交互。

JSON的2种格式:

格式一:

是一个对象格式的结构。{} 括起来,其中是属性。

{
    key1:value1,
    key2:value2,
    ...
}
1
2
3
4
5

举个栗子,以下是一个学生信息的JSON格式,和Python中的字典格式是一样的:

{
	"sid": "001",
	"name": "zhangsan",
	"age": 18
}
1
2
3
4
5

格式二:

外部是一个列表格式的结构,内部是一个个的对象格式的数据。

[
    {
        key1:value1,
        key2:value2 
    },
    {
         key3:value3,
         key4:value4   
    }
]
1
2
3
4
5
6
7
8
9
10

举个栗子,以下是一个学生信息的列表

[{
		"sid": "S001",
		"name": "zhangsan",
		"age": 18
	},
	{
		"sid": "S002",
		"name": "lisi",
		"age": 19
	},
	{
		"sid": "S003",
		"name": "wangwu",
		"age": 17
	}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

JSON格式是可以相互嵌套的,属性中可以再嵌套JSON。

# 2 字典转JSON

  • 使用 jsonEncode() 函数将字典转换为 JSON 字符串。

举个栗子:

需要引入 dart:convert 包。

import 'dart:convert';

void main() {
  Map<String, dynamic> data = {
    'name': '逗比',
    'age': 25,
  };

  String jsonString = jsonEncode(data);
  print(jsonString);
}
1
2
3
4
5
6
7
8
9
10
11

执行结果:

{"name":"逗比","age":25}

# 3 JSON转字典

  • 使用 jsonDecode() 函数将 JSON 字符串解析为 Dart 中的字典(Map)。
  • 返回的对象类型为 Map<String, dynamic>

举个栗子:

import 'dart:convert';

void main() {
  String jsonString = '{"name": "逗比", "age": 25}';
  Map<String, dynamic> data = jsonDecode(jsonString);

  print(data['name']);
  print(data['age']);
}
1
2
3
4
5
6
7
8
9

执行结果:

逗比 25

# 4 对象转JSON

举个栗子:

import 'dart:convert';

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'age': age,
    };
  }
}

void main() {
  Person person = Person('逗比', 25);
  String jsonString = jsonEncode(person.toJson());

  print(jsonString);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

我们需要先获取对象的字典格式的数据,所以在对象中定义了一个toJson()方法,返回字典数据,然后再通过jsonEncode()函数,将字典转换为JSON字符串:

# 5 JSON转对象

举个栗子:

import 'dart:convert';

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  factory Person.fromJson(Map<String, dynamic> json) {
    return Person(json['name'], json['age']);
  }
}

void main() {
  String jsonString = '{"name": "逗比", "age": 25}';

  // 先将Json 转换为字典
  Map<String, dynamic> data = jsonDecode(jsonString);
  // 使用工厂构造函数将字典转换为对象
  Person person = Person.fromJson(data);

  print(person.name);
  print(person.age);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

一般将JSON转换为字典就可以获取到数据了,如果有特殊的需求,再将字典转换为对象。

首先将JSON字符串转换为字典,然后通过工厂构造函数通过字典构建对象。

执行结果:

逗比 25

# 6 列表转JSON

简单数据格式的列表转换为JSON

举个栗子:

import 'dart:convert';

void main() {
  List<String> names = ['逗比', '二比', '牛比'];
  String jsonString = jsonEncode(names);
  print(jsonString);
}
1
2
3
4
5
6
7

直接调用 jsonEncode() 函数进行转换即可。

执行结果:

["逗比","二比","牛比"]

如果是对象列表呢?

对象列表转换为JSON

也是调用 jsonEncode() 函数,直接传入列表对象,jsonEncode() 函数会自动调用对象的 toJson() 方法,如果类中没有定义 toJson() 会报错。

举个栗子:

import 'dart:convert';

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'age': age,
    };
  }
}

void main() {
  List<Person> personList = [];
  personList.add(Person("逗比", 12));
  personList.add(Person("二比", 13));
  personList.add(Person("牛比", 14));

  String jsonString = jsonEncode(personList);
  print(jsonString);
}
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

# 7 JSON转列表

简单数据格式的JSON转列表

举个栗子:

import 'dart:convert';


void main() {
  String jsonString = '["逗比", "二比", "牛比"]';

  List<dynamic> myList = jsonDecode(jsonString);
  print(myList); // 输出:[逗比, 二比, 牛比]
}
1
2
3
4
5
6
7
8
9

直接调用 jsonDecode() 方法转换为 List<dynamic>

JSON转换为对象列表

举个栗子:

import 'dart:convert';

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  factory Person.fromJson(Map<String, dynamic> json) {
    return Person(json['name'], json['age']);
  }
}

void main() {
  String jsonString = '[{"name":"逗比","age":12},{"name":"二比","age":13},{"name":"牛比","age":14}]';

  // 首先将JSON转换为List<dynamic>
  List<dynamic> jsonDynamicList = jsonDecode(jsonString);

  // 然后遍历List<dynamic> 将其中的元素分别转换为Person对象
  List<Person> personList =
      jsonDynamicList.map((json) => Person.fromJson(json)).toList();

  for (var person in personList) {
    print('Name: ${person.name}, Age: ${person.age}');
  }
}
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

首先还是需要在类中定义工厂构造函数。

然后在转换的时候,先将JSON转换为 List<dynamic> 列表,这个时候列表中的元素是一个个Map类型的字段元素。所以需要遍历 List<dynamic> ,将其中的字典元素转换为 Person 对象,上面使用的是map 函数。

所以一个类如果要与JSON字符串实现相互转换,需要实现 toJson()方法 和 通过字典转换为对象的工厂构造方法。