class

类与对象基础(Classes & Objects Basics)

理论: 什么是类和对象?

  • 类(Class): 类是创建对象的蓝图或模板。它定义了对象的属性(数据)和行为(方法)。你可以将类想象成一个制造汽车的设计图纸,它描述了汽车有什么部件(属性)以及能做什么(方法)。
  • 对象(Object): 对象是类的实例。通过类这个蓝图,我们可以“制造”出具体的汽车。每个对象都有自己独立的数据副本,但共享类定义的方法。

示例: 定义类与创建对象

// main.dart

// 声明一个名为 'Car' 的类 (Class)
class Car {
  // 属性 (Properties / Fields):定义了对象的数据
  String brand; // 品牌
  String model; // 型号
  int year;     // 年份

  // 方法 (Methods):定义了对象的行为
  void startEngine() {
    print('$brand $model (${year}) 引擎启动!');
  }

  void drive() {
    print('$brand $model 正在行驶...');
  }
}

void main() {
  // 创建 Car 类的对象 (Object / Instance)
  // 使用 'new' 关键字 (可选,但推荐省略)
  Car myCar = Car(); // 或者 Car myCar = new Car();

  // 访问并设置对象的属性
  myCar.brand = 'Toyota';
  myCar.model = 'Camry';
  myCar.year = 2022;

  print('我的第一辆车是:${myCar.brand} ${myCar.model},生产年份:${myCar.year}');

  // 调用对象的方法
  myCar.startEngine();
  myCar.drive();

  print('---');

  // 创建另一个 Car 对象
  Car anotherCar = Car();
  anotherCar.brand = 'Honda';
  anotherCar.model = 'Civic';
  anotherCar.year = 2023;
  anotherCar.startEngine();
}

运行环境要求: Dart SDK 2.12+

注意事项

  • 命名规范: Dart中类的名称通常使用大驼峰命名法(PascalCase),例如Car而不是carmy_car
  • 属性初始化: 在Dart 2.12+ 强空安全模式下,如果一个类的属性是非空类型(如String, int),但没有在声明时提供初始值,那么就必须在构造函数中对其进行初始化。上面的范例中 brand, model, year都是非空类型,但我们没有定义构造函数。接下来会讲到如何解决这个问题。
  • new关键字: 在Dart 2.x 版本中,创建对象时可以省略new关键字,推荐省略以使代码更简洁。

构造函数(Constructors)

理论: 构造函数的作用

构造函数(Constructor)是一种特殊的方法,用于创建类的实例(对象),并对对象的属性进行初始化。每个类可以有一个或多个构造函数。

示例: 默认构造函数与命名构造函数

默认构造函数

当你没有显式定义任何构造函数时,Dart会为你提供一个隐式的、无参数的默认构造函数。上面的Car myCar = Car();就是调用了默认构造函数。

生成式构造函数

这是最常见的构造函数类型,用于创建新的类实例。

// main.dart

class Car {
  String brand;
  String model;
  int year;

  // 1. 生成式构造函数 (Generative Constructor)
  // 这种写法是语法糖,等同于在函数体内部用 this.brand = brand; 等方式赋值
  // 它接收参数并用于初始化实例变量
  Car(this.brand, this.model, this.year); // 简写形式

  /*
  // 等价于以下传统写法:
  Car(String brand, String model, int year) {
    this.brand = brand;
    this.model = model;
    this.year = year;
  }
  */

  void displayInfo() {
    print('品牌: $brand, 型号: $model, 年份: $year');
  }
}

void main() {
  // 使用生成式构造函数创建对象
  Car mySportsCar = Car('Porsche', '911', 2024);
  mySportsCar.displayInfo(); // 输出: 品牌: Porsche, 型号: 911, 年份: 2024

  // 注意:一旦显式定义了构造函数,Dart就不会再提供隐式默认构造函数了。
  // 因此,你不能再写 Car aCar = Car(); 这样的代码,除非你再定义一个无参构造函数。
}

命名构造函数

一个类可以有多个构造函数,通过给它们起不同的名字来区分,这被称为命名构造函数。它们非常适合提供多种创建对象的方式。

// main.dart

class Car {
  String brand;
  String model;
  int year;
  bool isElectric;

  // 主构造函数 (作为默认构造函数)
  Car(this.brand, this.model, this.year, {this.isElectric = false});

  // 命名构造函数:从JSON数据创建对象
  Car.fromJson(Map<String, dynamic> json)
      : brand = json['brand'] as String,
        model = json['model'] as String,
        year = json['year'] as int,
        isElectric = json['isElectric'] as bool? ?? false; // 如果json中不存在isElectric,则默认为false

  // 命名构造函数:创建电动汽车
  Car.electricCar(String brand, String model, int year)
      : this(brand, model, year, isElectric: true); // 重定向到主构造函数

  void displayInfo() {
    print('品牌: $brand, 型号: $model, 年份: $year, 电动: ${isElectric ? '' : ''}');
  }
}

void main() {
  // 使用主构造函数
  Car sedan = Car('BMW', '3 Series', 2023);
  sedan.displayInfo(); // 品牌: BMW, 型号: 3 Series, 年份: 2023, 电动: 否

  // 使用命名构造函数 fromJson
  Map<String, dynamic> carJson = {
    'brand': 'Tesla',
    'model': 'Model 3',
    'year': 2021,
    'isElectric': true
  };
  Car tesla = Car.fromJson(carJson);
  tesla.displayInfo(); // 品牌: Tesla, 型号: Model 3, 年份: 2021, 电动: 是

  // 使用命名构造函数 electricCar
  Car miniElectric = Car.electricCar('Mini', 'Cooper SE', 2022);
  miniElectric.displayInfo(); // 品牌: Mini, 型号: Cooper SE, 年份: 2022, 电动: 是
}

运行环境要求: Dart SDK 2.12+

注意事项

  • 空安全初始化: 如前所述,Dart 2.12+如果一个非空类型的属性没有在声明时赋值,就必须在构造函数中初始化。上述Car(this.brand, this.model, this.year)语法糖就是一种初始化方式。
  • this关键字: 在构造函数的参数列表中使用this.propertyName,可以直接将传入的参数赋值给同名的实例变量。
  • 初始化列表(Initializer List): 在构造函数体开始执行之前,可以在其签名后使用冒号:来执行一些表达式,通常用于初始化final字段或调用父类构造函数。Car.fromJson就是一个例子。
  • 重定向构造函数(Redirecting Constructors): 命名构造函数可以通过this关键字调用同一个类中的其他构造函数,但不能同时有初始化列表。Car.electricCar就是一个例子。
  • 工厂构造函数(Factory Constructors): 这是一种特殊的构造函数,不会总是创建新的实例。它可以在创建对象之前执行复杂的逻辑,比如从缓存中返回现有实例。我们将在下一模块介绍。

工厂构造函数与常量构造函数 (Factory & Constant Constructors)

理论

  • 工厂构造函数(Factory Constructor): 使用factory关键字修饰的构造函数。它不直接创建实例,而是返回一个实例。这意味着一个工厂构造函数可以:
    • 从缓存中返回一个已存在的实例。
    • 根据条件返回子类的实例。
    • 在创建实例之前执行一些复杂逻辑。
    • 工厂构造函数没有this关键字,因为在执行时实例可能还不存在。
  • 常量构造函数(Constant Constructor): 使用const关键字修饰的构造函数。它要求类的所有实例变量都是final的,并且在编译时就知道其值。使用const构造函数创建的实例是不可变的,并且在内存中是规范化的(如果两个常量对象具有相同的值,它们可能指向同一个内存地址)。

示例: 工厂构造函数

// main.dart

class Logger {
  final String name;

  // 静态私有实例,用于单例模式
  static final Map<String, Logger> _cache = <String, Logger>{};

  // 工厂构造函数
  factory Logger(String name) {
    if (_cache.containsKey(name)) {
      print('从缓存中获取 Logger: $name');
      return _cache[name]!; // 确保在缓存中存在,所以使用 !
    } else {
      print('创建新的 Logger: $name');
      final logger = Logger._internal(name); // 调用私有命名构造函数创建实例
      _cache[name] = logger;
      return logger;
    }
  }

  // 私有命名构造函数,实际创建实例
  Logger._internal(this.name);

  void log(String message) {
    print('[$name] $message');
  }
}

void main() {
  Logger logger1 = Logger('Auth'); // 创建新的 Logger: Auth
  logger1.log('用户登录成功');

  Logger logger2 = Logger('Auth'); // 从缓存中获取 Logger: Auth
  logger2.log('安全检查通过');

  Logger logger3 = Logger('UI');   // 创建新的 Logger: UI
  logger3.log('页面初始化完成');

  // 由于 logger1 和 logger2 名称相同,它们指向同一个实例
  print('logger1 和 logger2 是同一个实例吗? ${identical(logger1, logger2)}'); // true
  print('logger1 和 logger3 是同一个实例吗? ${identical(logger1, logger3)}'); // false
}

运行环境要求:Dart SDK 2.12+

示例: 常量构造函数

// main.dart

class Point {
  final int x;
  final int y;

  // 常量构造函数
  const Point(this.x, this.y);

  // 常量工厂构造函数 (也是可以的)
  // factory const Point.origin() => const Point(0, 0); // 已废弃,不能同时使用 factory 和 const
  // 应该直接定义为常量实例
  static const Point origin = Point(0, 0);
}

void main() {
  // 使用 const 关键字创建常量对象
  const Point p1 = Point(1, 2);
  const Point p2 = Point(1, 2);
  const Point p3 = Point(3, 4);

  // 运行时会检查是否是同一个常量实例
  // 如果两个常量对象具有相同的值,它们可能指向同一个内存地址
  print('p1 和 p2 是同一个实例吗? ${identical(p1, p2)}'); // true
  print('p1 和 p3 是同一个实例吗? ${identical(p1, p3)}'); // false

  // 如果不使用 const 关键字,即使值相同,也会创建不同的实例
  Point p4 = Point(1, 2);
  print('p1 和 p4 是同一个实例吗? ${identical(p1, p4)}'); // false
  print('p2 和 p4 是同一个实例吗? ${identical(p2, p4)}'); // false

  // 常量构造函数通常与 final 关键字结合使用,确保实例的不可变性
  // p1.x = 5; // 错误!x 是 final 字段
}

运行环境要求: Dart SDK 2.12+

注意事项

  • 工厂构造函数:
    • 不能使用this关键字来初始化实例变量。
    • 必须返回一个所属类的实例(或其子类的实例)。
    • 常用于实现单例模式、缓存机制或根据输入条件返回不同类型的对象。
  • 常量构造函数:
    • 类的所有实例变量必须是final的。
    • 不能有方法体,只能有初始化列表。
    • const关键字修饰的构造函数创建的实例是编译时常量。
    • 当你创建常量实例时,要确保使用const关键字,否则即使类有常量构造函数,也会创建非常量实例。
    • 建议将无状态、不可变的简单数据类定义为const构造函数,以提高性能和内存效率。

继承(Inheritance)

理论: 代码复用与类型层次

继承(Inheritance) 允许一个类(子类/派生类)从另一个类(父类/基类)获取其属性和方法。这促进了代码复用,并建立了“is-a”关系(例如,“狗是一种动物”)。

  • extends关键字: 用于声明一个类继承另一个类。
  • 方法重写(Method Overriding): 子类可以提供其父类中已存在方法的自定义实现。使用@override注解是最佳实践。
  • super关键字: 用于引用父类成员(属性或方法)或调用父类构造函数。

示例: 继承与方法重写

// main.dart

// 父类 (Superclass / Base Class)
class Animal {
  String name;
  int age;

  Animal(this.name, this.age);

  void eat() {
    print('$name 正在吃东西...');
  }

  void sleep() {
    print('$name 正在睡觉...');
  }

  void describe() {
    print('我是 $name, $age 岁。');
  }
}

// 子类 (Subclass / Derived Class),继承自 Animal
class Dog extends Animal {
  String breed; // 狗特有的属性

  // 子类构造函数
  // 必须调用父类的构造函数来初始化父类定义的属性
  Dog(String name, int age, this.breed) : super(name, age);

  // 子类特有的方法
  void bark() {
    print('$name 汪汪叫!');
  }

  // 方法重写 (Method Overriding)
  // 提供父类方法的自定义实现
   // 推荐使用 @override 注解,有助于编译器检查
  void eat() {
    super.eat(); // 调用父类的 eat 方法
    print('$name 喜欢吃狗粮。');
  }

  
  void describe() {
    super.describe(); // 调用父类的 describe 方法
    print('我是一只 $breed 的狗。');
  }
}

// 另一个子类
class Cat extends Animal {
  Cat(String name, int age) : super(name, age);

  void meow() {
    print('$name 喵喵叫!');
  }

  
  void eat() {
    print('$name 优雅地吃猫粮。');
  }
}

void main() {
  Animal genericAnimal = Animal('Bob', 5);
  genericAnimal.describe(); // 我是 Bob, 5 岁。
  genericAnimal.eat();      // Bob 正在吃东西...
  print('---');

  Dog myDog = Dog('Buddy', 3, 'Golden Retriever');
  myDog.describe(); // 我是 Buddy, 3 岁。我是一只 Golden Retriever 的狗。
  myDog.eat();      // Buddy 正在吃东西... Buddy 喜欢吃狗粮。
  myDog.bark();     // Buddy 汪汪叫!
  print('---');

  Cat myCat = Cat('Whiskers', 2);
  myCat.describe(); // 我是 Whiskers, 2 岁。
  myCat.eat();      // Whiskers 优雅地吃猫粮。
  myCat.meow();     // Whiskers 喵喵叫!
}

运行环境要求: Dart SDK 2.12+

注意事项

  • 单继承: Dart是一种单继承语言,一个类只能继承一个父类。
  • 私有成员: 父类的私有成员(以_开头的属性或方法)不能被子类直接访问或继承。
  • 构造函数链: 子类的构造函数必须调用其父类的构造函数。如果没有显式调用super(),Dart会自动调用父类的无参默认构造函数。如果父类没有无参构造函数,子类就必须显式调用一个父类的构造函数。
  • @override注解: 强烈建议在使用方法重写时加上@override注解。这不仅可以帮助阅读代码的人理解,还可以让编译器检查你是否真的重写了父类的方法(例如,方法名或参数列表错误会导致编译错误)。

抽象类与接口(Abstract Classes & Interfaces)

理论: 规范与多态

  • 抽象类(Abstract Class): 用abstract关键字修饰的类。它不能直接被实例化,主要目的是作为其他类的基类,定义一个接口和部分实现。抽象类可以包含:
    • 抽象方法(Abstract Method): 没有方法体的方法,子类必须实现它。
    • 普通方法和属性。
    • 构造函数(但不能有抽象构造函数)。
  • 接口(Interface): 在Dart中,每个类都隐式地定义了一个接口。这意味着任何类都可以作为接口被其他类实现。当你使用implements关键字时,你就承诺实现该接口中所有的方法和属性。接口主要用于:
    • 定义一组行为规范。
    • 实现多态性,一个对象可以表现出多种类型。

示例: 抽象类

// main.dart

// 抽象类 'Shape'
abstract class Shape {
  // 普通属性
  String name;

  // 普通构造函数
  Shape(this.name);

  // 抽象方法:没有方法体,子类必须实现
  double getArea();
  double getPerimeter();

  // 普通方法:有方法体
  void display() {
    print('这是一个 $name 形状.');
  }
}

// 具体类 'Circle' 继承并实现 'Shape'
class Circle extends Shape {
  double radius;

  Circle(String name, this.radius) : super(name);

  
  double getArea() {
    return 3.14159 * radius * radius;
  }

  
  double getPerimeter() {
    return 2 * 3.14159 * radius;
  }

  
  void display() {
    super.display(); // 调用父类的 display 方法
    print('圆的半径是 $radius');
  }
}

// 具体类 'Rectangle' 继承并实现 'Shape'
class Rectangle extends Shape {
  double width;
  double height;

  Rectangle(String name, this.width, this.height) : super(name);

  
  double getArea() {
    return width * height;
  }

  
  double getPerimeter() {
    return 2 * (width + height);
  }
}

void main() {
  // 无法直接实例化抽象类
  // Shape myShape = Shape('Generic'); // 错误:无法实例化抽象类

  Circle myCircle = Circle('圆形', 5.0);
  myCircle.display(); // 这是一个 圆形 形状. 圆的半径是 5.0
  print('圆的面积: ${myCircle.getArea()}');
  print('圆的周长: ${myCircle.getPerimeter()}');
  print('---');

  Rectangle myRectangle = Rectangle('矩形', 4.0, 6.0);
  myRectangle.display(); // 这是一个 矩形 形状.
  print('矩形的面积: ${myRectangle.getArea()}');
  print('矩形的周长: ${myRectangle.getPerimeter()}');
}

运行环境要求: Dart SDK 2.12+

示例: 接口

// main.dart

// 定义一个行为接口 (在Dart中,任何类都可以作为接口)
// 通常会定义一个抽象类或仅包含抽象方法的类来明确其作为接口的意图
abstract class Swimmer {
  void swim();
}

abstract class Runner {
  void run();
}

// 一个实现了 Swimmer 和 Runner 接口的类
class Athlete implements Swimmer, Runner {
  String name;

  Athlete(this.name);

  
  void swim() {
    print('$name 正在游泳...');
  }

  
  void run() {
    print('$name 正在跑步...');
  }

  void compete() {
    print('$name 正在参加比赛!');
  }
}

void main() {
  Athlete john = Athlete('John');
  john.compete();
  john.swim();
  john.run();
  print('---');

  // 多态性:一个 Athlete 对象可以被视为 Swimmer 或 Runner 类型
  Swimmer swimmer = john;
  swimmer.swim(); // Swimmer 接口只提供 swim 方法
  // swimmer.run(); // 错误:Swimmer 接口没有 run 方法

  Runner runner = john;
  runner.run();   // Runner 接口只提供 run 方法
}

运行环境要求: Dart SDK 2.12+

注意事项

  • 抽象类与普通类: 抽象类不能被实例化,必须被继承。继承抽象类的子类必须实现所有抽象方法,除非子类也是抽象类。
  • 抽象类与接口的区别:
    • 继承(extends): 一个类只能扩展一个父类(单继承),用于代码复用和“is-a”关系。子类会继承父类的实现。
    • 实现(implements): 一个类可以实现多个接口(多实现),用于定义行为规范和“can-do”关系。实现接口的类必须重新实现接口中所有的方法和属性(包括其getter/setter)。
  • Dart中的接口: 记住,在 Dart 中,即使你没有使用abstract关键字,任何类都可以被implements视为一个接口。但通常,我们会使用abstract关键字来明确一个类是设计来作为接口的。
  • @override对于implements也是好习惯: 虽然 implements 要求你实现所有接口成员,但使用@override注解仍然是一个好的做法,它能帮助你发现拼写错误等问题。

混入(Mixins)

理论:多重代码复用

混入(Mixin)是一种在不使用继承的情况下,复用类的代码的方式。它允许你将一个或多个类的功能“混合”到另一个类中,从而实现代码的横向复用。

  • mixin关键字: 用于定义一个混入类。
  • with关键字: 用于将一个或多个混入应用到类中。

混入可以包含方法、属性、甚至抽象方法,但它不能有构造函数(因为它本身不能被实例化)。

示例: 使用混入

// main.dart

// 定义一个混入:提供了飞行能力
mixin Flyable {
  void fly() {
    print('正在飞行...');
  }
}

// 定义另一个混入:提供了游泳能力
mixin Swimmable {
  void swim() {
    print('正在游泳...');
  }
}

// 定义一个普通的 Animal 类
class Animal {
  String name;
  Animal(this.name);
  void eat() => print('$name 正在吃东西。');
}

// Bird 类: 继承 Animal,并使用 Flyable 混入
class Bird extends Animal with Flyable {
  Bird(String name) : super(name);
  void chirp() => print('$name 叽叽喳喳...');
}

// Duck 类: 继承 Animal,并使用 Flyable 和 Swimmable 混入
class Duck extends Animal with Flyable, Swimmable {
  Duck(String name) : super(name);
  void quack() => print('$name 嘎嘎叫...');
}

// Submarine 类:一个非生物类,但需要 Swimmable 能力
// 注意:混入可以应用于任何类,不一定是继承了某个父类的类
class Submarine with Swimmable {
  String name;
  Submarine(this.name);
}


void main() {
  Bird sparrow = Bird('麻雀');
  sparrow.eat();  // 来自 Animal
  sparrow.chirp(); // 来自 Bird
  sparrow.fly();  // 来自 Flyable 混入
  print('---');

  Duck donald = Duck('唐老鸭');
  donald.eat();   // 来自 Animal
  donald.quack();  // 来自 Duck
  donald.fly();   // 来自 Flyable 混入
  donald.swim();  // 来自 Swimmable 混入
  print('---');

  Submarine uboat = Submarine('U-Boat');
  // uboat.eat(); // 错误:Submarine 没有 Animal 的 eat 方法
  uboat.swim(); // 来自 Swimmable 混入
}

运行环境要求: Dart SDK 2.12+

注意事项

  • 多重实现: 一个类可以with多个混入,它们之间用逗号分隔。
  • 无构造函数: 混入不能定义构造函数。
  • 解决继承菱形问题: 混入提供了一种在单继承语言中实现多重代码复用(而非多重继承)的有效方法,避免了传统多重继承带来的“菱形问题”。
  • on关键字(类型限制): 混入可以使用on关键字来限制只有特定类型的类才能使用它。
mixin ElectricVehicle on Car { // 只有继承或实现了 Car 的类才能使用这个混入
    void charge() {
        print('汽车正在充电...');
    }
}
// class MyElectricCar extends Car with ElectricVehicle {...} // 可以
// class MyBike with ElectricVehicle {...} // 错误,因为 MyBike 不是 Car
  • 混入的执行顺序: 如果有多个混入定义了相同名称的方法或属性,最靠右(最后)的混入会“获胜”,它的实现会覆盖左边的。

扩展方法(Extension Methods)

理论: 为现有类添加功能

扩展方法(Extension Methods)允许你为任何类(包括int,String,List等内置类型)添加新的功能,而无需修改原始类的代码,也无需创建新的子类。这是一种非常强大的、非侵入性的方式来增强现有类的行为。

  • extension关键字: 用于定义一个扩展。

示例: 扩展方法

// main.dart

// 定义一个扩展,为 String 类添加功能
extension StringExtensions on String {
  // 首字母大写的方法
  String capitalize() {
    if (isEmpty) {
      return this;
    }
    return this[0].toUpperCase() + substring(1);
  }

  // 判断是否是回文
  bool isPalindrome() {
    final cleanString = toLowerCase().replaceAll(RegExp(r'[^a-z0-9]'), '');
    return cleanString == cleanString.split('').reversed.join();
  }

  // 重复自身 n 次
  String repeat(int times) {
    StringBuffer buffer = StringBuffer();
    for (int i = 0; i < times; i++) {
      buffer.write(this);
    }
    return buffer.toString();
  }
}

// 定义一个扩展,为 List<int> 添加求和功能
extension IntListSum on List<int> {
  int sum() {
    int total = 0;
    for (var number in this) {
      total += number;
    }
    return total;
  }
}

// 为 null 类型的 String 扩展一个安全取值方法
extension NullableStringExtension on String? {
  String orEmpty() {
    return this ?? '';
  }
}

void main() {
  String greeting = 'hello world';
  print(greeting.capitalize()); // Hello world
  print(greeting.repeat(3));    // hello worldhello worldhello world

  String palindrome1 = 'Madam';
  String palindrome2 = 'A man, a plan, a canal: Panama';
  String notPalindrome = 'Dart';

  print('"$palindrome1" 是回文吗? ${palindrome1.isPalindrome()}'); // true
  print('"$palindrome2" 是回文吗? ${palindrome2.isPalindrome()}'); // true
  print('"$notPalindrome" 是回文吗? ${notPalindrome.isPalindrome()}'); // false
  print('---');

  List<int> numbers = [1, 2, 3, 4, 5];
  print('列表 ${numbers} 的和是:${numbers.sum()}'); // 15
  print('---');

  String? nullableText;
  print('可空字符串的默认值: "${nullableText.orEmpty()}"'); // ""

  nullableText = "Not null";
  print('非空字符串的值: "${nullableText.orEmpty()}"'); // "Not null"
}

运行环境要求: Dart SDK 2.6+ (Dart 2.12+ 包含空安全特性)

注意事项

  • 导入: 如果你将扩展定义在单独的文件中,你需要在使用它们的Dart文件中导入该文件。
  • 歧义处理: 如果两个不同的扩展为同一个类定义了相同名称的方法,或者一个扩展方法与类本身的成员方法同名,编译器会报错或者以类本身成员为优先。你可以通过显式导入带有as关键字的别名来解决命名冲突。
  • 不是虚拟成员: 扩展方法是静态分发的,这意味着在编译时确定要调用的方法。它们不是Dart对象上的虚拟成员。例如,如果你有一个Object类型的变量,但它实际上是一个String,你不能直接调用String的扩展方法,除非你先将其向下转型。
Object obj = 'test';
// obj.capitalize(); // 错误:Object 上没有 capitalize 方法
(obj as String).capitalize(); // 可以
  • 无法覆盖现有方法: 扩展方法不能用于覆盖类中已经存在的方法。

枚举(Enums)

理论: 有限的命名常量集合

枚举(Enum)是一种特殊的类,用于表示一组有限的、预定义的命名常量。它们提供了一种更安全、更具可读性的方式来处理固定数量的选项,而不是使用字符串或整数。

在Dart 2.17之后,枚举得到了极大的增强,现在它们可以有成员(方法、getter/setter、甚至实现接口和混入),类似于一个小型类。

示例: 基本枚举

// main.dart

// 定义一个表示交通信号灯颜色的枚举
enum TrafficLight {
  red,
  yellow,
  green,
}

void main() {
  TrafficLight currentLight = TrafficLight.red;

  // 访问枚举值
  print('当前信号灯是: ${currentLight.name}'); // red
  print('当前信号灯的索引位置: ${currentLight.index}'); // 0 ( red是第一个 )

  // 遍历所有枚举值
  print('所有交通信号灯颜色: ${TrafficLight.values}'); // [TrafficLight.red, TrafficLight.yellow, TrafficLight.green]
  for (var color in TrafficLight.values) {
    print(' - ${color.name}');
  }

  // 使用 switch 语句判断枚举值
  switch (currentLight) {
    case TrafficLight.red:
      print('请停车!');
      break;
    case TrafficLight.yellow:
      print('请准备!');
      break;
    case TrafficLight.green:
      print('请前进!');
      break;
  }
}

运行环境要求: Dart SDK 2.12+

示例: 增强型枚举(Enhanced Enums, Dart 2.17+)

// main.dart

// 增强型枚举
enum HttpStatus {
  ok(200, '请求成功'),
  notFound(404, '资源未找到'),
  internalServerError(500, '服务器内部错误');

  // 枚举成员可以有自己的属性
  final int code;
  final String description;

  // 枚举成员可以有自己的构造函数 (注意是 const 构造函数)
  const HttpStatus(this.code, this.description);

  // 枚举成员可以有方法
  bool isSuccess() => code >= 200 && code < 300;
  bool isClientError() => code >= 400 && code < 500;
  bool isServerError() => code >= 500 && code < 600;

  // 枚举成员可以重写 toString 方法
  
  String toString() => '$code: $description';
}

void main() {
  HttpStatus status = HttpStatus.ok;
  print(status); // 200: 请求成功
  print('isSuccess: ${status.isSuccess()}'); // true

  HttpStatus errorStatus = HttpStatus.notFound;
  print(errorStatus.description); // 资源未找到
  print('isClientError: ${errorStatus.isClientError()}'); // true
  print('isServerError: ${errorStatus.isServerError()}'); // false
}

运行环境要求: Dart SDK 2.17+

注意事项

  • 默认构造函数: 枚举成员的构造函数必须是const
  • 成员类型: 增强型枚举可以有实例字段(final的)、gettersetter和方法。它们不能有abstractexternal成员。
  • 继承与实现: 枚举可以实现接口(implements)和使用混入(with),但不能继承其他类(extends),因为它们已经隐式继承自Enum类。
  • 用途: 枚举非常适合表示固定的状态集合(如订单状态、用户角色、配置选项等),使得代码更具可读性和类型安全。

类型定义与泛型(Type Definitions & Generics)

理论: 代码的灵活性与重用性

  • 类型定义(Type Definition) - typedef: 用于给函数类型或者复杂的类型起别名,以提高代码的可读性。Dart 2.13后,更推荐使用内联函数类型。
  • 泛型(Generics): 允许你在定义类、函数或接口时使用一个或多个类型参数,使其能够处理多种数据类型,从而提高了代码的灵活性和重用性,同时保持类型安全。

示例: 类型定义

// main.dart

// 传统的 typedef (Dart 2.12 之前及兼容写法)
typedef MathOperation = int Function(int a, int b);

// 推荐的内联函数类型 (Dart 2.13+)
// List<int> 的别名
typedef IntList = List<int>;
// 返回 bool 接收 String 和 int 的函数类型
typedef Validator = bool Function(String text, int maxLength);

class Calculator {
  // 使用 typedef 作为方法参数类型
  int operate(int a, int b, MathOperation operation) {
    return operation(a, b);
  }
}

void main() {
  // 实现 MathOperation 类型的函数
  int add(int a, int b) => a + b;
  int subtract(int a, int b) => a - b;

  Calculator calc = Calculator();
  int resultAdd = calc.operate(10, 5, add);
  print('加法结果: $resultAdd'); // 15

  int resultSubtract = calc.operate(10, 5, subtract);
  print('减法结果: $resultSubtract'); // 5

  print('---');

  // 使用内联函数类型别名
  IntList myNumbers = [11, 22, 33];
  print('我的数字列表: $myNumbers');

  Validator lengthValidator = (text, max) => text.length <= max;
  print('Dart是否在5个字符内: ${lengthValidator("Dart", 5)}'); // true
  print('Flutter是否在5个字符内: ${lengthValidator("Flutter", 5)}'); // false
}

运行环境要求: Dart SDK 2.13+(旧typedef在2.12也可以运行)

示例: 泛型

// main.dart

// 泛型类示例:一个简单的堆栈 (Stack)
class MyStack<T> { // <T> 是类型参数
  final List<T> _items = [];

  void push(T item) {
    _items.add(item);
  }

  T pop() {
    if (_items.isEmpty) {
      throw StateError('栈为空,无法弹出!');
    }
    return _items.removeLast();
  }

  T peek() {
    if (_items.isEmpty) {
      throw StateError('栈为空,没有顶部元素!');
    }
    return _items.last;
  }

  bool get isEmpty => _items.isEmpty;
  int get length => _items.length;
}

void main() {
  // 创建一个用于存储整数的堆栈
  MyStack<int> intStack = MyStack<int>();
  intStack.push(10);
  intStack.push(20);
  print('intStack 顶部元素: ${intStack.peek()}'); // 20
  print('intStack 弹出: ${intStack.pop()}');    // 20
  print('intStack 顶部元素: ${intStack.peek()}'); // 10

  // 创建一个用于存储字符串的堆栈
  MyStack<String> stringStack = MyStack<String>();
  stringStack.push('Dart');
  stringStack.push('Flutter');
  print('stringStack 顶部元素: ${stringStack.peek()}'); // Flutter
  print('stringStack 弹出: ${stringStack.pop()}');    // Flutter
}

泛型方法

// main.dart

// 泛型方法示例:交换列表中两个元素的位置
void swap<T>(List<T> list, int i, int j) {
  final temp = list[i];
  list[i] = list[j];
  list[j] = temp;
}

// 泛型方法示例:从列表中查找第一个符合条件的元素
T? findFirst<T>(List<T> list, bool Function(T item) predicate) {
  for (var item in list) {
    if (predicate(item)) {
      return item;
    }
  }
  return null;
}

void main() {
  List<int> numbers = [1, 2, 3, 4, 5];
  print('原列表: $numbers');
  swap(numbers, 0, 4); // 交换索引0和4的元素
  print('交换后列表: $numbers'); // [5, 2, 3, 4, 1]

  List<String> names = ['Alice', 'Bob', 'Charlie'];
  print('原列表: $names');
  swap(names, 0, 2); // 交换索引0和2的元素
  print('交换后列表: $names'); // [Charlie, Bob, Alice]
  print('---');

  int? evenNumber = findFirst(numbers, (n) => n % 2 == 0);
  print('第一个偶数: $evenNumber'); // 2

  String? nameStartingWithC = findFirst(names, (name) => name.startsWith('C'));
  print('第一个以C开头的名字: $nameStartingWithC'); // Charlie
}

运行环境要求: Dart SDK 2.12+

注意事项

  • 类型安全: 泛型在编译时捕获类型错误,而不是在运行时。这有助于编写更健壮的代码。
  • 类型参数命名约定: 通常使用单个大写字母作为类型参数,如T(Type), E(Element), K(Key), V(Value)
  • 类型限制(Bounded Generics): 你可以使用extends关键字来限制泛型类型参数必须是某个特定类型或其子类型。
class NumberProcessor<T extends num> { // T 必须是 num 或其子类 (int, double)
  T data;
  NumberProcessor(this.data);
  num add(T other) => data.toDouble() + other.toDouble();
}
// NumberProcessor<int> processor = NumberProcessor(10); // 可以
// NumberProcessor<String> processorStr = NumberProcessor('hello'); // 错误
  • 运行时类型检查: 虽然泛型在编译时提供类型安全,但在运行时,Dart会擦除泛型类型信息(type erasure),但会保留类型参数的具体类型信息。你可以使用isas运算符进行运行时类型检查和转换。