从0到1掌握Flutter(四)方法与类

引言

本文接上篇:从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 中控制对象创建流程的特殊工具,他有三个主要的作用:

  1. 实现实例缓存,避免重复创建对象
  2. 支持返回子类对象
  3. 封装单例逻辑

它简化了简单工厂场景的实现,相当于内置的轻量级工厂方案。

例如开发一个日志系统,需要确保相同名称的日志器只创建一个实例:

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 结束');
  }
}

实现接口与继承的区别为:

  1. Dart 遵循单继承原则但允许多接口实现
  2. 继承允许选择性重写 父类方法,甚至可以通过 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 方法的灵活参数设计提升了代码复用性,类的封装与继承实现了业务逻辑的高效组织,异常处理则为程序稳定性提供保障。建议读者后续通过实际项目练习巩固。

相关推荐
小墙程序员9 小时前
Flutter 教程(六)路由管理
flutter
zacksleo14 小时前
鸿蒙Flutter开发故事:不,你不需要鸿蒙化
flutter·harmonyos
帅次15 小时前
Flutter DropdownButton 详解
android·flutter·ios·kotlin·gradle·webview
bst@微胖子16 小时前
Flutter项目之构建打包分析
flutter
江上清风山间明月18 小时前
Flutter开发There are multiple heroes that share the same tag within a subtree报错
android·javascript·flutter·动画·hero
无知的前端18 小时前
Flutter 性能优化:实战指南
flutter·性能优化
飞川00119 小时前
Flutter敏感词过滤实战:基于AC自动机的高效解决方案
android·flutter
小墙程序员19 小时前
Flutter 教程(五)事件处理
flutter
耳東陈19 小时前
Flutter开箱即用一站式解决方案
flutter
SoaringHeart19 小时前
Flutter进阶:日志信息的快速定位解决方案 DLog
前端·flutter