0%

Dart 语法原来这么好玩儿(二)

上一篇 Dart 语法的文章中介绍了对 FutureStreamsunwrap 操作以及 ListMap的展开、合并和过滤等等,总觉得有点意犹未尽,还有很多有意思的并没有提到,本篇文章来介绍一下 Dart 语言中面向对象的内容——构造函数,在我们日常开发中,对象的初始化(构造函数)有哪些方式,它们之间的区别在哪里,不同的构造函数都适用于哪些场景呢,本篇文章来聊聊这些内容。

Named 构造函数

如果没有定义构造函数,Dart 会默认创建一个不带参数的默认构造函数。它用于创建类的简单实例,通常不带参数或仅初始化对象的基本属性。Dart 还允许在同一类中定义多个构造函数,每个构造函数通过名称区分。命名构造函数提供了更多灵活性,适用于不同初始化需求。命名构造函数使用类名加点(ClassName.constructorName)的形式定义。

1
2
3
4
5
6
7
8
9
class Rectangle {
double width, height;

// 常规构造函数
Rectangle(this.width, this.height);

// 命名构造函数
Rectangle.square(double size) : width = size, height = size;
}

这里还可以使用重定向构造函数,简化代码逻辑,将多个构造函数重定向到同一个主构造函数,从而减少重复代码,修改 Rectangle.square 构造函数的代码:

1
2
// 重定向构造函数
Rectangle.square(double size) : this(size, size);

Flutter源码中这种类似用法有很多,如 EdgeInsets 类,有EdgeInsets.fromLTRB、EdgeInsets.all、EdgeInsets.only 等等。

Factory 构造函数

工厂构造函数使用 factory 关键字,可以在每次调用构造函数时返回相同的实例,也可以根据逻辑返回不同的实例。它适用于需要缓存或复杂实例创建逻辑的情况,比如我们常见的单例模式(Singleton Pattern)。

1
2
3
4
5
6
7
8
9
10
11
class Logger {
static final Logger _instance = Logger._internal();

// 私有命名构造函数
Logger._internal();

// 工厂构造函数返回单例
factory Logger() {
return _instance;
}
}

如下代码复杂一点的用法,利用 Factory 构造函数 来根据不同参数返回不同类型的形状对象(CircleSquare),可实现灵活的创建对象,不同于标准构造函数,Factory 构造函数可以返回子类,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enum ShapeType { circle, square }

class Shape {
final ShapeType type;
const Shape(this.type);
factory Shape.fromRadius(double radius) => Circle(radius);
factory Shape.fromRect(Rect rect) => Square(rect);
@override
String toString() => 'I am a shape of type $type';
}

class Circle extends Shape {
final double radius;
const Circle(this.radius) : super(ShapeType.circle);
@override
String toString() => super.toString() + ' and I am a circle';
}

class Square extends Shape {
final Rect rect;
const Square(this.rect) : super(ShapeType.square);
@override
String toString() => super.toString() + "and I am a square";
}

Factory 构造函数在对象创建的过程中提供了统一且灵活的接口,特别适用于需要根据不同条件创建不同子类实例的场景。实际项目中 Factory 构造函数的应用场景也很多,尤其在处理复杂对象创建和资源优化方面。

以下是 Factory 构造函数和普通构造函数的区别。

Static 构造函数

实际上在 Dart 中, Static 构造函数并不是像有的编程语言(如 C#)中那样的有内置特性。也就是说 Dart 中没有直接的静态构造函数概念,但可以通过静态方法来实现类似的功能。静态方法可以用于初始化类级别的共享资源、执行单例模式和延迟加载等场景。将上面的单例类(Logger)改成 Static 构造函数来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Logger {
static final Logger _instance = Logger._internal();

// 私有构造函数
Logger._internal();

// 静态构造函数:通过静态方法返回单例实例
static Logger initialize() {
print("Logger initialized");
return _instance;
}

void log(String message) {
print("Log: $message");
}
}

void main() {
// 调用静态构造函数
final logger = Logger.initialize();
logger.log("This is a log message.");
}

静态构造函数结合 late 变量可以达到也可以延迟加载的机制,例如,设计一个依赖初始化数据库的类,来看看是怎么实现的。

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
class DatabaseConnection {
static late DatabaseConnection _instance;
static bool _isInitialized = false;

DatabaseConnection._create();

// 静态构造函数用于初始化
static void initialize() {
if (!_isInitialized) {
_instance = DatabaseConnection._create();
print("Database connection initialized.");
_isInitialized = true;
}
}

static DatabaseConnection get instance => _instance;

void query(String sql) {
print("Executing query: $sql");
}
}

void main() {
DatabaseConnection.initialize();
DatabaseConnection.instance.query("SELECT * FROM users");
}

上面的代码中,late 关键字保证变量在第一次使用时才初始化,从而实现延迟加载, _internal 私有构造函数防止外部直接实例化。 getInstance 静态工厂方法是单例实例的全局访问入口。延迟加载常用于当初始化逻辑较为复杂,需要耗费大量资源时,例如,加载配置文件、初始化数据库连接等,或者不希望类在加载时就立即初始化,而是按需使用时才初始化。

FactoryStatic 构造函数区别

上面分别介绍了 Factory 构造函数和 Static 构造函数,那么它们之间有什么区别呢?Factory 构造函数返回已有实例或子类实例,控制具体对象的创建过程,而不是初始化与类绑定的静态资源;而 Static 构造函数用于初始化与类本身相关的资源,如设置静态变量或执行静态初始化逻辑,而不创建具体实例。

还有需要补充的一点就是对泛型参数的支持,在工厂构造函数中可以直接使用该类泛型参数,而无需在其函数中重新定义它,而是静态构造函数,必须重新定义泛型参数,因为静态构造函数不能从其类中继承泛型参数,借助代码来理解一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@immutable
class Person<P> {
final String name;
final P property;

const factory Person(String name, P property) = Person.fromProperties;

const Person.fromProperties(this.name, this.property);

// 无需重新定义泛型 P,直接使用
factory Person.fromPropertiesFactory(String name, P property) =>
Person.fromProperties(name, property);

// 必须重新定义泛型参数 <P>
static Person<P> fromPropertiesStatic<P>(String name, P property) =>
Person.fromProperties(name, property);

factory Person.fooBar(P property) =>
Person.fromProperties('Foo Bar', property);

static Person<P> bazQux<P>(P property) =>
Person.fromProperties('Baz Qux', property);
}

此外,工厂构造函数支持重定向等功能,而静态方法则无法提供这些功能。使用工厂构造函数创建对象有助于明确代码的目的和意图。

构造函数与抽象类

和其它语言中的抽象类一样,Dart的抽象类也是不能被实例化的,但抽象类构造函数可以用于初始化被继承的字段或逻辑,子类可以在构造函数中使用 super 调用父类的构造函数,这样方便为子类提供模板化的构造流程。在设计模式中,抽象类的构造函数常用于定义公共初始化逻辑,确保子类继承时遵循一致性。还有当一个类定义了 final 修饰的字段时,可以通过抽象类的构造函数初始化这些字段。也把二者之间的区别做个对比。

结合代码来理解一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Type { dog, cat }

abstract class Animal {
final Type type;
// 初始化被继承的字段或逻辑
const Animal({required this.type});
}

class Cat extends Animal {
// 使用 super 调用父类的构造函数
const Cat() : super(type: Type.cat);
}

class Dog extends Animal {
// 使用 super 调用父类的构造函数
const Dog() : super(type: Type.dog);
}

小结

文章中介绍了命名构造函数、FactoryStatic 构造函数、构造函数与抽象类以及它们之间的区别,也提到它们各自的应用场景,怎么样,这些构造函数的打开方式你 Get 到了吗?如有疑问,欢迎留言。

关注公众号,第一时间看到更新内容:

Flutter技术实践