flutter自学笔记4- dart 语法快速学习

536 阅读41分钟

笔记1:介绍一下flutter

笔记2-了解Flutter UI 页面和跳转

笔记3- 常用 Widget 整理

笔记4- dart 语法快速学习

笔记5- dart 编码规范

笔记6- 网络请求、序列化、平台通道介绍

笔记7- 状态管理、数据持久化

笔记8- package、插件、主题、国际化

笔记9 - 架构、调试、打包部署

笔记10- Widget 构建、渲染流程和原理、布局算法优化

笔记11- 性能、渲染、包体积、懒加载、线程、并发隔离

通过上一篇《flutter自学笔记3- 常用 Widget 整理》, 从整体上初步了解了常用 Widget 整理。

笔记4: 通过前一篇”常用 Widget 整理“ 我快速了解了常用UI功能,这为后续搭建复杂页面打好基础

由简入繁,较为复杂的底层原理后面学习的时候整理。

同时,现在也该准备了解一下dart语法了,毕竟功能开发不能光有UI,那么本文将快速了解dart的基本语法。

内容较长,初学的同学可以花一两个小时快速学习,也可做后续参考索引。

一、Dart历史版本

Dart是Google开发的一门编程语言,其版本历程可以归纳如下:

1、早期版本

  • 首个技术预览版:2011年10月,Google在GOTO开发者大会上首次发布了Dart编程语言的技术预览版。该版本包含了Dart语言的基本语法、库以及一个Dart VM(虚拟机)。
  • 1.0版本:2013年11月14日,Google发布了Dart 1.0版本。这是Dart的第一个稳定版本,标志着Dart正式进入生产环境。Dart 1.0提供了稳定的SDK,支持Dart-to-JavaScript编译,并带来了Dartium(内置Dart VM的Chromium浏览器)。

2、Dart 2.0及后续版本

  • 2.0版本:2018年8月8日,Dart 2.0版本发布。这个版本对Dart进行了全新改版,从底层重构了Dart,加入了很多面向未来的新特性,包括强类型系统和改进的开发工具,语言性能大幅提高。Dart 2.0还重写了Dart web platform,提供了一套高性能、可扩展的生产工具。Dart 2.0的发布标志着Dart作为主流编程语言的重生,为移动和Web应用程序实现快速开发并拥有出色的用户体验。
  • 2.1、2.2和2.3版本:2019年,Dart相继发布了2.1、2.2和2.3版本,这些版本在性能和功能上进行了进一步优化和完善。
  • 2.12版本:2021年3月3日,Dart 2.12版本发布,引入了Null Safety(空安全)功能。这是Dart语言的一个重要里程碑,提供了静态类型检查,防止空引用异常,从而提高代码的可靠性和安全性。这也是接入Flutter之后第一次大规模更新,工程项目中所有的依赖项、工程自身以及编码细节均进行了更新。

3、Dart 3.0及后续版本

  • 3.0版本:2023年3月,Dart 3.0版本发布。这个版本增加了空安全特性,并引入了新的核心功能,如记录(Records)模式(Patterns)和类修饰符(Class Modifiers),这些特性简化了复杂数据结构的处理,并使代码更加具有表达力。
  • 后续版本:截至2024年5月,Dart已经发布了多个后续版本,如3.4.0等。这些版本在性能、功能和稳定性上进行了持续优化和改进。

4、与Flutter的结合

  • Flutter 1.0版本发布:2018年12月4日,Google发布了Flutter 1.0版本。Flutter是一个基于Dart的UI工具包,用于构建跨平台的移动应用程序。Flutter的引入极大地推动了Dart的普及,特别是在移动开发领域。
  • Flutter与Dart的紧密结合:Flutter使用Dart作为其底层语言,并提供了丰富的UI组件、工具和库,使开发者能够快速构建美观、流畅的跨平台应用程序。Dart和Flutter之间有密切的关联,它们共同构成了构建跨平台应用程序的技术栈。

二、Dart 简介

1、入口函数

每个应用都有一个顶层的 main() 函数来作为运行入口:

void main() {
  print('Hello, World!');
}

2、变量

可以用 var 来定义变量,由于其支持类型推断,而不用显式指定它们的类型。

var name = 'Voyager I';
var year = 1977;
var antennaDiameter = 3.7;
var flybyObjects = ['Jupiter', 'Saturn', 'Uranus', 'Neptune'];
var image = {
  'tags': ['saturn'],
  'url': '//path/to/saturn.jpg'
};

3、枚举类型 (Enum)

enum directionType { 
  left, 
  right
}

4、流程控制语句

if ( ) { } else if ( ) { }

for (final object in Objects) {
}

for (int month = 1; month <= 12; month++) {
}

while (year < 2038) {
}

switch 等等

5、函数

int fibonacci(int n) {
  if (n == 0 || n == 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

var result = fibonacci(20);

=> (胖箭头) 简写语法用于仅包含一条语句的函数。该语法在将匿名函数作为参数传递时非常有用:

flybyObjects.where((name) => name.contains('turn')).forEach(print);

6、注释

// 单行注释.

/// 文档注释
/// 文档注释

/* 文档注释 */

7、导入 (Import)

// 引用核心库
import 'dart:math';

// 从外部包导入库
import 'package:test/test.dart';

// 导入文件
import 'path/to/my_other_file.dart';

8、类 (Class)

下面的示例中向你展示了一个包含三个属性、两个构造函数以及一个方法的类。其中一个属性不能直接赋值,因此它被定义为一个 getter 方法(而不是变量)。该方法使用字符串插值来打印字符串文字内变量的字符串。

class Person {
  ...
    
  // Method.
  void desc() {
    
  }
}

9、扩展类(继承)

Dart 支持单继承。

class Student extends Person {
  ...
}

10、Mixins

Mixin 多个类中重用代码的方法,避免了一些多重继承带来的复杂性和问题的机制,类似swift中协议

注意:

  • 循环依赖:避免Mixin之间形成循环依赖,这会导致编译错误。
  • 命名冲突:如果两个Mixin定义了相同名称的方法或属性,且它们被应用到同一个类中,那么最后应用的Mixin中的定义会覆盖前面的定义。
  • Mixin和接口的区别:Mixin不仅仅是接口的定义,它还可以包含实现代码,这使得它比接口更强大和灵活。

示例

一个类可以同时使用多个Mixin

mixin MixinA {
  void methodA() {
    print("MethodA from MixinA");
  }
}

mixin MixinB {
  void methodB() {
    print("MethodB from MixinB");
  }
}

class MyClass with MixinA, MixinB {
  // MyClass现在可以使用MixinA和MixinB中定义的方法
}

void main() {
  MyClass myObject = MyClass();
  myObject.methodA(); // 输出: MethodA from MixinA
  myObject.methodB(); // 输出: MethodB from MixinB
}

11、接口

在Dart中,接口并不是通过interface关键字来定义的,而是通过抽象类(abstract class)或普通类来实现接口的功能。一个类通过implements关键字来实现一个或多个接口(即抽象类或普通类定义的规范)。

  1. 定义接口
    • 使用抽象类定义接口时,该类可以包含抽象方法(没有方法体的方法)和具体实现的方法。
    • 普通类也可以作为接口被实现,但这种情况下,实现类需要覆写普通类中的所有方法。
  2. 实现接口
    • 一个类可以使用implements关键字来实现一个或多个接口。
    • 实现类必须提供接口中所有抽象方法的实现,否则该类本身也将成为抽象类。
  3. 接口的多重实现
    • Dart支持一个类实现多个接口,这通过逗号分隔多个接口名称并在类定义后使用implements关键字来实现。

12、抽象类

抽象类是一种特殊的类,它主要用于定义一组子类必须遵循的规则或标准。

  1. 定义抽象类
    • 使用abstract关键字来定义抽象类。
    • 抽象类可以包含抽象方法(没有方法体的方法)和具体实现的方法。
  2. 继承抽象类
    • 子类通过extends关键字来继承抽象类。
    • 子类必须实现抽象类中定义的所有抽象方法,否则子类也将成为抽象类。
  3. 抽象类的用途
    • 抽象类主要用于定义一组相关类之间的共同行为。
    • 它提供了一种机制来约束子类的实现,确保子类具有某些特定的方法或属性。

13、接口与抽象类的比较

  1. 定义方式
    • 接口在Dart中是通过抽象类或普通类来实现的,没有专门的interface关键字。
    • 抽象类则直接使用abstract class关键字来定义。
  2. 实现方式
    • 接口通过implements关键字来实现。
    • 抽象类通过extends关键字来继承。
  3. 方法的实现
    • 接口中的方法通常是抽象的,需要在实现类中提供具体实现。
    • 抽象类中可以包含具体实现的方法,也可以包含抽象方法。
  4. 多重继承
    • Dart不支持传统的多重继承,但可以通过实现多个接口来达到类似的效果。
    • 抽象类只能被单继承。
  5. 实例化
    • 接口本身不能被实例化,它只定义了一组方法签名。
    • 抽象类也不能被直接实例化,但可以通过其子类来实例化。

14、异步

使用 asyncawait 关键字可以让你避免回调地狱 (Callback Hell) 并使你的代码更具可读性。

const oneSecond = Duration(seconds: 1);
// ···
Future<void> printWithDelay(String message) async {
  await Future.delayed(oneSecond);
  print(message);
}

15、异常

抛出异常

if (astronauts == 0) {
  // 使用 `throw` 关键字抛出一个异常
  throw StateError('No astronauts.');
}

捕获异常:

使用 try 语句配合 oncatch(两者也可同时使用)关键字来捕获一个异常:

try {
  ...
} on IOException catch (e) {
  print('Exception object: $e');
} finally {
  // end
}

三、基础表达式

3.1、变量

在Dart编程语言中,变量是用于存储数据的容器。每个变量都有一个名称(标识符)和一个类型,该类型决定了变量可以存储的数据类型。Dart是一种静态类型语言,意味着在编译时变量的类型就已经确定。然而,Dart也提供了类型推断功能,允许在不显式指定类型的情况下声明变量。

1、变量声明
  • 使用varlet(尽管let在Dart中并不常用,它来自JavaScript)或具体类型来声明变量。
  • var允许类型推断,编译器会根据赋值的类型自动推断变量的类型。
  • 推荐在明确知道变量类型时使用具体类型声明,以提高代码的可读性和安全性。
// 使用var进行类型推断
var number = 42; // 推断为int类型
var name = "Alice"; // 推断为String类型
2、变量命名
  • 变量名必须以字母、下划线(_)或美元符号($)开头。
  • 后续字符可以是字母、数字、下划线或美元符号。
  • 变量名是大小写敏感的。
  • 避免使用Dart的关键字作为变量名。
3、类型注解
  • 可以在变量名之前使用类型注解来指定变量的类型。
  • 如果类型注解与赋值的类型不匹配,编译器会报错。
// 使用具体类型声明变量
int age = 30;
String occupation = "Developer";
4、常量
  • 使用const关键字声明常量。
  • 常量必须在声明时初始化,并且其值在之后不能改变。
  • 常量的类型也是静态的,必须在编译时确定。
// 声明常量
const pi = 3.14; // 常量值不能改变
const greeting = "Hello, World!";
可空类型和空安全
  • Dart 2.12及更高版本引入了空安全特性。
  • 变量可以声明为可空类型,表示该变量可以存储null值。
  • 使用?后缀来表示可空类型,例如String?表示一个可能为null的字符串。
// 可空类型
String? middleName; // 可能为null的字符串
int? optionalNumber; // 可能为null的整数
5、延迟初始化
  • 使用late关键字可以声明一个延迟初始化的非空变量。
  • late变量必须在第一次使用之前被初始化。
// 延迟初始化
late String lastName; // 必须在第一次使用前初始化
lastName = "Smith"; // 初始化lastName   
6、 final关键字
  • 用途final关键字用于声明一个只能被赋值一次的变量。一旦final变量被初始化后,其值就不能再被改变。
final String country = "USA";
// country的值在之后不能被改变
7、类型别名(Type Aliases)
  • 用途:类型别名允许你为复杂的类型表达式创建一个简短的名字,以提高代码的可读性。
typedef Person = Map<String, dynamic>;
Person person = {"name": "Alice", "age": 30};

注意:在Dart的新版本中,更推荐使用typedef的等价写法type来定义类型别名。

8、动态类型(dynamic
  • 用途dynamic类型允许你在运行时动态地确定变量的类型。使用dynamic声明的变量可以在运行时被赋予任何类型的值,并且Dart不会在编译时对其进行类型检查。
dynamic dynamicVar = 10;
dynamicVar = "Hello"; // 可以在运行时改变类型

注意 :虽然dynamic提供了灵活性,但它也放弃了Dart的强类型检查带来的好处。因此,除非确实需要动态类型,否则建议尽量避免使用

9、隐式类型转换和显式类型转换
  • 隐式类型转换:在某些情况下,Dart会自动将一种类型的值转换为另一种类型,这称为隐式类型转换。例如,将int类型的值赋给double类型的变量时,Dart会自动进行转换。

  • 显式类型转换:当需要显式地将一个类型的值转换为另一个类型时,可以使用显式类型转换。这通常通过类型名称后跟一个点(.)和as关键字来实现。

num numVar = 10.5; // num是Dart中的一个通用数字类型,可以是int或double
int intVar = numVar.toInt(); // 显式类型转换,将num转换为int
double doubleVar = numVar as double; // 在这个例子中,实际上不需要显式转换,因为numVar已经是double类型
// 但这里展示了如何使用as关键字进行显式类型转换的语法
10、变量作用域
  • 局部变量:在函数、方法或代码块内部声明的变量是局部变量,它们只能在该函数、方法或代码块内部被访问。

  • 全局变量:在函数、方法或代码块外部声明的变量是全局变量,它们可以在整个程序中被访问。

var globalVar = "I am global";

void someFunction() {
  var localVar = "I am local";
  print(localVar); // 可以访问局部变量
  // print(globalVar); // 也可以访问全局变量
}

3.2、操作符 Operators

1、算术操作符

  • 加法(+):计算两个操作数的和。
  • 减法(-):计算两个操作数的差。
  • 乘法(*):计算两个操作数的乘积。
  • 除法(/):计算两个操作数的商,返回一个双精度浮点数(double)。
  • 整除(~/):计算两个操作数的商,返回一个整数(int)。
  • 取余(%):计算两个操作数的余数。

2、赋值操作符

  • 简单赋值(=):将右操作数的值赋值给左操作数。
  • 复合赋值:包括+=、-=、*=、/=、~/=、%=等,它们进行运算并将结果赋值给左操作数。
  • 空值合并赋值(??=):在变量为null时为其分配一个新值。

3、比较操作符

  • 等于(==):检查两个操作数是否相等。
  • 不等于(!=):检查两个操作数是否不相等。
  • 大于(>):检查左侧操作数是否大于右侧操作数。
  • 小于(<):检查左侧操作数是否小于右侧操作数。
  • 大于等于(>=):检查左侧操作数是否大于等于右侧操作数。
  • 小于等于(<=):检查左侧操作数是否小于或等于右侧操作数。

4、逻辑操作符

  • 逻辑与(&&):如果两个操作数都为true,则返回true。
  • 逻辑或(||):如果两个操作数中至少有一个为true,则返回true。
  • 逻辑非(!):返回操作数的布尔值的相反值。

5、位操作符

  • 按位与(&):对两个操作数的每一位执行与操作。
  • 按位或(|):对两个操作数的每一位执行或操作。
  • 按位异或(^):对两个操作数的每一位执行异或操作。
  • 按位取反(~):对操作数的每一位执行取反操作。
  • 左移(<<):将左操作数的二进制表示向左移位,右侧补0。
  • 右移(>>):将左操作数的二进制表示向右移位,左侧补0(如果是有符号整数则根据符号位填充)。

6、条件(三元)操作符

  • 条件表达式:如果条件为真,则返回值1,否则返回值2。语法为“条件 ? 值1 : 值2”。

7、类型操作符

  • is:检查对象是否是指定的类型。
  • is!:检查对象是否不是指定的类型。
  • as:类型转换,将对象转换为指定的类型。如果转换失败,会抛出异常。
  • as?:尝试类型转换,如果失败则返回null。

8、级联操作符

  • ..:允许在同一个对象上面做一系列的操作。

9、空操作符

  • 空合并操作符(??):如果左侧操作数为null,则返回右侧操作数,否则返回左侧操作数。

10、一元操作符

  • 一元后缀:包括expr++(递增)、expr--(递减)、()、[]、?.[]、.、?.等。
  • 一元前缀:-expr(负数)、!expr(取反)、~expr(一元位补码)、++expr(递增)、--expr(递减)、await expr(异步)等。

3.3、注释

注释是开发者在代码中添加的说明性文字,用于解释代码的功能、目的或行为。注释不会被编译器执行,也不会影响代码的运行结果。Dart支持以下几种注释:

  1. 单行注释:以//开头,直到行末的文本都被视为注释。例如:

    
    // 这是一个单行注释
    
  2. 多行注释:以/*开头,以*/结尾,中间的内容都被视为注释。多行注释可以跨越多行。例如:

    /*
     * 这是一个多行注释
     * 它可以跨越多行
     */
    
  3. 文档注释:以////**开头,通常用于生成API文档。Dart SDK提供了一个名为dartdoc的工具,可以读取这些注释并生成文档。例如:

    /// 这是一个文档注释,用于描述函数的功能
    void myFunction() {
      // 函数实现
    }
    

注释的主要目的是提高代码的可读性和可维护性,帮助其他开发者理解代码

3.4、注解

在Dart中,注解(Annotations)用于为代码元素(如类、方法、变量等)提供额外的信息或元数据。这些注解不会直接影响代码的运行,但可以被编译器、开发工具或运行时框架用于各种目的,如代码分析、文档生成、警告提示等。

@Deprecated

@Deprecated注解用于标记某个元素(如类、方法、属性等)为已过时(deprecated)

示例

// 标记这个类为已过时
@Deprecated('Use NewClass instead')
class OldClass {
  void oldMethod() {
    // ...
  }
}
 
class NewClass {
  void newMethod() {
    // ...
  }
}
 
void main() {
  // 使用已过时的类和方法会生成警告
  OldClass old = OldClass();
  old.oldMethod();
 
  // 推荐使用新的类和方法
  NewClass newOne = NewClass();
  newOne.newMethod();
}

在上面的示例中,OldClass和它的oldMethod方法都被标记为已过时,并给出了推荐使用NewClass的提示。

@override

@override注解用于标明一个方法重写了父类中的方法。这有助于避免拼写错误或不正确的重写。如果子类中的方法没有正确地重写父类中的方法(例如,方法签名不匹配),那么编译器会报错。

示例

class Animal {
  void makeSound() {
    print('Animal makes a sound');
  }
}
 
class Dog extends Animal {
  // 重写父类的方法
  @override
  void makeSound() {
    print('Dog barks');
  }
}
 
void main() {
  Dog dog = Dog();
  dog.makeSound(); // 输出: Dog barks
}

在这个示例中,Dog类重写了Animal类的makeSound方法,并使用了@override注解来标明这一点。

@pragma

@pragma注解用于控制编译器的某些行为或提供额外的编译时指令。它通常与Dart的编译器指令一起使用,以改变代码在编译时的处理方式。

示例

// 假设有一个自定义的pragma用于控制某种编译行为
@pragma('dart:vm:entry-points')
void myEntryPoint() {
  // ...
}

然而,需要注意的是,@pragma注解的具体用法和效果取决于Dart编译器的实现和版本,以及是否有相应的编译器指令支持。在大多数情况下,开发者不需要直接使用@pragma注解,而是依赖于Dart语言本身提供的特性和注解。

定义自己的注解

开发者还可以定义自己的注解,以满足特定的需求。自定义注解是通过创建类来实现的,通常这些类不包含逻辑,仅用于标识。例如:

// 定义一个自定义注解
class MyAnnotation {
  final String info;
  const MyAnnotation(this.info);
}
 
// 使用自定义注解
@MyAnnotation('This is a custom annotation')
class MyClass {
  void doSomething() {
    print('Doing something');
  }
}

3.5、导库

在Dart中,import语句用于导入其他Dart文件、Dart标准库或第三方库中的代码。以下是import语句的各种使用方法:

1. 导入Dart标准库

Dart标准库包含了一系列内置的库,如dart:math用于数学运算,dart:io用于输入输出操作等。导入标准库的语法如下:


import 'dart:math';

2. 导入第三方库

第三方库通常是通过包管理器(如Pub)安装的。导入第三方库的语法如下:


import 'package:flutter/material.dart';

在这个例子中,flutter/material.dart是一个第三方库,它提供了Flutter框架中的Material Design组件。

3. 导入项目中的其他Dart文件

你可以导入项目中其他目录下的Dart文件。这可以通过相对路径或绝对路径来实现。以下是使用相对路径导入Dart文件的示例:


import 'widgets/text_demo.dart';

在这个例子中,text_demo.dart文件位于项目目录下的widgets子目录中。

4. 为导入的库指定前缀

当两个不同的库中有相同名称的类、函数或变量时,你可以为其中一个或两个库指定一个前缀,以避免命名冲突。使用as关键字指定前缀的语法如下:

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;

在这个例子中,lib2lib2/lib2.dart库的前缀。当你需要使用该库中的类、函数或变量时,可以通过lib2.前缀来引用它们。

5. 选择性地导入库的一部分

你可以使用showhide关键字来选择性地导入库的一部分。show关键字用于只导入指定的类、函数或变量,而hide关键字用于导入除了指定项之外的所有内容。

// 只导入foo和bar
import 'package:lib1/lib1.dart' show foo, bar;
 
// 导入除foo之外的所有内容
import 'package:lib1/lib1.dart' hide foo;

6. 导入自定义的Dart包

如果你有一个自定义的Dart包,并且你希望在其他Dart文件中使用它,你可以在pubspec.yaml文件中声明这个依赖项,并在Dart文件中使用import语句来导入它。例如,如果你有一个名为my_package的自定义包,并且它有一个名为my_library.dart的文件,你可以这样导入它:


import 'package:my_package/my_library.dart';

请注意,在导入自定义包之前,你需要确保它已经被正确地添加到你的项目的pubspec.yaml文件中,并且已经通过Pub包管理器进行了安装。

综上所述,Dart中的import语句提供了灵活的方式来导入代码,无论是标准库、第三方库还是项目中的其他Dart文件。通过合理地使用import语句,你可以有效地组织你的代码,并避免命名冲突。

3.6、关键字

关键字注释关键字注释
abstract用于定义抽象类和方法as用于为导入的库或类型指定别名
assert用于在调试期间检查条件是否为真async标记一个函数为异步函数
await用于等待一个异步操作的结果break终止循环或switch语句
case与switch语句一起使用,定义要匹配的值catch捕获try块中抛出的异常
class定义一个类const定义一个编译时常量
continue跳过当前循环的剩余部分covariant标记泛型类型为协变的
default在switch语句中执行未匹配case的代码deferred标记一个库为延迟加载
do与while循环一起使用,确保循环体至少执行一次dynamic表示动态类型
else与if语句一起使用,当if条件为假时执行enum定义一个枚举类型
export导出库中的公共成员extends表示类继承
extension定义一个扩展方法external声明外部函数或方法
factory定义一个工厂构造函数false布尔值false
final定义一个不可变的变量finally无论是否异常都执行
for定义一个循环Function函数类型
get定义一个getter方法hide从导入的库中隐藏成员
if根据条件执行代码implements实现接口
import导入Dart文件或库in检查值是否存在于集合中
is检查对象类型late允许延迟初始化非空变量
library定义一个库mixin定义一个mixin
new(已废弃)创建对象null空值
on指定要捕获的异常类型operator定义重载的运算符
part标记为库的一部分required标记必需的命名参数
rethrow重新抛出异常return从函数返回
set定义一个setter方法show显示导入的库中的成员
static定义静态成员super引用父类成员
switch多分支选择结构sync*标记一个生成器函数为同步的(注意:sync本身不是关键字,但sync*是)
this引用当前对象实例throw抛出异常
true布尔值truetry尝试执行可能抛出异常的代码
type(非关键字,但用于类型别名和类型检查)typedef定义类型别名
var定义变量(动态类型)
whenDart 2.17中引入的匹配表达式关键字(之前版本不是关键字)with用于混合应用其他类的成员到当前类
while定义循环,当条件为真时执行循环体yield在生成器函数中产生一个值,但不结束函数
yield*在生成器函数中产生一个值序列,来自另一个可迭代对象或生成器函数
术语/概念注释术语/概念注释
var声明变量,类型由编译器自动推断void表示函数没有返回值
nullable可空类型(Dart 2.12+ 的空安全特性)non-nullable非空类型(Dart 2.12+ 的空安全特性)
lateinit(非Dart关键字,但类似概念)延迟初始化变量(注意:Dart中实际使用的是late关键字)generic泛型,用于定义可重用组件
collection集合类型,如List、Set、Map等iterable可迭代对象,支持迭代操作
function函数,执行特定任务的代码块lambda匿名函数或闭包
closure闭包,捕获其所在作用域的变量并可以延迟执行的函数callback回调函数,作为参数传递给其他函数并在某个时刻被调用的函数
future表示异步操作的结果stream表示一系列异步事件的数据流
metadata元数据,用于为代码添加额外信息(如注解)annotation注解,一种特殊的元数据,用于为代码提供信息或指示
packageDart包,包含Dart代码和相关资源的集合module(非Dart核心概念,但相关)模块,通常指代码的组织单元
build system构建系统,用于编译、打包和部署Dart应用dependency依赖项,一个包所依赖的其他包或库

四、类型

1、基本类型

Dart 类型描述
int整数类型,表示没有小数部分的数字
double浮点数类型,表示有小数部分的数字
String字符串类型,用于表示文本数据
bool布尔类型,只有两个值:truefalse
Tuple(非Dart标准术语,但类似概念)记录类型,可以表示为 (value1, value2),其中 value1value2 可以是任意类型。Dart本身不直接支持这种元组(Tuple)语法,但可以通过列表(List)或其他数据结构来模拟。
Function函数类型,表示可执行的代码块,可以接收参数并返回值
List<T>列表类型,也称为数组,可以包含零个或多个类型为 T 的元素
Set<T>集合类型,包含零个或多个类型为 T的唯一元素
Map<K, V>映射类型,包含键值对,其中键的类型为 K,值的类型为 V
Runes用于表示Unicode字符的集合,但在Dart中,更常见的是使用字符API(如StringcodeUnits属性)来处理字符和Unicode码点
Symbol符号类型,表示Dart中的标识符(如变量名、函数名等)的引用
Null(在Dart的空安全中更常用null字面量)空值类型,表示没有值。在Dart的空安全特性中,null可以显式地分配给任何类型的变量,但必须使用?!后缀来指示该变量或表达式可能为null或不为null

注意

  • Dart本身不直接支持元组(Tuple)的概念,但可以通过列表(List)来模拟元组的行为。例如,一个包含两个元素的列表可以作为一个简单的元组使用。
  • Runes类型在Dart中不是最常用的,因为Dart的字符串(String)类型已经提供了处理Unicode字符的丰富API。通常,您会使用StringcodeUnits属性来获取字符串中字符的Unicode码点,或者使用其他字符处理函数。
  • 在Dart的空安全特性中,null字面量用于表示空值,但类型系统要求您显式地处理可能的null值。这通常通过类型后缀(如?表示可为null的类型)和空合并运算符(??)或空断言运算符(!)来实现。

2、元组

dart 版本需要3.0以上才支持元组(Tuple 或者叫 Records)

var record = ('first', a: 2, b: true, 'last');

(int, int) swap((int, int) record) {
  var (a, b) = record;
  return (b, a);
}

注意:

如果是1.0、2.0版本 可以使用集合,例如ListMap ,或Set 或者自定义类型

3、集合类型

1. List(列表)
  • 定义:List 是一种有序的集合,用于存储多个数据项。它保持元素的插入顺序,并允许重复元素的存在。
  • 创建:可以使用字面量或构造函数来创建 List。例如:
var emptyList = []; // 创建一个空列表
var numberList = [1, 2, 3, 4, 5]; // 创建一个包含整数的列表
var mixedList = [1, 'hello', true]; // 创建一个包含不同类型元素的列表
  • 常用方法:List 提供了丰富的操作方法,如添加元素(addaddAllinsertinsertAll)、删除元素(removeremoveAtremoveLast)、访问元素(通过索引)、查找元素(containsindexOf)、排序(sort)等。
2. Set(集合)
  • 定义:Set 是一种无序且不重复的集合,用于存储一组唯一的数据项。它提供了高效的元素查找和去重功能。
  • 创建:可以使用字面量或构造函数来创建 Set。例如:
var emptySet = Set<int>(); // 创建一个空的整数集合
var numberSet = {1, 2, 3, 4, 5}; // 创建一个包含整数的集合,注意集合中的元素是唯一的
  • 常用方法:Set 提供了添加元素(addaddAll)、删除元素(remove)、查找元素(contains)、判断集合是否为空(isEmpty)、获取集合大小(lengthsize,尽管 Dart 中更常用 length)等方法。此外,Set 还支持集合操作,如交集(intersection)、并集(union)、差集(difference)等。
3. Map(映射)
  • 定义:Map 是一种键值对的集合,每个键对应一个值。它提供了高效的键值对查找和访问。
  • 创建:可以使用字面量或构造函数来创建 Map。例如:
var emptyMap = Map<String, int>(); // 创建一个空的字符串到整数的映射
var studentScores = {'Alice': 95, 'Bob': 87, 'Charlie': 92}; // 创建一个包含学生分数的映射
  • 常用方法:Map 提供了添加键值对([] 操作符或 putIfAbsent)、访问值([] 操作符)、删除键值对(remove)、查找键或值(containsKeycontainsValue)、遍历(forEach)等方法。Map 还支持将键或值视为 Iterable 对象进行迭代和操作。

4、泛型 Generics

泛型允许在定义类、接口或方法时不指定具体的类型,而是在使用时才指定类型。这样可以使代码更加灵活和可重用。

1、泛型在 Dart 中的使用
  1. 泛型类

    通过泛型可以完成对一组类的操作对外开放相同的接口,如 List、Set、Map 等。在定义类时,可以使用尖括号 <> 来指定泛型类型参数。例如:

    class MyList<T> {
      List<T> list = <T>[];
     
      void add(T value) {
        this.list.add(value);
      }
     
      List<T> getList() {
        return list;
      }
    }
    

    在上面的代码中,T 是一个占位符,代表任意类型。在实例化 MyList 类时,可以传入具体的类型来替换 T

  2. 泛型方法

    泛型方法允许在方法定义时不指定参数类型或返回类型,而是在调用时才指定。这样可以实现传入什么类型,就返回什么类型的功能。例如:

    T getValue<T>(T value) {
      return value;
    }
    

    在调用 getValue 方法时,可以传入任意类型的参数,并返回相同类型的值。

  3. 泛型接口

    Dart 中没有 interface 关键字来定义接口,但可以使用 abstract class 来模拟接口的行为。在定义泛型接口时,同样可以使用尖括号 <> 来指定泛型类型参数。例如:

    abstract class Cache<T> {
      T getByKey(String key);
      void setByKey(String key, T value);
    }
    

    然后,可以实现这个泛型接口:

    class MemoryCache<T> implements Cache<T> {
      @override
      T getByKey(String key) {
        return null; // 这里应该实现具体的查找逻辑
      }
     
      @override
      void setByKey(String key, T value) {
        print("value=${value} 已存储");
      }
    }
    
2、泛型的好处
  1. 类型安全:泛型可以在编译时检查类型错误,从而避免运行时错误。
  2. 代码复用:通过泛型,可以编写更加通用的代码,减少重复代码。
  3. 可读性和维护性:使用泛型可以使代码更加清晰和易于理解,因为泛型提供了明确的类型信息。
3、泛型的约束

在 Dart 中,可以使用 extends 关键字来约束泛型的类型参数。例如:

class Foo<T extends SomeBaseClass> {
  // Implementation goes here...
}

在上面的代码中,T 必须是 SomeBaseClass 或其子类。这样可以确保泛型类型参数满足一定的条件。

4、泛型的运行时绑定

Dart 的泛型类型是在运行时绑定的。这意味着在运行时,可以知道泛型集合中具体存储的元素类型。例如:

var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // 输出: true

在上面的代码中,names 是一个 List<String> 类型的集合,在运行时可以检查其类型是否为 List<String>

5、别名 Typedefs

“别名”通常指的是为某个类型或函数类型创建一个新的名称,以提高代码的可读性和可维护性。

1. 类型别名(Type Alias)

可以使用 typedef 关键字来定义类型别名(尽管在新版本的 Dart 中,typedef 已经成为定义类型别名的隐式方式,无需显式声明)。

例如:

// 定义一个函数类型别名,表示接受两个 int 参数并返回 int 的函数
typedef IntOperation = int Function(int a, int b);
 
// 使用类型别名声明变量
IntOperation add = (int a, int b) => a + b;

在上面的代码中,IntOperationint Function(int a, int b) 的别名。这样,当需要声明这种类型的变量时,就可以使用 IntOperation 而不是完整的函数签名。

2. 导入别名(Import Alias)

在 Dart 中,当你导入一个模块时,可以为该模块指定一个别名。这在你需要同时导入多个具有相同名称的类或函数时特别有用,或者当你想要为模块提供一个更简短或更具描述性的名称时。

例如:

// 使用别名 'util' 导入 'package:my_package/utils.dart'
import 'package:my_package/utils.dart' as util;
 
// 现在可以使用别名 'util' 来访问导入模块中的类和函数
var result = util.someFunction();

在上面的代码中,utilpackage:my_package/utils.dart 的别名。通过使用别名,可以更容易地引用该模块中的类和函数,而无需每次都使用完整的包路径。

6、类型安全

1、类型系统的特点
  1. 静态类型检查和运行时检查

    • Dart 结合使用静态类型检查和运行时检查来确保变量的值总是与变量的静态类型匹配。
    • 静态类型检查在编译时进行,有助于在代码编写阶段就发现问题。
    • 运行时检查则在程序执行时进行,以确保类型安全在运行时仍然得到保障。
  2. 类型声明是可选的

    • 尽管类型是强制的,但 Dart 允许省略类型声明,因为 Dart 会在编译时执行类型推断。
    • 类型推断有助于减少代码冗余,同时仍然保持类型安全。
  3. 空安全

    • Dart 引入了空安全机制,允许开发者在编译阶段就发现代码中可能存在的空指针异常。
    • 空安全类型(如 String?)与不可为空类型(如 String)是区分开的,这有助于避免空值引发的错误。
2、类型安全的好处
  1. 减少运行时错误

    • 类型安全有助于在编译阶段就捕获类型不匹配的错误,从而减少运行时错误的发生。
  2. 提高代码可读性

    • 明确的类型声明使得代码更加易于理解和维护。
    • 类型信息可以帮助开发者更好地理解代码的意图和行为。
  3. 优化代码性能

    • 类型安全使得编译器能够利用类型信息来优化生成的代码。
    • 在 AOT(Ahead-Of-Time)编译模式下,Dart 可以生成预编译的本地代码,进一步提高性能。
3、Dart 中的类型安全实践
  1. 使用显式的类型声明

    • 尽管类型声明是可选的,但显式地声明类型可以提高代码的可读性和可维护性。
  2. 利用泛型

    • 泛型允许在定义类、接口或方法时不指定具体的类型,而是在使用时才指定。
    • 这可以提高代码的复用性,并确保类型安全。
  3. 空值检查

    • 在使用可能为空的变量之前,进行空值检查以避免空指针异常。
    • Dart 提供了空合并操作符(??)和空条件操作符(?.)等语法特性来简化空值处理。
  4. 类型推断

    • 利用 Dart 的类型推断功能来减少代码冗余。
    • 但要注意,在某些情况下,显式地声明类型可能更有助于理解代码的意图。
4、示例代码

以下是一个简单的 Dart 示例代码,展示了类型安全和空安全的使用:

void main() {
  // 显式地声明类型
  String name = "Alice";
  int age = 30;
 
  // 使用泛型集合
  List<String> hobbies = ["reading", "hiking"];
 
  // 空安全示例
  String? optionalName = null;
  print(optionalName?.length ?? "Name is null");
 
  // 类型推断示例
  var inferredType = "Hello, Dart!";
  // Dart 会在编译时推断出 inferredType 的类型为 String
}

在这个示例中,我们展示了如何显式地声明类型、使用泛型集合、以及处理空值。这些实践都有助于提高 Dart 代码的类型安全性。

五、匹配、检查和转换

1、模式匹配

在Dart语言中,模式匹配(Patterns)是一个强大的语法特性,它允许开发者以声明性的方式检查值的形式,并在匹配时解构该值。以下是对Dart中所有模式匹配的详细归纳:

1、模式匹配的基本概念

  • 定义:模式是Dart语言中的一个语法类别,表示一组可能与实际值相匹配的值的形状。模式可以匹配一个值、解构一个值或者两者兼而有之,这取决于模式的上下文和形状。
  • 作用:模式匹配提供了一种方便的语法,用于测试和验证值的形式,并在匹配时访问和提取值的组成部分。

2、模式匹配的类型

Dart支持多种类型的模式匹配,包括但不限于:

  1. 常量模式:匹配等于某个常量的值。例如:
switch(a) {
  case 0:
    print('0');
  case 1:
    print('1');
}
  1. 变量模式:匹配任何值,并将该值绑定到变量上。例如:
switch(obj) {
  case (var a, var b):
    print('a = $a, b = $b');
}
  1. 列表模式:匹配列表,并可以解构列表中的元素。例如:
var numList = [1, 2, 3];
var [a, b, c] = numList;
print(a + b + c); // 输出6
  1. 记录模式(Record Pattern):匹配记录(或对象),并可以解构记录中的字段。例如:
var user = {'name': 'toly', 'age': 29};
var {'name': name, 'age': age} = user;
print('User $name is $age years old.');
  1. 映射模式(Map Pattern):匹配Map对象,并可以解构Map中的键值对。例如:
Map<String, int> hist = {'a': 23, 'b': 100};
for (var MapEntry(key: key, value: count) in hist.entries) {
  print('$key occurred $count times');
}
  1. 通配符模式:使用下划线_作为占位符,忽略匹配值的部分内容。例如:
var a = [1, 'aa'];
switch(a) {
  case [_, 'aa']:
    print('Matched');
}
  1. 剩余元素模式:在列表模式中使用...来表示剩余元素,允许匹配任意长度的列表。例如:
var a = [1, 'aa', 2, 3, 4];
switch(a) {
  case [1, ..., 4]:
    print('Matched');
}

3、模式匹配的使用场景

模式匹配在Dart语言中有着广泛的应用场景,包括但不限于:

  1. 局部变量声明和赋值:使用模式变量声明来匹配和解构值,并将其绑定到新的局部变量上。
  2. for和for-in循环:在循环中使用模式来迭代和解构集合中的值。
  3. switch语句和表达式:在switch语句和表达式中使用模式来匹配不同的值,并执行相应的代码块。
  4. 逻辑或模式:在switch语句或表达式中使用逻辑或模式来让多个案例共享一个主体。

4、模式匹配的注意事项

  1. 结构一致性:在解构List、Record、Map对象时,左侧的解构结构需要与对象数据结构完全一致。如果结构不一致,将在运行时报错。
  2. 变量命名:在解构时,可以使用变量模式或命名模式来绑定值。如果懒得为变量起名字,也可以直接让字段名称为变量名(但需要注意作用域和命名冲突)。
  3. 通配符和剩余元素:使用通配符_来忽略匹配值的部分内容,使用剩余元素模式...来匹配任意长度的列表。

2、类型检查

在Dart语言中,类型检查和转换操作符是开发者在编程过程中用于确保类型安全和进行类型转换的重要工具。以下是对Dart中所有类型检查和转换操作符的详细归纳:

  1. is
    • 作用:检查一个对象是否属于某个类型。
    • 返回值:布尔值。如果对象是该类型的实例,则返回true;否则返回false
    • 用法示例:if (obj is SomeType)
  2. is!
    • 作用:检查一个对象是否不属于某个类型。
    • 返回值:布尔值。如果对象不是该类型的实例,则返回true;否则返回false
    • 用法示例:if (obj is! SomeType)

3、类型转换

  1. as
    • 作用:将一个对象显式地转换为指定的类型。
    • 注意事项:如果对象的实际类型与目标类型不匹配,则会抛出CastException异常。
    • 用法示例:var typedObj = someObj as SomeType;
  2. runtimeType 属性
    • 作用:返回一个对象的实际类型的Type对象。
    • 用法示例:var type = obj.runtimeType;

4、其他相关操作符

虽然以下操作符不是直接用于类型检查和转换的,但它们在处理空值和条件访问时非常有用,与类型安全紧密相关:

  1. 空值合并操作符 ??
    • 作用:在左侧操作数为null时返回右侧操作数,否则返回左侧操作数。
    • 用法示例:int b = a ?? 123;
  2. 空值条件赋值操作符 ??=
    • 作用:仅当左侧操作数为null时,才将右侧操作数的值赋给左侧变量。
    • 用法示例:int? a; a ??= 3;
  3. 安全访问操作符 ?.
    • 作用:在对象可能为null时安全地访问其属性或方法。
    • 用法示例:String? str; print(str?.length);

使用建议

  • 在进行类型转换时,建议使用try-catch块来捕获可能的异常,以确保程序的稳定性。
  • 在处理可能为null的对象时,使用空值合并操作符、空值条件赋值操作符和安全访问操作符可以避免空指针异常。
  • 类型检查和转换操作符应与Dart的静态类型检查机制相结合,以提高代码的类型安全性和可读性。

六、函数

Dart语言中的函数方法是编程的核心部分,它们允许开发者定义可重复使用的代码块,这些代码块可以接受参数、执行操作,并可能返回结果。以下是Dart函数方法的所有主要使用方式:

1、定义和调用函数

  1. 普通函数

    • 定义时需要指定返回类型和参数列表。
    • 示例:int add(int a, int b) { return a + b; }
    • 调用时使用函数名和圆括号内的参数。
    • 示例:int result = add(3, 5);
  2. 无返回值的函数

    • 使用void作为返回类型。
    • 示例:void sayHello(String name) { print("Hello, $name!"); }

2、参数类型

  1. 必需参数

    • 在调用函数时必须提供的参数。
    • 示例:void greet(String name, int age) { print("Hello, $name! You are $age years old."); }
  2. 可选参数

    • Dart支持命名参数和位置参数两种可选参数。

      • 命名参数

        :在调用函数时指定参数名。

        • 示例:void greet(String name, {int age = 0, String? message}) { ... }
      • 位置参数

        :使用方括号 [] 包围,调用时可以省略。

        • 示例:void greet(String name, [int age = 0, String? message]) { ... }

3、箭头函数

  • 箭头函数(也称为单行函数)是一种简洁的函数定义方式,适用于简单的表达式。
  • 示例:int add(int a, int b) => a + b;

4、匿名函数

  • 匿名函数是没有名字的函数,通常用于作为参数传递给其他函数。
  • 示例:var greet = (String name) { print("Hello, $name!"); };

5、函数作为参数

  • Dart支持将函数作为参数传递给其他函数。
  • 示例:void performOperation(int a, int b, Function(int, int) operation) { ... }

6、返回函数

  • Dart函数可以返回另一个函数。
  • 示例:Function getOperation(String operation) { ... }

7、默认参数

  • Dart支持为函数参数提供默认值。
  • 示例:void greet(String name, {String greeting = "Hello"}) { ... }

8、可变参数

  • Dart支持可变参数,使用...表示。
  • 示例:void printNumbers(int... numbers) { ... }

9、高阶函数

  • 高阶函数是指接受函数作为参数或返回函数的函数。
  • Dart中的许多内置函数(如forEachmap等)都是高阶函数。

10、递归函数

  • 递归函数是在其定义内调用自身的函数。
  • 示例:计算阶乘的函数int fact(int n) { if (n == 0 || n == 1) return 1; return n * fact(n - 1); }

11、闭包

  • 闭包是指可以访问其词法作用域内变量的函数对象。
  • 在Dart中,匿名函数和嵌套函数都可以形成闭包。

12、函数类型

  • 在Dart中,函数本身也是类型,可以将函数类型作为变量类型、参数类型或返回类型。
  • 示例:typedef IntToInt = int Function(int);

13、主函数(main函数)

  • Dart程序的入口点是main函数。
  • main函数可以返回一个void类型,并且可以接受一个可选的List<String>参数,该参数包含命令行参数。

七、控制流

控制流语句用于控制代码的执行顺序。它们允许开发者根据条件、循环或其他逻辑来决定代码的哪些部分应该被执行。以下是Dart中主要的控制流语句:

1、条件语句

  1. if 语句

    • 用于基于条件的真假来执行不同的代码块。
    • 示例:if (condition) { // 执行代码 }
  2. else if 语句

    • if语句结合使用,用于检查额外的条件。
    • 示例:else if (otherCondition) { // 执行代码 }
  3. else 语句

    • 当所有前面的ifelse if条件都不满足时执行。
    • 示例:else { // 执行代码 }
  4. 三元条件运算符

    • 简洁的条件表达式,返回两个值中的一个。
    • 示例:var result = condition ? valueIfTrue : valueIfFalse;

2、循环语句

  1. for 循环

    • 基于迭代器的循环,常用于遍历集合。

    • 示例:for (var i = 0; i < length; i++) { // 执行代码 }

    • Dart还支持增强的

      for
      

      循环(

      for-in
      

      ),用于遍历可迭代对象。

      • 示例:for (var item in iterable) { // 执行代码 }

1.1、 forEach 方法

List<String> fruits = ['apple''banana''cherry'];
fruits.forEach((fruit) {
 print('Fruit: $fruit');
});

更好的复用:

void printFruit(String fruit) {
 print('Fruit: $fruit');
}
​
List<String> fruits = ['apple''banana''cherry'];
fruits.forEach(printFruit);
  1. 函数式编程风格forEach 是一个高阶函数,它接受一个函数作为参数。这种风格更符合函数式编程的范式。
  2. 匿名函数或lambda表达式:在 forEach 的调用中,你通常会传递一个匿名函数(也称为 lambda 表达式)来定义对每个元素的操作。
  3. 不可变性:由于 forEach 不返回任何值(返回类型是 void),它通常用于那些不需要基于遍历结果进一步计算的场景。
  4. 只读访问:在 forEach 的 lambda 表达式中,你通常不会修改外部变量或集合本身,因为它鼓励的是对集合元素的只读访问。
  5. 性能考虑:在某些情况下,由于 Dart 的闭包和函数调用的开销,forEach 可能会比 for-in 循环稍微慢一些,尽管这种差异在大多数情况下是可以忽略不计的。

1.2、 for-in 循环

for (final person in people) {
 // ...
}
  1. 命令式编程风格for-in 循环是命令式编程中更传统的遍历方式。

  2. 简洁性for-in 循环通常更简洁,特别是当你不需要传递额外的参数给迭代函数时。

  3. 可变性和副作用:与 forEach 相比,for-in 循环更容易在循环体内修改外部变量或集合本身。因此,它更适合那些需要基于遍历结果进一步计算的场景。

  4. 性能for-in 循环通常与 forEach 在性能上相差无几,但在某些情况下可能会稍微快一些,特别是当编译器能够对循环进行更多优化时。

  5. while 循环

    • 当条件为真时重复执行代码块。
    • 示例:while (condition) { // 执行代码 }
  6. do-while 循环

    • 至少执行一次代码块,然后检查条件是否仍然为真。
    • 示例:do { // 执行代码 } while (condition);

3、跳转语句

  1. break 语句

    • 立即退出循环或switch语句。
    • 示例:break;
  2. continue 语句

    • 跳过当前循环的剩余部分,并继续下一次迭代。
    • 示例:continue;
  3. return 语句

    • 从函数返回结果并结束函数的执行。
    • 示例:return value;
  4. throw 语句

    • 抛出一个异常。
    • 示例:throw Exception("An error occurred");
  5. try-catch 语句

    • 捕获并处理异常。

    • 示例:

      try {
        // 可能会抛出异常的代码
      } catch (e) {
        // 处理异常的代码
      } finally {
        // 无论是否抛出异常都会执行的代码
      }
      
  6. assert 语句

    • 用于在开发阶段检查条件是否为真,如果为假则抛出异常。
    • 示例:assert(condition, "Message if the assertion fails");
    • 注意:在Dart的发布模式下,assert语句会被忽略。
  7. rethrow 语句

    • catch块中重新抛出当前捕获的异常。
    • 示例:catch (e) { // 处理一些逻辑; rethrow; }

八、类、对象

类(class)是用于定义对象的蓝图或模板。通过类,你可以创建具有特定属性和行为的对象。以下是 Dart 中类的一些基本概念和用法:

定义类

你可以使用 class 关键字来定义一个类。以下是一个简单的示例:

class Person {
  // 属性(字段)
  String name;
  int age;
 
  // 构造函数
  Person(this.name, this.age);
 
  // 方法
  void greet() {
    print('Hello, my name is $name and I am $age years old.');
  }
}

创建对象

使用 new 关键字(在 Dart 2.12 及以后的版本中,new 关键字是可选的)和类的构造函数来创建对象:

void main() {
  Person person = Person('Alice', 30);
  person.greet();  // 输出: Hello, my name is Alice and I am 30 years old.
}

访问修饰符

Dart 中的访问修饰符有 public(默认)、protected(用 _ 下划线前缀表示)和 private(用 __ 双下划线前缀表示,不过 Dart 实际只支持 _ 的约定俗成的私有)。

class Rectangle {
  double _width;  // 受保护的字段
  double __height;  // 私有字段(虽然 Dart 不强制执行双下划线私有)
 
  Rectangle(double width, double height) {
    this._width = width;
    this.__height = height;
  }
 
  double get area() {
    return _width * __height;
  }
 
  // 不能从类外部访问 _width 和 __height
}

继承

Dart 支持类的继承,使用 extends 关键字:

class Animal {
  String name;
 
  Animal(this.name);
 
  void speak() {
    print('The animal speaks.');
  }
}
 
class Dog extends Animal {
  Dog(String name) : super(name);
 
  @override
  void speak() {
    print('The dog says: Woof!');
  }
}
 
void main() {
  Dog dog = Dog('Buddy');
  dog.speak();  // 输出: The dog says: Woof!
}

抽象类

使用 abstract 关键字可以定义一个抽象类,抽象类不能直接实例化,但可以包含抽象方法和具体方法:

abstract class Shape {
  double area();  // 抽象方法
 
  void display() {
    print('This is a shape.');
  }
}
 
class Circle extends Shape {
  double radius;
 
  Circle(this.radius);
 
  @override
  double area() {
    return 3.14159 * radius * radius;
  }
}
 
void main() {
  Shape shape = Circle(5.0);
  print(shape.area());  // 输出: 78.53975
  shape.display();  // 输出: This is a shape.
}

接口(混入)

Dart 没有接口(interface)的显式概念,但你可以使用混入(mixins)来实现类似接口的功能:

mixin Movable {
  void move() {
    print('Moving...');
  }
}
 
class Car with Movable {
  String make;
  String model;
 
  Car(this.make, this.model);
}
 
void main() {
  Car car = Car('Toyota', 'Camry');
  car.move();  // 输出: Moving...
}

构造函数

Dart 支持多种构造函数,包括命名构造函数和工厂构造函数:

class Person {
  String name;
  int age;
 
  // 默认构造函数
  Person(this.name, this.age);
 
  // 命名构造函数
  Person.fromMap(Map<String, dynamic> map) {
    this.name = map['name'];
    this.age = map['age'];
  }
 
  // 工厂构造函数
  factory Person.fromJson(String jsonString) {
    Map<String, dynamic> map = jsonDecode(jsonString);
    return Person.fromMap(map);
  }
}
 
void main() {
  Person person1 = Person('Alice', 30);
  Person person2 = Person.fromMap({'name': 'Bob', 'age': 25});
  Person person3 = Person.fromJson('{"name": "Charlie", "age": 35}');
}

扩展(Extension)

扩展提供了一种扩展现有类的功能的方式,而无需修改原有的类。这使得你可以为任何现有的类(包括系统库中的类)添加新的方法、属性等,甚至是在没有源代码的情况下。扩展不会改变类的实际定义,也不会影响类的继承关系,新的方法只是添加到该类的对象的实例上。

使用扩展的示例

extension StringExtension on String {
  // 添加反转字符串的方法
  String reverse() {
    return split('').reversed.join('');
  }
}
 
void main() {
  String text = "hello";
  print(text.reverse()); // 输出: olleh
}

在这个例子中,StringExtensionString 类型添加了一个名为 reverse 的方法,调用时与调用类本身的方法无异。

可调用对象(Callable Objects)

在 Dart 中,如果一个类的实例实现了 call 方法,那么这个实例就可以像函数那样被调用,这个对象就被称为可调用对象。call 方法可以定义在任何类中,让类实例像函数一样被调用。这个方法与普通函数没有区别,包括参数和返回值等。

使用可调用对象的示例

class WannabeFunction {
  String call(String a, String b, String c) {
    return '$a$b$c!';
  }
}
 
void main() {
  var wf = WannabeFunction();
  var out = wf('Hi', ' NTopic,', 'CN');
  print(out); // 输出: Hi NTopic, CN!
}

九、类型修饰符(type modifiers)

类型修饰符(type modifiers)主要用于控制类型的行为和可见性。尽管 Dart 没有像一些其他语言那样明确的类型修饰符关键字(如 finalstatic 等被视作独立的关键字而非类型修饰符的一部分),但我们可以讨论一些与类型相关的关键特性和修饰方式。

以下是一些在 Dart 中与类型行为紧密相关的关键概念:

final 和 const

  • final 用于声明一个变量,该变量的值一旦被赋值后就不能再改变。final 可以用于类属性、局部变量以及顶级变量。
  • const 也用于声明一个不可变的变量,但它有额外的限制:const 变量的值必须在编译时就能确定,且必须是编译时常量。const 通常用于顶级变量或静态变量。
final int maxValue = 100; // 可以在运行时初始化
const int pi = 3.14159;   // 必须在编译时就能确定值

var、dynamic 和类型注解

  • var 关键字用于声明变量,但不指定变量的具体类型。Dart 会根据赋值来推断变量的类型。
  • dynamic 类型表示变量可以在运行时接受任何类型的值,并且Dart不会在编译时对其进行类型检查。
  • 类型注解(Type Annotations)允许你显式地指定变量的类型,这有助于在编译时进行类型检查,从而提高代码的健壮性。
var name = 'Alice'; // Dart 会推断为 String 类型
dynamic age = 30;   // 可以在运行时改变为其他类型
String greeting = 'Hello!'; // 显式指定为 String 类型

public、protected 和 private

在 Dart 中,没有像 其他语言 中的 publicprotectedprivate 这样的显式访问修饰符关键字用于类型本身(尽管它们用于类成员)。相反,Dart 使用约定俗成的方式来控制成员的可见性(例如,使用下划线 _ 前缀表示私有成员)。

  1. public(公共)
    • Dart 中的成员默认是 public 的,这意味着它们可以在任何地方被访问,只要它们所属的类的实例是可访问的。
    • 不需要在成员前加任何特定的关键字来标记它们为 public,因为这是默认行为。
  2. private(私有)
    • Dart 使用下划线 _ 前缀来表示私有成员。私有成员只能在定义它们的类内部被访问。
    • 例如,_myPrivateField 是一个私有字段,_myPrivateMethod() 是一个私有方法。
  3. 没有直接的 protected(受保护)
    • Dart 没有直接的 protected 关键字。然而,它有一个约定俗成的做法,即使用单个下划线 _ 前缀(尽管这通常被视为私有成员的标记)

类型别名(Type Aliases)

  • 使用 typealias 关键字可以为类型创建别名,这在处理复杂类型或提高代码可读性时非常有用。

typealias Point = List<num>; // Point 现在是一个 List<num> 的别名

可空类型(Nullable Types)和非空类型(Non-nullable Types)

  • Dart 2.12 引入了空安全特性,其中类型默认是非空的。如果你希望一个变量可以是 null,你需要在类型后面加上一个问号(?)。
String? maybeName; // maybeName 可以是 String 或 null
String name = 'Bob'; // name 不能是 null

泛型(Generics)

  • 泛型允许你在定义类、函数或方法时指定一个或多个类型参数,这些参数在类、函数或方法被实例化或调用时会被具体的类型所替代。
class Box<T> {
  T content;
 
  Box(this.content);
}
 
var box = Box<String>('Hello'); // Box<String> 的实例

工厂构造函数(Factory Constructors)和命名构造函数(Named Constructors)

  • 尽管它们不是直接的类型修饰符,但工厂构造函数和命名构造函数对于创建具有特定类型的对象非常重要。
class Person {
  String name;
 
  // 工厂构造函数
  factory Person(String name) {
    if (name.isEmpty) {
      return AnonymousPerson();
    } else {
      return NamedPerson(name);
    }
  }
 
  // 命名构造函数
  Person.named(String name) {
    this.name = name;
  }
}
 
class AnonymousPerson implements Person {
  // 实现 Person 的相关方法
}
 
class NamedPerson implements Person {
  String name;
 
  NamedPerson(this.name);
 
  // 实现 Person 的相关方法
}

请注意,上述示例中的 Person 类实际上是一个抽象概念,通过工厂构造函数和命名构造函数,它可以根据不同的需求返回不同类型的实例(AnonymousPersonNamedPerson)。

十、并发

Dart中的异步编程是构建高效、响应式应用的关键。通过异步编程,Dart应用可以在不阻塞主线程的情况下执行耗时操作,如网络请求、文件I/O、数据库操作等。以下是对Dart异步相关应用的详细介绍:

1、异步编程关键功能

  • Future 结果Future 是一个表示异步操作结果的类
  • Stream 事件流:处理连续的异步事件流,实现响应式编程。
  • async/await:简化异步编程,提高代码的可读性和可维护性,而无需阻塞主线程。
  • Isolate 内存隔离:利用 Isolate 的独立性来避免共享状态和线程安全性问题(竞争条件和死锁等问题)。
  • error 错误处理:在异步编程中,确保捕获并处理错误,以避免应用程序崩溃或产生未处理的异常。

2、应用场景

  1. 网络请求

    Dart中的网络请求通常是异步的。通过使用如http包中的http.get或http.post方法,可以发起异步网络请求,并返回一个Future对象。然后,可以使用.then()方法处理请求成功的情况,使用.catchError()方法处理请求失败的情况。

  2. 文件I/O

    文件读写操作也是异步的。Dart提供了File类来处理文件操作,如读取文件内容、写入文件等。这些操作都返回Future对象,可以使用相同的方式来处理结果和错误。

  3. 用户输入和UI更新

    在客户端应用中,用户输入和UI更新通常需要异步处理。例如,当用户点击按钮时,可以发起一个异步操作(如网络请求),并在操作完成后更新UI。通过使用async和await关键字,可以使代码看起来更像是同步执行的,但实际上是在事件循环的驱动下异步完成的。

  4. 定时器和计时器

    Dart中的定时器和计时器也可以通过Stream来处理。例如,可以使用Stream.periodic方法创建一个定期发出值的流,然后监听这个流来处理定时事件。

3、最佳实践

  1. 避免阻塞主线程

    在客户端应用中,主线程通常用于处理用户输入和更新UI。因此,应该避免在主线程上执行耗时操作。通过使用异步编程,可以将耗时操作放在后台线程上执行,从而保持主线程的响应性。

  2. 使用async和await简化代码

    async和await关键字提供了一种更简洁、更直观的方式来编写异步代码。它们使代码看起来像是同步执行的,但实际上是在事件循环的驱动下异步完成的。这有助于减少回调地狱(callback hell)和使代码更易于理解和维护。

  3. 处理错误和异常

    在异步编程中,错误和异常处理是非常重要的。应该使用.catchError()方法或try-catch语句来处理异步操作中的错误和异常,以确保应用的稳定性和可靠性。

  4. 合理管理资源

    在异步编程中,应该合理管理资源,如内存、文件句柄等。避免资源泄漏和过度使用资源是确保应用性能和稳定性的关键。

4、Future 异步结果

  • 定义:Future代表一个异步操作的结果,该操作可能尚未完成。Future对象提供了一个接口,允许你注册回调函数以处理操作完成后的结果或错误。
  • 状态:Future有两种状态:未完成(pending)和完成(completed)。完成状态可以是成功(带有结果值)或失败(带有错误)。
  • 创建:Future可以通过多种方式创建,包括使用Future构造函数、Future.delayed、Future.value、Future.error等。
  • 处理:Future的结果通常通过.then()方法处理成功情况,通过.catchError()方法处理错误情况。

这个示例展示了如何使用Future来处理异步操作,比如模拟一个网络请求。

import 'dart:async';
 
Future<String> fetchData() async {
  // 模拟一个耗时操作,比如网络请求
  await Future.delayed(Duration(seconds: 2));
  
  // 返回数据,这里我们简单返回一个字符串
  return "Hello, Dart!";
}
 
void main() {
  // 调用fetchData函数,它是一个返回Future<String>的函数
  fetchData().then((data) {
    // 当fetchData成功完成时,这里会被调用
    print("Received data: $data");
  }).catchError((error) {
    // 如果fetchData发生错误,这里会被调用
    print("Error occurred: $error");
  });
  
  // 你可以继续在这里做其他事情,而不会被fetchData阻塞
  print("This will print before the data is received.");
}
  • 异步操作通常返回一个 Future 对象,表示将来会完成的操作结果

  • async 关键字用于声明一个异步函数。

  • await 关键字用于等待一个异步操作的结果,而不会阻塞当前函数的执行。

5、async和await

上面示例代码中,async和await关键字提供了一种更简洁、更直观的方式来编写异步代码。

它们使代码看起来像是同步执行的,但实际上是在事件循环的驱动下异步完成的。这有助于减少回调地狱(callback hell)和使代码更易于理解和维护。

6、Stream 数据流

  • 定义:Stream表示一系列可能在未来产生的值,可以多次发出数据。Stream对象允许你注册监听器来监听流中新值的到来。
  • 类型:Stream主要有两种类型:单订阅流(single-subscription stream)和广播流(broadcast stream)。单订阅流只能有一个监听器,而广播流可以有多个监听器。
  • 创建:Stream可以通过Stream.fromIterable、Stream.periodic等方法创建,也可以通过异步生成器函数(async*)生成。
  • 处理:通过listen()方法可以订阅Stream,并处理流中的数据。Stream提供了许多方法用于转换流中的数据,如map()、where()、reduce()等。

这个示例展示了如何使用Stream来处理一系列异步事件,比如处理一个定时器生成的数值流。

import 'dart:async';
 
void main() {
  // 创建一个每秒生成一个数值的流
  Stream<int> counterStream = Stream.periodic(Duration(seconds: 1), (i) => i);
  
  // 订阅这个流,并处理每个事件
  var subscription = counterStream.listen((value) {
    print("Counter value: $value");
    
    // 在这里我们可以根据条件取消订阅
    if (value >= 5) {
      subscription.cancel();
      print("Stream canceled after reaching value 5.");
    }
  }, onError: (error) {
    // 处理流中的错误
    print("Stream error: $error");
  }, onDone: () {
    // 当流结束时调用
    print("Stream has ended.");
  });
  
  // 注意:由于这个示例中的流是无限生成的,我们通过在达到某个条件时取消订阅来结束它。
  // 在实际应用中,你可能会有其他方式来结束流,比如根据用户输入或某个外部事件。
}

7、Isolate 内存隔离

Isolate 是 Dart 中并发执行的核心机制。每个 Isolate 都是独立的,拥有自己的内存和事件循环,这意味着它们之间不能直接共享内存。这种设计既保证了并发执行的高效性,又避免了多线程编程中的常见问题,如死锁和竞态条件。

  • 特点

    • 每个 Isolate 都有自己的内存空间和事件循环。
    • Isolate 之间不能直接共享内存,但可以通过消息传递进行通信。
    • Isolate 通常用于执行耗时的任务,如网络请求、文件读写等,而不会阻塞主 Isolate(即主线程)的执行。
    • 更好利用多核提高性能
    • 不用再为加锁操作烦恼了,更容易避免死锁产生
  • 使用示例

    import 'dart:async';
    import 'dart:isolate';
     
    void main() async {
      // 创建一个新的 Isolate
      final isolate = await Isolate.spawn(entryPointFunction, 'Hello from main!');
      // 向 Isolate 发送消息
      final receivePort = ReceivePort();
      await isolate.send(receivePort.sendPort, [receivePort.sendPort]);
      final result = await receivePort.future;
      print('Result from isolate: $result');
    }
     
    void entryPointFunction(SendPort sendPort, List<dynamic> args) {
      // 接收主 Isolate 发送的消息
      final receivePort = ReceivePort();
      sendPort.send(receivePort.sendPort);
      // 处理消息并返回结果
      final message = args[0] as String;
      final response = 'Hello from isolate: $message';
      receivePort.first.then((sendPort) => sendPort.send(response));
    }
    

十一、空安全

1. 可空类型和非可空类型

在 Dart 空安全中,类型后面加 ? 表示该类型是可空的,即它可以持有 null 值。否则,该类型是非可空的,不能持有 null 值。

String? nullableString; // 可空字符串
String nonNullableString = "Hello"; // 非可空字符串

2. 显式空值赋值

在声明可空变量时,可以显式地将其初始化为 null


int? nullableInt = null;

3. 类型提升(Type Promotion)

当可空变量被赋予一个非空值时,Dart 会进行类型提升,将其视为非可空类型。

String? maybeString;
...
  
if (maybeString != null) {
  // 在这个块中,maybeString 被提升为非可空类型 String
  print(maybeString.length);
}

4. 空合并运算符(Null Coalescing Operator)

?? 运算符用于在左侧表达式为 null 时返回右侧表达式的值。

String? nullableString;
String result = nullableString ?? "Default String"; // 如果 nullableString 为 null,则 result 为 "Default String"

5. 空条件运算符(Null Conditional Operator)

?. 运算符用于在左侧表达式不为 null 时调用其成员(属性或方法),否则返回 null

class Person {
  String? name;
  
  String? greet() {
    return "Hello, $name";
  }
}
 
Person? person;
String? greeting = person?.greet(); // 如果 person 为 null,则 greeting 也为 null

6. 空断言运算符(Null Assertion Operator)

! 运算符用于在编译时断言左侧表达式不为 null。如果运行时该表达式为 null,则会抛出异常。

String? nullableString;
String nonNullableString = nullableString!; // 如果 nullableString 为 null,则这里会抛出异常

7. 可空参数和返回值

函数参数和返回值也可以是可空的。

String? getNullableString() {
  return null;
}
 
void printString(String? str) {
  if (str != null) {
    print(str);
  }
}

8. 延迟初始化(Late Initialization)

使用 late 关键字可以延迟变量的初始化,但必须在第一次使用前进行初始化。

late String lateString;
 
void initialize() {
  lateString = "Initialized";
}
 
void printLateString() {
  initialize();
  print(lateString);
}

9. 泛型中的空安全

泛型类型参数也可以是可空的。

List<String?>? nullableListOfNullableStrings;
List<String> nonNullableListOfStrings = ["a", "b", "c"];

上面 List 集合就是默认泛型,也可以自定义,以下定义一个Box泛型类,它有一个类型参数 T

class Box<T> {
  T? content;
 
  Box(this.content);
}
 
void main() {
  // 创建一个包含非空String的Box
  Box<String> stringBox = Box("Hello, World!");
  print(stringBox.content); // 输出: Hello, World!
 
  // 创建一个包含可空String的Box(尽管在这里我们直接传入了null,但content的类型是String?)
  Box<String?> nullableStringBox = Box(null);
  print(nullableStringBox.content); // 输出: null
 
  // 尝试从nullableStringBox中获取content并调用length属性(需要空安全处理)
  if (nullableStringBox.content != null) {
    print(nullableStringBox.content!.length); // 如果content不为null,则输出其长度
  } else {
    print("The content is null."); // 否则,输出提示信息
  }
}

10. 集合中的空安全

Dart 的集合类型(如 ListMap)也支持空安全。

List<String?>? nullableList;
Map<String, String?>? nullableMap;
 
List<String> nonNullableList = ["a", "b", "c"];
Map<String, String> nonNullableMap = {"key": "value"};