3.Dart语法

26 阅读25分钟

1.main函数

main(List<String> args) {
  // List<String> args -> 列表<String> - 泛型
  print("Hello World");
}

  • Dart语言的入口也是main函数,并且必须显示的进行定义;

  • Dart的入口函数main是没有返回值的;

  • 传递给main的命令行参数,是通过List完成的,从字面值就可以理解List是Dart中的集合类型,其中的每一个String都表示传递给main的一个参数;

  • 定义字符串的时候,可以使用单引号或双引号;

  • 每行语句必须使用分号结尾,很多语言并不需要分号,比如Swift、JavaScript;

2.变量和常量的声明

声明常量 const 和final

  // final声明常量
  final height = 1.88;
  // height = 2.00;

  //const声明常量
  const address = "广州市";
  // address = "北京市";

那么const 和final 的区别是什么呢?

const必须赋值 常量值(编译期间需要有一个确定的值),final可以通过计算/函数获取一个值(运行期间来确定一个值,其实用法不用太纠结,一般就用final就行,final用的更多,只有在声明常量构造函数等特殊时间用下const

  const date1 = DateTime.now(); 写法错误
  final date2 = DateTime.now();
class Person {
  final String name;
  const Person(this.name);
}
    
main(List<String> args) {
  // 在Dart2.0之后, new可以省略
  // 调用的时间必须加上const Person("why"),如果不这样调用,即便class Person的成员变量都是final创建的也不是常量构造函数
  const p1 = const Person("why");
  const p2 = const Person("why");
  const p3 = const Person("lilei");

  print(identical(p1, p2));
  print(identical(p2, p3));
}

上面的代码注意几点:

  • 在Dart2.0之后, new可以省略
  • 前面的打印一个是true,一个是false
  • identical 是判断两个对象是不是同一个对象

常量构造函数的必要要求

  1. 使用 const 关键字:构造函数前面必须加 const
  2. 成员变量必须是 final:类中所有的实例变量都必须是用 final 声明的(即不可变)。
  3. 不能有函数体:构造函数不能有 {} 块(body)。
  1. 为什么成员变量必须是 final? 保证不可变性:const 的定义就是“常量”。如果允许成员变量在运行时被修改(非 final),那么这个对象就不再是常量了。 安全共享:正因为对象不可变,Dart 编译器才能放心地让多个变量指向同一个内存地址(规范化)。如果对象能被修改,改了其中一个,所有指向它的变量都会跟着变,这会导致严重的逻辑混乱。
  2. 为什么不能有函数体 {}? 编译时计算 vs 运行时执行:构造函数的函数体是在 程序运行(Runtime) 时执行的指令(比如 print 或复杂的逻辑运算)。 预先构建:常量对象要求在 编译阶段 就在内存里分配好模型。编译器无法在编译时执行你的动态代码逻辑,> 所以它只允许你通过“初始化列表”给变量赋值。
  3. 为什么要有 const 关键字? 编译器标记:这给了编译器一个明确的信号:这个构造函数是用于生产常量的。编译器会据此进行特殊校验(检查是否所有字段都是 final,是否满足常量表达式等)。 性能优化的依据:有了这个标记,编译器才能在构建“常量池”时识别出这些类。

声明变量 类型推导var 和直接声明类型

下面是直接声明,好处是类型直接可以看明白

 String name = "why";
 int age = 20;
 double height = 1.88;
 bool isStudent = true;
 List<String> names = ["why", "lilei", "hanmeimei"];
 Map<String, int> ages = {"why": 20, "lilei": 18, "hanmeimei": 19};
 Set<String> namesSet = {"why", "lilei", "hanmeimei"};

下面是var,是类型推导,虽然是声明变量,但是第一次声明了一种类型后,第二次赋值的时间你不能给他另外一种类型。

// var声明变量
var age = 20;
// age = "abc"; 这句代码会报错,因为类型一旦确定,就不能再修改了
age = 30;

dynamic和Object

前面我们说var第一次声明了一种类型,比方说string,第二次赋值你只能再给他一个string,如果你想给他一个int,他就会报错,而dynamic可以允许你这么干,不过我们一般不这么干,因为类型变来变去会让人混乱,不太好。

dynamic name = 'coderwhy';
print(name.runtimeType); // String
name = 18;
print(name.runtimeType); // int

在dart中类型判断可以用runtimeType来获取 Object,在dart中所有对象都继承于Object,这个就是一个最底层的基类

题外问题identical 和 ==

默认行为下这俩是一样的,如果不重写 == 的话,dart中也有运算符重载,identical对于对象来说本质是比较对象是否相等

3.数据类型

3.1 数字类型 (Numbers)

在 Dart 中,你不需要过多关心数值是否有符号或精度深度,最常用的就是 intdouble

注意intdouble 可表示的范围并不是固定的,它取决于运行 Dart 的平台(例如在 移动端 VM 上和在 Web 编译器下有所不同)。

1. 整数类型 int

int age = 18;
int hexAge = 0x12; // 支持十六进制
print(age);    // 18
print(hexAge); // 18

2. 浮点类型 double

double height = 1.88;
print(height);

3. 字符串与数字的转化

  • 字符串转数字:使用 parse() 方法。
  • 数字转字符串:使用 toString()toStringAsFixed()(保留小数)。
// 1. 字符串转数字
var one = int.parse('111');
var two = double.parse('12.22');
print('${one} ${one.runtimeType}');    // 111 int
print('${two} ${two.runtimeType}');    // 12.22 double

// 2. 数字转字符串
var num1 = 123;
var num2 = 123.456;
var num1Str = num1.toString();
var num2Str = num2.toString();
var num2StrD = num2.toStringAsFixed(2); // 四舍五入保留两位小数

print('${num1Str} ${num1Str.runtimeType}');   // 123 String
print('${num2Str} ${num2Str.runtimeType}');   // 123.456 String
print('${num2StrD} ${num2StrD.runtimeType}'); // 123.46 String

3.2 布尔类型 (Booleans)

Dart 使用 bool 类型来表示布尔值,取值为 truefalse

重要:Dart 是类型安全的,不支持“非 0 即真”或“非空即真”,这句话怎么理解呢?就是说如果需要逻辑真假判断的时间,判断的表达式或者条件必须是一个明确的真假,而不能你给一个模糊的,比方说你不能说一个非空字符串就是真的,必须要调下不为空的方法,如果判断条件是3,那也不行,你要写成 >0类似这种,就是说表达式的结果必须严格是一个bool值

var isFlag = true;
print('$isFlag ${isFlag.runtimeType}');

// 错误示例
var message = 'Hello Dart';
if (message) { // 编译报错:这里必须是一个布尔表达式
  print(message);
}

3.3 字符串类型 (Strings)

Dart 字符串是 UTF-16 编码单元序列。单行和多行这里基本和swift中的一模一样

1. 定义字符串

可以使用单引号 ' 或双引号 "

var s1 = 'Hello World';
var s2 = "Hello Dart";
var s3 = 'Hello\'Fullter'; // 内部有单引号时可以用反斜杠转义
var s4 = "Hello'Fullter";  // 或者内外使用不同的引号

2. 多行字符串

使用三个单引号 ''' 或三个双引号 """

var message1 = '''
哈哈哈
呵呵呵
嘿嘿嘿''';

3. 字符串拼接 (插值)

使用 ${expression}。如果表达式只是一个变量(标识符),可以省略 {},这句话可以理解为,只能是一个变量,不能调变量调方法,或者变量.

var name = 'coderwhy';
var age = 18;
print('my name is ${name}, age is $age');

3.4 集合类型 (Collections)

Dart 内置了三种最常用的集合:List (列表)、Set (集合)、Map (映射)。

3.4.1 集合的定义方式

类型描述定义示例
List有序、可重复var list = [1, 2, 3];
Set无序、不重复var set = {'a', 'b'};
Map键值对 (Key-Value)var map = {'key': 'value'};
// List 定义
var letters = ['a', 'b', 'c']; // 类型推导
List<int> numbers = [1, 2, 3]; // 明确指定

// Set 定义
var lettersSet = {'a', 'b'};
Set<int> numbersSet = {1, 2, 3};

// Map 定义
var infoMap1 = {'name': 'why', 'age': 18};
Map<String, Object> infoMap2 = {'height': 1.88, 'address': '北京市'};

3.4.2 常用操作

1. 公共属性

  • length:获取集合长度。

2. List/Set 的增删改查

numbers.add(5);      // 添加
numbers.remove(1);   // 删除指定元素
numbers.contains(2); // 是否包含
numbers.removeAt(3); // List 特有:根据索引删除

3. Map 的特有操作

print(infoMap1['name']); // 根据key获取value
print(infoMap1.entries); // 获取所有条目
print(infoMap1.keys);    // 获取所有key
print(infoMap1.values);  // 获取所有value
print(infoMap1.containsKey('age')); // 是否包含某个key
infoMap1.remove('age');  // 根据key删除

4. 函数的使用

4.1 函数的基本定义

Dart 是一种真正的面向对象语言,所以函数也是对象,具有类型 Function。这意味着函数可以作为变量定义,或者作为其他函数的参数、返回值。

1. 定义方式

返回值 函数名称(参数列表) {
  函数体
  return 返回值
}

示例代码:

main(List<String> args) {
  print(sum(20, 30));
}

// 返回值的类型是可以省略的(开发中不推荐)
int sum(int num1, int num2) {
  return num1 + num2;
}

2. 箭头函数

如果函数中只有一个表达式,可以使用箭头语法,这个和js中的箭头函数不太一样,js箭头函数是为了解决函数二义性的。 示例代码:

void bar() => print("bar函数被调用");

4.2 函数的参数与重载

1. 函数重载问题

什么是函数重载? 函数重载就是方法名相同,只是参数不同。 在 Dart 中是没有函数重载的。

2. 可选参数

由于没有重载,Dart 通过可选参数来实现各种调用需求。

注意:只有可选参数才可以有默认值,必须参数(必选参数)不能有默认值且必须传参。

1) 位置可选参数 [参数类型 参数名 = 默认值]

  • 原则:实参和形参在匹配时,是根据位置进行匹配的。因为有顺序,所以是用[],list有顺序,也是用[]
  • 如果不设置默认值:在空安全环境下,必须使用可空类型(类型后加 ?),此时默认值为 null

示例代码

// 位置可选参数: [int age, double height]
void sayHello2(String name, [int age = 10, double height = 2]) {
  print('name=$name age=$age height=$height');
}

// 不设置默认值的写法 (必须使用可空类型 ?)
void sayHello2Nullable(String name, [int? age, double? height]) {
  // 因为只是打印输出,不需要进行非空判断,如果是使用,必须进行非空判断
  print('name=$name age=$age height=$height');
}

main() {
  sayHello2("why");          // name=why age=10 height=2.0
  sayHello2("why", 18);      // name=why age=18 height=2.0
  
  sayHello2Nullable("why");  // name=why age=null height=null
}

2) 命名可选参数 {参数类型 参数名 = 默认值}

  • 调用时:通过 参数名: 值 的形式,顺序可以随意,因为和顺序没关系,和map类似,所以用{}
  • 如果不设置默认值:同样需要使用可空类型(类型后加 ?),默认值为 null

示例代码:

// 命名可选参数有默认值
void sayHello3(String name, {int age = 10, double height = 3.14}) {
  print('name=$name age=$age height=$height');
}

// 命名可选参数无默认值 (使用可空类型 ?)
void sayHello3Nullable(String name, {int? age, double? height}) {
  // 因为只是打印输出,不需要进行非空判断,如果是使用,必须进行非空判断
  print('name=$name age=$age height=$height');
}

main() {
  sayHello3("why", height: 1.88); // age 使用默认值 10
  sayHello3("kobe", age: 30, height: 1.98); // 所有参数都传入
  sayHello3Nullable("why");       // age=null height=null
  sayHello3Nullable("james", age: 38); // height=null
}

4.3 函数是一等公民

动作阶段(传统语言):函数像是一个**“遥控器按钮”**。你只能去“按”它(调用),它才会产生一个动作(执行代码)。你没法把“按按钮”这个动作揣在兜里带走,也没法把它递给另一个遥控器。

物品阶段(Dart / 一等公民):函数从“动作”进化成了一个**“实体光盘”或者“U盘”**。 可以存起来:你可以把光盘放进抽屉(赋值给变量)。 可以送人:你可以把光盘递给朋友(作为参数传给另一个函数)。 可以制造:一个工厂可以生产一张光盘作为产品卖给你(作为函数返回值)。

1. 直接传递定义的函数

示例代码

main(List<String> args) {
  // 1.直接找到另外一个定义的函数传进去
  test(bar);
}

void test(Function foo) {
  foo();
}

void bar() {
  print("bar函数被调用");
}

2. 函数类型别名 typedef

当函数的参数类型变得复杂时,使用 typedef 封装以提高可读性,就是起别名,容易看,不要那么长,太丑陋。

示例代码

typedef Calculate = int Function(int num1, int num2);

void test(Calculate calc) {
  print(calc(20, 30));
}

main() {
  // 传入匿名函数
  test((num1, num2) {
    return num1 + num2;
  });
}

3. 函数作为返回值

示例代码:

Calculate demo() {
  return (num1, num2) {
    return num1 * num2;
  };
}

main() {
  var demo1 = demo();
  print(demo1(20, 30)); // 600
}

4.4 匿名函数、作用域与闭包

1. 匿名函数,类似于js中箭头函数

定义格式:(参数列表) {函数体};

示例代码:

var movies = ['盗梦空间', '星际穿越', '少年派', '大话西游'];

// 使用 forEach 配合匿名函数
movies.forEach((item) {
  print(item);
});

// 箭头形式的匿名函数
movies.forEach((item) => print(item));

2. 词法作用域

作用域是由代码的结构 ({}) 决定的。

示例代码:

var name = 'global';
main() {
  void foo() {
    print(name); // 会一层层向外查找,直到找到 global
  }
  foo();
}

3. 词法闭包

闭包可以访问其词法范围内的变量,即使函数在其他地方被使用。

示例代码:

makeAdder(num addBy) {
  return (num i) {
    return i + addBy; // 捕获了词法范围内的 addBy
  };
}

main() {
  var adder2 = makeAdder(2);
  print(adder2(10)); // 12
  
  var adder5 = makeAdder(5);
  print(adder5(10)); // 15
}

4.5 返回值规则

所有函数都返回一个值。

如果没有手动指定返回值,则语句会隐式附加 return null; 到函数体末尾。

示例代码:

main() {
  print(foo()); // 输出 null
}

foo() {
  print('foo function');
}

五. 运算符

5.1 数字运算 (除法、整除、取模)

Dart除了别的语言的除法,还有取整除法。

var num = 7;
print(num / 3);  // 除法操作: 结果 2.3333... (double)
print(num ~/ 3); // 整除操作: 结果 2 (int)
print(num % 3);  // 取模操作: 结果 1 (int)

5.2 ??= 赋值操作

  • 当原来的变量为 null 时,那么将值赋值给这个变量;当原来的变量有值时,那么 ??= 不执行,这个记住就行,也很简单。

示例代码:

main(List<String> args) {
  var name = null;
  name ??= "lilei";
  print(name); // 输出: lilei (原先为 null,被赋值)

  var name2 = 'coderwhy';
  name2 ??= 'james';
  print(name2); // 输出: coderwhy (原先已有值,不执行赋值)
}

5.3 条件运算符 ??

格式:expr1 ?? expr2

  • ?? 前面的数据有值,那么就使用 ?? 前面的数据;如果前面的数据为 null,那么就使用后面的值,这个和三目运算符差不多

示例代码:

var name = null;
var temp = name ?? "lilei";
print(temp); // lilei

5.4 级联语法 ..

级联语法允许你对同一个对象进行连续的一系列操作(赋值、调用方法),而不需要重复写对象名,类似于链式调用。

示例代码:

class Person {
  String name = "";

  void run() {
    print("running");
  }

  void eat() {
    print("eating");
  }
}

main(List<String> args) {
  // 普通写法
  // var p = Person();
  // p.name = "why";
  // p.run();
  // p.eat();

  // 级联语法写法
  var p = Person()
            ..name = "why"
            ..eat()
            ..run();
}

六. 流程控制

Dart 的流程控制与大部分主流语言相似,但有其特有的强制规范。

6.1 ifelse

  • 核心注意点:不支持“非空即真”或者“非0即真”,必须有明确的 bool 类型。

6.2 循环操作

1. 基础 for 循环

示例代码:

// 1. 基础 for 循环
for (var i = 0; i < 10; i++) {
  print(i);
}

// 2. 遍历数组 (普通方式)
var names = ["why", "cba", "cba"];
for (var i = 0; i < names.length; i++) {
  print(names[i]);
}

2. for-in 遍历

遍历 ListSet 类型最简洁的方式,dart中的for in 针对于Iterable 接口都可以用for in,类似于js中的for of

var names = ["why", "cba", "cba"];
for (var name in names) {
  print(name);
}

6.3 switch-case

  • 支持整数、字符串或编译时常量。
  • 注意:在 Dart 中,每一个 case 语句默认情况下必须以一个 break 结尾。
main(List<String> args) {
  var direction = 'east';
  switch (direction) {
    case 'east':
      print('东面');
      break;
    case 'south':
      print('南面');
      break;
    default:
      print('其他方向');
  }
}

七. 类和对象

Dart 是一个面向对象的语言,面向对象中非常重要的概念就是类,类产生了对象

7.1 类的定义

在 Dart 中,定义类用 class 关键字。类通常有两部分组成:成员(member)和方法(method)。

1. 定义方式

class 类名 {
  类型 成员名;
  返回值类型 方法名(参数列表) {
    方法体
  }
}

2. 注意点

在方法中使用属性时,通常可以省略 this;但是当命名冲突(如构造函数参数与属性同名)时,this 不能省略。

class Person {
  String name = "";

  eat() {
    print('$name在吃东西'); // 省略了 this
  }
}

main(List<String> args) {
  // 从 Dart 2 开始,new 关键字可以省略
  var p = Person(); 
  p.name = 'why';
  p.eat();
}

7.2 构造方法

7.2.1 普通构造方法

当类中没有明确指定构造方法时,将默认拥有一个无参的构造方法。

注意

  1. 当有了自己的构造方法时,默认的构造方法将会失效。
  2. Dart 不支持函数重载,因此不能创建多个同名但参数不同的构造方法。

语法糖写法:

class Person {
  String name;
  int age;

  // 普通写法
  // Person(String name, int age) {
  //   this.name = name;
  //   this.age = age;
  // }

  // 语法糖写法,就是说上面的三行可以写成下面的一行
  Person(this.name, this.age);

  @override
  String toString() => 'name=$name age=$age';
}

7.2.2 命名构造方法

由于不支持重载,如果需要多个构造方法,必须使用命名构造方法。就是说如果想实现不同的参数初始化要采用不同的名字的方法,而不能使用同一个名字,同一个名字就是重载了。

class Person {
  String name;
  int age;
 // 是 Dart 中的初始化列表(Initializer List),这里的 name = '' 和 age = 0 就是在对象生成前,把成员变量 name 初始化为空字符串,age 初始化为 0。
  Person() : name = '', age = 0;
     // 构造函数体(这里被省略了,因为不需要执行额外的逻辑)
   { }

  // 命名构造方法
  Person.withArguments(this.name, this.age);

  // 常用于 Map 转对象
  Person.fromMap(Map<String, dynamic> map)
      : name = map['name'],
        age = map['age'];
}

var p1 = Person.withArguments('why', 18);
var p2 = Person.fromMap({'name': 'kobe', 'age': 30});

补充知识点:Object 和 dynamic 的区别

  • Objectdynamic 都可以用作包含所有类型的引用(例如父类引用指向子类对象)。
  • Object 调用方法时,编译时会报错(编译器只认为它是 Object 类型,不包括具体的子类方法)。
  • dynamic 调用方法时,编译时不报错,但是运行时会存在安全隐患,如果对象没有该方法会抛出异常。建议在明确知道类型时声明具体类型。

7.2.3 初始化列表

用于在构造函数体执行之前初始化变量。特别适用于初始化 final 变量。初始化列表不仅可以赋初值,还可以包含一些表达式(如三目运算符)。如果在创建对象时需要根据传入的参数或外部常量来决定属性的最终值,初始化列表非常有用。就是构造函数,创建对象后,在执行后面的大括号的代码前进行一些变量,常量初始化工作。

  // 完整的初始化列表
  // 1.构造参数        2. 初始化列表 (冒号后面跟表达式)          3. 构造函数体
  构造函数名(参数) : 变量名1 = 初始值, 变量名2 = 初始值 {
    // 构造函数体:通常用来执行一些在这个时刻需要运行的代码(如打印日志、调用方法等)
  }

  • 当不需要写任何构造函数体时,大括号 {} 是可以被完全省略的:
class Person {
  final String name;
  final int age;

  // 使用传入参数做三元表达式计算并赋值,并且因为不需要执行多余逻辑,省略了最后的 {} 
  Person(this.name, {int? age}) : this.age = age ?? 18;
}

  • 当涉及到继承时,初始化列表中还包含了 super 的调用。它也是必须写在初始化列表区域的(即在冒号 : 的最后面)
class Animal {
  int age;
  Animal(this.age);
}

class Dog extends Animal {
  String name;
  
  // 初始化自身属性,然后再调用父类的构造方法
  Dog(this.name, int age) : name = "默认狗狗", super(age) {
     print("初始化完毕!");
  }
}

疑问:上面的super(age) 为啥不能放在大括号里面?

大括号 {} 里的代码被执行时,对象必须已经“完整”地被创建出来了。 一个“完整”的 Dog 对象,不仅需要包含它自己的 name 属性被初始化好,还必须要求它继承来的父类部分( Animal的 age 属性)也已经被完全初始化好。 如果允许把 super(age) 放到大括号 {} 里面: 那意味着在进入大括号时,父类的部分还没有被初始化。此时如果你在大括号里写了 print(this.age) 或者调用了父类的方法,由于父类还没初始化,就会导致严重的安全问题甚至是崩溃(访问了未初始化的内存)。 因此,Dart 强制要求:必须在进入子类的大括号 {} 之前,就把子类自己和父类的所有属性都初始化完毕。 这就是为什么 super(age) 必须放在初始化列表中完成,而绝对不能放在大括号里的原因。

import 'dart:math';

const temp = 20;

class Point {
  final num x;
  final num y;
  final num distance;

  // 初始化列表
  Point(this.x, this.y) : distance = sqrt(x * x + y * y);
}

class Person {
  final String name;
  final int age;

  // 在初始化列表中,不仅可以通过传入参数赋值,也可以结合三目运算等逻辑表达式赋默认值
  Person(this.name, {int? age}) : this.age = age ?? (temp > 20 ? 30 : 50) {
    // 构造函数体
  }
}

7.2.4 重定向构造方法

在一个构造方法中调用另外一个构造方法(在冒号后面使用 this 调用)。常用于将主构造函数的调用重定向到一个私有的内部构造函数以提供默认参数。简单来说调用一个构造函数的,实际又去调用另外一个了构造函数了,这样可以把一些代码抽出来。

class Person {
  String name;
  int age;

  // 构造函数的重定向:重定向到内部构造函数
  Person(String name) : this._internal(name, 0);

  // 内部私有构造函数
  Person._internal(this.name, this.age);
  
  // 重定向到主构造函数
  Person.fromName(String name) : this._internal(name, 0);
}

7.2.5 常量构造方法

如果传入相同参数时希望返回同一个对象,可以使用常量构造方法。

要求

  1. 所有的成员变量必须是 final 修饰的。
  2. 构造方法前加 const 修饰。
  3. 创建对象时必须使用 const 关键字。

为啥有上面的要求呢?
在 Dart 中,常量构造函数(Constant Constructor) 的存在主要是为了性能优化内存节省。它的核心目标是:在编译时(Compile-time)就确定对象的值,并且在整个程序的生命周期中,相同的常量对象只会在内存中创建一份(单例/享元模式)

为了实现这个目标,Dart 编译器必须对这类对象施加非常严格的限制。以下是常量构造函数的三大核心要求及其原因分析:

1. 为什么所有的成员变量必须是 final 修饰的?

原因:保证状态的绝对不可变(Immutable)

常量对象(const 对象)的核心特征是它的值在编译时就确定了,并且在程序运行期间绝对不能被修改

如果类的成员变量不是 final 的(也就是可以被重新赋值),那么这个对象的状态就可能在运行过程中发生改变。假如我们在内存中共享了这一个对象,在某个地方改了它的普通变量,那其他引用了这个“常量”对象的地方也会莫名其妙地发生改变。

为了防止这种情况,Dart 强制要求拥有常量构造函数的类,其所有属性必须是 final 的,以此保证对象一旦在编译期生成,就永远是一块“不可变的石头”。

2. 为什么构造方法前必须加 const 修饰?

原因:给编译器一个明确的指令

普通的构造函数在调用时,会在**运行时(Run-time)**在堆内存中动态分配空间并创建新对象。

而在构造方法前加上 const,等于是在告诉 Dart 编译器:“请特殊对待这个构造函数。只要传入的参数是确定的常量,就在编译阶段(Compile-time)把这个对象构建出来,并把它放进常量池里共享。”

如果没有这个标识,编译器会把它当做普通的运行时构造函数处理,也就无法实现编译期优化和内存共享了。 (注:带有 const 修饰的构造函数,其函数体 {} 里不允许有任何执行代码,这也进一步保证了它只能做纯粹的赋值操作。

3. 为什么创建对象时必须使用 const 关键字?

原因:决定你要“从常量池共享”还是“在运行时新建”

一个带有 const 构造函数的类(比如 class Person),其实你有两种方式来创建它:

方式一:普通创建(不加 const,或者用 new
var p1 = Person('why');
var p2 = Person('why');
print(identical(p1, p2)); // 结果是 false!

即使构造函数有 const 标识,如果你在这时不加 const,Dart 就会把它当做普通对象,在运行时为你创建两个完全独立的“新”对象,分配两块不同的内存。

方式二:常量创建(必须加 const
const p1 = Person('why');
const p2 = Person('why');
print(identical(p1, p2)); // 结果是 true!

当你显式地使用 const 关键字创建对象时,你是在要求 Dart:“去常量池里找找看,有没有参数完全一样也是 Person('why') 的对象?如果有,直接把原来的引用给我;如果没有,在编译时建一个放到常量池里。


总结

这三者的要求是一环扣一环的:

  1. 成员全部 final:先保证了这个类的实例具备“不可被篡改”的物理基础。
  2. 构造方法加 const:告诉编译器这具备了在编译期生成并存入常量池的资格。
  3. 调用时加 const:在实际写代码的时候,主动触发这种“编译期创建并去常量池里寻找相同实例”的共享机制。
class Person {
  final String name;
  const Person(this.name);
}

main() {
  const p1 = Person('why');
  const p2 = Person('why');
  print(identical(p1, p2)); // true,说明相同的常量参数只会创建一个实例
}

7.2.6 工厂构造方法

普通的构造函数会自动返回创建出来的对象,不能手动返回。 工厂构造函数最大的特点是:可以手动地返回一个对象。 使用 factory 关键字,可以手动控制对象的返回(例如从缓存中获取或返回子类对象)。

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

  factory Person(String name) {
    if (_cache.containsKey(name)) {
      return _cache[name]!;
    } else {
      final p = Person._internal(name);
      _cache[name] = p;
      return p;
    }
  }

  Person._internal(this.name);
}
补充,dart中没有public,private这些关键字,只有一个_,代表当前属性,和方法只针对当前文件是可见的,外边都看不到,不能用。类似于swift中fileprivate

7.3 setter 和 getter

默认属性可以直接访问(例如 p.name = 'why';)。如果需要监控属性的访问过程或者格式化输出,可以通过自定义的 getter 和 setter 来实现,使用 setget 关键字。类似于oc中不使用get set 关键字就是直接对属性指针赋值,而使用关键字后就是使用set get 后是通过方法赋值。写了get和set方法访问还是之前的.方式来访问 他的核心作用是

  • 封装私有属性:通常用 _ 声明私有属性,然后通过公开的get和 set 方法让外界安全地访问或修改它。
  • 计算属性:某个属性的值可能不是固定存下来的,而是通过其他属性动态计算出来的,这时候就可以用get。
  • 添加业务逻辑:在给属性赋值(set)或取值(get)时,可以加入数据校验、格式化、甚至触发其他副作用(比如更新 UI)。
class Rectangle {
  double width, height;

  Rectangle(this.width, this.height);

  // 【 Getter (读方法) 】
  // 语法:返回值类型 get 属性名 { ... return 值; }
  // 注意:没有参数列表的小括号 () 
  double get area {
    return width * height;
  }

  // 【 Setter (写方法) 】
  // 语法:set 属性名(参数类型 参数名) { ... }
  // 注意:必须且只能接收一个参数
  set area(double value) {
    // 假设修改面积时,高度不变,只改变宽度
    width = value / height;
  }
}

当代码只有一行的时候,可以用箭头函数简写

class User {
  int _age = 0; // 私有属性

  // 简写 Getter
  int get age => _age;

  // 简写 Setter,并加入了简单的校验逻辑
  set age(int value) => _age = value < 0 ? 0 : value;
}


7.4 类的继承

使用 extends 关键字,子类使用 super 访问父类。

注意:子类继承父类所有的成员变量和方法,但不继承构造方法。子类重写父类的方法记得要加上override

class Animal {
  int age;
  Animal(this.age);
  run() => print('奔跑ing');
}

class Person extends Animal {
  String name;
  // 必须显式调用父类构造函数(如果父类没有无参默认构造)
  Person(this.name, int age) : super(age);

  @override
  run() {
    super.run();
    print('$name也在奔跑');
  }
}

7.5 抽象类

抽象类使用 abstract 声明。抽象方法没有方法体。dart中抽象类有点类似于swift中的协议,还有一点特殊的地方,还可以定义一些共同的方法,放到抽象类里面,子类就可以用这些方法

注意

  1. 抽象类不能实例化(除非定义了工厂构造函数,然后通过其返回子类实例)。
  2. 继承自抽象类后,子类必须实现抽象类中的抽象方法。
  3. 抽象类中也可以包含普通的方法实现(如 getInfo)。
abstract class Shape {
  // 抽象方法:没有方法体,子类必须实现
  double getArea();

  // 普通方法:可以有具体实现,子类可以直接复用
  void printInfo() {
    print("这是一个形状");
  }
}

你可以把抽象类看作是“半成品”,让子类来完成剩下的部分:

// 正确:用具体的子类继承抽象类
class Circle extends Shape {
  double radius;
  Circle(this.radius);

  // 必须实现父类的抽象方法 1
  @override
  double getArea() {
    return 3.14 * radius * radius;
  }

  // 必须实现父类的抽象方法 2
  @override
  String getName() {
    return "圆形";
  }
}

void main() {
  // 实例化具体的子类
  var circle = Circle(10);
  
  // 可以调用自己实现的方法
  print(circle.getName()); // 输出:圆形
  
  // 也可以调用抽象类里已经实现好的普通方法(继承来的)
  circle.printInfo(); // 输出:这是一个形状的面积:314.0
}

为什么要用抽象类? 通过上面的例子,我们可以看出抽象类的三大作用:

  • 制定规范(协议):强制所有的图形子类(Circle,Rectangle 等等)都必须拥有一种叫 getArea() 的能力,统一了所有子类对外的 API。这就是面向对象里经常讲的多态的基础,这个应该是最重要的因素。
  • 代码复用:像上面的 printInfo() 方法,可以直接写在抽象父类里,这样无论将来派生出多少种子类图形,都不用再次重复写这段打印逻辑了。
  • 作类型约束:我们可以把类型声明为抽象类,这样后续可以往里面塞入任何实现了这个抽象类的子类,方便代码的扩展和解耦。(比如:List shapes = [Circle(10), Rectangle(10, 20)];)

7.6 隐式接口

Dart 中没有 interfaceprotocol 关键字。默认情况下,每一个类都隐式定义了一个包含所有实例成员的接口。 因此,当将一个类当作接口使用时(通过 implements),那么实现这个接口的类,必须实现这个接口中的所有方法。而继承(extends)只能单继承。

简单总结下其实就是dart中没有 protocolinterface ,要定义协议也是通过class @override 协议类里面的方法,特别注意的由于定义协议的是一个普通类,所以定义的方法,必须要写{},这个代表实现,其实这种在dart中并不常用,常用的是用抽象类,抽象类就可以不写{}了

class Runner {
  void running() {}
}

class Flyer {
  void flying() {}
}

class Animal {
  void eating() => print("动物吃东西");
}

class SuperMan extends Animal implements Runner, Flyer {
  @override
  void eating() {
    super.eating(); // 继承可以调用 super
  }

  @override
  void running() => print("超人在跑");
  
  @override
  void flying() => print("超人在飞");
}

为啥Map 是一个抽象类,但是可以通过new创建对象?

在 Dart 中,抽象类通常是不能被直接实例化的,因为它们可能包含没有具体实现的方法。但是,Dart 提供了:工厂构造函数(Factory Constructor)。 如果在抽象类中定义了工厂构造函数,当你尝试“实例化”这个抽象类时,实际上是由工厂构造函数在底层创建并返回了该抽象类的一个具体子类的实例

MapListSet 等 Dart 核心集合类正是使用了这种机制。我们以 Map 为例来具体说明:

1. Map 是一个抽象类

在 Dart 的核心库源码中,Map 其实是一个抽象类(在 Dart 3 中标记为 abstract interface class):

abstract interface class Map<K, V> {
  // 定义了各种方法,比如 length, keys, values, putIfAbsent 等,但没有具体实现
  int get length;
  void clear();
  // ...
}

正常逻辑下,你不能通过 new Map() 或者 Map() 来实例化一个抽象类。

2. Map 的工厂构造函数

但是我们平时写代码经常这样写:

Map<String, int> myMap = Map(); // 或者 new Map()

为什么没有报错呢?因为 Map 类内部定义了一个基础的工厂构造函数

abstract interface class Map<K, V> {
  
  // 这是一个工厂构造函数
  // 它的作用是在外部调用 Map() 时,实际构造并返回一个 LinkedHashMap
  external factory Map(); 

  // ... 还有其他工厂构造函数
  factory Map.from(Map<dynamic, dynamic> other) = LinkedHashMap<K, V>.from;
}

(注:external 关键字表示这个方法的具体实现在其他的底层代码中(通常在 SDK 内部),但逻辑上它等价于返回一个 LinkedHashMap)

3. 返回了什么?(子类实例)

当你执行 var m = Map(); 时,Dart 底层真正做的事情是:

  1. 调用 Map 的工厂构造函数。
  2. 工厂构造函数负责实例化一个叫 LinkedHashMap 的类。
  3. 把这个 LinkedHashMap 实例返回给你。

LinkedHashMapMap 的一个具体的、非抽象的实现类。它实现了 Map 接口中所有的抽象方法,并保证了插入的顺序。

你可以用代码验证一下:

void main() {
  var myMap = Map();
  // 打印运行时真正的类型
  print(myMap.runtimeType); 
  // 输出结果: _CompactLinkedCustomHashMap<dynamic, dynamic> (这是 LinkedHashMap 在虚拟机中的具体底层内部类)
  
  // 验证它确实是 Map
  print(myMap is Map); // true
}

4. 为什么要这样设计?(工厂模式的优势)

Dart 集合库采用这种设计有几个巨大的好处:

  1. 隐藏实现细节(面向接口编程): 开发者只需要知道它是一个 Map,知道它有 putgetlength 这些方法就行了,不需要关心底层到底是红黑树、哈希表还是什么其他结构。
  2. 灵活返回不同的子类Map 还可以有其他的工厂构造函数(命名构造函数),用来返回不同的底层实现,以适应不同的场景:
    • Map() -> 默认返回 LinkedHashMap(保持插入顺序)。
    • Map.identity() -> 返回一个基于相同对象引用比较(而不是值相等比较)的 HashMap。
    • Map.unmodifiable() -> 返回一个不可变的 Map 子类,一旦创建就不能再修改。

总结

这就是 Dart 中**“实例化抽象类”**的障眼法:你以为你在实例化 Map,其实你是在呼叫 Map 的包工头(工厂构造函数),包工头转身从仓库里拿了一个真正干活的兄弟(LinkedHashMap 这个子类实例)交给了你。


7.7 Mixin 混入

Mixin 是一种在多个类层次结构中复用类代码的方法。使用 mixin 关键字定义,通过 with 混入。 通过 Mixin 混入的类,不需要手动再去实现方法(它自带了实现),可以直接使用。如果出现了同名方法,后面的混入(或是本类的重写)会覆盖前面的。

mixin Runner { 
  void running() => print('在奔跑'); 
}
mixin Flyer { 
  void flying() => print('在飞翔'); 
}

class Animal {
  void eating() => print('吃东西');
}

class SuperMan extends Animal with Runner, Flyer {
  @override
  void running() {
    print("SuperMan running"); // 重写混入的方法
  }
}

main() {
  final sm = SuperMan();
  sm.running(); // SuperMan running
  sm.flying(); // 在飞翔
}

7.8 类属性和类方法

使用 static 定义类级别的成员和方法。实例对象不能访问静态成员和静态方法,只能通过类名直接调用。

class Person {
  // 成员变量(实例属性)
  String? name;

  // 静态属性(类属性)
  static String? courseTime;

  // 对象方法(实例方法)
  void eating() => print("eating");

  // 静态方法(类方法)
  static void gotoCourse() => print('去上课');
}

main() {
  Person.courseTime = "8:00";
  print(Person.courseTime);

  Person.gotoCourse();
}

7.9 枚举类型

使用 enum 关键字定义固定数量的常量值。结合 switch 语句使用时非常方便。

1. 定义与属性

  • index: 索引,从 0 开始。
  • values: 包含每个枚举值的 List。
enum Colors { red, green, blue }

main() {
  final color = Colors.red;

  switch (color) {
    case Colors.red:
      print("红色");
      break;
    case Colors.blue:
      print("蓝色");
      break;
    case Colors.green:
      print("绿色");
      break;
  }

  print(Colors.red.index); // 0
  print(Colors.values); // [Colors.red, Colors.green, Colors.blue]
}

限制

  1. 不能子类化、混入或实现枚举。
  2. 不能显式实例化枚举。

八. 泛型 (Generics)

泛型是 Dart 强类型系统的重要组成部分。通过使用泛型,可以提高代码的复用性,同时获得更好的编译时类型安全检查。

8.1 为什么使用泛型?

使用泛型(Generics)主要是为了解决两个核心问题:代码复用类型安全

通俗地讲,泛型就像是一个**“可以随意更换的模具”**。

  • 代码复用(少写废话):假设你有一个倒模机器(类或方法),如果不用泛型,你想倒正方形的冰块就得做个正方形的机器,倒圆形的冰块就得再造个圆形的机器;用了泛型,你只需要造一台机器,把“形状”作为参数(如 <T>)传进去,这台机器就能根据你的需要倒出任何形状的冰块。
  • 类型安全(提前排雷):如果没有泛型,你可能会不小心把“石头”放进这个本来只装“水”的冰块盒子里,程序运行时才会爆炸。有了泛型,你在盒子外面贴了张标签 <String>,一旦你试图往里塞个数字 18,编译器在写代码的时候就会直接给你画红线报错,这就叫类型安全。

8.2 List 和 Map 的泛型

List 使用时的泛型写法:

main(List<String> args) {
  // 1. 创建 List 的普通方式(未指定泛型,默认元素都是 Object)
  var names1 = ['why', 'kobe', 'james', 111];
  print(names1.runtimeType); // 输出: List<Object>

  // 2. 限制类型:明确指定 List 中只能存放 String
  var names2 = <String>['why', 'kobe', 'james']; 
  // var errorNames = <String>['why', 'kobe', 'james', 111]; // 最后一个元素是 int,会报错
  
  List<String> names3 = ['why', 'kobe', 'james']; 
  // List<String> errorNames2 = ['why', 'kobe', 'james', 111]; // 最后一个元素是 int,会报错
}

Map 使用时的泛型写法:

main(List<String> args) {
  // 1. 创建 Map 的普通方式
  var infos1 = {1: 'one', 'name': 'why', 'age': 18}; 
  print(infos1.runtimeType); // 输出: _InternalLinkedHashMap<Object, Object>

  // 2. 对类型进行显式限制(Key 和 Value 都是 String)
  Map<String, String> infos2 = {'name': 'why'}; 
  // Map<String, String> errorInfos = {'name': 'why', 'age': 18}; // 18 是 int,不能放在作为 String 的 value 中
  
  var infos3 = <String, String>{'name': 'why'}; 
  // var errorInfos2 = <String, String>{'name': 'why', 'age': 18}; // 同样 18 会报错
}

8.3 类定义的泛型

如果我们需要定义一个用于存储位置信息的 Location 类,但是并不确定使用者希望传入的是 int 类型、double 类型,甚至是一个字符串。如何定义呢?

初始方案一:使用 Object 类型。 但在之后使用时,存入和取出都需要进行极其不方便的类型转换判断。

class Location {
  Object x;
  Object y;

  Location(this.x, this.y);
}

main(List<String> args) {
  Location l1 = Location(10, 20);
  print(l1.x.runtimeType); // 输出: Object
}

更优方案二:使用泛型(Type Parameter T)。 让类的调用者来决定传入的确切类型。

class Location<T> {
  T x;
  T y;

  Location(this.x, this.y);
}

main(List<String> args) {
  // 指定类型为 int
  Location l2 = Location<int>(10, 20);
  print(l2.x.runtimeType); // 输出: int 

  // 指定类型为 String
  Location l3 = Location<String>('aaa', 'bbb');
  print(l3.x.runtimeType); // 输出: String
}

进阶:限制泛型的类型上限 (extends)

如果我们希望调用者传入的类型只能是数字相关类型(intdoublenum 的子类),怎么对其限制呢?可以使用 extends 关键字。

// 使用 T extends num 限制泛型 T 必须继承自 num
class Location<T extends num> {
  T x;
  T y;

  Location(this.x, this.y);
}

main(List<String> args) {
  Location l2 = Location<int>(10, 20);
  print(l2.x.runtimeType);
	
  // Location l3 = Location<String>('aaa', 'bbb'); 
}

8.4 泛型方法的定义

最初,Dart 仅仅在“类”这一个层面上支持泛型。后来,引入了一种称为“泛型方法”的新语法,允许在普通方法和函数中直接使用类型参数

// 定义一个获取列表中第一个元素的泛型函数
// 第一处 <T> 声明这是一个泛型方法;第二处 返回值 T;第三处 参数 List<T>
T getFirst<T>(List<T> ts) {
  return ts[0];
}

main(List<String> args) {
  var names = ['why', 'kobe'];
  
  // 虽然这里也可以写成 getFirst<String>(names),但 Dart 有强大的类型推导能力可以省略
  var first = getFirst(names); 
  
  print('$first ${first.runtimeType}'); // 输出: why String
}

九. Dart 库的导入与使用

在 Dart 中,任何一个 .dart 文件都是一个库,即使你没有使用 library 关键字显式地声明。


9.1 库的导入

import 语句用来导入一个库,后面跟一个字符串形式的 URI 来指定表示要引用的具体位置,基本语法如下:

import '库所在的 URI';

常见的库 URI 有以下三种不同的形式:

1. 导入 Dart 标准版库

使用 dart: 前缀表示导入的是 Dart 官方内置的标准库,比如 dart:iodart:htmldart:mathdart:core(其中 dart:core 是自动全局导入的,可以省略写出)。

// ------------ 1. 系统的库: import 'dart:库的名字'; ------------
import 'dart:math';

main(List<String> args) {
  final num1 = 20;
  final num2 = 30;
  // 调用 math 库中的方法
  print(min(num1, num2)); // 20
}

2. 使用相对路径导入自定义库

通常用于在自己项目中引入自己编写的、按照拆分原则导出的其他 dart 文件。也可以使用绝对路径,但在项目中通常推荐相对路径。

import "utils/utils.dart";

main(List<String> args) {
  print(sum(20, 30));
  print(dateFormat());
}

3. 导入第三方包管理的库

使用 Pub 包管理工具引入的第三方库,通常使用 package: 作为前缀。 例如,使用第三方网络请求库 http

import 'package:http/http.dart' as http;

main(List<String> args) async {
  var url = 'http://123.207.32.32:8000/home/multidata';
  var response = await http.get(url);
  print('Response status: ${response.statusCode}');
  print('Response body: ${response.body}');
}

9.2 库的内容控制与重命名

1. 库文件内容的显示和隐藏(show / hide

如果引入的库非常庞大,但你只希望导入库中的某些具体内容,或者刻意想把库里面的某个可能会冲突的名字隐藏掉,可以使用 showhide 关键字。

  • show 关键字:只显示列出的成员(屏蔽库中其他的成员)。
  • hide 关键字:只隐藏列出的成员(显示库中其他的成员)。
// 默认情况下在导入一个库时,导入这个库中所有的内容

// 举例:通过 show 执行要导入的内容,只导入 sum 和 mul
import "utils/math_utils.dart" show sum, mul;

// 举例:通过 hide 隐藏某个要导入的内容,只隐藏 mul,导入除 mul 之外的其它内容
import "utils/math_utils.dart" hide mul;

2. 库命名冲突的解决(as

当不同的库(或者当前文件)内部出现了相同的类名或方法名称时,就会产生命名冲突。这种情况下,可以使用 as 关键字给导入的库指定一个命名空间前缀(起别名),和js中的很像。

// as 关键字给库起别名
import 'utils/math_utils.dart' as mUtils;

main(List<String> args) {
  // 必须通过别名访问库中的方法,避免和当前文件的重名方法冲突
  print(mUtils.sum(20, 30));
}

// 假设当前文件里也有一个同名的 sum 方法
void sum(num1, num2) {
  print(num1 + num2);
}

9.3 库的定义与拆分管理

1. library 关键字

通常在定义一个库文件时,我们可以使用 library 关键字在文件顶部给这个库起一个正式的名字。 但目前在实际开发中发现,库的名字并不影响导入,因为 import 语句使用的是字符串 URI 定位文件,所以写不写 library 影响不大。

library math;

2. 过时的拆分方式:part 关键字

在开发中,如果一个库文件里面代码太大,将所有内容全部塞到一个文件是不合理的,我们通常希望能将功能进行拆分。在之前的版本中,Dart 提供过 partpart of 的语法。

  • 子文件使用 part of 声明属于哪个主文件。
  • 主文件使用 part 引入子文件。
// ------------ mathUtils.dart 文件 ------------
// 声明自己是 utils.dart 的一部分
part of "utils.dart";

int sum(int num1, int num2) {
  return num1 + num2;
}

// ------------ dateUtils.dart 文件 ------------
part of "utils.dart";

String dateFormat(DateTime date) {
  return "2020-12-12";
}

// ------------ utils.dart 文件 (主库) ------------
part "mathUtils.dart";
part "dateUtils.dart";

// ------------ 外部调用测试 ------------
import "lib/utils.dart"; // 依然只引主库即可

main() {
  print(sum(10, 20));  // 此时可以通过 utils 获取到两个子文件的方法
}

注意:官方指南中已经不建议使用 part 这种方式来拆分包了(因为它的耦合度过高且作用域容易混乱)。

3. 官方推荐的拆分方式:export 关键字

既然不推荐使用 part,那如果库非常大,如何进行合理的拆分和管理呢? 最佳实践是:将每一个拆分出去的文件作为独立普通的库文件,然后在统一的一个出口文件里,使用 export 关键字把它们统统“导出”去。

// ------------ mathUtils.dart 文件 (独立库) ------------
int sum(int num1, int num2) {
  return num1 + num2;
}

// ------------ dateUtils.dart 文件 (独立库) ------------
String dateFormat(DateTime date) {
  return "2020-12-12";
}

// ------------ utils.dart 文件 (统一导出库) ------------
library utils;

export "mathUtils.dart";
export "dateUtils.dart";

// ------------ 外部调用测试 ------------
import "lib/utils.dart"; // 外部调用者只需要按这一行代码,依然能拿到所有工具

main() {
  print(sum(10, 20)); 
}

这种做法不仅结构更加清晰,每个拆分出去的文件本身也可以被独立引用并在其内部添加对应的 import,是现在 Flutter/Dart 开源库的标准做法。


9.4 包管理利器:pubspec.yaml 详解

在上面的第三方包导入小节中,我们提到了使用第三方包管理的库(例如 http)。那么,我们到底应该如何告诉项目去下载并使用这些库呢? 答案就是:修改项目根目录下的配置文件 —— pubspec.yaml

1. 什么是 YAML?

YAML(发音:yam-el)是一种用于编写配置文件的标记语言(类似 JSON,但更易读)。 Dart 和 Flutter 项目的核心配置元数据文件都叫 pubspec.yaml。里面定义了项目名称、版本号、以及项目依赖了哪些别人的代码(依赖包)

极其重要的排版规则:YAML 文件对空格缩进要求极其严格!不允许使用 Tab 键缩进,只能使用空格,并且父子级关系主要靠 2 个空格的缩进来识别。一旦缩进错位,立刻报错。

2. 如何添加并使用新的第三方库?

这里以我们之前用到的 http 库为例,具体流程如下:

第一步:在 pubspec.yaml 中声明依赖

打开你的 pubspec.yaml 文件,找到 dependencies(项目运行依赖) 这一栏,然后在下面配置你需要引入的库及其版本号,同时注意严格的 2 个空格缩进

# 项目基本信息
name: my_dart_project
description: A starting point for Dart libraries or applications.
version: 1.0.0

# 运行依赖配置:项目想要跑起来,必须依赖这里的库
dependencies:
  # 注意:这里和左边缘必须相隔 2 个空格!
  http: ^1.2.0    # 引入的具体第三方库名字,以及版本号。^符号代表兼容该版本
  
  # 如果不指定版本,可以直接写 any(极不推荐,容易因为版本更新导致项目崩溃)
  # some_other_lib: any 

# 开发测试依赖:只在开发敲代码和测试时用的库,打包部署后不需要
dev_dependencies:
  test: ^1.24.0

第二步:下载(获取)这个第三方库

配置好以后,系统并不会马上拥有这个库的代码,你需要告诉 Dart 管理工具去云端把代码拉取下来。

  • 如果是在 VS Code 或 Android Studio 中: 通常你修改完这个 yaml 文件并按下 Ctrl+S (或 Cmd+S) 保存时,编辑器会自动在后台帮你运行下载命令。
  • 如何手动运行命令: 在终端的当前项目目录下,运行以下命令(它会帮你下载包并在本地创建映射):
    • 纯 Dart 项目执行:dart pub get
    • Flutter 项目执行:flutter pub get

第三步:在代码中导入并使用它

下载完成后,你就可以像我们之前讲的那样,回到你的 xxx.dart 代码文件中,畅通无阻地导入并使用了:

// 使用 package: 前缀导入它
import 'package:http/http.dart' as http;

void fetch() async {
  // 可以正常使用了
  var response = await http.get('http://123...');
}

总结

配置第三方库就是:pubspec.yamldependencies 节点 ➔ 运行 pub get ➔ 在代码里用 package: 快乐 import

十. Dart 可选类型(空安全)

Dart 从 2.12 版本开始全面引入了健全的空安全(Null Safety)。以下是其核心机制总结:

1. 声明可空类型 (?)

Dart 默认变量是 非空(non-nullable) 的。要允许变量为 null,必须在类型后加 ?

String name = "Tom"; // 非空类型,必须赋值且不能为 null
String? nickname;    // 可空类型,默认值为 null

2. 类型提升 (Type Promotion)

Dart 编译器基于控制流分析。如果在代码块中先判断了变量不为 null,编译器会自动将其“提升”为非空类型,后续可直接安全调用,无需额外解包。

String? message = "Hello";
print(message)  // 直接打印不用非空判断
if (message != null) {
  print(message.length); // 编译器确认此处 message 不为空,直接按 String 类型处理
}

3. 空断言操作符 (!)

当你绝对确定一个可空变量在当前逻辑下必定不为 null 时,可使用 ! 将其强转为非空类型,和Swift中强制解包差不多

⚠️ 注意:如果该变量实际值为 null,程序会在运行时抛出异常导致崩溃。

String? name = getNameOrNull();
int length = name!.length; // 强制断言 name 不为空

4. 避空操作符 (Null-aware Operators)

提供优雅的语法糖以处理可选值,避免繁琐的 if 判空逻辑。

  • ?. (条件属性访问) 左侧不为空才执行后续属性/方法访问;为空则直接短路返回 null,不报错。

    int? len = name?.length; 
    
  • ?? (空值合并操作符) 左侧为空则返回右侧的默认值;左侧不为空则返回左侧原本的值。

    String displayName = inputName ?? "Guest"; 
    
  • ??= (空值条件赋值) 仅当左侧变量当前为 null 时,才将右侧的值赋给它;否则什么都不做。

    String? greeting;
    greeting ??= "Hello"; // 此时为 null,执行赋值
    greeting ??= "Hi";    // 此时已有值,不执行赋值
    

5. late 关键字(延迟初始化)

用于声明非空类型,但推迟它的初始化时机(常用于 Flutter 的 initState 等生命周期方法中)。

⚠️ 注意:告诉编译器“我保证会在使用前赋值”。如果未赋值就提前读取该变量,会抛出 LateInitializationError

late String title;
title = "Dart Basics"; // 必须在使用前完成赋值
print(title.length);   // 无需判空或加 !,直接当作通常的非空变量使用