引言
本文接上篇:从0到1掌握Flutter(三)Dart语法
本篇系统讲述了 Dart 语言中方法、类与异常处理机制,包括基本用法、代码示例以及使用场景。重点讲述 Dart 与其他语言的不同之处。
对于具备 Java/Kotlin 背景的学习者,可以通过对比学习法快速定位知识缺口,理解语法的共性。
💡 Dart 的命名规范
- 变量名、函数名和方法名: 使用 小驼峰 命名格式。例如
myVariable
,aFunction
。 - 类名: 使用 大驼峰 命名格式,即所有单词的首字母全部大写,如
MyClass
。 - 常量、文件名、库和包名: 小写字母和下划线的形式 ,例如
my_constant_value
。
一、方法
1.1 方法基础
💡 方法定义
最规范的写法需要明确声明参数和返回值的类型:
arduino
int add(int i, int j) {
return i + j;
}
虽然 Dart 支持省略类型声明,但这样会降低代码可读性(写法不推荐):
javascript
add(i, j) { // 类型被隐式推断为dynamic
return i + j;
}
当方法体只有一个表达式时,可以使用箭头语法简化。简化后的箭头函数写法:
arduino
int add(int i, int j) => i + j; // 适用于简单返回表达式的情况
这三种写法在实际开发中的优先级应该是:明确类型声明 > 箭头语法简化 > 省略类型声明。
💡 方法类型定义
当你有非常复杂的函数类型,通过 typedef
可以给它们重新取个简短的名字。
java
typedef ManyOperation = int Function(int firstNum, int secondNum);
typedef IntList = List<int>;
// 使用示例:
void main() {
ManyOperation addOperation = (int firstNum, int secondNum) {
return firstNum + secondNum;
};
IntList myNumbers = [1, 2, 3, 4, 5];
}
💡 方法对象
前面在《从0到1掌握Flutter(三)Dart语法》中提到过,在 Dart 语言中万物皆对象,方法不仅仅是一段可执行的代码,本身也是对象,类型是 Function
。
我们可以像操作普通变量一样来处理方法,很像 kotlin 中的方法。这种特性非常灵活,最直观的体现是变量存储方法:
dart
// 声明一个与函数签名匹配的变量
Function calculate;
// 将加法函数赋值给变量
calculate = (int a, int b) => a + b;
// 通过变量调用方法
print(calculate(3,5)); // 输出 8
// 可以随时更换绑定的方法实现
calculate = (int x, int y) => x * y;
这种特性在处理回调时尤其方便。例如 Android 开发是,Java 中要实现点击事件需要定义整个 OnClickListener 接口。但在 Dart 中,我们可以直接把回调方法作为参数传递:
javascript
//打印方法
void log(){
print('按钮被点击了!');
}
// 定义接收回调的方法
void setClickListener(void Function() onClick) {
// 在适当时机触发回调
onClick();
}
// 使用时直接传入方法
setClickListener(log);
💡 匿名方法
匿名方法既没有名称的方法,这种方法常用于快速定义方法逻辑:
javascript
// 定义接收回调的方法
void setClickListener(void Function() onClick) {
// 在适当时机触发回调
onClick();
}
// 使用时直接传入匿名方法
setClickListener((){
print('按钮被点击了!');
});
//将匿名方法赋值给对象
Function calculate = (int a, int b) => a + b;
💡 私有方法
Dart 中没有访问控制修饰符,方法的访问控制是通过命名约定来实现的。声明私有方法需要在方法名前加上下划线 _
。这种命名规范不仅适用于方法,也适用于类成员变量:
csharp
class Product {
// 外部可访问的公开方法
double getFinalPrice() {
return _calculateDiscount(basePrice);
}
// 私有方法
double _calculateDiscount(double price) {
return price * 0.9; // 打九折
}
}
1.2 方法参数
💡 可选命名参数
在 Dart 中,通过花括{}
号包裹的参数为可选命名参数。
这种参数传递方式可以灵活处理参数缺省的情况。来看一个典型应用场景:
scss
int add({int? i, int? j}) {
return (i ?? 0) + (j ?? 0);
}
//调用该方法时,参数的传递方式非常灵活:
//完全省略参数:会返回0+0=0
add()
//单参数传递:相当于1+0=1
add(i:1)
//完整参数传递:得到3,且参数顺序可交换为add(j:2, i:1),结果不变
add(i:1, j:2)
💡 可选位置参数
通过方括号[]
包裹的参数为可选位置参数。传值时按照参数位置顺序传递
php
int multiply([int? a, int? b]) {
// 当任意参数未传递时,视为乘以1(保持原值)
final num1 = a ?? 1;
final num2 = b ?? 1;
return num1 * num2;
}
// 调用示例:
void main() {
print(multiply()); // 输出:1 (1x1)
print(multiply(5)); // 输出:5 (5x1)
print(multiply(3,4)); // 输出:12
}
💡 默认参数值
在定义可选命名参数和可选位置参数时,我们可以在定义方法时为参数提供默认值,当调用者不传递该参数时,就会自动使用预设的默认值:
arduino
// 位置可选参数形式
int addPositional([int i = 1, int j = 2]) => i + j;
// 命名可选参数形式
int addNamed({int i = 1, int j = 2}) => i + j;
这种设计保证了方法调用的灵活性,又避免了因参数缺失导致的运行时错误,提升了代码健壮性。
💡 必需参数
当我们需要强制调用者传递某个命名参数时,可以使用 required
修饰符。这个标识符会触发编译时检查,确保该参数在调用时必定被传递,否则会产生编译错误。
javascript
// 强制要求传递name参数
void printName({required String name}) {
print('Hello, $name!');
}
// 正确调用
printName(name: 'Alice'); // 输出:Hello, Alice!
// 错误调用(编译不通过)
// printName(); ❌ 缺少必需的命名参数'name'
二、类
2.1 构造函数
💡 初始化列表
当我们需要通过构造函数为类的属性赋值时,常常会重复编写为属性赋值的代码。Dart 专门提供了简洁的语法糖-初始化列表来优化这个流程:
kotlin
class Point {
num x;
num y;
// 最简写法:主构造参数直接映射到属性(隐式初始化列表)
Point(this.x, this.y);
// 命名构造:通过表达式初始化(显式初始化列表)
Point.xAxis(int position) : x = position, y = 0; // 根据参数计算初始值
// 数据转换构造:从 Map 解析数据初始化
Point.fromMap(Map<String, num> data): x = data['x']!, y = data['y']!;
// 混合写法:参数绑定与初始化列表组合使用
Point.yAxis(this.y) : x = 0; // y 通过参数绑定,x 设置默认值
}
💡 命名构造函数
我们知道在 Java 这类语言里可以通过参数不同来重载构造函数,但在 Dart 里这个套路却行不通。当尝试在 Dart 中用传统重载方式写构造函数时:
kotlin
class Point {
num x;
num y;
Point(this.x, this.y);
Point(this.y); // 这里会直接报错!❌
}
Dart 准备了另一种更优雅的解决方案------命名构造函数。可以使用 类名.构造名() 的格式定义,它避免了参数命名相同导致的歧义,让代码意图更清晰:
ini
class Point {
num x;
num y;
// 主构造函数
Point(this.x, this.y);
// 命名构造函数:当只需要 y 坐标时,自动设置 x=0
Point.yAxis(this.y) : x = 0; // 使用初始化列表更高效
// 另一个命名构造:创建原点
Point.origin() : x = 0, y = 0;
}
//创建时
var p1 = Point(2, 3); // 主构造函数
var p2 = Point.yAxis(5); // 创建在 y 轴上的点 (0,5)
var origin = Point.origin(); // 轻松获得原点坐标
💡 重定向构造函数
重定向构造函数提供了一种优雅的复用构造函数逻辑的方式。当我们需要用不同的方式创建对象,但本质上都是调用同一个核心构造逻辑时,可以使用这个特性。
kotlin
class Temperature {
double celsius;
// 主构造函数处理摄氏度
Temperature(this.celsius);
// 重定向构造函数:处理华氏度转摄氏度
Temperature.fromFahrenheit(double fahrenheit)
: this((fahrenheit - 32) * 5/9); // 转换计算后调用主构造函数
// 另一个重定向构造函数:处理开尔文温度
Temperature.fromKelvin(double kelvin)
: this(kelvin - 273.15);
}
在构造函数声明后使用冒号:
连接其他构造函数即可。重定向构造函数本身不能有方法体,它的唯一作用就是把构造请求引导到正确的构造函数去。
💡 常量构造函数
当我们需要创建一些永远不会改变状态的对象时,可以通过常量构造函数将它们定义为编译时常量,这样既能提升性能又能保证数据一致性。
常量构造函数的所有成员变量都必须是 final
修饰的,这样它们初始化后就不能被修改,其次必须使用 const
关键字来定义构造函数:
kotlin
class Point {
final num x; // 固定x坐标
final num y; // 固定y坐标
// 常量构造函数使用const修饰
const Point (this.x, this.y);
}
在使用时有个重要特性:当我们用 const
关键字创建多个相同值的对象时,它们实际上会指向同一个内存实例:
csharp
void main() {
// 使用const创建两个相同坐标点
var p1 = const ImmutablePoint(0, 0);
var p2 = const ImmutablePoint(0, 0);
print(p1 == p2); // 输出true,说明是同一个实例
}
💡 工厂构造函数
工厂构造函数是 Dart 中控制对象创建流程的特殊工具,他有三个主要的作用:
- 实现实例缓存,避免重复创建对象
- 支持返回子类对象
- 封装单例逻辑
它简化了简单工厂场景的实现,相当于内置的轻量级工厂方案。
例如开发一个日志系统,需要确保相同名称的日志器只创建一个实例:
dart
class Logger {
final String name;
static final cache = <String, Logger>{};
// 工厂构造函数像智能管家
factory Logger(String name) {
if (cache.containsKey(name)) {
print('从缓存取出$name');
return cache[name]!;
} else {
final newLogger = Logger._create(name);
cache[name] = newLogger;
return newLogger;
}
}
Logger._create(this.name);
}
再来看单例模式的经典实现。其实同样的方法用工厂构造和静态方法都能实现:
csharp
class AppManager {
static AppManager _singleton;
// 工厂构造版本
factory AppManager() {
_singleton ??= AppManager._init();
return _singleton;
}
// 静态方法版本
static AppManager get instance {
_singleton ??= AppManager._init();
return _singleton;
}
AppManager._init();
}
这两种方式在使用时的区别很直观:
ini
// 工厂构造的调用方式更符合直觉
var mgr1 = AppManager();
// 静态方法需要显式调用
var mgr2 = AppManager.instance;
虽然效果相同,但工厂构造的语法更贴近常规的对象创建方式,对外提供简洁统一的构造接口。
2.2 类成员
💡 Getters 和 Setters
在 Dart 中,get 和 set 是特殊类型的方法。get 用于读取属性值,set 用于设置属性值。它们和普通方法的主要区别是语法和使用方法。
使用 get/set 访问属性,就像直接访问变量一样。例如:
arduino
class Rectangle {
num x; // 左边界
num y; // 上边界
num width;
num height;
Rectangle(this.x, this.y, this.width, this.height);
// 动态计算右边界
num get rightEdge => x + width;
// 修改右边界时同步更新左边界
set rightEdge(num value) => x = value - width;
}
void main() {
final rect = Rectangle(0, 0, 10, 10);
print(rect.rightEdge); // 使用get
rect.rightEdge = 15; //使用set
}
如果没有使用 get/set,那需要创建特定的方法来读取和设置属性:
arduino
class Rectangle {
num x; // 左边界
num y; // 上边界
num width;
num height;
Rectangle(this.x, this.y, this.width, this.height);
// 动态计算右边界
num getRightEdge() => x + width;
// 修改右边界时同步更新左边界
void setRightEdge(num value) {
x = value - width;
}
}
void main() {
final rect = Rectangle(0, 0, 10, 10);
print(rect.getRightEdge()); // 使用 getRightEdge 方法
rect.setRightEdge(15); // 使用 setRightEdge 方法
}
💡 操作符重载
在 Dart 语言中,允许类自定义某些操作符的行为。可以在类中覆盖操作符,比如加法操作符 '+',减法操作符 '-' 等。你只需要在类中定义一个方法来实现操作符重载,方法名就是你想要重载的操作符前面加上 operator
关键字,例如:
java
class Vector {
final int x, y;
Vector(this.x, this.y);
//操作符重载
Vector operator +(Vector v) {
return Vector(x + v.x, y + v.y);
}
}
void main() {
Vector v1 = Vector(1, 3);
Vector v2 = Vector(2, 2);
Vector v3 = v1 + v2;//直接使用 + 即可
print('v3: (${v3.x}, ${v3.y})');//打印 v3: (3, 5)
}
2.3 继承与组合
💡 抽象类
定义抽象类时需要使用 abstract
修饰符,其中可以包含具体实现的成员和抽象方法。这里有个注意点:抽象方法不需要额外添加 abstract
关键字,这点和 Java 不同:
csharp
abstract class BasePerson {
String name;
// 抽象方法,子类必须实现
void printProfile();
}
抽象类本身不能被直接实例化,但可以通过工厂方法创建实现类实例:
typescript
abstract class BasePerson {
String name;
BasePerson(this.name);
// 工厂方法返回具体子类实例
factory BasePerson.create(String name) {
return Developer(name);
}
void printProfile();
}
// 继承抽象类必须实现所有抽象方法
class Developer extends BasePerson {
Developer(String name) : super(name);
@override
void printProfile() {
print('开发工程师: $name');
}
}
//实际使用时,通过工厂方法创建实例:
void main() {
var person = BasePerson.create("XX");
print(person.runtimeType); // 输出: Developer
person.printProfile(); // 输出: 开发工程师: XX
}
💡 隐式接口
Dart 中有一个和 Java 显著不同的点:Dart 中没有 interface 关键字,每个类都会自动生成对应的接口模板。
当我们想让一个类完整遵循另一个类的行为规范,但又不希望继承其具体实现时,就可以使用 implements
关键字来实现这个隐式接口:
csharp
// 抽象类可作为接口使用
abstract class EventCallback {
void onSuccess();
void onError();
}
// 具体类也可以作为接口使用
class OtherCallback {
void onStart(){
print('开始');
}
void onEnd(){
print('结束');
}
}
// 实现时必须完整覆盖所有接口方法
class CustomCallback implements EventCallback,OtherCallback {
@override
void onSuccess() => print('处理成功逻辑');
@override
void onError() => print('记录错误日志');
@override
void onEnd() {
print('CustomCallback 开始');
}
@override
void onStart() {
print('CustomCallback 结束');
}
}
实现接口与继承的区别为:
- Dart 遵循单继承原则但允许多接口实现
- 继承允许选择性重写 父类方法,甚至可以通过
super
调用父类原始实现;而接口实现则是强制性的,必须完整实现接口定义的所有成员。
💡 Mixins 混入
Mixins 是一种强大的代码复用机制,特别适合需要组合多个类功能的场景。它的核心原理是通过 with
关键字将多个类的功能"混合"到当前类中,形成一种特殊的多继承结构:
scala
// 注意:被混入的类不能包含构造函数
class Bird {
void fly() => print("展翅高飞");
}
class Fish {
void swim() => print("畅游水中");
}
// 通过 with 关键字混合两个类
class FlyingFish with Bird, Fish {
void specialSkill() => print("水空两栖");
}
这里创建的 FlyingFish 实例将同时拥有 fly(), swim() 和 specialSkill() 三个方法。
当混合的类存在方法冲突时,Dart 采用"后来居上"的覆盖原则:
scala
class Morning {
String greet() => "早安!";
}
class Evening {
String greet() => "晚安!";
}
// 混合顺序决定方法优先级
class HybridGreeter with Morning, Evening {}
void main() {
print(HybridGreeter().greet()); // 输出"晚安!"
}
当与继承结合使用时,也遵循"后来居上"的覆盖原则:
scala
class Afternoon {
String greet() => "午安!";
}
// 混合顺序决定方法优先级
class HybridGreeter extends Afternoon with Morning, Evening {}
void main() {
print(HybridGreeter().greet()); // 输出"晚安!"
}
// 等价简写形式:
// class HybridGreeter = Afternoon with Morning, Evening;
Mixins 解决了传统 OOP 的局限性,既突破了单继承限制,又避免了接口需要重复实现的缺点。
这种特性在构建需要组合多种行为的复杂对象时尤为有用,比如游戏角色系统、跨领域能力模型等场景。
💡 扩展方法
扩展方法允许向已有的类添加新的功能,同时不需要创建该类的子类或修改该类。
在 Dart 文件中使用关键词 extension
,后跟扩展名称,关键词 on
,以及你希望扩展的类。然后,中括号里包含你添加的方法即可。
例如,要给 List<int>
添加一个求和的扩展方法,可写成:
csharp
extension Sum on List<int> {
int sum() {
return this.fold(0, (current, next) => current + next);
}
}
即使它原来的定义中并没有这个方法,也可以直接调用 List<int>.sum()
方法:
csharp
void main() {
print([1, 2, 3].sum()); // 输出 6
}
2.4 特殊类
💡 可调用类
在 Dart 中,一个类定义了 call()
方法,那么该类的实例可以像函数一样被调用用:
javascript
class ExampleClass {
String call(String name, int age) {
return 'Name: $name, Age: $age';
}
}
void main() {
var example = ExampleClass();
var result = example('John', 20);
print(result); // 输出: Name: John, Age: 20
}
三、异常处理
Dart 的异常机制与 Java 不同,与 kotlin 类似,是非检查型的,开发者不需要在方法签名中声明可能抛出的异常类型,编译器也不会强制要求处理特定异常,这种设计更灵活但也需要开发者更主动地处理潜在问题。
💡 Exception 与 Error
在异常类型方面,Dart 的基础架构提供了 Exception 和 Error 两个核心类型。
Exception
:通常表示程序正常逻辑中可以预期的错误
Error
:表示程序设计或逻辑上的错误,如数组越界、null 引用等。
💡 异常抛出
Dart 的异常抛出机制非常灵活,任何非空对象都可以作为异常抛出,不局限于特定类型。这种设计在某些场景下能简化代码:
php
// 抛出标准异常类型
throw Exception('数据解析异常');
// 直接抛出字符串
throw '网络连接中断';
// 抛出数字代码
throw 404;
// 甚至可以抛出方法
throw (){
};
💡 异常处理
当处理异常时,Dart 采用了 on-catch 组合语法判断错误类型:
dart
try {
} on Exception catch (e) {
//捕获Exception
} on int catch (e) {
//捕获int类型
} on Function catch (e) {
//捕获方法
} catch (e) {
//捕获其他异常
rethrow; // 重新抛出给上层
} finally {
}
- 使用
on
关键字可以针对特定异常类型进行处理,类似 Java 的 catch 块类型声明 catch
语句可以接收两个参数:异常对象和堆栈追踪信息rethrow
关键字允许在当前处理完成后继续传播异常
四、总结
本篇围绕方法、类、异常处理三大核心模块讲述,巩固了 Flutter 开发的基础编程能力。
总的来看,Dart 方法的灵活参数设计提升了代码复用性,类的封装与继承实现了业务逻辑的高效组织,异常处理则为程序稳定性提供保障。建议读者后续通过实际项目练习巩固。