1. Dart 中 Object、dynamic 和 var 有何不同
- Object
- 定义:Object 是 Dart 中所有类的基类,表示所有对象的父类。每个 Dart 对象都继承自 Object。
- 编译时类型检查:当变量类型为 Object 时,编译器会进行类型检查,并限制使用非 Object 方法。 如果需要调用特定子类的方法,通常需要先进行类型转换。
- 使用场景:适用于声明一个可以存储任何类型对象的变量,但不需要直接调用该对象的子类方法的场景。
Object obj = "Hello";
print(obj); // 可以直接打印
// obj.length 报错,因为编译器只识别到 Object 基类的方法,无法识别 String 的特有方法。
if (obj is String) {
print(obj.length); // 类型判断后,可以使用 String 的方法
}
-
dynamic
- 定义:dynamic 表示一个可以接收任何类型的变量,同时会跳过类型检查。
- 编译时类型检查:dynamic 变量在编译时不做类型检查,因此可以调用任何方法。若调用不存在的方法或属性,代码会在运行时而非编译时报错。
- 使用场景:适用于需要极大灵活性的场景,通常用于处理类型不确定的变量,但不推荐随意使用,因为它会绕过编译时检查,降低类型安全性。
dynamic dyn = "Hello";
print(dyn.length); // 正常访问,输出结果:5
dyn = 123;
print(dyn.length); // 运行时报错:int 类型没有 length 属性
// NoSuchMethodError: Class 'int' has no instance getter 'length'.
dynamic 提供了高度的灵活性,但容易出错,建议仅在类型无法确定时使用,并谨慎调用属性和方法
-
var
- 定义:var 表示“变量”,是一种类型推断的关键字,并不代表具体的数据类型。
- 编译时类型检查:var 会在变量第一次赋值时自动推断类型,之后该变量的类型是固定的,不能再赋值其他类型的值。
- 使用场景:var 适合用于声明变量时自动推断类型,既保证了代码的灵活性,又不会失去类型检查。
var msg = 'hello';
print(msg.runtimeType);
msg = 123; // A value of type 'int' can't be assigned to a variable of type 'String'.
var 是 Dart 推荐的变量声明方式,既能提供类型安全,又使代码简洁。
2. 为什么Object 是 Dart 中所有类的基类?
- 使用 is 关键字检查类型
由于所有类都继承自 Object,我们可以使用 is 关键字检查任意类型对象是否是 Object 的实例。
void main() {
int number = 42;
String text = "Hello";
List<int> numbers = [1, 2, 3];
print(number is Object); // true
print(text is Object); // true
print(numbers is Object); // true
}
- 检查 runtimeType 属性
所有 Dart 对象都具有一个 runtimeType 属性,该属性定义在 Object 类中,表示对象的运行时类型。 因为只有 Object 的子类才能使用 runtimeType,所以所有 Dart 对象能直接访问 runtimeType 就证明它们继承自 Object。
void main() {
var example = 100;
print(example.runtimeType); // 输出 int
}
- 自定义类不继承任何类
在 Dart 中,即使一个类没有显式地继承任何类,它也会自动继承 Object。这表明 Object 是 Dart 所有类的隐式基类。
class Person {
final String name;
const Person({required this.name});
@override // 重写了Object的toString
String toString() {
return "Person: $name";
}
}
void main(List<String> args) {
test();
Person p = const Person(name: "junmo");
print(p.toString()); // 默认的 toString 方法来自 Object 类
}
3. dart 的extension运算
在 Dart 中,extension 是一种用于向现有类添加新功能(方法、属性等)而无需修改原始类或创建子类的特性。extension 允许你在不改变类本身的情况下增强其功能,尤其对一些内置类型(如 String、int、List 等)很有帮助。
扩展 List 类
假设我们想要给 List 添加一个 sum 方法来计算数字列表的总和。
extension MyList on List<int> {
int get sum => fold(0, (s, cur) => s += cur);
}
void main() {
List <int> nums = [1, 3, 4, 5];
print(nums.sum); // 13
}
4. mixin 混入和 interface 接口在 Dart 中有何不同?
在 Dart 中,mixin 和 interface 是两种实现多态性和代码复用的不同方式。尽管两者在设计模式上有些相似,但它们的作用、用法和限制有所不同。下面是它们之间的主要区别及应用场景。
- mixin 混入
mixin 是一种代码复用机制,允许将一组方法和属性“混入”到一个或多个类中,而无需通过继承来实现。
mixin 可以用来实现共享行为和功能,使代码更加模块化和复用。
特点
- 不能实例化:mixin 本身不能创建实例。
- 无状态或少状态:通常用于实现无状态的功能模块(例如,提供一些工具方法或通用的行为)。
- 无需继承关系:任何类都可以直接使用 mixin,无需与 mixin 有继承关系。
mixin Logger {
void log(String message) {
print('Log: $message');
}
}
class Service with Logger {
void doSomething() {
log("Service is doing something.");
}
}
void main() {
var service = Service();
service.doSomething(); // 输出: Log: Service is doing something.
}
在这里:
- Logger 是一个 mixin,提供了 log 方法。
- Service 类使用 `with Logger` 来混入 Logger 的功能,而不需要继承 Logger。
适用场景
- 需要复用功能,但不希望创建新的继承层级。
- 多个类共享某种功能,且这些功能通常与类的核心职责无关(如日志、格式化工具等)。
2. interface 接口
在 Dart 中,没有显式的 interface
关键字。实际上,Dart 中的每个类都可以用作接口。
通过实现(implements)某个类,其他类可以定义相同的方法和属性,但需要自己实现这些方法。
特点
• 必须实现接口中的所有方法:如果一个类实现了某个接口,那么它必须提供该接口中所有方法和属性的具体实现。
• 支持多继承:一个类可以实现多个接口,从而实现类似多继承的效果。
• 主要用于定义行为规范:接口只描述类的结构,不包含具体实现,因此适合用来定义一组行为规范。
用法
class Printer {
void printDocument();
}
class Scanner {
void scanDocument();
}
class MultiFunctionDevice implements Printer, Scanner {
@override
void printDocument() {
print("Printing document...");
}
@override
void scanDocument() {
print("Scanning document...");
}
}
void main() {
var device = MultiFunctionDevice();
device.printDocument(); // 输出: Printing document...
device.scanDocument(); // 输出: Scanning document...
}
在这里:
- Printer 和 Scanner 充当接口,因为它们只是定义了方法而没有具体实现。
- MultiFunctionDevice 类实现了这两个接口,因此必须提供 printDocument 和 scanDocument 的实现。
适用场景
- 定义一组规范,确保实现类包含某些方法。
- 适用于需要为多种类提供一组标准行为的情况。
补充:
extend
用于继承抽象类的实现
当一个类 使用 extend 继承 抽象类时,它继承了抽象类中所有的已实现方法、属性以及未实现的抽象方法。
继承抽象类时,子类可以选择性地重写继承的方法和属性,必须实现父类中未实现的抽象方法
implement
用于实现抽象类的接口
当一个类 使用 implement
实现 抽象类时,它不会继承任何实现,即使抽象类中有已实现的方法。
使用 implement 时,子类必须重新实现抽象类中所有的方法和属性,包括抽象的和已实现的## 1. Dart 中 Object、dynamic 和 var 有何不同
- Object
- 定义:Object 是 Dart 中所有类的基类,表示所有对象的父类。每个 Dart 对象都继承自 Object。
- 编译时类型检查:当变量类型为 Object 时,编译器会进行类型检查,并限制使用非 Object 方法。 如果需要调用特定子类的方法,通常需要先进行类型转换。
- 使用场景:适用于声明一个可以存储任何类型对象的变量,但不需要直接调用该对象的子类方法的场景。
Object obj = "Hello";
print(obj); // 可以直接打印
// obj.length 报错,因为编译器只识别到 Object 基类的方法,无法识别 String 的特有方法。
if (obj is String) {
print(obj.length); // 类型判断后,可以使用 String 的方法
}
- dynamic
- 定义:dynamic 表示一个可以接收任何类型的变量,同时会跳过类型检查。
- 编译时类型检查:dynamic 变量在编译时不做类型检查,因此可以调用任何方法。若调用不存在的方法或属性,代码会在运行时而非编译时报错。
- 使用场景:适用于需要极大灵活性的场景,通常用于处理类型不确定的变量,但不推荐随意使用,因为它会绕过编译时检查,降低类型安全性。
dynamic dyn = "Hello";
print(dyn.length); // 正常访问,输出结果:5
dyn = 123;
print(dyn.length); // 运行时报错:int 类型没有 length 属性
// NoSuchMethodError: Class 'int' has no instance getter 'length'.
dynamic 提供了高度的灵活性,但容易出错,建议仅在类型无法确定时使用,并谨慎调用属性和方法
- var
- 定义:var 表示“变量”,是一种类型推断的关键字,并不代表具体的数据类型。
- 编译时类型检查:var 会在变量第一次赋值时自动推断类型,之后该变量的类型是固定的,不能再赋值其他类型的值。
- 使用场景:var 适合用于声明变量时自动推断类型,既保证了代码的灵活性,又不会失去类型检查。
var msg = 'hello';
print(msg.runtimeType);
msg = 123; // A value of type 'int' can't be assigned to a variable of type 'String'.
var 是 Dart 推荐的变量声明方式,既能提供类型安全,又使代码简洁。
2. 为什么Object 是 Dart 中所有类的基类?
- 使用 is 关键字检查类型
由于所有类都继承自 Object,我们可以使用 is 关键字检查任意类型对象是否是 Object 的实例。
void main() {
int number = 42;
String text = "Hello";
List<int> numbers = [1, 2, 3];
print(number is Object); // true
print(text is Object); // true
print(numbers is Object); // true
}
- 检查 runtimeType 属性
所有 Dart 对象都具有一个 runtimeType 属性,该属性定义在 Object 类中,表示对象的运行时类型。 因为只有 Object 的子类才能使用 runtimeType,所以所有 Dart 对象能直接访问 runtimeType 就证明它们继承自 Object。
void main() {
var example = 100;
print(example.runtimeType); // 输出 int
}
- 自定义类不继承任何类
在 Dart 中,即使一个类没有显式地继承任何类,它也会自动继承 Object。这表明 Object 是 Dart 所有类的隐式基类。
class Person {
final String name;
const Person({required this.name});
@override // 重写了Object的toString
String toString() {
return "Person: $name";
}
}
void main(List<String> args) {
test();
Person p = const Person(name: "junmo");
print(p.toString()); // 默认的 toString 方法来自 Object 类
}
3. dart 的extension运算?
在 Dart 中,extension 是一种用于向现有类添加新功能(方法、属性等)而无需修改原始类或创建子类的特性。extension 允许你在不改变类本身的情况下增强其功能,尤其对一些内置类型(如 String、int、List 等)很有帮助。
扩展 List 类
假设我们想要给 List 添加一个 sum 方法来计算数字列表的总和。
extension MyList on List<int> {
int get sum => fold(0, (s, cur) => s += cur);
}
void main() {
List <int> nums = [1, 3, 4, 5];
print(nums.sum); // 13
}
4. mixin 混入和 interface 接口在 Dart 中有何不同?
在 Dart 中,mixin 和 interface 是两种实现多态性和代码复用的不同方式。尽管两者在设计模式上有些相似,但它们的作用、用法和限制有所不同。下面是它们之间的主要区别及应用场景。
- mixin 混入
mixin 是一种代码复用机制,允许将一组方法和属性“混入”到一个或多个类中,而无需通过继承来实现。
mixin 可以用来实现共享行为和功能,使代码更加模块化和复用。
特点
- 不能实例化:mixin 本身不能创建实例。
- 无状态或少状态:通常用于实现无状态的功能模块(例如,提供一些工具方法或通用的行为)。
- 无需继承关系:任何类都可以直接使用 mixin,无需与 mixin 有继承关系。
mixin Logger {
void log(String message) {
print('Log: $message');
}
}
class Service with Logger {
void doSomething() {
log("Service is doing something.");
}
}
void main() {
var service = Service();
service.doSomething(); // 输出: Log: Service is doing something.
}
在这里:
- Logger 是一个 mixin,提供了 log 方法。
- Service 类使用
with Logger
来混入 Logger 的功能,而不需要继承 Logger。
适用场景
- 需要复用功能,但不希望创建新的继承层级。
- 多个类共享某种功能,且这些功能通常与类的核心职责无关(如日志、格式化工具等)。
- interface 接口
在 Dart 中,没有显式的 interface
关键字。实际上,Dart 中的每个类都可以用作接口。
通过实现(implements)某个类,其他类可以定义相同的方法和属性,但需要自己实现这些方法。
特点
- 必须实现接口中的所有方法:如果一个类实现了某个接口,那么它必须提供该接口中所有方法和属性的具体实现。
- 支持多继承:一个类可以实现多个接口,从而实现类似多继承的效果。
- 主要用于定义行为规范:接口只描述类的结构,不包含具体实现,因此适合用来定义一组行为规范。
用法
class Printer {
void printDocument();
}
class Scanner {
void scanDocument();
}
class MultiFunctionDevice implements Printer, Scanner {
@override
void printDocument() {
print("Printing document...");
}
@override
void scanDocument() {
print("Scanning document...");
}
}
void main() {
var device = MultiFunctionDevice();
device.printDocument(); // 输出: Printing document...
device.scanDocument(); // 输出: Scanning document...
}
在这里:
- Printer 和 Scanner 充当接口,因为它们只是定义了方法而没有具体实现。
- MultiFunctionDevice 类实现了这两个接口,因此必须提供 printDocument 和 scanDocument 的实现。
适用场景
- 定义一组规范,确保实现类包含某些方法。
- 适用于需要为多种类提供一组标准行为的情况。
补充:
extend
用于继承抽象类的实现
当一个类 使用 extend 继承 抽象类时,它继承了抽象类中所有的已实现方法、属性以及未实现的抽象方法。
继承抽象类时,子类可以选择性地重写继承的方法和属性,必须实现父类中未实现的抽象方法
implement
用于实现抽象类的接口
当一个类 使用 implement
实现 抽象类时,它不会继承任何实现,即使抽象类中有已实现的方法。
使用 implement 时,子类必须重新实现抽象类中所有的方法和属性,包括抽象的和已实现的
5. Dart 中的空安全是什么?
Dart 中的空安全(Null Safety)是一项语言特性,旨在防止 null 引发的运行时错误,使开发人员更清晰地处理可能为 null 的情况,从而提高代码的可靠性和可读性。
空安全的核心概念
在启用空安全的 Dart 中,所有类型默认是 非空类型。这意味着 变量默认不允许为 null,除非你显式地将其标记为可以为空(使用 ? 符号)。这使得 Dart 编译器可以在编译时帮助你捕捉潜在的 null 引发的错误,而不是在运行时才发现。
使用空安全的语法
1. 非空类型(默认)
- 不带 ? 的类型(例如 int、String 等)是 非空类型,即该变量不能为空。
- 编译器会强制检查这些变量是否被赋值。
int number = 42; // 非空类型
number = null; // 错误:无法将 `null` 赋值给 `int`
int a;
print(a); // The non-nullable local variable 'a' must be assigned before it can be used.
2. 可空类型(带 ?)
如果一个变量可以为空,则需要在类型后添加 ?(例如 int?、String?),表示该变量可能为 null。
int? number = 42; // 可空类型
number = null; // 允许:`number` 可以为空
3. 空值检查和处理
Dart 提供了多种方式来安全地处理可能为空的变量:
- 条件访问符 ?. 用于在对象可能为 null 时访问其属性或方法。只有在对象不为 null 时才会执行后续操作,否则直接返回 null。
String? name;
print(name?.length); // 输出: null,不会引发错误
- 默认值运算符 ??:当变量为 null 时提供一个默认值。
String? name;
print(name ?? "默认值"); // 输出: 默认值
- 非空断言 !:在你确信某个可空变量不为 null 时,可以在变量后加 !,表示断言该值不为空。
如果断言失败(即变量实际上为 null),将引发运行时错误。
String? name;
print(name!.length); // 如果 name 为 null,这将抛出异常
4. Late 变量
Dart 提供了 late 关键字,用于声明稍后初始化的非空变量。这在某些情况下很有用,例如,变量在构造函数或异步方法中延迟赋值。
late String description;
void setup() {
description = "Dart 是一门编程语言";
}
空安全的好处
-
提高代码可靠性:通过空安全机制,编译器在编译时会捕捉潜在的 null 错误,使代码更稳定、健壮。
-
增强可读性和意图表达:使用空安全特性后,变量是否可以为 null 一目了然,从而提高代码的可读性。
-
减少运行时错误:空安全机制可以在编译阶段防止许多因 null 引发的运行时错误,提升代码质量。
6. Dart 中的 Isolate
Isolate 是 Dart 中的一种线程模型,与传统的多线程有些区别。在 Dart 中,每个 Isolate 拥有自己的内存堆,且 Isolate 之间不能直接共享内存,因此避免了数据竞争的风险。
特点
- 独立内存空间:每个 Isolate 拥有自己的内存堆,与其他 Isolate 隔离,确保并发安全。
- 无数据竞争:由于没有共享内存,Isolate 不会出现传统多线程中的数据竞争问题。
- 通信方式:不同的 Isolate 通过消息传递(SendPort 和 ReceivePort)进行通信,而不是共享内存。
使用场景
Dart 的 Isolate 非常适合需要高性能计算的任务,如图像处理、文件解析、大型数据操作等。
尤其是在 Flutter 中,Isolate 可以避免阻塞主线程,从而确保界面的流畅性。
void executeTask(List<dynamic> args) {
int taskId = args[0] as int ;
SendPort sendPort = args[1];
print("taskId, $taskId"); // taskId, 1
Future.delayed(Duration(seconds: 2), (){
sendPort.send({taskId: taskId, "res" : "success"});
});
}
void testIsolate() async {
ReceivePort port = ReceivePort();
await Isolate.spawn(executeTask, [1, port.sendPort]);
port.listen((dynamic msg){
if (msg is Map) {
print(msg); // {1: 1, res: success}
}
});
}
int main() {
testIsolate();
}
7. Dart 中的Event loop
Event Loop(事件循环)
Event Loop 是 Dart 的事件处理机制,负责处理异步任务队列。它确保 Dart 程序可以以非阻塞的方式执行任务。
工作原理
- 主线程:Dart 应用默认运行在主线程上,主线程中只有一个事件循环。
- 事件队列:异步操作(如定时器、I/O 操作)完成后,会将任务添加到事件队列中。
- 微任务队列:某些异步任务会添加到一个称为“微任务队列”的优先级队列中,比如
Future.microtask
。 - 任务调度顺序:事件循环会优先执行微任务队列中的任务,然后再处理事件队列中的任务。
void main() {
print('任务1');
Future.delayed(Duration(seconds: 1), () {
print('任务2');
});
Future(() {
print('任务3');
});
Future.microtask(() {
print('任务4');
});
print('任务5');
}
输出结果可能是:
任务1
任务5
任务4
任务3
任务2
- 任务4 是微任务,它会在主任务完成后立即执行。
- 任务3 是普通异步任务,添加到事件队列中,等待微任务执行完成后运行。
- 任务2 是延迟异步任务,它会在 1 秒后添加到事件队列中。
8. Dart 中的Future
Future 是 Dart 用于处理异步操作的核心类,表示一个将来会完成的任务(例如网络请求、文件读取等)。Future 的结果可以是完成或失败。
工作流程
- 未完成(Pending):Future 刚开始执行异步任务时处于未完成状态。
- 完成(Complete):Future 成功返回结果,或失败返回错误。
常用方法
- then:用于在 Future 完成时处理结果。
- catchError:用于捕获 Future 执行过程中的错误。
- whenComplete:无论成功或失败都执行的回调。
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class Baidu extends StatefulWidget {
const Baidu({super.key});
@override
_BaiduState createState() => _BaiduState();
}
class _BaiduState extends State<Baidu> {
get resp => null;
Future<dynamic> fetchData() async {
final url = Uri.parse("https://www.baidu.com");
final resp = await http.get(url);
if (resp.statusCode == 200) {
return resp.body;
} else {
throw Exception("fetch data failed");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("baidu"),),
body: FutureBuilder(
future: fetchData(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot){
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: const CircularProgressIndicator(),);
} else if (snapshot.hasError) {
return Center(child: Text('${snapshot.error}'),);
} else {
print(snapshot.data);
return Center(child: Text('${snapshot.data}'),);
}
}),
);
}
}
使用 FutureBuilder:FutureBuilder 通过监听 Future 的状态来更新 UI,显示加载进度、错误信息,或者最终的请求数据。
snapshot.connectionState 用于判断请求状态,snapshot.data 包含请求返回的数据
9. Dart 语言 final 和 const 有什么不同?
在 Dart 语言中,final 和 const 都用于声明不可变的变量,但它们有一些关键的区别,尤其是在初始化时机和使用场景上。
- final 关键字
- 定义:final 变量在第一次被赋值后就不可更改,但可以在运行时动态赋值。
- 初始化时机:final 变量在运行时初始化。这意味着可以根据条件动态赋值。
- 使用场景:适用于在运行时确定、但一旦确定后不可变的值,比如网络请求的结果、时间戳等。
示例:
final timeNow = DateTime.now(); // 在运行时赋值
final userName = getUserInput(); // 从用户输入或其他运行时数据源获取值
- const 关键字
- 定义:const 变量是编译时常量,意味着它们的值在编译期间就确定,并且始终不可变。
- 初始化时机:const 变量在编译时初始化,要求所有的赋值都是常量表达式。
- 使用场景:适用于值在编译时就可以确定的常量,比如固定字符串、数学常量等。
示例:
const pi = 3.14159; // 编译时常量
const list = [1, 2, 3]; // 常量列表
const myConstObject = MyClass(); // 常量对象
- final 与 const 的区别
- 初始化时机不同:final 是在运行时初始化,而 const 是在编译时初始化。
- 可变性:final 可以根据不同的运行时条件赋值一次,而 const 必须在编译期间确定,并且始终不可变。
- 类中的使用:在类中,const 变量必须是 static const,而 final 变量则可以是实例变量。
举例比较
void main() {
final timeNow = DateTime.now(); // 合法,运行时确定
// const compileTime = DateTime.now(); // 不合法,编译时无法确定
const list1 = [1, 2, 3]; // 完全不可变
final list2 = [1, 2, 3]; // list2 本身不可变,但内部元素可以改变
list2[0] = 10; // 合法
// list1[0] = 10; // 不合法,list1 完全不可变
}
总结
final 适合在运行时确定且不可再改变的值。 const 适合在编译时就能确定的完全不可变的值。
10. Dart 中有哪些访问修饰符?
在 Dart 中,没有传统的 public、private、protected 修饰符。 Dart 使用一种简洁的命名约定来控制可见性,只有以下两种访问控制:
- public(公开)
- 定义:默认情况下,Dart 中的所有类、函数、变量都是公开的。
- 命名约定:公开的成员名称直接使用普通的命名方式,无需前缀。
- 访问范围:公开的成员可以被同一库(library)或其他库引用和访问。
示例:
class Person {
String name = 'Alice'; // 公开变量
void sayHello() { // 公开方法
print('Hello, $name!');
}
}
在上面的代码中,name 和 sayHello 都是公开的,其他地方可以直接访问。
- private(私有)
- 定义:Dart 使用下划线 _ 前缀来表示私有成员。
- 命名约定:私有成员名称以 _ 开头。
- 访问范围:私有成员只能在同一个库(library)内访问,不会对其他库公开。
示例:
class Person {
String _id = '12345'; // 私有变量
void _displayId() { // 私有方法
print('ID: $_id');
}
}
在这个示例中,_id
和 _displayId
都是私有的,只能在定义它们的库中使用,无法在库外直接访问。
补充说明:Dart 库(Library)与访问控制
在 Dart 中,文件是通过 library 系统组织的。每个文件默认是一个独立的库,或者可以通过 part 关键字将多个文件组合成一个库。 因此,访问控制是在库级别而不是类级别上进行的。
举例说明:
假设有两个 Dart 文件: file1.dart
class MyClass {
String publicVar = 'public'; // 公开变量
String _privateVar = 'private'; // 私有变量
}
file2.dart
import 'file1.dart';
void main() {
var obj = MyClass();
print(obj.publicVar); // 可以访问
print(obj._privateVar); // 错误,无法访问
}
在 file2.dart 中,可以访问 publicVar,但尝试访问 _privateVar 会报错,因为它是私有的,只能在 file1.dart 中访问。
总结
- Dart 只有两种访问级别:公开(默认)和私有(用 _ 表示)。
- 访问控制基于库(library),而不是类或包。
Dart 语言 命名参数、可选参数 是什么?
在 Dart 中,函数参数有几种不同的传递方式,其中 命名参数
和 可选参数
是常用的两种。它们允许你在调用函数时更加灵活地传递参数,并使代码更加易读。
- 命名参数(Named Parameters)
- 定义:命名参数使用大括号 {} 包围,允许在调用函数时通过名称传递参数。
- 特点:命名参数在调用时可以按需传递,顺序不重要。命名参数默认是可选的,但可以通过 required 关键字标记为必填。
- 优点:代码可读性高,调用函数时可以清晰地表明每个参数的含义。
示例
void createUser({required String name, int age = 18, String? city}) {
print('Name: $name, Age: $age, City: ${city ?? "Unknown"}');
}
void main() {
createUser(name: 'Alice', age: 25, city: 'New York'); // 指定所有参数
createUser(name: 'Bob'); // 只传必需参数
}
在上面的例子中:
- name 是必填参数,因为使用了 required 关键字。
- age 是可选的命名参数,默认值为 18。
- city 是可选的命名参数,可以传 null 或省略。
调用时,可以按任意顺序传递命名参数:
createUser(name: 'Charlie', city: 'London');
- 可选位置参数(Optional Positional Parameters)
- 定义:可选位置参数使用方括号 [] 包围,是一组按照位置传递的可选参数。
- 特点:调用时可以省略这些参数,如果省略则会使用默认值(若有指定)。
- 优点:可以在参数列表较短时使用,调用时更加简洁,但不适合参数较多且易混淆的情况。
示例
void greet(String firstName, [String? lastName, String? title = 'Mr/Ms']) {
print('Hello, $title ${lastName ?? firstName}');
}
void main() {
greet('Alice'); // 只传一个参数
greet('Alice', 'Smith'); // 传两个参数
greet('Alice', 'Smith', 'Dr.'); // 传三个参数
}
在这个例子中:
- firstName 是必需的,而 lastName 和 title 是可选的。
- 如果未传递 lastName,则默认使用 firstName。
- title 默认值为 'Mr/Ms'。
命名参数和可选位置参数的区别
- 命名参数:更灵活,可读性高,尤其适合参数较多的情况。
- 可选位置参数:适合参数较少、调用简单的场景。
总结
- 命名参数 {}:通过名称传递,顺序不重要,默认可选,但可以标记为必填。
- 可选位置参数 []:按位置传递,通常在参数少的情况下使用。
在 Dart 中,命名构造函数和工厂构造函数都用于创建对象实例,但它们的使用场景和工作原理有所不同。
-
命名构造函数(Named Constructor)
• 定义:命名构造函数是为类定义的一个带有特定名称的构造函数,可以通过 类名.构造函数名 的方式来调用。 • 用法:一个类可以有多个命名构造函数,以便为不同的初始化需求提供方便的创建方式。 • 特点:命名构造函数与默认构造函数一样,直接返回类的一个新实例。
示例
class Point {
double x, y;
// 默认构造函数
Point(this.x, this.y);
// 命名构造函数,用于创建原点
Point.origin() : x = 0, y = 0;
// 命名构造函数,用于创建单位坐标
Point.unitX() : x = 1, y = 0;
}
void main() {
Point p1 = Point(3, 4); // 使用默认构造函数
Point p2 = Point.origin(); // 使用命名构造函数创建原点
Point p3 = Point.unitX(); // 使用命名构造函数创建单位坐标
}
在这个例子中:
- Point.origin 和 Point.unitX 是 Point 类的命名构造函数,分别创建原点和单位坐标。
- 命名构造函数在逻辑上帮助区分不同的初始化方式,但每次调用都会创建一个新实例。
- 工厂构造函数(Factory Constructor)
- 定义:工厂构造函数使用 factory 关键字定义,可以选择性地返回类的现有实例,而不必每次都创建新对象。
- 用法:工厂构造函数通常用于单例模式或缓存对象,或者在构造函数返回的类型与定义的类本身不完全一致的情况下。
- 特点:工厂构造函数可以控制实例的创建过程,因此不会总是创建新对象。
示例:单例模式
class DatabaseConnection {
// 创建一个静态私有实例
static final DatabaseConnection _instance = DatabaseConnection._internal();
// 工厂构造函数,返回唯一实例
factory DatabaseConnection() {
return _instance;
}
void query(String sql) {
print('Executing query: $sql');
}
}
void main() {
var db1 = DatabaseConnection();
var db2 = DatabaseConnection();
print(identical(db1, db2)); // 输出 true,说明 db1 和 db2 是同一个实例
}
总结
- 命名构造函数:适合需要提供多种初始化方式的情况,每次调用都会创建一个新对象。
- 工厂构造函数:适合需要控制实例创建、返回缓存实例或单例的情况,使用 factory 关键字。