文章目录
Dart中的类和单例模式
先了解Dart中的类:
Dart
也是一门面向对象的开发语言,面向对象中非常重要的概念就是类,通过类的初始化创建一个对象
Dart 是支持基于 mixin 继承机制的面向对象语言,所有对象都是一个类的实例,而除了 Null
以外的所有的类都继承自 Object
类。 基于 mixin 的继承 意味着尽管每个类(top class Object?
除外)都只有一个超类,一个类的代码可以在其它多个类继承中重复使用。 扩展方法 是一种在不更改类或创建子类的情况下向类添加功能的方式。
类的定义
- 在
Dart
中,定义类用class
关键字 - 类通常有两部分组成:成员(member)和方法(method)。
- 当未指明其父类的时候, 默认是继承自
Object
的, 定义类的伪代码如下:
class 类名 {
类型 成员名;
返回值类型 方法名(参数列表) {
方法体
}
}
- 在
Dart
语言中, 在类中使用属性(成员/实例变量)时, 有必要时是通过this
获取的 - 但是下面在
getsize
方法中并没有加this
- 这里需要注意的是:
Dart
的开发风格中,在方法中通常使用属性时,会省略this
,但是有命名冲突时,this
不能省略
// 创建类
class Point {
// 定义变量
int x;
void getsize() {
print('x = $x');
}
}
// 类的初始化
main(List<String> args) {
// 从Dart2开始,new关键字可以省略
var point = new Point();
point.x = 1;
point.getsize();
}
使用类的成员
对象的 成员 由函数和数据(即 方法 和 实例变量)组成。方法的 调用 要通过对象来完成,这种方式可以访问对象的函数和数据。
使用(.
)来访问对象的实例变量或方法:
var p = Point(2, 2);
// Get the value of y.
assert(p.y == 2);
// Invoke distanceTo() on p.
double distance = p.distanceTo(Point(4, 4));
使用 ?.
代替 .
可以避免因为左边表达式为 null 而导致的问题:
// If p is non-null, set a variable equal to its y value.
var a = p?.y;
构造函数
- 当通过类创建一个对象时,会调用这个类的构造方法
- 在
Dart
语言中,如果类中没有明确指定构造方法时,将默认拥有一个无参的构造方法() - 上面得到的
point
对象调用的就是默认的无参构造方法
- 在
- 也可以根据自己的需求自定义构造方法
- 当我们创建了自己的构造方法时,默认的无参的构造方法将会失效,不能使用,否则会报错
- 因为
Dart
本身不支持函数的重载, 所以如果我们明确的写一个默认的构造方法,就会和我们自定义的构造方法冲突
class Student {
String name;
int age;
Student(String name, int age) {
this.name = name;
this.age = age;
}
}
- 在上面构造方法中主要实现的就是通过构造函数的参数给类的户型赋值
- 为了简化这一过程,
Dart
提供了一种更加简洁的语法糖形式
class Student1 {
String name;
int age;
// 这里和上面的Studeng的构造方法等价
Student1(this.name, this.age);
}
命名构造方法
- 在实际开发中, 很明显一个构造方法的确是不够我们使用的
- 而且
Dart
又不支持函数的重载, 不能创建爱你相同名称不同参数的构造方法 - 这就衍生出了另外一中构造方法:命名构造方法
class Model {
String name;
int age;
Model(this.name, this.age);
// 命名构造方法
Model.withNameAndAge(String name, int age) {
this.name = name;
this.age = age;
}
// 命名构造方法
Model.initJson(Map<String, Object> map) {
this.name = map['name'];
this.age = map['age'];
}
}
// 初始化对象
main() {
// 普通构造方法
var model0 = Model('name', 12);
// 命名构造方法
var model1 = Model.withNameAndAge('titan', 12);
var model2 = Model.initJson({
'name': 'jun', 'age': 18});
}
初始化列表
几种方式定义的属性都是可变的, 如果定义的属性是final
不可重新赋值的又该如何实现
class Teacher {
final String name;
final int age;
// 1. 这里会有一个错误提示: All final variables must be initialized, but 'age' and 'name' are not
Teacher(String name, int age) {
//2. 这里也会有一个错误提示: 'name' can't be used as a setter because it's final
this.name = name;
this.age = age;
}
}
- 上面第一处错误主要是因为: 在
Dart
中在执行下面{ }
中的代码的时候, 表示Teacher
对象已经初始化完毕了 - 所以在执行
{ }
之前, 必须保证name
和age
被初始化了 - 而且
final
修饰的属性是不可被重新赋值的, 所以才会报错 - 或者也可以使用函数中的命名可选参数处理
class Size {
final double width;
final double height;
final double area;
// 命名可选参数
Size(this.width, this.height, {
this.area = 10 });
}
- 上面通过命名可选参数的形式, 给参数设置默认值也是可以的, 但是不同的是
area
只能设置具体的数值, 不能设置表达式 - 初始化列表的形式不但可以设置具体的数值, 也可以设置默认值为表达式的形式
class Size {
final double width;
final double height;
final double area;
// 多个属性使用逗号分隔
Size(double width, double height):
this.width = width,
this.height = height,
this.area = width * height;
}
重定向构造方法
- 下面的构造函数中, 我们只能通过传入两个参数来获取一个对象
- 如果在某些情况下, 希望只通过一个
name
变量来获取一个对象 - 这种情况下, 就可以通过在构造方法中去调用另外一个构造方法, 这个时候可以使用重定向构造方法
- 需要注意的是: 在一个构造函数中,去调用另外一个构造函数, 是在冒号后面使用this调用
class Point {
String name;
int age;
Point(this.name, this.age);
// 重定向的构造方法
Point.fromName(String name): this(name, 0);
}
// 使用方法
var point = Point.fromName("name");
print(point.age); // 输出: 0
常量构造函数
- 在某些情况下, 我们希望通过构造函数, 只要传入相同的参数, 那么得到的对象就是同一个
- 在
Dart
中判断两个对象是否是同一个的方法是通过函数identical
判断, 返回值是一个布尔值
// 普通构造函数
class Person {
String name;
int age;
Person(this.name, this.age);
}
// 初始化列表
class Size {
final double width;
final double height;
final double area;
// 多个属性使用逗号分隔
Size(double width, double height):
this.width = width,
this.height = height,
this.area = width * height;
}
main(List<String> args) {
var p1 = Person("name", 10);
var p2 = Person("name", 10);
// 判断两个对象是不是同一个
print(identical(p1, p2)); /// false
var s1 = Size(10, 20);
var s2 = Size(10, 20);
// 判断两个对象是不是同一个
print(identical(s1, s2)); /// false
}
- 很明显上面两种方式初始化的对象都不是同一个
- 其实在
Dart
中如果将构造方法前加const
进行修饰,那么可以保证相同的参数,创建出来的对象是相同的 - 这样的构造方法就称之为常量构造方法
// 常量构造方法
class Teacher {
final String name;
const Teacher(this.name);
}
main(List<String> args) {
// 常量构造方法
// 这里的const不可以省略
var t1 = const Teacher("name");
var t2 = const Teacher("name");
print(identical(t1, t2)); /// true
// 这里的const可以省略
const t3 = Teacher("name");
const t4 = Teacher("name");
print(identical(t3, t4)); /// true
print(identical(t1, t4)); /// true
}
常量构造方法有一些注意点:
- 拥有常量构造方法的类中,所有的成员变量必须是
final
修饰的. - 为了可以通过常量构造方法,创建出相同的对象,不再使用
new
关键字,而是使用const
关键字 - 如果是将结果赋值给
const
修饰的标识符时,const
可以省略.
工厂构造方法
- 在
Dart
提供了factory
关键字, 用于通过工厂去获取对象 - 普通的构造函数, 会默认返回创建出来的对象, 不需要我们手动
return
- 工厂构造方法, 需要手动返回一个对象
- 同样和上面一样的目的, 只要传入相同的参数, 那么得到的对象就是同一个, 下面通过工厂构造函数的方式实现
main(List<String> args) {
var p1 = Person.fromName("titan");
var p2 = Person.fromName("titan");
print(identical(p1, p2)); // true
}
class Person {
String name;
// 用于缓存创建的对象, 避免大量的创建和销毁对象
static final Map<String, Person> _cache = <String, Person>{
};
factory Person.fromName(String name) {
if (_cache.containsKey(name)) {
return _cache[name];
} else {
final p = Person(name);
_cache[name] = p;
return p;
}
}
Person(this.name);
}
类的继承
setter和getter
Dart
中类定义的属性默认是可以直接被外界访问的Dart
中也存在setter
和getter
方法, 用于监听累的属性被访问的过程
main() {
var people = People('top');
people.setName = 'top';
print(people.getName);
print(people.name);
var person = Person('titan');
person.setName = 'jun';
print(person.getName);
}
class People {
String name;
// setter
set setName(String value) {
this.name = value;
}
// getter
String get getName {
return 'titanjun';
}
People(this.name);
}
- 上面
setName
和getName
是自定义的, 你也可以命名为setterName
和getterName
等 - 还有就是上述两个方法不是系统自动生成的, 是需要我们手动添加的
- 简单的方式也可以使用箭头函数
class Person {
String name;
// setter
set setName(String value) => this.name = value;
// getter
String get getName => 'titanjun';
Person(this.name);
}
类的继承
- 在
Dart
中同样支持类的继承, 继承使用extends
关键字,子类中使用super
来访问父类 - 父类中除了构造方法外, 所有的成员变量和方法都会被继承
- 子类可以拥有自己的成员变量, 并且可以对父类的方法进行重写
- 子类中可以调用父类的构造方法,对某些属性进行初始化:
- 子类的构造方法在执行前,将隐含调用父类的无参默认构造方法(没有参数且与类同名的构造方法)
- 如果父类没有无参默认构造方法,则子类的构造方法必须在初始化列表中通过
super
显式调用父类的某个构造方法
class People {
String name;
People(this.name);
void eat() {
print('people -- eat');
}
}
class Person extends People {
int age;
Person(String name, int age): this.age = age, super(name);
void eat() {
// 这里的super, 看个人需求是否调用
super.eat();
print('Person -- eat');
}
}
main(List<String> args) {
var people = People('name');
people.eat();
var person = Person("top", 10);
person.eat();
}
抽象类
- 在
Dart
中抽象类是使用abstract
声明的类 - 在
Dart
中没有具体实现的方法(没有方法体),就是抽象方法 - 抽象方法,必须存在于抽象类中, 抽象类不能实例化
- 抽象类中的抽象方法必须被子类实现, 抽象类中的已经被实现方法, 可以不被子类重写
abstract class Size {
int width;
int height;
Size(this.width, this.height);
void getSize();
int getArea() {
return this.width * this.height;
}
}
class Area extends Size {
void getSize() {
print('width = $width, height = $height');
}
Area(int width, int height): super(width, height);
}
main(List<String> args) {
// 实例化Size会报错: Abstract classes can't be instantiated
// var size = Size(20, 2);
var area = Area(10, 2);
area.getArea();
print(area.getArea());
}
多继承
在Dart
中只有单继承, 是不支持多继承的, 但是我们却可以通过其他方式间接实现多继承问题
隐式接口
Dart
中的接口比较特殊, 没有一个专门的关键字来声明接口, 默认情况下所有的类都是隐式接口- 默认情况下,定义的每个类都相当于默认也声明了一个接口,可以由其他的类来实现
- 在
Dart
开发中,我们通常将用于给别人实现的类声明为抽象类 - 当将一个类能够做接口使用时, 那么实现这个接口的类, 必须实现这个接口中的所有方法
- 在
Dart
中通过implements
来实现多继承问题, 但是必须实现这个接口中的所有方法, 而且在方法的实现中不能调用super
方法
abstract class Woman {
void eat();
void student() {
print("student");
}
}
class Man {
void run() {
print("runner");
}
}
class Student implements Woman, Man {
void eat() {
print("eat");
}
void student() {
print("student--student");
}
void run() {
// 这里不能调用super方法
// super.run();
print("run");
}
}
main(List<String> args) {
var stu = Student();
stu.eat();
stu.run();
stu.student();
}
Mixin混入
- 在通过
implements
实现某个类时,类中所有的方法都必须被重新实现(无论这个类原来是否已经实现过该方法) - 但是某些情况下,一个类可能希望直接复用之前类的原有实现方案
- Dart提供了另外一种方案: Mixin混入的方式
- 除了可以通过
class
定义类之外,也可以通过mixin
关键字来定义一个类。 - 只是通过
mixin
定义的类用于被其他类混入使用,通过with
关键字来进行混入
- 除了可以通过
mixin Runner {
run() {
print('在奔跑');
}
}
mixin Flyer {
fly() {
print('在飞翔');
}
}
// 这里可以对原方法不做任何实现
class Bird with Runner, Flyer {
}
main(List<String> args) {
var bird = Bird();
bird.run();
bird.fly();
}
类的扩展extension
扩展函数就是可以在类的外部声明额外的类的函数,并且可以扩展系统函数,让使用更方便,代码更简洁,
熟悉Kotlin的同学都非常熟悉了,真的很好用。
- 在 Dart 版本 2.7 及其以上时支持关键字
extension
。请确保你工程项目中pubspec.yaml
文件中:
environment:
sdk: ">=2.7.0 <3.0.0"
-
Dart中的扩展使用
extension
、on
关键字 ,形如:extension <extension name> on <type> { (<member definition>)* }
例如
String
的扩展:extension NumberParsing on String { int parseInt() { return int.parse(this); } double parseDouble() { return double.parse(this); } }
调用如下:
print("40".parseInt());
也可以利用
getter
操作符简化:extension NumberParsing on String { int get parseInt { return int.parse(this); } double get parseDouble { return double.parse(this); } }
此时调用如下:
print("40".parseInt);
Flutter(able) 的单例模式
有了上边的类的基础,再看单例模式就简单多了。
在实际编码过程中,单例模式常见应用有:
- 全局日志的 Logger 类、应用全局的配置数据对象类,单业务管理类。
- 创建实例时占用资源较多,或实例化耗时较长的类。
- 等等…
一般来说,要在代码中使用单例模式,结构上会有下面这些约定俗成的要求:
- 单例类(Singleton)中包含一个引用自身类的静态属性实例(instance),且能自行创建这个实例。
- 该实例只能通过静态方法
getInstance()
访问。 - 类构造函数通常没有参数,且被标记为私有,确保不能从类外部实例化该类。
遵循以上这些要求,我们就不难能用 Dart 写出一个普通的单例模式:
方式一:普通单例
class Singleton {
static Singleton _instance;
// 私有的命名构造函数
Singleton._internal();
static Singleton getInstance() {
if (_instance == null) {
_instance = Singleton._internal();
}
return _instance;
}
}
同时,在实现单例模式时,也需要考虑如下几点,以防在使用过程中出现问题:
- 是否需要懒加载,即类实例只在第一次需要时创建。
- 是否线程安全,在 Java、C++ 等多线程语言中需要考虑到多线程的并发问题。由于 Dart 是单线程模型的语言,所有的代码通常都运行在同一个 isolate 中,因此不需要考虑线程安全的问题。
- 在某些情况下,单例模式会被认为是一种 反模式,因为它违反了 SOLID 原则中的单一责任原则,单例类自己控制了自己的创建和生命周期,且单例模式一般没有接口,扩展困难。
- 单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源,比如 DB,我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现 mock 替换。
Dart 化单例
方式二:利用getter操作符:
如上文所说的,Dart 语言作为单线程模型的语言,实现单例模式时,我们本身已经可以不用再去考虑 线程安全 的问题了。Dart 的很多其他特性也依然可以帮助到我们实现更加 Dart 化的单例。
使用 getter 操作符,可以打破单例模式中既定的,一定要写一个 getInstance()
静态方法的规则,简化我们必须要写的模版化代码,如下的 get instance
:
class Singleton {
//私有化构造
Singleton._internal();
static Singleton _instance;
//重写getter方法
static get instance {
if (_instance == null) {
_instance = Singleton._internal();
}
return _instance;
}
}
Dart 的 getter 的使用方式与普通方法大致相同,只是调用者不再需要使用括号,这样,我们在使用时就可以直接使用如下方式拿到这个单例对象:
final singleton = Singleton.instance;
方式三:利用工厂构造函数
而 Dart 中特有的 工厂构造函数(factory constructor)也原生具备了 不必每次都去创建新的类实例 的特性,将这个特性利用起来,我们就可以写出更优雅的 Dart(able) 单例模式了,如下:
class Singleton {
static Singleton _instance;
Singleton._internal();
// 工厂构造函数
factory Singleton() {
if (_instance == null) {
_instance = Singleton._internal();
}
return _instance;
}
}
这里我们不再使用 getter 操作符额外提供一个函数,而是将单例对象的生成交给工厂构造函数,此时,工厂构造函数仅在第一次需要时创建 _instance
,并之后每次返回相同的实例。这时,我们就可以像下面这样使用普通构造函数的方式获取到单例了:
final singleton = Singleton();
方式四:利用 Dart 空安全及箭头函数等特性 更加简洁
如果你还掌握了 Dart 空安全及箭头函数等特性,那么还可以使用另一种方式进一步精简代码,写出像下面这样 Dart 风味十足的代码:
class Singleton {
static Singleton _instance;
Singleton._internal() {
_instance = this;
}
factory Singleton() => _instance ?? Singleton._internal();
}
方式五:利用操作符 late
操作符(工厂构造函数+空安全+箭头函数)
这里,使用 ??
作为 _instance
实例的判空操作符,如果为空则调用构造函数实例化否则直接返回,也可以达到单例的效果。
以上,Dart 单例中懒加载,是使用判空来实现的(if (_instance == null)
或 ??
),但是在 Dart 空安全特性里还有一个非常重要的操作符 late
,它在语言层面就实现了实例的懒加载,如下面这个例子:
这样没用 late
操作符写就达不到懒加载目的,类加载时候就已经初始化:
class Singleton {
/// 私有化构造方法,可避免外部暴露构造函数,进行实例化
Singleton._();
/// 静态私有成员变量
static final Singleton _instance = Singleton._();
///重写_instance 的get 方法
static Singleton get instance => _instance;
}
工厂构造函数+空安全+箭头函数 ,使用late
之后
class Singleton {
Singleton._internal();
factory Singleton() => _instance;
static late final Singleton _instance = Singleton._internal();
}
对比以上写法,被标记为 late
的变量 _instance
的初始化操作将会延迟到字段首次被访问时执行,而不是在类加载时就初始化。这样,Dart 语言特有的单例模式的实现方式就这么产生了。
Flutter 中的单例(Flutter化)
说到工厂构造函数/空安全操作符等 Dart 语法上的特性,Flutter 应用中的例子已经屡见不鲜了,但光看单例模式的定义,我们还必须联想到 Flutter 中另一个非常重要的 widget,那就是 InheritedWidget。
简单说下InheritedWidget:
InheritedWidget
和React
中的context
功能类似,可以实现跨组件数据的传递。
定义一个共享数据的InheritedWidget
,需要继承自InheritedWidget
updateShouldNotify
方法是对比新旧_InheritedStateContainer
,是否需要对更新相关依赖的Widget
引用下图源《Flutter 开发之旅从南到北》—— 第九章图 9.4
上面代码中,我们通过继承 InheritedWidget 就实现了自己的可遗传组件 _InheritedStateContainer
,其中的 data
变量表示全局状态数据,在这里就可以被认为是整个应用的一个单例对象。
_InheritedStateContainer
还接受 child
参数作为它的子组件,child
表示的所以子组件们就都能够以某种方式得到 data
这个单一的全局数据了。
使用的时候用_InheritedStateContainer将需要用到共享数据的widget进行包裹。
约定俗成地,Flutter 源码经常会提供一些 of
方法(类比 getInstance()
)作为帮助我们拿到全局数据的辅助函数。
以 Flutter 中典型的 Theme 对象为例。我们通常会在应用的根组件 MaterialApp
中创建 ThemeData
对象作为应用统一的主题样
式对象:
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
在其他任意的组件中,我们可以使用 Theme.of(context)
拿到该对象了,且这个对象全局唯一。如下所示,我们可以将该 ThemeData
对象中的 primaryColor
应用在 Text
中:
// 使用全局文本样式
Text(
'Flutter',
style: TextStyle(color: Theme.of(context).primaryColor),
)
这个角度来看,InheritedWidget 完全可以被我们看作是最原生、最 Flutter 的单例应用了.
小节:
从实现普通单例到应用 getter 操作符 的 Dart 单例,到使用 工厂构造函数 Dart 单例,再到使用了 工厂构造函数 + 空安全语法 + 箭头函数 的 Dart 单例,最后结合对 InheritedWidget 概念的理解,看到了 Flutter 中特有的单例模式。