类 - Part I

类的知识点较多,所以分成两节进行讲解,本节是Part I,下一节是Part II。


类是对象的蓝本,用于创建对象。

类的声明是使用关键字class,所有类都直接或间接继承最顶端的Object类。

*示例代码使用了dart核心库的identical函数,用于检查两个变量是否指向同一对象

属性和方法

类中可以声明数据和函数,即对象的属性和方法。

普通的属性和方法是与对象实例绑定,它们被称之为实例变量和实例方法;使用static修饰的属性和方法,它们与类绑定,通常称为类变量和类方法。

实例变量和实例方法、类变量和类方法,在类中都可以通过名称直接访问,实例变量和实例方法还可以通过this访问;在类的外部,实例变量和方法是通过实例.变量方式进行访问,而类变量和类方法必须通过类名.变量方式才能访问

1. // 声明一个表示'大剑'(《黑暗之魂》系列游戏的一类武器)的类
2. class GreatSword {
3. // 实例变量
4. String name; // 名称
5. final int damage = 100; // 基础伤害值(不可变)
6. int extraDamage = 0; // 其他附加伤害

8. // 类变量
9. static String category = 'weapon'; // 大剑的分类是武器

11. // 类方法
12. static upgrade(GreatSword sword) { // 升级,即增加附加伤害
13. sword.extraDamage += 10;
14. }

16. // 实例方法
17. info() => 'GreatSword - damage: ${damage} , extraDamage: ${this.extraDamage}'; // 通过名称或this访问实例变量
18. }

20. main() {
21. // 使用默认构造函数(见下一小节)进行实例化
22. var sword = new GreatSword();
23. print(sword.damage);
24. print(sword.info()); // GreatSword - damage: 100 , extraDamage: 0

26. // 使用类方法
27. GreatSword.upgrade(sword);
28. print(sword.extraDamage);
29. print(sword.info()); // GreatSword - damage: 100 , extraDamage: 10
30. }

构造函数

构造函数用于将类进行实例化(即创建对象),需要配合关键字new或const使用(在 Dart 2.0 中,new与const变为可选)。

没有显式声明构造函数的类,将默认拥有一个与类同名且没有参数的构造函数(无参默认构造函数)。

构造函数最常见的操作是使用参数对属性赋值,Dart 为此提供了简写方式(在参数列表中直接使用this.属性)。

除了普通的构造函数,Dart 还支持类名.构造函数名方式的命名构造函数

1. class GreatSword {
2. String name; // 名称
3. final int damage = 100; // 基础伤害值
4. int extraDamage = 0; // 其他附加伤害

6. // 构造函数
7. GreatSword(String name) {
8. this.name = name;
9. }
10. // 以上构造函数的简写方式
11. // GreatSword(this.name);

13. // 命名构造函数
14. GreatSword.enhanced(this.name, this.extraDamage);
15. // 等同于以下写法
16. // GreatSword.enhanced(String name, int extraDamage) {
17. //   this.name = name;
18. //   this.extraDamage = extraDamage;
19. // }
20. }

22. main() {
23. // 使用不同构造函数进行实例化
24. var sword = new GreatSword('Claymore'); // 普通大剑
25. var enhancedSword = new GreatSword.enhanced('Claymore', 20); // 强化版大剑
26. }

默认情况下,构造函数总是会自动新建一个对象,函数体内不需要也不能使用return语句。

如果不需要构造函数每次调用都新建对象,比如从缓存中获取已存在的对象,则可以使用工厂构造函数。

使用factory修饰的构造函数是工厂构造函数,跟普通构造函数不同,它必须使用return语句返回对象

1. class GreatSword {
2. String name; // 名称

4. // 使用Map类型(后续章节将进行讲解)作为对象缓存
5. static final Map cache = {};

7. // 工厂构造函数
8. factory GreatSword(String name) {
9. // 对象已存在缓存中
10. if (cache.containsKey(name)) {
11. return cache[name];
12. } else {
13. // 对象不存在缓存中,使用库私有的(后续章节将进行讲解)构造函数新建对象
14. final sword = new GreatSword._(name);
15. cache[name] = sword; // 存入缓存
16. return sword;
17. }
18. }

20. // 库私有的(后续章节将进行讲解)命名构造函数
21. GreatSword._(this.name);
22. }

24. main() {
25. // 进行多次实例化
26. var sword1 = new GreatSword('Claymore');
27. var sword2 = new GreatSword('Claymore');
28. // 使用顶层函数identical检查是否同一对象
29. print(identical(sword1, sword2)); // true - 是同一对象
30. }

如果希望创建不可变对象,可以将所有实例变量使用final修饰,构造函数使用const修饰,最后使用const创建对象即可

1. // 声明一个表示'篝火'(《黑暗之魂》系列游戏的存盘点)的类
2. class Bonfire {
3. final String name; // 名称

5. const Bonfire(this.name);
6. }

8. main() {
9. // 使用const进行多次实例化
10. var bonfire1 = const Bonfire('home');
11. var bonfire2 = const Bonfire('home');
12. // 使用顶层函数identical检查是否同一对象
13. print(identical(bonfire1, bonfire2)); // true - 是同一对象
14. // bonfire1.name = 'castle'; // 错误,尝试修改不可变对象
15. }

初始化列表

构造函数还支持初始化列表,书写方式是在参数列表后跟一个以冒号开头的,使用逗号分隔的赋值列表。

初始化列表先于构造函数体执行,常用于final属性的初始化,即没有初始化的final属性可以在初始化列表中进行赋值。

初始化列表还可使用this进行构造函数转发,使得构造函数逻辑得以复用,构造函数转发和赋值操作不能同时出现

1. class GreatSword {
2. String name; // 名称
3. final int damage; // 基础伤害值(没有初始化)
4. int extraDamage = 0; // 其他附加伤害

6. // 在初始化列表中设置final变量damage
7. GreatSword(this.name, [this.extraDamage = 0]) : damage = 100; // 普通大剑基础伤害100

9. // 在初始化列表中调用静态方法计算extraDamage
10. GreatSword.magic(this.name, this.damage) : extraDamage = magicPower(damage); // 魔法大剑的附加伤害要根据基础伤害计算得出

12. // 通过初始化列表转发到其他构造函数,其中this代表类名
13. GreatSword.bastard(): this('Bastard Sword', 10); // 混种大剑
14. GreatSword.moonlight(): this.magic('Moonlight GreatSword', 90); // 月光大剑

16. // 计算魔法武器的附加伤害
17. static magicPower(int damage) {
18. return damage * 0.15;
19. }
20. }

22. main() {
23. var sword = new GreatSword('Claymore');
24. var bastard = new GreatSword.bastard();
25. var moonlight = new GreatSword.moonlight();
26. print(sword.name); // Claymore
27. print(sword.damage); // 100
28. print(sword.extraDamage); // 0
29. print(bastard.name); // Bastard Sword
30. print(bastard.damage); // 100
31. print(bastard.extraDamage); // 10
32. print(moonlight.name); // Moonlight GreatSword
33. print(moonlight.damage); // 90
34. print(moonlight.extraDamage); // 13.5
35. }

Getter/Setter 方法

跟很多语言不同,Dart 通过一种特殊的getter/setter方法对对象属性进行读写,它们虽是方法却有跟属性一样的访问方式。

所有普通属性都有一对隐含的getter/setter,final属性只有getter。

自定义getter/setter也是支持的,书写方式是在方法名前添加get或set,getter有返回值无参数而setter正好相反

1. class GreatSword {
2. String name; // 名称
3. final int damage = 100; // 基础伤害值
4. int extraDamage = 0; // 其他附加伤害

6. // damage隐含的getter
7. // int get damage => damage;

9. // name和extraDamage隐含的getter和setter
10. // String get name => name;
11. // set name(String name) => this.name = name;
12. // int get extraDamage => extraDamage;
13. // set extraDamage(int extraDamage) => this.extraDamage = extraDamage;

15. // 自定义的getter和setter
16. // 总伤害
17. int get totalDamage {
18. return damage + extraDamage; // 基础伤害与附加伤害之和
19. }
20. set totalDamage(int totalDamage) {
21. extraDamage = totalDamage - damage; // 通过总伤害计算出附加伤害
22. }

24. // 打印信息
25. info() {
26. return '${name} - totalDamage: ' + this.totalDamage.toString(); // 通过this或直接访问属性
27. }
28. }

30. main() {
31. // 所有对属性的读写都是通过getter和setter
32. var sword = new GreatSword();
33. sword.name = 'GreatSword';
34. sword.totalDamage = 200;
35. print(sword.damage); // 100
36. print(sword.extraDamage); // 100
37. print(sword.info()); // GreatSword - totalDamage: 200

39. sword.damage = 180; // 错误,damage的setter不存在
40. }

getter和setter带来的好处是,你可以随时更改属性的内部实现,而且不用修改外部调用代码

1. // 版本1的GreatSword
2. class GreatSword1 {
3. final String name = 'GreatSword';
4. }

6. // 版本2的GreatSword,将属性name转换为一个getter
7. class GreatSword2 {
8. String get name {
9. return greatName();
10. }

12. greatName() => 'GreatSword';
13. }

15. main() {
16. // 使用两个不同版本的GreatSword,对name的访问方式不变
17. var sword = new GreatSword1();
18. print(sword.name); // GreatSword
19. sword = new GreatSword2();
20. print(sword.name); // GreatSword
21. }

子类

使用extends来创建子类,子类中使用super来访问父类。

除了构造函数,父类所有的属性(setter/getter)和方法都被子类继承,而且都可以被重写(override)。

子类的构造函数在执行前,将隐含调用父类的无参默认构造函数(没有参数且与类同名的构造函数)。

如果父类没有无参默认构造函数,则子类的构造函数必须在初始化列表中通过super显式调用父类的某个构造函数。

1. // 大剑
2. class GreatSword {
3. String name; // 名称
4. final int damage = 100; // 基础伤害值

6. // 总伤害的getter
7. int get totalDamage => damage; // 总伤害

9. // 一个参数的构造函数
10. GreatSword(this.name);
11. }

13. // 特大剑
14. class UltraGreatSword extends GreatSword {
15. int extraDamage = 20; // 附加伤害值

17. // 重写getter
18. int get totalDamage => super.damage + extraDamage; // 父类的伤害加上自己的附加伤害为总伤害(super可省略)

20. // 父类没有无参默认构造函数,必须使用super(代表父类名)调用父类的构造函数
21. UltraGreatSword(String name) : extraDamage = 50, super(name);
22. }

24. main() {
25. var black = new UltraGreatSword('Black Knight Greatsword');
26. print(black.name); // Black Knight Greatsword
27. print(black.damage); // 100
28. print(black.extraDamage); // 50
29. }