深入理解Dart:类、泛型、库的使用技巧

3,972

介绍

本片文章有点长,主要讲了 Dart 中 类、泛型和库 几个重要的概念。请耐心看下去。并没有给出过多的解释,我觉得大家通过实例慢慢去消化,自己去理解,才是对大家最大的帮助。如有不合适,评论留言,下次我会给每一段代码更多的解释

Class 类

类(Class)是面向对象程序设计,实现信息封装的基础。类是一种用户定义的类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象。

Dart是一门使用类和单继承的面向对象语言,具有类和基于mixin的继承。所有的对象都是一个类的实例,所有的类都继承自Object。基于mixin的继承意味着尽管任何一个类(除了Object)都只有一个父类,但是类主体可以在多个类层次结构中复用。

Dart的类与其它语言都有很大的区别,比如在dart的类中可以有无数个构造函数,可以重写类中的操作符,有默认的构造函数,由于dart没有接口,所以dart的类也是接口,因此你可以将类作为接口来重新实现

构造函数

使用new语句来构造一个类,构造函数的名字可能是 ClassName,也可以是 ClassName.otherName

var example = new Example(1, 2);  	//new 可省略 var example = Example(1, 2);
print(example); 					// Example(1, 2)

// 这种写法是不是很像 JAVA 啊
class Example {
    int x;
    int y;
    Example(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

// 但在 Dart 中是可以简化成这样的 (推荐)
class Example {
    int x;
    int y;
    Example(this.x, this.y);
}

命名构造函数

使用命名构造函数可以为一个类实现多个构造函数, 或者使用命名构造函数来更清晰的表明你的意图

var example = Example.fromJson({'x': 2, 'y': 2});

class Example {
    int x;
    int y;
    
    Example(this.x, this.y);
    
    // 命名构造函数
    Example.fromJson(Map json) {
		x = json['x'];
        y = json['y'];
    }
}

重定向构造函数

有时候构造函数的目的只是重定向到该类的另一个构造函数。一个重定向构造函数是没有代码的,在构造函数声明后, 使用冒号调用其他构造函数

var example = Example.alongXAxis(0);
print(example);

class Example {
    int x;
    int y;
    
    Example(this.x, this.y);
    
    // 重定向构造函数,使用冒号调用其他构造函数
    Example.alongXAxis(int x) : this(x, 0);
}

初始化列表

在构造函数体执行之前可以初始化实例参数。 使用逗号分隔初始化表达式。初始化列表非常适合用来设置 final 变量的值

class Example {
    final String name;
    final int age;
    final String description;
    
    // 初始化列表
    Example(x, y) : 
    	x = x, 
    	y = y, 
    	description = description;
}

调用超类构造函数

  • 超类命名构造函数不会传递,如果希望使用超类中定义的命名构造函数创建子类,则必须在子类中实现该构造函数
  • 如果超类没有默认构造函数,则你需要手动的调用超类的其他构造函数
  • 调用超类构造函数的参数无法访问 this
  • 在构造函数的初始化列表中使用 super(), 需要把它放到最好
class Parent {
    int x;
    int y;
    
  	// 父类命名构造函数不会传递
    Parent.fromJson(x, y)
        : x = x,
    	y = y {
            print('父类命名构造函数');
        }
}

class Child extends Parent {
    int x;
    int y;
    
    // 若超类没有默认构造函数,需要手动调用超类其他构造函数 	
    Child(x, y) : super.fromJson(x, y) {
        // 调用父类构造函数的参数无法访问 this
        print('子类构造函数');
    }
    
    // 在构造函数的初始化列表中使用 super(), 需要把它放到最好
    Child.fromJson(x, y)
        : x = x,
    	y = y,
    	super.fromJson(x, y) {
            print('子类命名构造函数');
        }
}

常量构造函数

注意事项:

  1. 定义 const 构造函数要确保所有实例变量都是 **final **

  2. const 关键字放在构造函数名称之前

class Example {
    // 定义 const 构造函数要确保所有实例变量都是 final
    final int x;
    final int y;
    
    static final Example origin = const Example(0, 0);
    
    // const 关键字放在构造函数名称之前,且不能有函数体
    const Example(this.x ,this.y);
}

工厂构造函数

  • 工厂构造函数是一种构造函数,与普通构造函数,工厂函数不会自动生成实例,而是通过代码来决定返回的实例对象
  • 如果一个构造函数并不总是返回一个新的对象,则使用 factory 来定义这个构造函数
  • 工厂构造函数无法访问 this
class Singleton {
    String name;
    // 工厂构造函数无法访问this,所以这里要用 static (这个变量是属于类的,而不是属于对象的)
    static Singleton _cache;
    
    // 工厂方法构造函数,关键字 * factory *
    factory Singleton([String name = 'singleton']) => Singleton._cache ??= Singleton._newObject(name);
    
    // 定义一个命名构造函数来生产实例
    Singleton._newObject(this.name);
}

=========================================================================================

// 工厂函数
class Massage {
  void doMassage(){
    print('按摩');
  }
}
class FootMassage extends Massage {
  @override
  doMassage() {
    print('脚底按摩');
  }
}

class BodyMassage extends Massage {
  @override
  void doMassage() {
    print('全身按摩');
  }
}

class SpecialMassage extends Massage {
  @override
  void doMassage() {
    print('特殊按摩');
  }
}

Massage massageFactory(String type){
  switch (type) {
    case 'foot':
      return new FootMassage();
    case 'body':
      return new BodyMassage();
    default:
      return new SpecialMassage();
  }
}

Setter 和 Getter

  • 每个实例变量都隐含的具有一个 getter, 如果变量不是 final 的则还有一个 setter
  • 可以通过实行 getter 和 setter 来创建新的属性,使用 getset 关键字定义 getter 和 setter
  • getter 和 setter 的好处是,你可以开使用实例变量,可以把实例变量用函数包裹起来,而调用你代码的地方不需要修改
class Rectangle {
    int left;
    int top;
    int width;
    int height;
    
    Rectangle(this.left, this.top, this.width, this.height);
    
   
    int get right => left + width;   					// 获取right值
    set right(int value) => left = value - width;     	//设 置right值,同时left也发生变化
   
    int get bottom => top + height;
    set bottom(int value) => top = value - height;
}

抽象类

Dart 抽象类主要用于定义标准,子类可以继承抽象类,也可以实现抽象类接口,抽象类通过 abstract 关键字来定义

  • 抽象类不能被实例化,除非定义一个工厂构造函数或者继承它的子类可以
  • 抽象类通常用来定义接口,以及部分实现
  • 抽象类通常具有抽象方法,抽象方法不需要关键字,以分号结束即可
  • 抽象方式使用时,需要重写抽象类的成员变量和方法,包括私有的
  • 一个类可以 implement 一个普通类。Dart 任何一个类都是接口
  • 一个类可以 implement 多个接口

extends抽象类 和 implements的区别:

  1. 如果要复用抽象类里面的方法,并且要用抽象方法约束子类的话我们就用extends继承抽象类
  2. 如果只是把抽象类当做标准的话我们就用 implements实现抽象类

经典案例:

// 定义一个抽象类,并定义两个抽象方法
abstract class Animal {
    eat();
    run();
    printInfo() {
        print('我是一个抽象类里面的普通方法');
    }
}

// 定义一个 Dog 继承 Animal 并重写其中的方法
class Dog extends Animal  {
	@override
    eat() {
		print('小狗在吃骨头');
    }
    
    @override
    run() {
		print('小狗在跑');
    }
}

class Cat extends Animal {
    @override
    eat() {
		print('小猫在吃老鼠');
    }
    
    @override
    run() {
        print('小猫在跑');
    }
}

main() {
    Dog dog = Dog();	// 实例化 Dog
    dog.eat();  		// 小狗在吃骨头
    dog.run();			// 小狗在跑
    dog.printInfo();	// 我是一个抽象类里面的普通方法
    
    Cat cat = Cat();	
    cat.eat();			// 小猫在吃老鼠
    cat.run();			// 小猫在跑
    cat.printInfo();	// 我是一个抽象类里面的普通方法
}

不知道大家知不知道 @override 是个啥,可以当做是重写吧

可调用类

实现 call() 方法可以让类像函数一样能够被调用

class TransferClass {
    call(String a, String b, String c) => '$a $b $c!';
}

main() {
    var transfer = TransferClass();
    var String = transfer("dart","flutter","top"); 
    print('$test');					// dart flutter top!
    print(transfer.runtimeType);	// TransferClass
    print(test.runtimeType);		// String
    print(transfer is Function);	// false
}

Mixin 泛型

使用泛型,很多的容器对象,在创建对象时都可以定义泛型类型,跟 Java 一样

var list = List<String>();
var map = Map<int, String>();

// 运行时可判断泛型
print(list is List<String>); 	// true
print(list.runtimeType);		// JSArray<String>

泛型函数

Dart1.21 开始可以使用泛型函数

泛型函数可以在以下几个地方使用类型参数:

<1> 函数的返回值类型

<2> 参数的类型

<3> 局部变量的类型

main() {
    K addCache<K, V>(K key, V value) {
        K temp = key;
        print('${key} : ${value}');  // dart : flutter
       	return temp;
    }
    
    var key = addCache('dart', 'flutter');
    print(key);  // dart
}

泛型构造函数

要在使用构造函数时指定一个或多个类型,可将类型放在类名称后面的尖括号<...>中

main() {
    var phone = Phone<String>('6888');
    print(phone.mobileNumber);   // 6888
}

class Phone<T> {
    final T mobileNumber;
    Phone(this.mobileNumber);
}

泛型限制

实现泛型类型时,您可能希望限制其参数的类型,可以在<>里面使用extends

main() {
    var footMessage = FootMessage();
    var message = Message<FootMessage>(footMessage);
    message.message.doMessage();	// 脚底按摩
}

class Message<T extends FootMessage> {
    final T message;
    Message(this.message);
}

class FootMessage {
    void doMessage() {
        print('脚底按摩');
    }
}

使用 import 去指定如何在另一个库的范围内使用来自一个库的命名空间

import 后的参数必须参数为库的 URL (Uniform Resource Identifier统一资源标识符)

例如,Dart web应用程序通常使用 Dart : html 库,它们可以像这样导入:

import 'dart:html';

使用核心库

对于内置的库,URI 使用特殊的 dart: scheme

import 'dart:math';
import 'dart:io';
import 'dart:convert';
....

void main() {
    print(sqrt(4));  // math > 开平方2.0
}

载入第三方库

对于其他的库,你可以使用文件系统路径或者 package: scheme

如果需要载入第三方库我们是需要在 pubspec.yaml 文件中声明需要引用的库,使用 Packages get 进行拉取

编写 pubspec.yaml:

dependencies:
	flutter:
		sdk: flutter
	
	cupertion_icons: ^0.1.0     
	dio: ^2.1.0

调用:

import 'package:dio/dio.dart';   // Dio 一个很强大的网络请求库

void main() {
    getHttp();
}

void getHttp() async {
    try {
        Response response = await Dio().get("http://www.baidu.com");
    } catch (e) {
        print(e);
    }
}

在上方的例子中我们使用了 asyncawait ,大家可以自行预习,下篇文章应该会讲到

载入文件

我们先创建一个 mylib.dart 的文件,并写入以下内容,写啥都不要紧,随便写

class MyLib {
    String name;
    static MyLib _cache;
    factory MyLib([String name = 'singleton']) => MyLib._catch ??= MyLib._newObject(name);
    
    MyLib._newObject(this.name);
}

我们在另一个文件中引入 mylib.dart 文件, 我这里是同一级目录,所以直接这么引入哈

import 'mylib.dart';

void main() {
    var myLib = MyLib();  // 实例化 mylib.dart 中的类
}

指定库前缀

如果两个库有冲突的标识符,可以为其中一个或两个库都指定前缀来避免冲突

假设我们有 mylib1.dartmylib2.dart 这两个文件都有一个名为 MyLib 的类

class MyLib() {
	String name;
    MyLib(this.name);
}

这个时候我们需要在一个地方同时引入这两个文件,并实例化 MyLib

import 'mylib1.dart';
import 'mylib2.dart';
   
void main() {
	var myLib1 = MyLib();
    var myLib2 = MyLib();
}

不用我说了,大家看着都有问题,那我们是不是可以指定一个前缀(别名) 呢, 在dart里可以使用 as 来为引入的文件指定一个前缀

import 'mylib1.dart' as lib1;
import 'mylib2.dart' as lib2;
   
void main() {
	var myLib1 = lib1.MyLib();
    var myLib2 = lib2.MyLib();
}

选择性载入

如果你只需要使用库的一部分功能,则可以选择需要导入的内容

  • show 只载入库的某些部分
  • hide 筛选掉库的某些部分
import 'mylib1.dart' as lib1 show Test
import 'mylib2.dart' as lib2 hide Test

延迟载入

  1. 使用 await 关键字暂停代码执行一直到库加载完成
  2. 可提高程序的启动速度
  3. 用在不常使用的功能
  4. 用在载入时间过长的包
  5. 执行 A/B 测试,例如尝试各种算法的不同实现

使用 deferred as 导入,使用标识符调用 loadLibrary() 加载库

import 'mylib1.dart' deferred as lazyLib;

void main() {
	lazyLoad()''
}

lazyLoad () async {
    await lazyLib.loadLibrary();
	var t = lazyLib.Test();
    t.test();
}

自定义库

用 library 来来命名库,用part来指定库中的其他文

  1. part 可以把一个库分开到多个 Dart 文件中

  2. 或者我们想让某一些库共享它们的私有对象的时候,可以需要使用 part

  3. import 不会完全共享作用域,而 part 之间是完全共享的。如果说在A库中import了B库,B库import了C库,A库是没有办法直接使用C库的对象的。而B,C若是A的part,那么三者共享所有对象。并且包含所有导入

注意:不必在应用程序中(具有顶级main()函数的文件)使用library,但这样做可以让你在多个文件中执行应用程序

// mylib/tool.dart
part of mylib;
void printTool() => print('tool')
// mylib/util.dart
part of mylib;
void printUtil() => print('util');
// mylib/mylib.dart
library mylib;

part 'util.dart';
part 'tool.dart';

void printMyLib() => print('mylib');
import 'mylib/mylib.dart';

void main() {
  printMyLib();
  printUtil();
  printTool();
}