Dart速来系列2-1,关于类你所应该知道的大多数高级特性

314 阅读25分钟

一、类的定义

在Dart中,类的定义主要包括了类名,构造函数,属性和方法。

class Student {
  String name;  // 声明一个名为 'name' 的字符串类型属性
  int age;  // 声明一个名为 'age' 的整数类型属性

  Student(this.name, this.age);  // 声明一个构造函数,用于创建 Student 对象

  void study() {  // 定义一个方法
    print('$name is studying.');
  }
}

在这个例子中,我们定义了一个名为 Student 的类。

它有两个属性 nameage,以及一个方法 study

通过 Student 类,我们可以创建出具有学生特征的对象。

二、类的成员

类的成员主要包括了类的属性(fields)和方法(methods)。

属性存储了类的状态。

而方法则定义了类的行为。

class Student {
  String name;
  int age;

  Student(this.name, this.age);

  void study() {
    print('$name is studying.');
  }
}

在这个例子中,nameage 就是 Student 类的属性,而 study 是它的方法。

三、类的实例化

创建类的对象,或者说实例化类,可以使用 new 关键字,也可以省略 new

void main() {
  Student stu = Student('Tom', 20);
  print('Name: ${stu.name}, Age: ${stu.age}'); // 输出:Name: Tom, Age: 20
  stu.study(); // 输出:Tom is studying.
}

在这个例子中,我们使用 new 关键字(实际上,现在Dart语言允许我们省略 new 关键字)创建了一个 Student 类的对象,并通过构造函数初始化了 nameage 两个属性。

四、Getter和Setter

在Dart中,我们可以通过定义getter和setter方法来访问和修改类的属性,以实现对属性的封装和控制。

class Square {
  double side;

  Square(this.side);

  double get area => side * side;  // 定义一个 getter 方法,用于获取面积

  set area(double value) => side = sqrt(value);  // 定义一个 setter 方法,用于设置面积
}

void main() {
  Square square = Square(2);
  print(square.area);  // 输出:4.0
  square.area = 9;
  print(square.side);  // 输出:3.0
}

在这个例子中,我们定义了一个名为 Square 的类,它有一个属性 side 和两个方法 areaarea 是一个 getter 方法,它返回正方形的面积;而 area= 是一个 setter 方法,它根据给定的面积值计算并设置正方形的边长。

十七、this和super

在 Dart 中,superthis 是两个特殊的关键字,它们分别用于引用当前对象的父类和当前对象自身。

this 关键字

this 在 Dart 中用于指向当前实例。它通常用在类的方法中,来引用调用该方法的对象。特别是当实例变量的名称和方法的参数名称相同时,你可以使用 this 来区分它们。

以下是一个例子:

class Circle {
  double radius;

  Circle(double radius) {
    this.radius = radius;
  }

  double area() {
    return 3.14 * this.radius * this.radius;
  }
}

void main() {
  Circle myCircle = Circle(5);
  print(myCircle.area());  // Prints: 78.5
}

在这个示例中,我们在 Circle 的构造函数中使用 this 来区分实例变量 radius 和构造函数的参数 radius。我们还在 area 方法中使用 this 来引用实例变量 radius

super 关键字

super 在 Dart 中用于指向当前实例的父类。当你需要在子类中调用父类的方法或构造函数时,可以使用 super 关键字。

以下是一个例子:

class Person {
  String name;

  Person(this.name);

  void greeting() {
    print("Hello, my name is $name.");
  }
}

class Student extends Person {
  String major;

  Student(String name, this.major) : super(name);

  @override
  void greeting() {
    super.greeting();
    print("I'm studying $major.");
  }
}

void main() {
  Student alice = Student("Alice", "Computer Science");
  alice.greeting();  
  // Prints: Hello, my name is Alice.
  // Prints: I'm studying Computer Science.
}

在这个示例中,我们在 Student 的构造函数中使用 super 来调用父类 Person 的构造函数。我们还在 Student 类的 greeting 方法中使用 super 来调用父类的 greeting 方法。

五、静态成员:静态属性 和 静态方法

  • 在Dart中,静态成员属于类本身,而不属于类的实例。
  • 静态成员可以是静态属性(静态变量)或静态方法。
  • 我们使用 static 关键字来定义静态成员。
  • 这些成员在类的所有实例之间共享,并且可以通过类名直接访问,而不需要创建类的实例。

下面详细解释一下Dart中类的静态成员的特点和用法:

  1. 静态字段(静态变量): 静态字段是与类关联的变量,可以在类的任何方法、构造函数或静态方法中使用。它们在所有类的实例之间共享相同的值。
class Circle {
  static const double pi = 3.14;
  static int numberOfCircles = 0;

  double radius;

  Circle(this.radius) {
    numberOfCircles++;
  }
}

void main() {
  var circle1 = Circle(5);
  var circle2 = Circle(10);

  print(Circle.pi); // 输出: 3.14
  print(Circle.numberOfCircles); // 输出: 2
}

在上述示例中,pinumberOfCircles都是静态字段。pi是一个常量静态字段,表示圆周率。numberOfCircles是一个整型静态字段,用于记录创建的圆的数量。在每次创建Circle实例时,我们在构造函数中将numberOfCircles加一。通过类名直接访问静态字段,无需创建类的实例。

  1. 静态方法: 静态方法是与类关联的方法,可以在不创建类的实例的情况下调用。它们通常用于执行与类相关的操作,而不依赖于类的实例。
class MathUtils {
  static int sum(int a, int b) {
    return a + b;
  }
}

void main() {
  var result = MathUtils.sum(5, 3);
  print(result); // 输出: 8
}

在上述示例中,sum()是一个静态方法,用于计算两个整数的和。我们可以通过类名直接调用静态方法,而无需创建类的实例。

静态字段和静态方法在以下情况下特别有用:

  • 当某个数据在类的所有实例之间共享时,可以使用静态字段来存储它。
  • 当需要在类级别执行某个操作而不涉及特定实例时,可以使用静态方法。

需要注意的是,静态成员只能访问静态成员,无法访问非静态成员。静态方法也不能使用实例变量,因为它们不属于任何实例。静态成员可以通过类名直接访问,也可以通过类的实例访问(但不推荐这样做,因为这样会产生混淆)。

通过使用静态成员,我们可以在类的不同实例之间共享数据和操作,以及在不创建实例的情况下执行特定的操作。这提供了更灵活和高效的代码实现方式。


六、类的构造函数

在Dart中,构造函数是用于创建类实例的特殊方法。它们在对象创建时被调用,并且负责初始化对象的状态。Dart提供了多种类型的构造函数,包括默认构造函数、命名构造函数和工厂构造函数

  • 默认构造函数
  • 命名构造函数
  • 工厂构造函数
  • 初始化列表
  • 构造函数的链式调用
  • 常用的构造函数用例

1. 默认构造函数

默认构造函数是在类中没有显式声明任何构造函数时自动提供的构造函数。它的名称与类名相同,并且没有参数。默认构造函数可以用来创建对象的实例,并且可以进行对象属性的初始化。

class MyClass {
  String name;

  MyClass() {
    name = 'Default';
  }
}

在上面的示例中,MyClass 类有一个默认构造函数,它初始化了 name 属性为 'Default'

2. 命名构造函数

命名构造函数是通过给构造函数添加名称来定义的。它们允许您为同一个类创建多个不同的构造函数,每个构造函数具有不同的参数列表。通过命名构造函数,您可以根据不同的需求来实例化对象。

class MyClass {
  String name;

  MyClass.defaultName() {
    name = 'Default';
  }

  MyClass.customName(String newName) {
    name = newName;
  }
}

在上面的示例中,MyClass 类有两个命名构造函数:defaultNamecustomNamedefaultName 构造函数不接受任何参数,而 customName 构造函数接受一个 newName 参数,并将其赋值给 name 属性。

3. 工厂构造函数

工厂构造函数是用于创建对象的特殊构造函数。与其他构造函数不同,工厂构造函数可以返回一个已存在的实例或者返回一个新的实例。工厂构造函数使用 factory 关键字进行声明。

实际上,工厂构造函数甚至可以返回其他类型的对象,而不是返回类的实例。

下面是工厂构造函数的基本语法:

class ClassName {
  factory ClassName() {
    // return an instance of ClassName
  }
}

这些场景往往可以用到工厂构造函数

    1. 对象缓存: 在某些情况下,我们可能希望复用已经创建的对象,而不是每次都创建新的对象。这在某些资源密集型的操作中尤为有用。通过使用工厂构造函数,我们可以在创建对象之前检查缓存,如果存在相同类型的对象,则直接返回缓存中的对象,而不需要创建新的对象。
    1. 单例模式: 单例模式是一种常见的设计模式,它要求一个类只能有一个实例。工厂构造函数可以帮助我们实现单例模式,通过在工厂构造函数中进行逻辑判断,如果已经存在该类的实例,则返回现有的实例,否则创建新的实例并返回。
    1. 对象初始化: 有时我们希望在创建对象之前进行一些额外的初始化操作,例如设置默认值或从外部数据源加载数据。通过工厂构造函数,我们可以在对象实例化之前执行这些初始化操作,并返回一个完全初始化的对象。

1. 对象缓存

class Cache {
  static final Map<String, Cache> _cache = {};

  final String key;

  // 私有构造函数
  Cache._(this.key);

  // 工厂构造函数
  factory Cache(String key) {
    if (_cache.containsKey(key)) {
      // 如果缓存中已存在相同的键,则直接返回缓存的对象
      return _cache[key];
    } else {
      // 如果缓存中不存在相同的键,则创建新的对象,并将其添加到缓存中
      var cache = Cache._(key);
      _cache[key] = cache;
      return cache;
    }
  }
}

void main() {
  var cache1 = Cache("key");
  var cache2 = Cache("key");

  print(identical(cache1, cache2)); // 输出: true(相同的对象)
}

在上述示例中,我们创建了一个简单的缓存类Cache,其中包含一个静态的缓存对象_cache,用于存储已经创建的Cache实例。工厂构造函数Cache通过传入的key参数来判断是否存在相同的缓存对象。如果缓存中已经存在相同的键,则直接返回缓存中的对象;否则,创建一个新的缓存对象,并将其添加到缓存中。通过使用工厂构造函数,我们可以避免重复创建相同的对象,提高代码的效率。

2. 单例模式

class Singleton {
  static Singleton _instance;

  // 私有构造函数
  Singleton._();

  // 工厂构造函数
  factory Singleton() {
    if (_instance == null) {
      // 如果实例不存在,则创建新的实例
      _instance = Singleton._();
    }
    return _instance;
  }
}

void main() {
  var singleton1 = Singleton();
  var singleton2 = Singleton();

  print(identical(singleton1, singleton2)); // 输出: true(相同的对象)
}

在上述示例中,我们创建了一个单例类Singleton,其中包含一个静态的实例对象_instance。工厂构造函数Singleton检查_instance是否为空,如果为空,则创建新的实例;否则,直接返回现有的实例。通过使用工厂构造函数,我们确保了Singleton类只能有一个实例存在。

3. 对象初始化

class Configuration {
  final String apiKey;
  final String apiUrl;

  // 私有构造函数
  Configuration._({required this.apiKey, required this.apiUrl});

  // 工厂构造函数
  factory Configuration({String apiKey = '', String apiUrl = ''}) {
    // 在创建对象之前,可以进行一些初始化操作
    // 例如从外部数据源加载配置信息,设置默认值等
    return Configuration._(apiKey: apiKey, apiUrl: apiUrl);
  }
}

void main() {
  var config1 = Configuration(apiKey: 'abc123', apiUrl: 'https://example.com');
  var config2 = Configuration(); // 使用默认值

  print(config1.apiKey); // 输出: abc123
  print(config2.apiUrl); // 输出: ''
}

在上述示例中,我们创建了一个配置类Configuration,其中包含apiKeyapiUrl两个属性。工厂构造函数Configuration可以接收可选的命名参数,并在创建对象之前进行一些初始化操作。在示例中,我们可以根据传入的参数设置配置信息,如果没有提供参数,则使用默认值。通过使用工厂构造函数,我们可以在对象创建之前执行必要的初始化逻辑,并返回一个完全初始化的对象。

4. 初始化列表

初始化列表允许您在构造函数体执行之前对实例变量进行初始化。它们在构造函数的参数列表后面使用冒号 : 来表示,并用逗号分隔多个初始化表达式。

class MyClass {
  String name;
  int age;

  MyClass(String newName, int newAge)
      : name = newName,
        age = newAge {
    print('Object initialized with name: $name, age: $age');
  }
}

在上面的示例中,构造函数使用初始化列表对 nameage 实例变量进行初始化。

5. 构造函数的链式调用

Dart中的构造函数可以通过调用同一个类中的其他构造函数来实现链式调用。使用 this 关键字可以在当前构造函数中调用其他构造函数。

class MyClass {
  String name;
  int age;

  MyClass(this.name) {
    age = 18;
  }

  MyClass.withAge(this.name, this.age);

  MyClass.defaultValues() : this.withAge('Default', 20);
}

在上面的示例中,MyClass 类的构造函数之间实现了链式调用。MyClass 构造函数接受 name 参数,并在内部设置 age 默认值为 18。MyClass.withAge 构造函数接受 nameage 参数。MyClass.defaultValues 构造函数通过链式调用调用了 MyClass.withAge 构造函数,并传递了默认的名称和年龄值。

6. 常用的构造函数用例

构造函数在实际开发中有多种常见用例,例如:

  • 初始化对象的属性。
  • 对象的复制和克隆。
  • 创建单例对象。
  • 实现对象池。

以下是一些常见情况的详细说明:

初始化对象的属性。
  1. 初始化对象的属性:构造函数通常用于初始化类的实例属性。通过构造函数参数,您可以传递初始值,并在构造函数中将这些值分配给对象的属性。这使得对象在创建时具有预期的初始状态。
class Person {
  String name;
  int age;

  Person(String name, int age) {
    this.name = name;
    this.age = age;
  }
}

在上面的示例中,构造函数 Person 接受 nameage 参数,并将它们分配给类的属性。

对象的复制和克隆。
  1. 对象的复制和克隆:有时候您可能需要创建一个对象的副本,或者从现有对象创建一个新对象,以便在进行修改时保留原始对象的状态。构造函数可以用于实现对象的复制和克隆。
class Point {
  double x;
  double y;

  Point(this.x, this.y);

  Point.copy(Point other) {
    x = other.x;
    y = other.y;
  }
}

在上面的示例中,Point 类具有一个拷贝构造函数 Point.copy,它接受一个 Point 类型的对象 other,并将其属性值复制到新创建的对象中。

创建单例对象
  1. 创建单例对象:单例是一种只允许存在一个实例的特殊对象。构造函数可以被设计为私有的,这样就无法通过常规方式创建多个实例。相反,构造函数可以在类内部控制实例化过程,并返回相同的实例。
class Singleton {
  static Singleton _instance;

  factory Singleton() {
    if (_instance == null) {
      _instance = Singleton._internal();
    }
    return _instance;
  }

  Singleton._internal();
}

在上面的示例中,Singleton 类使用一个私有的构造函数 _internal 和一个工厂构造函数来创建单例对象。通过静态变量 _instance 的控制,只有在首次调用时才会创建实例,之后的调用都返回相同的实例。

实现对象池
  1. 实现对象池:对象池是一种用于管理和重用对象的技术。它通过预先创建一组对象并存储在池中,然后在需要时从池中获取对象,使用完后再将其返回池中以供重用。
class Connection {
  String _id;
  bool _isBusy;

  Connection._(this._id) : _isBusy = false;

  factory Connection() {
    if (_connections.isEmpty) {
      return Connection._(Uuid().v4());
    } else {
      return _connections.removeLast();
    }
  }

  static List<Connection> _connections = [];

  void release() {
    _isBusy = false;
    _connections.add(this);
  }
}

在上面的示例中,Connection 类使用私有的构造函数 _ 和一个工厂构造函数来实现对象池。首次创建对象时,会生成一个唯一的 _id,并将其标记为未使用状态。在后续创建时,如果对象池中有可用的对象,则从池中获取对象并返回,否则会创建新的对象。使用完对象后,可以通过 release 方法将对象返回给池中。

四、继承

在Dart中,类的继承是一种机制,它允许一个类(子类)继承另一个类(父类)的属性和方法。通过继承,子类可以重用父类的代码,并且可以添加、覆盖或修改父类的行为。这样可以提高代码的可重用性和可维护性。

下面详细解释一下Dart中类的继承的特点和用法:

  1. 基本语法: Dart中的继承使用关键字extends来实现。子类可以继承父类的属性和方法,并可以添加自己的属性和方法。继承的关系是单向的,一个类只能有一个父类(单继承),但可以实现多个接口(多实现)。
class Animal {
  String name;

  void eat() {
    print("正在吃...");
  }
}

class Dog extends Animal {
  void bark() {
    print("汪汪汪!");
  }
}

void main() {
  var dog = Dog();
  dog.name = "旺财";
  dog.eat(); // 输出: 正在吃...
  dog.bark(); // 输出: 汪汪汪!
}

在上述示例中,Animal是父类,Dog是子类。Dog通过extends关键字继承了Animal的属性和方法。子类可以访问父类的属性name和方法eat(),并且还可以定义自己的方法bark()

  1. 覆盖方法: 子类可以覆盖(重写)父类的方法,以改变方法的实现或添加特定的行为。要覆盖父类的方法,子类需要使用@override注解来标记重写的方法。
class Animal {
  void eat() {
    print("正在吃...");
  }
}

class Dog extends Animal {
  @override
  void eat() {
    print("正在啃骨头...");
  }
}

void main() {
  var dog = Dog();
  dog.eat(); // 输出: 正在啃骨头...
}

在上述示例中,子类Dog覆盖了父类Animaleat()方法,改变了方法的实现。当调用dog.eat()时,输出的信息是子类中重写后的实现。

  1. 构造函数的继承: 子类可以继承父类的构造函数,以便在创建子类对象时初始化继承的属性。如果子类没有显式定义构造函数,则会隐式继承父类的无参构造函数。如果子类显式定义了构造函数,则需要使用super关键字来调用父类的构造函数。
class Animal {
  String name;

  Animal(this.name);
}

class Dog extends Animal {
  Dog(String name) : super(name);
}

void main() {
  var dog = Dog("旺财");
  print(dog.name); // 输出: 旺财
}

在上述示例中,子类Dog通过显式定义构造函数,并使用super关键字调用父类Animal的构造函数来初始化继承的属性name

  1. 抽象类和接口: Dart中的抽象类(使用abstract关键字)和接口(使用implements关键字)提供了更严格的约束和多态性。抽象类不能被实例化,只能用作其他类的父类,而接口定义了一组要求实现的方法。
abstract class Animal {
  void eat();
}

class Dog implements Animal {
  @override
  void eat() {
    print("正在吃...");
  }
}

void main() {
  var dog = Dog();
  dog.eat(); // 输出: 正在吃...
}

在上述示例中,Animal是一个抽象类,定义了一个抽象方法eat()Dog类通过implements关键字实现了Animal接口,并实现了eat()方法。

通过继承,我们可以构建类之间的层次结构,实现代码的重用和扩展。继承允许子类继承父类的属性和方法,并可以添加自己的行为或修改继承的行为。这提供了灵活性和可扩展性,使得代码更加模块化和可维护。

六、抽象类

在Dart中,抽象类是一种特殊的类,它不能被实例化,只能被用作其他类的父类。抽象类用于定义一组共同的属性和方法,供子类继承和实现。抽象类可以包含抽象方法,这些方法没有具体的实现,需要在子类中进行实现。

以下是关于Dart类的抽象的详细解释:

  1. 定义抽象类: 在Dart中,可以使用关键字abstract来定义抽象类。抽象类可以包含抽象方法和非抽象方法。抽象方法在抽象类中没有具体的实现,需要在子类中进行实现。
abstract class Animal {
  String name;

  void eat(); // 抽象方法
  void sleep(); // 抽象方法

  void move() {
    print("$name正在移动..."); // 非抽象方法
  }
}

在上述示例中,Animal是一个抽象类。它定义了一个抽象属性name,以及两个抽象方法eat()sleep()。同时,Animal还定义了一个非抽象方法move(),它有具体的实现。

  1. 继承抽象类: 其他类可以通过继承抽象类来使用抽象类的属性和方法。继承抽象类的子类必须实现父类中的所有抽象方法,否则子类也必须声明为抽象类。
class Dog extends Animal {
  @override
  void eat() {
    print("狗正在吃...");
  }

  @override
  void sleep() {
    print("狗正在睡觉...");
  }
}

void main() {
  var dog = Dog();
  dog.name = "旺财";
  dog.eat(); // 输出: 狗正在吃...
  dog.sleep(); // 输出: 狗正在睡觉...
  dog.move(); // 输出: 旺财正在移动...
}

在上述示例中,Dog类继承了抽象类AnimalDog类必须实现Animal中的抽象方法eat()sleep(),并且可以使用父类中定义的非抽象方法move()

  1. 抽象类的特点:
  • 抽象类不能被实例化,只能被用作其他类的父类。
  • 抽象类可以包含抽象方法和非抽象方法。抽象方法没有具体的实现,需要在子类中实现。
  • 子类继承抽象类时,必须实现父类中的所有抽象方法。
  • 如果子类没有实现父类中的所有抽象方法,则子类也必须声明为抽象类。
  • 抽象类可以包含实例变量和非抽象方法的具体实现。

抽象类在面向对象编程中有多种应用,其中一些包括:

  • 定义一组共同的属性和方法,供多个相关的子类继承和实现。
  • 强制子类实现特定的行为,以确保一致性和规范性。
  • 提供默认的实现,以减少重复代码并提高代码的可维护性。

五、接口

在Dart中,接口是一种约定,用于定义类应该具有的方法和属性。接口定义了一组要求实现的方法和属性,类可以通过实现接口来满足这些要求。接口提供了一种机制,使得类可以在不共享相同基类的情况下具有相似的行为。

以下是关于Dart类的接口的详细解释:

  1. 定义接口: 在Dart中,接口可以通过抽象类或者特定的关键字implements来定义。抽象类可以作为接口使用,其中所有的方法都是抽象的,或者使用implements关键字定义一个接口。

使用抽象类定义接口:

abstract class Animal {
  void eat();
  void sleep();
}

class Dog implements Animal {
  @override
  void eat() {
    print("狗正在吃...");
  }

  @override
  void sleep() {
    print("狗正在睡觉...");
  }
}

void main() {
  var dog = Dog();
  dog.eat(); // 输出: 狗正在吃...
  dog.sleep(); // 输出: 狗正在睡觉...
}

在上述示例中,Animal是一个抽象类,定义了两个抽象方法eat()sleep()Dog类通过implements关键字实现了Animal接口,并实现了接口中的两个方法。

  1. 实现多个接口: 在Dart中,一个类可以实现多个接口,使用逗号分隔每个接口的名称。
abstract class CanSwim {
  void swim();
}

abstract class CanFly {
  void fly();
}

class Duck implements CanSwim, CanFly {
  @override
  void swim() {
    print("鸭子正在游泳...");
  }

  @override
  void fly() {
    print("鸭子正在飞行...");
  }
}

void main() {
  var duck = Duck();
  duck.swim(); // 输出: 鸭子正在游泳...
  duck.fly(); // 输出: 鸭子正在飞行...
}

在上述示例中,Duck类实现了CanSwimCanFly两个接口。它分别实现了swim()fly()方法,满足了这两个接口的要求。

  1. 接口的特点:
  • 接口定义了类应该具有的行为,但不提供实现。实现接口的类必须提供接口中定义的方法的具体实现。
  • 类可以实现一个或多个接口,这使得类能够具有多态性,即使用接口类型的变量可以引用具体实现了该接口的对象。
  • 接口可以继承其他接口,通过继承可以建立接口之间的层次结构。
  • 类可以同时继承父类并实现接口,但需要将extends关键字放在implements关键字之前。
class Animal {
  void eat() {
    print("动物正在吃...");
  }
}

abstract class CanSwim {
  void swim();
}

abstract class CanFly {
  void fly();
}

class Duck extends Animal implements CanSwim, CanFly {
  @override
  void swim() {
    print("鸭子正在游泳...");
  }

  @override
  void fly() {
    print("鸭子正在飞行...");
  }
}

void main() {
  var duck = Duck();
  duck.eat(); // 输出: 动物正在吃...
  duck.swim(); // 输出: 鸭子正在游泳...
  duck.fly(); // 输出: 鸭子正在飞行...
}

在上述示例中,Duck类继承了Animal父类,并实现了CanSwimCanFly两个接口。它既具有父类的方法eat(),又满足了接口中定义的swim()fly()方法。

接口在Dart中提供了一种灵活的方式来定义类的行为和要求,使得类之间可以具有共同的行为,而不需要共享相同的基类。接口提供了代码的重用和模块化,并促进了代码的可维护性和可扩展性。

抽象和接口的对比

  1. 定义方式:
  • 抽象类:抽象类使用关键字abstract定义,可以包含抽象方法和非抽象方法,也可以包含实例变量。
  • 接口:接口使用abstract关键字定义抽象方法和implements关键字来实现接口,接口中只能定义抽象方法和常量。
  1. 实现方式:
  • 抽象类:其他类可以通过继承抽象类来使用抽象类的属性和方法。子类必须实现抽象类中的所有抽象方法,但可以选择性地实现非抽象方法。
  • 接口:其他类可以通过实现接口来满足接口的要求。实现接口意味着类必须实现接口中定义的所有方法,以确保类具有所需的行为。
  1. 多继承:
  • 抽象类:Dart中的类只能继承一个抽象类,即单继承。
  • 接口:一个类可以实现多个接口,即多实现。这使得类能够具有不同接口的行为,实现多态性。
  1. 实现限制:
  • 抽象类:子类可以扩展抽象类,继续添加新的属性和方法。
  • 接口:类可以实现多个接口,但接口本身不能定义具体的实现。接口仅定义了类应该具有的方法,具体的实现在实现类中完成。
  1. 使用场景:
  • 抽象类:通常用于定义具有一组共同行为的类层次结构,并在子类中提供通用的属性和方法实现。抽象类可以提供默认的行为,并要求子类提供特定的行为。
  • 接口:用于定义一组方法,以约束类应该具有的行为。接口提供了一种合同,保证了类可以实现接口中定义的方法,从而确保类具有特定的行为。

十四、混合

当我们编写代码时,经常会遇到需要在多个类中共享和重用某些功能的情况。而继承和接口的方式并不总是最合适的选择。这时候,混合就是一个非常有用的概念。

以下是关于Dart类的混合的详细解释:

  1. 定义混合: 混合是一个类,它提供了一组方法和属性,可以被其他类通过使用关键字with来引入和重用。混合类可以包含实例变量、方法和getter/setter等。
mixin Logger {
  void log(String message) {
    print("日志: $message");
  }
}

class Circle with Logger {
  double radius;

  Circle(this.radius);

  double get area => 3.14 * radius * radius;
}

void main() {
  var circle = Circle(5);
  print(circle.area); // 输出: 78.5
  circle.log("计算圆的面积"); // 输出: 日志: 计算圆的面积
}

在上述示例中,Logger是一个混合类,它定义了一个log()方法。Circle类通过使用关键字with引入了Logger混合,从而获取了Logger类中定义的方法。Circle类还定义了一个area属性和构造函数。

  1. 使用混合: 通过使用关键字with,我们可以在类中引入一个或多个混合,从而将混合的功能合并到类中。在使用混合时,类可以获取混合类中定义的方法和属性。
mixin Logger {
  void log(String message) {
    print("日志: $message");
  }
}

mixin Messenger {
  void sendMessage(String message) {
    print("发送消息: $message");
  }
}

class User with Logger, Messenger {
  String name;

  User(this.name);
}

void main() {
  var user = User("John");
  user.log("登录系统"); // 输出: 日志: 登录系统
  user.sendMessage("欢迎登录"); // 输出: 发送消息: 欢迎登录
}

在上述示例中,User类通过使用关键字with引入了LoggerMessenger两个混合。User类继承了LoggerMessenger类中定义的方法log()sendMessage()

  1. 混合的优势:
  • 代码重用:混合允许将特定功能从一个或多个类中提取出来,并将其组合到其他类中,从而实现代码的重用和模块化。
  • 灵活性:通过使用混合,可以将不同的功能组合到一个类中,而无需创建复杂的类层次结构。这使得代码更加灵活,易于组织和维护。
  • 多重继承:通过使用混合,一个类可以获取多个混合类中的功能,实现了一种类似多重继承的机制。

需要注意的是,混合类不能有显式的构造函数,因为混合类不可实例化。混合类只能通过使用with关键字来引入和重用其中的方法和属性。

通过使用混合,我们可以将特定功能从一个或多个类中提取出来,并将其合并到其他类中,从而实现代码的重用和模块化。

再来几个混合的例子

  1. 序列化混合: 在处理对象序列化和反序列化的情况下,可以创建一个序列化混合,其中定义了将对象转换为字符串和从字符串转换回对象的方法。其他类可以通过引入该混合来获取序列化和反序列化的功能。
mixin Serializable {
  String toJson() {
    // 将对象转换为 JSON 字符串
  }

  void fromJson(String json) {
    // 从 JSON 字符串中恢复对象
  }
}

class Person with Serializable {
  // ...
}

void main() {
  var person = Person();
  var json = person.toJson(); // 将对象转换为 JSON 字符串
  person.fromJson(json); // 从 JSON 字符串中恢复对象
}

在上述例子中,Serializable混合定义了将对象转换为 JSON 字符串和从 JSON 字符串恢复对象的方法。Person类引入了Serializable混合,从而获得了序列化和反序列化的功能。

  1. 数据验证混合: 在处理数据验证和校验的情况下,可以创建一个数据验证混合,其中定义了验证数据的方法。其他类可以通过引入该混合来获取数据验证的功能。
mixin DataValidator {
  bool validateData() {
    // 验证数据的逻辑
  }
}

class UserForm with DataValidator {
  // ...
}

class LoginForm with DataValidator {
  // ...
}

void main() {
  var userForm = UserForm();
  var isUserFormValid = userForm.validateData(); // 验证用户表单数据

  var loginForm = LoginForm();
  var isLoginFormValid = loginForm.validateData(); // 验证登录表单数据
}

在上述例子中,DataValidator混合定义了验证数据的方法。UserFormLoginForm类引入了DataValidator混合,从而获得了数据验证的功能。

通过混合,我们可以将具有相似功能的方法和属性提取出来,使得不同的类可以轻松地获取这些功能。这样可以提高代码的重用性、可维护性和可扩展性,并使代码更加模块化和灵活。

七、枚举

Dart的枚举(Enumeration)是一种用于定义一组有限值的数据类型,它允许我们在代码中以一种更加直观和易读的方式表示特定的取值范围。

以下是关于Dart枚举的详细解释:

  1. 定义枚举: 在Dart中,可以使用enum关键字来定义一个枚举。枚举类型由一组命名的常量值组成。
enum Color {
  red,
  green,
  blue
}

在上述示例中,我们定义了一个名为Color的枚举,它包含了三个常量值:redgreenblue。这些常量值是Color枚举的成员。

  1. 访问枚举成员: 通过枚举名称和成员名称的组合,我们可以访问枚举的特定成员。
void main() {
  var favoriteColor = Color.blue;
  print(favoriteColor);  // 输出: Color.blue
}

在上述示例中,我们创建了一个变量favoriteColor并将其赋值为Color.blue,然后通过print函数输出该变量。结果是Color.blue,它表示枚举Color的成员blue

  1. 枚举成员的索引: 每个枚举成员都有一个关联的索引,它表示成员在枚举中的位置,从0开始计数。
void main() {
  print(Color.red.index);    // 输出: 0
  print(Color.green.index);  // 输出: 1
  print(Color.blue.index);   // 输出: 2
}

在上述示例中,我们使用.index属性访问枚举成员的索引。

  1. 使用枚举: 枚举可以用于多种场景,例如表示有限的选项、状态、类型等。通过使用枚举,我们可以提高代码的可读性、减少错误,并使代码更具有可维护性。
enum HTTPMethod {
  get,
  post,
  put,
  delete
}

void makeHTTPRequest(HTTPMethod method) {
  print("发起一个 ${method.toString()} 请求");
}

void main() {
  var requestMethod = HTTPMethod.get;
  makeHTTPRequest(requestMethod);  // 输出: 发起一个 HTTPMethod.get 请求
}

在上述示例中,我们定义了一个名为HTTPMethod的枚举,表示HTTP请求的不同方法。然后,我们定义了一个函数makeHTTPRequest,接受一个HTTPMethod枚举作为参数,用于发起相应的HTTP请求。在main函数中,我们创建了一个变量requestMethod并将其赋值为HTTPMethod.get,然后将其作为参数传递给makeHTTPRequest函数。

十二、类的扩展方法

Dart 2.7及以上版本支持扩展方法,允许我们为已有的类添加新的实例方法,而无需修改原始类的代码。我们使用 extension 关键字来定义扩展。

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
}

void main() {
  print('42'.parseInt());  // 输出:42
}

在这个例子中,我们为 String 类添加了一个 parseInt 方法,该方法可以将字符串转换为整数。

十三、类型检查和转换

在Dart中,我们可以使用 is 关键字进行类型检查,使用 as 关键字进行类型转换。

void main() {
  var obj = 'Dart';

  if (obj is String) {
    print('obj is a String');
    String str = obj as String;
    print(str.length);
  }
}

在这个例子中,我们首先检查 obj 是否是 String 类型,然后再将 obj 转换为 String 类型,并打印其长度。

十六、运算符重载

当我们定义自定义类时,我们希望类的对象能够以更直观和自然的方式进行操作和运算。这时候,运算符重载就能够提供一种简洁、直观和易于理解的方式来表达类对象之间的操作。

运算符重载使得类的实例可以像内置类型一样使用常见的运算符,这样可以使代码更加简洁和易读,同时也符合人们对于运算符的直觉。

举个例子,假设我们有一个自定义的Vector类,表示二维向量。我们希望能够对两个Vector对象执行向量加法运算。如果没有运算符重载,我们可能需要编写一个方法来执行向量加法,如add方法:

class Vector {
  double x;
  double y;

  Vector(this.x, this.y);

  Vector add(Vector other) {
    return Vector(this.x + other.x, this.y + other.y);
  }
}

void main() {
  var v1 = Vector(2, 3);
  var v2 = Vector(4, 5);

  var result = v1.add(v2);
  print(result);  // 输出: Instance of 'Vector'
}

使用运算符重载,我们可以通过重载加法运算符+来实现更直观和简洁的语法:

class Vector {
  double x;
  double y;

  Vector(this.x, this.y);

  Vector operator +(Vector other) {
    return Vector(this.x + other.x, this.y + other.y);
  }
}

void main() {
  var v1 = Vector(2, 3);
  var v2 = Vector(4, 5);

  var result = v1 + v2;
  print(result);  // 输出: Instance of 'Vector'
}

通过重载加法运算符+,我们可以直接使用+运算符来执行向量加法,使得代码更加简洁和易读。

Dart的运算符重载的一些重要点:

  1. 运算符重载的方法名: Dart中的运算符重载使用特定的方法名来表示不同的运算符,这些方法名以operator关键字开头,后面跟着对应的运算符符号。
  2. 运算符重载的方法签名: 运算符重载的方法必须具有特定的方法签名,以匹配相应的运算符。根据不同的运算符,方法签名可能有不同的参数和返回类型。
  3. 常见的运算符重载: 以下是一些常见的运算符和相应的方法名,可以用于进行运算符重载:
  • 算术运算符:+, -, *, /, %, ~/

    • operator +
    • operator -
    • operator *
    • operator /
    • operator %
    • operator ~/
  • 关系运算符:<, >, <=, >=, ==, !=

    • operator <
    • operator >
    • operator <=
    • operator >=
    • operator ==
    • operator !=
  • 逻辑运算符:&&, ||, !

    • operator &&
    • operator ||
    • operator !
  • 位运算符:&, |, ^, ~, <<, >>

    • operator &
    • operator |
    • operator ^
    • operator ~
    • operator <<
    • operator >>
  • 赋值运算符:=, +=, -=, *=, /=, %=, ~/=, <<=, >>=, &=, |=, ^=

    • operator =
    • operator +=
    • operator -=
    • operator *=
    • operator /=
    • operator %=
    • operator ~/=
    • operator <<=
    • operator >>=
    • operator &=
    • operator |=
    • operator ^=
  • 索引运算符:[], []=

    • operator []
    • operator []=
  • 函数调用运算符:()

    • operator ()