Dart语法精炼(三)

97 阅读9分钟

该部分内容主要分为四部分, 认真看, 一小时, 带你学完Dart基础语法:

测试工具我选择的是VSCode, 打开空白文件夹开始Dart基础语法的学习吧...

dart_learn的仓库

Dart语法精炼(一)

Dart语法精炼(二)

Dart语法精炼(三)

Dart语法精炼(四)

Dart中文网

设计个满足需求的函数

  • 需求是, Person 有两个属性, 名字肤色.
  • 要求初始化时, 传入相同的名字, 返回的是同一对象.
  • 要求初始化时, 传入相同的肤色, 返回的是同一对象.

方案一: 使用常量构造函数

一起构造两个属性

这种情况必须当名字和颜色完全相同时, 才能够保证书输出统一对象.

class Person1 {
  final String name;
  final String color;
  const Person1(this.name, this.color);
}

void main(List<String> arguments) {
  const p1 = Person1('lilei', '红');
  const p2 = Person1('lilei', '红');
  const p3 = Person1('lilei', '蓝');
  print(identical(p1, p2)); // true
  print(identical(p2, p3)); // false
}

显然, Person1 并不能够满足需求, 所以要单独构造两个属性, 但不能够满足条件:

class Person2 {
  final String name;
  final String color = "white";
  const Person2(this.name);
  // ❌错误: The unnamed constructor is already defined.
  // 常量构造函数, 不能够有多个构造函数, 所以不能满足输入同一颜色返回同一对象的操作
  // 在  Dart语法精炼(二)中第10  知识点有讲解.
  const Person2(this.color);
}

方案二: 使用工厂构造函数

11. 工厂构造函数

11.1 普通构造函数 的返回值

  • 普通的构造函数: 会自动返回创建出来的对象, 不能手动返回
class Person3 {
  String name;
  Person3(this.name) {
    // return this; // 系统默认帮助实现了
  }
}

11.2 工厂构造函数

  • 工厂构造函数最大的特点: 可以手动的, 且必须返回一个对象,
  • 看似多占用内存, 但是减少了对象创建和销毁的过程,
  • 如果外面初始化对象过多, 不使用工厂模式, 也是多占内存, 反过来看工厂模式整体来看还是为了节省内存的.
  • 使用factory 关键字, 修饰命名构造函数的形式来完成工厂模式的创建.(factory Person.formMap(){})
  • 注: 一旦自定义了类的初始化方法, 系统就不会生成默认的初始化方法了.
class Person4 {
  String name;
  String color;

  // 使用关键字 static 可以声明 类变量 或 类方法。
  static final Map<String, Person4> _nameCache = {};
  static final Map<String, Person4> _colorCache = {};

  factory Person4.withName(String name) {
    if (_nameCache.containsKey(name)) {
      return _nameCache[name]!;
    } else {
      final p = Person4(name, "default");
      _nameCache[name] = p;
      return p;
    }
  }

  factory Person4.withColor(String color) {
    if (_colorCache.containsKey(color)) {
      return _colorCache[color]!;
    } else {
      final p = Person4("default", color);
      _colorCache[color] = p;
      return p;
    }
  }

  // 自定义初始化构造函数, 因为当自定义了withName/withColor 后, 系统就会默认不生成Person4()的构造方法, 这里需要自定义初始化构造方法
  Person4(this.name, this.color);
}

在使用上, 满足只要名称或者颜色相同, 就能得到同一对象:

  final p3 = Person4.withName("zhangsan");
  final p4 = Person4.withName("zhangsan");
  print(identical(p3, p4)); // true

  final p5 = Person4.withColor("red");
  final p6 = Person4.withColor("red");
  print(identical(p5, p6)); // true

11.3 通常定义属性选择用 final

  • p3, p4, p5, p6 也可以用var 修饰, 这里使用final, 就是一种语法习惯
  • 类似于Swift的let/var, 一般都用let, 这里也是一样, 用final修饰, 一下就知道后续不可变了, 比较安全.

12. 类的getter和setter

  • 实例对象的每一个属性都有一个隐式的 Getter 方法,如果为非 final 属性的话还会有一个 Setter 方法,
  • 一般在使用上直接访问属性即可:
void main(List<String> arguments) {
  final p = Person();
  // 直接访问属性 set
  p.name = 'lilei';
  // 直接访问属性 get
  print(p.name);   // lilei
  print(p.height); // 1.88
}

class Person {
  String? name;
  double height = 1.88;
}
  • 可以使用 getset 关键字自定义属性的 Getter 和 Setter 方法:
void main(List<String> args) {
  final p = Person2();
  // 通过setter/getter 访问
  p.setName = "lilei";
  print(p.getName); // lilei

  p.setHeight = 2.88;
  print(p.getHeight); // 2.88
}

class Person2 {
  String? name;
  double height = 1.88;

  // setter 语法糖
  set setName(String name) {
    this.name = name;
  }

  // getter 语法糖
  String get getName {
    return this.name ?? "default";
  }
  
  // setter/getter 也可以这么写
  double get getHeight => this.height;
  set setHeight(double height) => this.height = height;
}
  • 也可以使用 getset 关键字为额外的属性添加 Getter 和 Setter 方法:
void main(List<String> arguments) {
  final r = Rectangle(15, 5, 30, 50);
  r.right = 70;
  print(r.left);   // 40.0
  print(r.bottom); // 55.0
}

// 矩形 自己定义了left, top, width, height 四个属性,
// 还有两个 额外的 计算型属性 right 、bottom
class Rectangle {
  double left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  // Define two calculated properties: right and bottom.
  double get right => left + width;
  set right(double value) => left = value - width;
  double get bottom => top + height;
  set bottom(double value) => top = value - height;
}

13. 类的继承

13.1 extends 关键字就表示继承自谁

  • 定义动物类型父类:
class Animal {
  // 年龄
  int age;
  // 初始化方法
  Animal(this.age);
  // 吃 方法
  void eat() {}
}
  • 定义具体动物'狗'继承自动物类:
// extends 就表示继承自谁
class Dog extends Animal {
  // 定义属于自己的属性 - 狗狗名称
  String name;

  // 为super传入age 完成初始化
  // : super(age) 就是在初始化列表里面调用的super
  Dog(this.name, int age) : super(age);

  // @override 重写一个方法, 需要使用@override关键词
  @override
  void eat() {
    // TODO: implement eat
    super.eat();
  }
}

使用上:

void main(List<String> arguments) {
  final animla = Animal(20);
  print(animla.age); // 20

  final dog = Dog('wangcai', 3);
  print('${dog.name} ${dog.age}'); // wangcai 3
}

13.2 extends 在泛型里面还被用作类型限制

  • 一种常见的非空类型处理方式,
  • 将子类限制继承 Object (而不是默认的 Object?)
class Foo<T extends Object> {
  // provided: 提供, 供给
  // Any type provided to Foo for T must be non-nullable.
}

限制泛型参数类型eg: 当实现一个泛型时,如果需要限制它参数的类型,可以使用extends 关键字

void main(List<String> arguments) {
  final math = Subject();
  final xiaoming = Learn(math);
  xiaoming.math.score(); // 100分
}

class Subject {
  void score() {
    print("100分");
  }
}

// 泛型限制
class Learn<T extends Subject> {
  final T math;
  Learn(this.math);
}

13.2 浅谈一下泛型

14. 抽象类的使用

14.1 抽象类的定义

  • 抽象类 用abstract 关键字修饰,
  • 可以只声明方法, 不实现方法, 但是子类必须实现, 这个方法叫做抽象方法,
  • 也可以声明方法, 并且实现它, 子类就可以不实现.

eg: 定义一个 形状 抽象类

abstract class Shape {
  // 1.1 可以只声明方法, 不实现方法, 但是子类必须实现
  double getArea();

  // 1.2 也可以声明方法, 并且实现它, 子类可以不实现
  String getInfo() {
    return "我是形状抽象类";
  }
}

// 注意一: 继承自抽象类后, 必须实现抽象类的抽象方法
class Rectangle extends Shape {
  @override
  double getArea() {
    return 100.00;
  }
}
  • 抽象类不能被实例化,只有继承它的子类可以.

14.2 验证抽象类不能被实例化

void main(List<String> arguments) {
  // 直接实例化形状抽象类 
  // ❌报错: Abstract classes can't be instantiated.
  final shape = Shape(); 
}
  • MapList 都是抽象类, 但是都能直接实例化, 为什么???
bstract class List<E> 
abstract class Map<K, V>

实例化:

final map = Map();       // 不报错
print(map.runtimeType);  // _Map<dynamic, dynamic>

final list = [];         // 不报错
print(list.runtimeType); // List<dynamic>

Map源码片段

在 Dart 中,Map 是一个接口(interface),而不是一个抽象类。接口是一种特殊的抽象类,它只包含了抽象方法的定义,而没有具体的实现。和抽象类一样,接口也不能被直接实例化。

在 Dart 中,我们使用 Map 接口的实现类来创建实例。Dart 中内置了 Map 接口的多种实现类,如 LinkedHashMapHashMap 等。我们可以通过调用这些实现类的构造函数来创建实例。

并且Map的初始化采用了工厂模式(factory), 且被external 关键字修饰, 而这, 也就是抽象类可以被实例化的原因.

在 Dart 中,external factory 关键字组合用于声明一个外部工厂构造函数,它表示这个构造函数是由其他外部资源(比如 JavaScript 代码、本地库等)提供的,而不是由 Dart 代码实现的。这个关键字组合的作用是告诉 Dart 编译器不要生成这个构造函数的实现代码,而是将其链接到其他的外部资源。

14.3 external 关键字

通过分析Map的实例化源码来了解external 关键字.

  1. Map的工厂函数声明
  • external 标识符: 代表我这里只做一个实例化方法(Map())的声明, 具体实现是由其他外部资源完成的.
  • external 作用: 将方法的实现和方法的声明分离.
external factory Map();
  1. Map的工厂函数实现
  • 在 Dart 2.0之前, external 修饰的函数实现, 是通过补丁, 将文件放在Dart的SDK中的, 通过dart 中Map 的源码文件, 路径为:flutter/bin/cache/dart-sdk/lib/_internal/vm/lib/map_patch.dart
@patch
factory Map() => new LinkedHashMap<K, V>();
  • 通过@patch 修饰后, 来继续实现实例化方法(Map()), 这么做的好处就是可以在runtime过程中, 动态获取运行平台, 使用不同的实现方式来实现.

  • @patch 作用: 关联某个类中用external 修饰的方法的实现.

  • 在 Dart 2.0之后, external factory Map() 的具体代码实现是由 Dart 运行时系统提供的,而不是由 Dart 代码实现的。所以就不需要我们来关心实现了.

14.3 抽象类的实例化

  • 采用工厂(factory)构造函数, 返回一个子类的实例化对象,
  • 一旦在抽象类中定义了工厂构造方法后,子类将不能通过继承(extends), 只能通过接口(implements)来完成实现,
  • 一旦在抽象类中定义了工厂构造方法后,子类中将 不能定义除工厂构造方法外的其它构造方法了,
  • 如果把抽象类当做 接口 实现的话 必须得实现抽象类里面定义的所有属性和方法。
void main(List<String> arguments) {
  final shape = Shape();
  print(shape.runtimeType); // triangle
  print(shape.getArea());   // 18.8
  print(shape.getInfo());   // 我是三角形
}

abstract class Shape {
  double getArea();
  String getInfo() {
    return "我是形状抽象类";
  }

  // 开启了工厂模式
  factory Shape() {
    return Triangle();
  }
}

// 抽象类开启了工厂模式, 子类只能通过接口(implements)实现
class Triangle implements Shape {
  @override
  double getArea() {
    return 18.8;
  }

  @override
  String getInfo() {
    return "我是三角形";
  }
}

15. 隐式接口

  • Dart中没有关键字来定义接口的
  • 其他语言比如用interface/protocol 来定义接口
  • Dart 默认情况下所有的类都是隐式接口
  • Dart 只支持单继承, 所以Person 想会跑, 又想会飞, 就不能通过继承来实现
class Runner {
  void running() {
    print("running");
  }
}

class Flyer {
  void flying() {
    print("flying");
  }
}

class Person {
  void eating() {}
}

Person 无法既继承Runner, 又继承Flyer.

15.1 接口的使用 (implements)

  • 当一个类当做接口使用时, 那么实现这个接口的类, 必须实现这个接口中的所有方法
  • 当Runner 和 Flyer 当做接口时, 实现接口的类就是 SuperMan, 标识就是implements 关键字, 所以SuperMan 要实现Runner 和Flyer 的所有方法
  • 只可以单继承extends Person; 可以多接口, 用逗号隔开
  • 继承(extends)才可以调用super, 比如eating; 接口(implements)不可以, 比如running , flying.
  • 当Person 也存在running 的时候, SuperMan里面可以不实现, 因为继承自person, 就证明已经有了, 不需要再实现一次了.
class SuperMan extends Person implements Runner, Flyer {
  @override
  void eating() {
    // TODO: implement eating
    super.eating();
  }

  @override
  void running() {
    // TODO: implement running
  }

  @override
  void flying() {
    // TODO: implement flying
  }
}

结语

路漫漫其修远兮,吾将上下而求索~

作者简书

作者GitHub

.End