Skip to content

Dart教程 - 7 面向对象(1)

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

什么是类?

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

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

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

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

什么是对象?

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

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

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

7.1 类和对象

1 类的定义

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

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

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

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

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

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

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

2 类的使用

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

dart
/**
 * 定义类
 */
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();
}

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

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

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

执行结果:

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

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

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

3 默认构造函数

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

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

举个栗子:

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

dart
/**
 * 定义类
 */
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();
}

执行结果:

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

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

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

就像下面这样:

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

  // 无参构造函数
  Student() {}
}

4 this的作用

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

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

举个栗子:

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

dart
/**
 * 定义类
 */
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();
}

执行结果:

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

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

5 构造函数的简写

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

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

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

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

6 命名构造函数

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

dart
/**
 * 定义类
 */
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();
}

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

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

执行结果:

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

7 初始化列表

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

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

举个栗子:

dart
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);
}

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

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

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

  Person(String name, int age)
      : name = name.toUpperCase(); // 使用初始化列表赋值final属性
}

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

举个栗子:

Main.dart

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);
}

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

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

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

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

shell
dart --enable-asserts Main.dart

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

8 重定向构造方法

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

dart
class Person {
  String name;
  int age;

  Person(this.name, this.age);
  
  Person.fromName(String name) : this(name, 0);
  
  Person.defaultValue() : this.fromName("逗比");
}

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

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

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

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

9 常量构造方法

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

举个栗子:

dart
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
}

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

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

举个栗子:

dart
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
}

必须注意:

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

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

10 私有构造函数

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

举个栗子:

dart
class Person {
  String name;
  int age;

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

  // 私有命名构造函数
  Person._initByNameAndAge(this.name, this.age);
}

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

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

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

11 工厂构造函数

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

举个栗子:

dart
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();
}

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

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

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

什么是单例模式?

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

看一下下面的代码:

Singleton.dart

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

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

  factory Singleton() {
    return _instance;
  }

  void displayMessage() {
    print('This is a singleton instance.');
  }
}

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

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

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

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

Main.dart

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

  print(identical(instance1, instance2)); // 输出:true
}

12 静态变量

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

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

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

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

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

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

dart
/**
 * 定义类
 */
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
}

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

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

13 静态方法

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

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

dart
/**
 * 定义类
 */
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个学生
}

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

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

14 内置方法

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

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

举个栗子:

dart
class Person {
  String name;
  int age;

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

void main() {
  var person = Person('John', 25);
  print(person);  // 输出:Instance of 'Person'
}

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

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

举个栗子:

dart
class Person {
  String name;
  int age;

  Person(this.name, this.age);

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

void main() {
  var person = Person('DouBi', 12);
  print(person);  // 输出:Person{name: John, age: 25}
}

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

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

15 级联操作

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

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

举个栗子:

dart
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();
}

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

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

7.2 封装

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

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

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

  • 封装
  • 继承
  • 多态

下面依次开始讲起。

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

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

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

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

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

1 私有属性和方法

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

定义私有成员的方式:

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

举个栗子:

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}");
  }
}

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

  phone.producer = "小米";
  phone._voltage = 24;
  phone.call();
  phone._get_run_voltage();
}

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

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

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

Phone.dart

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}");
  }
}

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

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

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

dart
import 'domain/Phone.dart';

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

  phone.producer = "小米";
  // phone._voltage = 24;  // 报错,无法访问私有变量
  phone.call();
  //phone._get_run_voltage();  // 报错,无法访问私有方法
}

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

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

执行结果:

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

2 getter和setter

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

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

举个栗子:

Phone.dart

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("参数错误"); // 限制传入的参数值,进行报错处理,异常处理后面再学习
    }
  }
}

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

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

Main.dart

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);
}

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

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

执行结果:

华为
12
小米
24