Dart语法详解:一文涵盖全面,了解Dart语法只需这篇!

633 阅读16分钟

Dart是一种由谷歌开发的现代编程语言,专门用于构建高性能、可维护的应用程序,尤其在移动应用和Web应用开发中表现出色。本文将深入探讨Dart语言的各个方面,从基础概念到高级特性,全面解析Dart的语法和用法。

目录

基础概念

变量和数据类型

变量声明

在Dart中,使用关键字varfinalconst来声明变量。var是一种自动类型推断的变量,final用于声明只能赋值一次的变量,const用于声明常量。

var name = 'Alice';  // 自动推断类型
final age = 30;      // 不可变的变量
const PI = 3.14159;  // 常量

在Dart中,constfinal在使用时有一些区别。

final关键字:
  1. 延迟赋值: final变量可以在运行时被赋值,但只能被赋值一次。这意味着可以在构造函数中给final变量赋值,也可以通过计算得出其值。然而,一旦final变量被赋值,就不能再改变。
  2. 非常量上下文: final变量可以存在于非常量上下文中,比如构造函数中。
  3. 不需要立即赋值: final变量声明时可以不立即赋值,但必须在构造函数完成之前赋值。
void main(){
  final int value;
  value = 123;
  
  //错误写法
  final num =3.14159;
  num = 123;   
const关键字:
  1. 编译时常量: const变量在编译时就必须被赋值,并且只能包含编译时可确定的值。这些值可以是数字、字符串、布尔值以及其他仅由编译时常量构成的表达式。
  2. 常量上下文: const变量只能存在于常量上下文中,比如类字段、方法参数、构造函数等。不能在非常量上下文中使用const
  3. 性能优势: 由于const变量在编译时就确定了值,它们在运行时会被直接替换成值,因此在一些情况下可以提供性能优势。
class Example {
  static const int myConstant = 42;
}

//错误写法
void main(){
  const value; 
  value = 123;
}

总结:

  • 使用final当需要一个在运行时确定值的不可变变量,但可以在构造函数中赋值。
  • 使用const当需要一个在编译时确定的常量,它只能包含编译时常量构成的值。

需要注意的是,const关键字要求其值在编译时就能确定,因此它有更严格的限制。

数据类型

数字类型

Dart提供了整数和浮点数类型。整数可以是有符号或无符号的,浮点数有doublenum两种类型。

int score = 42;
double pi = 3.14;
num temperature = 98.6;  // 可以是整数或浮点数
字符串类型

字符串由字符组成,可以使用单引号或双引号表示。Dart支持多行字符串和字符串插值。

String message = 'Hello, Dart!';
String multiline = '''
  This is a
  multi-line
  string.
''';
String name = 'Alice';
String interpolated = 'Hello, $name!';
布尔类型

布尔类型表示真(true)或假(false)值。

bool isRaining = true;
bool isSunny = false;
列表类型

List是一种有序的集合类型,用于存储多个值。List允许存储相同类型或不同类型的元素,并且可以根据索引访问这些元素:

List<int> numbers = [1, 2, 3];
List<String> names = ["Alice", "Bob"];
List dynamicList = [1, "two", 3.0];  // 动态类型列表
创建List

可以使用字面量语法或构造函数来创建List

// 使用字面量创建List
var numbers = [1, 2, 3];

// 使用构造函数创建List
var numbers = List<int>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
访问List中的元素

通过索引来访问List中的元素,索引从0开始:

var numbers = [1, 2, 3];
print(numbers[0]); // 输出: 1
添加和修改元素

可以使用add方法来向List末尾添加元素:

var numbers = [1, 2, 3];
numbers.add(4); // 添加元素

也可以使用[]操作符来修改指定索引的元素:

var numbers = [1, 2, 3];
numbers[1] = 5; // 修改元素
删除元素

使用remove方法来删除指定值的元素:

var numbers = [1, 2, 3];
numbers.remove(2); // 删除值为2的元素
遍历List

可以使用for-in循环来遍历List中的元素:

var numbers = [1, 2, 3];
for (var number in numbers) {
  print(number);
}

也可以使用forEach方法:

var numbers = [1, 2, 3];
numbers.forEach((number) {
  print(number);
});
List的长度

可以使用length属性获取List中元素的数量:

var numbers = [1, 2, 3];
print(numbers.length); // 输出: 3
List的其他常用属性和方法
  1. addAll(iterable):将一个可迭代对象中的所有元素添加到列表的末尾。
List<int> numbers = [1, 2, 3];
numbers.addAll([4, 5, 6]);
print(numbers);  // 输出: [1, 2, 3, 4, 5, 6]
  1. insert(index, element):在指定索引位置插入一个元素。
List<String> colors = ["red", "blue", "green"];
colors.insert(1, "yellow");
print(colors);  // 输出: [red, yellow, blue, green]
  1. removeAt(index):根据索引移除指定位置的元素。
List<int> numbers = [1, 2, 3, 4, 5];
numbers.removeAt(2);
print(numbers);  // 输出: [1, 2, 4, 5]
  1. contains(element):检查列表是否包含指定的元素。
List<String> fruits = ["apple", "banana", "orange"];
print(fruits.contains("banana"));  // 输出: true
  1. indexOf(element):返回指定元素在列表中的索引,如果不存在则返回 -1。
List<String> colors = ["red", "blue", "green"];
print(colors.indexOf("green"));  // 输出: 2
  1. sort():对列表中的元素进行排序。
List<int> numbers = [3, 1, 4, 1, 5, 9, 2];
numbers.sort();
print(numbers);  // 输出: [1, 1, 2, 3, 4, 5, 9]
  1. clear():清空列表中的所有元素。
List<String> items = ["item1", "item2", "item3"];
items.clear();
print(items);  // 输出: []
  1. where 方法:

where 方法用于从集合中筛选出满足指定条件的元素,并返回一个新的集合。

List<int> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

List<int> evenNumbers = numbers.where((number) => number % 2 == 0).toList();

print(evenNumbers);  // 输出: [2, 4, 6, 8, 10]

在上面的示例中,where 方法会筛选出列表 numbers 中的偶数,并将它们组成一个新的列表 evenNumbers

where 方法会遍历集合中的每个元素,对每个元素应用传入的回调函数,如果回调函数返回 true,则该元素被保留在新的集合中,如果返回 false,则被过滤掉。

需要注意,where 方法不会修改原始集合,而是返回一个包含筛选结果的新集合。

  1. any 方法:

any 方法用于检查集合中是否存在满足指定条件的元素。

List<int> numbers = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10];

bool hasEvenNumber = numbers.any((number) => number % 2 == 0);
bool hasNegativeNumber = numbers.any((number) => number < 0);

print(hasEvenNumber);      // 输出: true
print(hasNegativeNumber);  // 输出: false

在上面的示例中,any 方法分别检查列表 numbers 中是否存在偶数和是否存在负数。

any 方法会遍历集合中的每个元素,对每个元素应用传入的回调函数,如果有至少一个元素使得回调函数返回 true,则 any 方法返回 true,否则返回 false

需要注意,any 方法在找到满足条件的元素后会停止遍历,从而提高了效率。

  1. every 方法:

every 方法用于检查集合中的所有元素是否都满足指定条件。

List<int> numbers = [2, 4, 6, 8, 10];

bool allEven = numbers.every((number) => number % 2 == 0);
bool allPositive = numbers.every((number) => number > 0);

print(allEven);      // 输出: true
print(allPositive);  // 输出: true

在上面的示例中,every 方法分别检查列表 numbers 中的元素是否都为偶数和是否都为正数。

every 方法会遍历集合中的每个元素,对每个元素应用传入的回调函数,如果所有元素使得回调函数返回 true,则 every 方法返回 true,否则返回 false

需要注意,every 方法在遇到不满足条件的元素时会立即返回 false,从而提高了效率。

  1. map 方法: "map" 表示一种映射操作,它是一种对集合的元素进行转换的过程。在 Dart 中,可以使用 map() 方法来对集合中的每个元素进行映射操作,得到一个新的集合。
List<int> numbers = [1, 2, 3, 4, 5];
List<int> squaredNumbers = numbers.map((number) => number * number).toList();
print(squaredNumbers);  // 输出: [1, 4, 9, 16, 25]
Set

Set 是一种集合类型,用于存储一组唯一的元素,而且这些元素是无序的。这意味着在 Set 中不会有重复的元素,用它最主要的功能就是去除数组重复内容:

  List myList = ['1','2','3','1','2','1','2'];
  print(myList); //[1, 2, 3, 1, 2, 1, 2]
  var s = new Set();
  s.addAll(myList);
  print(s.toList()); //[1, 2, 3]
映射类型

Map是一种用来存储键值对(key-value pairs)的数据结构。每个键(key)必须是唯一的,而值(value)则可以重复。Map可以用来表示关联性的数据,例如字典、配置设置等:

Map<String, int> ages = {
  "Alice": 30,
  "Bob": 25,
};
Map dynamicMap = {
  "name": "Charlie",
  "age": 28,
};
创建Map

可以使用字面量语法或构造函数来创建Map

// 使用字面量创建Map
var person = {'name': 'Alice', 'age': 30};

// 使用构造函数创建Map
var person = Map<String, dynamic>();
person['name'] = 'Alice';
person['age'] = 30;
Map的键和值

Map的键可以是任何类型,但通常是Stringint类型。值可以是任何类型,包括基本数据类型、对象、甚至是另一个Map

访问Map中的值

通过键来访问Map中的值:

var person = {'name': 'Alice', 'age': 30};
print(person['name']); // 输出: Alice
添加和修改键值对

可以使用[]操作符来添加或修改键值对:

var person = {'name': 'Alice', 'age': 30};
person['job'] = 'Engineer'; // 添加新键值对
person['age'] = 31; // 修改已有键的值
删除键值对

使用remove方法来删除指定的键值对:

var person = {'name': 'Alice', 'age': 30};
person.remove('age'); // 删除键值对
遍历Map

可以使用forEach方法来遍历Map中的键值对:

var person = {'name': 'Alice', 'age': 30};
person.forEach((key, value) {
  print('$key: $value');
});
Map的长度

可以使用length属性获取Map中键值对的数量:

var person = {'name': 'Alice', 'age': 30};
print(person.length); // 输出: 2
Map的常用方法

Dart中的Map还有许多其他有用的方法,如containsKey用于检查是否包含指定键,containsValue用于检查是否包含指定值,keys获取所有键的集合,values获取所有值的集合等。

var person = {'name': 'Alice', 'age': 30};
print(person.containsKey('name')); // 输出: true
print(person.containsValue(30));    // 输出: true
print(person.keys.toList());        // 输出: [name, age]
print(person.values.toList());      // 输出: [Alice, 30]
类型判断

在Dart中进行类型判断的几种常见方式

1. 使用 is 关键字:

is 关键字可以用来检查一个值是否是指定类型或其子类型的实例。

var value = 42;

if (value is int) {
  print('value is an integer');
} else {
  print('value is not an integer');
}
2. 使用 as 关键字:

as 关键字用于将一个值强制转换为指定类型,如果转换失败则会抛出异常。

var value = '42';
var integerValue = int.tryParse(value);

if (integerValue is int) {
  print('Successfully converted to an integer');
} else {
  print('Conversion to integer failed');
}
3. 使用 runtimeType 属性:

runtimeType 属性返回一个对象的实际运行时类型。

var value = 42;

if (value.runtimeType == int) {
  print('value is an integer');
} else {
  print('value is not an integer');
}
4. 使用类型判断方法:

在Dart中,每个对象都有一个 Object 类的方法 runtimeType,以及其他一些方法,如 isAisInstanceOf,用于类型判断。

var value = 42;

if (value is int) {
  print('value is an integer');
}

if (value is Object) {
  print('value is an instance of Object');
}

if (value is Object) {
  print('value is an instance of Object');
}
5. 使用类型推断:

在某些情况下,Dart的类型推断能判断变量的类型,尽管这不是一种显式的类型判断方法。

var value = 42;

if (value is String) {
  // 这个分支永远不会执行,因为 value 已经被推断为 int 类型
} else {
  print('value is an integer');
}

类型转换

类型转换来将一个值从一种数据类型转换为另一种数据类型。Dart提供了几种类型转换的方式,包括显式转换和隐式转换。

1. 显式类型转换(Explicit Type Conversion):

显式类型转换需要使用特定的类型转换函数或构造函数来将值从一种类型转换为另一种类型。

  • 使用构造函数进行类型转换:
int intValue = 42;
double doubleValue = intValue.toDouble();
  • 使用类型转换函数:
double doubleValue = 3.14159;
int intValue = doubleValue.toInt();
2. 隐式类型转换(Implicit Type Conversion):

Dart是一门强类型语言,但也支持隐式类型转换。在某些情况下,Dart会自动进行类型转换以保持表达式的一致性。

  • 数值类型之间的隐式转换:
int intValue = 42;
double doubleValue = intValue;  // 隐式转换为 double
  • 类型推断:
var value = 42;  // 推断为 int
value = 3.14159; // 隐式转换为 double

需要注意的是,类型转换可能会导致数据丢失或产生不可预料的结果,因此在进行类型转换时要确保数据的准确性和完整性。

3. 将 Number 转换为 String:

可以使用 toString() 方法来将 intdouble 转换为 String

int intValue = 42;
double doubleValue = 3.14159;

String intValueAsString = intValue.toString();
String doubleValueAsString = doubleValue.toString();

print(intValueAsString);      // 输出: 42
print(doubleValueAsString);   // 输出: 3.14159
4. 将 String 转换为 Number:

可以使用 int.parse() 来将 String 转换为 int,以及使用 double.parse() 来将 String 转换为 double。请注意,这些方法会在无法解析字符串为数字时抛出异常。

String intString = "42";
String doubleString = "3.14159";

int parsedInt = int.parse(intString);
double parsedDouble = double.parse(doubleString);

print(parsedInt);      // 输出: 42
print(parsedDouble);   // 输出: 3.14159

如果需要处理可能无法解析为数字的字符串,可以使用 tryParse() 方法,它会返回 null 而不是抛出异常。

String nonNumericString = "abc";

int? parsedInt = int.tryParse(nonNumericString);
double? parsedDouble = double.tryParse(nonNumericString);

print(parsedInt);      // 输出: null
print(parsedDouble);   // 输出: null

注意,使用 tryParse() 方法会返回可空类型,因为解析失败时会返回 null

4. isNaN

在Dart中,isNaN 是一个用于检查一个数字是否为 NaN(Not-a-Number的方法。NaN 表示一个特殊的非数字值,它通常用于表示无效的数学运算结果。

可以使用 isNaN 方法来检查一个 double 类型的数字是否为 NaN。这个方法返回一个布尔值,如果数字是 NaN,则返回 true,否则返回 false

示例:

double nanValue = double.nan;
double regularValue = 42.0;

print(nanValue.isNaN);        // 输出: true
print(regularValue.isNaN);    // 输出: false

在上面的示例中,nanValue 是一个 NaN 值,因此调用 isNaN 方法返回 true。而 regularValue 是一个普通的数字,所以调用 isNaN 方法返回 false

需要注意的是,isNaN 方法只适用于 double 类型的数字。对于 int 类型,以及其他类型,该方法不适用。

运算符

算术运算符

这些运算符用于执行基本的数学运算。

  • +:加法
  • -:减法
  • *:乘法
  • /:除法
  • %:取模(取余数)
int a = 10;
int b = 3;
print(a + b);  // 输出: 13
print(a - b);  // 输出: 7
print(a * b);  // 输出: 30
print(a / b);  // 输出: 3.3333333333333335
print(a % b);  // 输出: 1

比较运算符

这些运算符用于比较两个值的大小关系。

  • ==:等于
  • !=:不等于
  • <:小于
  • >:大于
  • <=:小于等于
  • >=:大于等于
int a = 10;
int b = 3;
print(a == b);  // 输出: false
print(a != b);  // 输出: true
print(a < b);   // 输出: false
print(a > b);   // 输出: true
print(a <= b);  // 输出: false
print(a >= b);  // 输出: true

逻辑运算符

这些运算符用于组合多个条件。

  • &&:逻辑与
  • ||:逻辑或
  • !:逻辑非
bool isTrue = true;
bool isFalse = false;
print(isTrue && isFalse);  // 输出: false
print(isTrue || isFalse);  // 输出: true
print(!isTrue);            // 输出: false

赋值运算符

这些运算符用于给变量赋值。

  • =:赋值
  • +=:加赋值
  • -=:减赋值
  • *=:乘赋值
  • /=:除赋值
  • %=:取模赋值
  • ??=:空值赋值
int a = 10;
int b = 3;
a += b;  // 相当于 a = a + b;
String name; 
name ??= "wang"; // 当 name 为 null 时,将其赋值为 "wang"

条件运算符

条件运算符用于根据条件返回不同的值,如三元条件运算符(condition ? expr1 : expr2)。

int age = 17;
String status = age >= 18 ? "成年人" : "未成年人";

位运算符

这些运算符用于操作二进制位。

  • &:按位与
  • |:按位或
  • ^:按位异或
  • ~:按位取反
  • <<:左移
  • >>:右移
int a = 5;  // 二进制: 101
int b = 3;  // 二进制: 011
print(a & b);  // 输出: 1 (二进制: 001)
print(a | b);  // 输出: 7 (二进制: 111)
print(a ^ b);  // 输出: 6 (二进制: 110)
print(~a);     // 输出: -6 (二进制: 11111111111111111111111111111010)
print(a << 1); // 输出: 10 (二进制: 1010)
print(a >> 1); // 输出: 2 (二进制: 10)

特殊运算符

在Dart中,??!! 是两种特殊的运算符,用于处理空值(null)的情况,!? 也是两种特殊的运算符,用于处理变量的可空性(nullability)和非空断言。

1. ?? 运算符(空值合并运算符):

?? 运算符,也称为空值合并运算符,用于提供默认值,当操作数为 null 时使用。

语法:

expression1 ?? expression2
  • 如果 expression1 的值不为 null,则结果为 expression1 的值。
  • 如果 expression1 的值为 null,则结果为 expression2 的值。

示例:

String name = null;
String displayName = name ?? "Guest";

print(displayName);  // 输出: Guest

在上述示例中,如果 name 的值为 null,那么 displayName 将被赋值为 "Guest"。

2. !! 运算符(非空断言运算符):

!! 运算符,也称为非空断言运算符,用于断言一个表达式的值不为 null,并将其强制转换为非空类型。如果值为 null,则会引发一个异常。

语法:

expression!!

示例:

String name = null;
String displayName = name!!;  // 如果 name 为 null,会引发异常

在上述示例中,使用 !! 运算符会断言 name 不为 null,如果 name 的值为 null,则会引发异常。

1. ? 运算符(空值感知运算符):

? 运算符,也称为空值感知运算符,用于指示一个变量可以为 null。它在Dart中的新的空值安全特性中使用。

在变量类型后加上 ? 表示这个变量可以为 null

int? nullableValue = null;

在使用可空变量时,需要通过条件判断或空值合并等方式来处理变量为 null 的情况。

2. ! 运算符(非空断言运算符):

! 运算符,也称为非空断言运算符,用于断言一个变量的值不为 null,从而告诉编译器跳过空值检查。

String? nullableValue = "Hello"; 
String nonNullableValue = nullableValue!; 
print(nonNullableValue); // 输出: Hello

在这个例子中,nullableValue 被标记为可空类型,但是在赋值给 nonNullableValue 时使用了 ! 运算符,表示我们确定 nullableValue 不会为 null,因此编译器不会触发空值检查。

然而,使用 ! 运算符需要小心,如果使用它来断言一个实际上为 null 的变量,会导致运行时异常。

综上所述,? 运算符和 ! 运算符在Dart中用于处理空值的情况,能更好地控制变量的可空性和安全性。

控制流

条件语句

条件语句用于在满足特定条件时执行不同的代码块。

1. if 语句:

if语句用于在给定条件为真时执行特定的代码块。

if (condition) {
  // 当条件为真时执行的代码
}
2. if - else 语句:

if语句也可以与else语句一起使用,以便在条件为假时执行另一个代码块。

if (condition) {
  // 当条件为真时执行的代码
} else {
  // 当条件为假时执行的代码
}
3. if - else if - else 语句:

可以使用多个else if语句来处理多个不同的条件,然后在最后使用一个else块来处理所有不满足前面条件的情况。

if (condition1) {
  // 当条件1为真时执行的代码
} else if (condition2) {
  // 当条件2为真时执行的代码
} else {
  // 当条件1和条件2都不为真时执行的代码
}
4. 三元条件表达式:

三元条件表达式是一种紧凑的方式,用于根据条件选择不同的值或执行不同的代码块。它由一个问号?和一个冒号:组成。

var result = condition ? valueIfTrue : valueIfFalse;
5. switch 语句:

switch语句用于根据表达式的值选择不同的执行路径。在特定值匹配时,会执行相应的代码块。

switch (expression) {
  case value1:
    // 当expression等于value1时执行的代码块
    break;
  case value2:
    // 当expression等于value2时执行的代码块
    break;
  // 可以添加更多的case分支
  default:
    // 当expression不等于任何case时执行的代码块
}

循环语句

Dart提供多种循环语句,如forwhiledo...while

for (int i = 0; i < 5; i++) {
  print(i);
}

int count = 0;
while (count < 3) {
  print("Hello");
  count++;
}

int n = 0;
do {
  print("World");
  n++;
} while (n < 3);

中断和跳转

Dart允许使用breakcontinue来中断循环或跳过迭代。还可以使用标签实现更精确的控制流。

for (int i = 0; i < 5; i++) {
  if (i == 3) {
    break;  // 终止循环
  }
  print(i);
}

for (int i = 0; i < 5; i++) {
  if (i == 3) {
    continue;  // 跳过当前迭代
  }
  print(i);
}

outerLoop:
for (int i = 0; i < 3; i++) {
  for (int j = 0; j < 3; j++) {
    if (i == 1 && j == 1) {
      break outerLoop;  // 标签跳出外部循环
    }
    print("$i, $j");
  }
}

函数

函数定义

函数是Dart中的基本构建块之一,允许将一组操作封装为一个可复用的代码块。

函数名称

函数名称应具有描述性,采用小驼峰式命名法。

void printMessage(String message) {
  print(message);
}

参数列表

函数参数是指在定义函数时所声明的变量,用于接收函数调用时传递进来的值。函数可以有零个、一个或多个参数。

1. 位置参数(Positional Parameters):

这是函数定义中最常见的参数类型。它们按照声明的顺序进行匹配,函数调用时需要按照相同的顺序传递值。位置参数在函数定义时用在参数列表中。

void greet(String name, int age) {
  print("Hello, $name! You are $age years old.");
}

void main() {
  greet("Alice", 25);  // 输出: Hello, Alice! You are 25 years old.
}
2. 默认参数(Default Parameters):

Dart 允许为函数参数提供默认值,这样在调用函数时可以选择性地省略这些参数。

void greet(String name, [int age = 30]) {
  print("Hello, $name! You are $age years old.");
}

void main() {
  greet("Bob");         // 输出: Hello, Bob! You are 30 years old.
  greet("Carol", 28);   // 输出: Hello, Carol! You are 28 years old.
}
3. 命名参数(Named Parameters):

通过使用花括号 {},可以在函数调用时通过参数名来传递值,而不用考虑参数的顺序。

void greet({String name, int age}) {
  print("Hello, $name! You are $age years old.");
}

void main() {
  greet(name: "David", age: 22);  // 输出: Hello, David! You are 22 years old.
}
4. 必须命名参数(Required Named Parameters):

使用 required 关键字来声明必须传递的命名参数。

void greet({required String name, int age}) {
  print("Hello, $name! You are $age years old.");
}

void main() {
  greet(name: "Eve", age: 29);  // 输出: Hello, Eve! You are 29 years old.
}
5. 将函数作为参数传递:
void performOperation(int a, int b, Function operation) {
  print(operation(a, b));
}

int add(int a, int b) {
  return a + b;
}

int subtract(int a, int b) {
  return a - b;
}

void main() {
  performOperation(5, 3, add);       // 输出: 8
  performOperation(5, 3, subtract);  // 输出: 2
}

在上面的示例中,performOperation 函数接受一个函数作为参数。可以传递不同的操作函数(addsubtract),从而实现不同的操作。

6. 使用匿名函数作为参数:
void performAction(Function action) {
  print("Performing action...");
  action();
}

void main() {
  performAction(() {
    print("Custom action executed!");
  });
}

在这个示例中,performAction 函数接受一个函数作为参数。可以使用匿名函数来定义想要执行的自定义操作。

7. 使用箭头函数作为参数:
void performTask(String taskName, Function(String) taskFunction) {
  print("Performing task: $taskName");
  taskFunction(taskName);
}

void main() {
  performTask("Initialization", (task) => print("$task completed."));
}

在这个示例中,performTask 函数接受一个函数作为参数,并传递一个字符串参数给该函数。可以使用箭头函数来简洁地定义执行任务的操作。

通过将函数作为参数传递,可以使代码更加模块化,将不同的功能分离开来,使得代码更具有可读性和可维护性。

返回类型

函数可以有返回值,使用箭头(=>)指定返回表达式。

int add(int a, int b) {
  return a + b;
}

String getMessage() => "Hello, Dart!";

匿名函数(Lambda)

匿名函数也被称为 lambda 函数或闭包。匿名函数是一种没有名字的函数,它可以作为值赋给变量,也可以作为参数传递给其他函数。匿名函数通常用于临时需要一小段代码的情况,而不需要为其定义一个单独的函数名称。

void main() {
  var multiply = (int a, int b) {
    return a * b;
  };

  print(multiply(5, 3));  // 输出: 15
}

闭包

闭包(Closure)是一种函数对象,它可以访问在其外部作用域中定义的变量,即使在该外部作用域已经退出执行。在 Dart 中,函数是一等公民,因此函数可以作为另一个函数的返回值,也可以被赋值给变量。这就导致了闭包的存在。

闭包可以让函数捕获其定义时所在的上下文中的变量,即使这些变量在函数调用时不再处于作用域内。这为编写灵活的、可重用的代码提供了便利。

Function createCounter() {
  int count = 0;

  // 返回一个闭包
  return () {
    count++;
    return count;
  };
}

void main() {
  var counter = createCounter();

  print(counter());  // 输出: 1
  print(counter());  // 输出: 2
  print(counter());  // 输出: 3
}

在上面的示例中,createCounter 函数返回了一个闭包,这个闭包可以访问 createCounter 函数作用域内的 count 变量。每次调用 counter() 都会使 count 增加,即使 createCounter 函数已经退出执行。

闭包使得可以维持某些状态,而这些状态会随着函数调用的多次而保留。这在很多情况下都很有用,比如计数、记忆化和回调函数等。

需要注意,使用闭包时要小心内存泄漏,因为闭包会保持对外部变量的引用,这可能导致这些变量不会被垃圾回收。所以在使用闭包时,确保不会意外地造成内存泄漏。

面向对象编程

类和对象

类的声明

Dart是面向对象的语言,一切皆为对象。使用class关键字来声明类。

class Person {
  String name;
  int age;
}

对象的创建

使用new关键字创建对象。

var person = Person();
person.name = "Alice";
person.age = 30;

构造函数

构造函数用于创建类的对象,并初始化对象的属性。Dart 提供了多种类型的构造函数,使能够根据需要进行对象的创建和初始化。

默认构造函数(Default Constructor):

默认构造函数是没有参数的构造函数,它在类中没有显式地定义参数列表时自动提供。

class Person {
  String name;
  int age;

  // 默认构造函数
  Person(this.name, this.age);
}

void main() {
  var person = Person("Alice", 25);
  print(person.name);  // 输出: Alice
  print(person.age);   // 输出: 25
}
命名构造函数(Named Constructor):

命名构造函数允许为类定义多个不同的构造函数,每个构造函数可以有自己的参数列表和初始化逻辑。

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  // 命名构造函数
  Person.fromBirthYear(String name, int birthYear) {
    this.name = name;
    this.age = DateTime.now().year - birthYear;
  }
}

void main() {
  var person1 = Person("Alice", 25);
  var person2 = Person.fromBirthYear("Bob", 1990);

  print(person1.age);  // 输出: 25
  print(person2.age);  // 输出: 年龄根据当前年份和出生年份计算得出
}
常量构造函数(Const Constructor):

常量构造函数创建的对象在编译时就确定,并且对象的所有属性都必须是不可变的。

class Circle {
  final double radius;

  const Circle(this.radius);
}

void main() {
  const circle = Circle(5.0);
  print(circle.radius);  // 输出: 5.0
}
初始化列表(Initializer List):

初始化列表允许在构造函数体执行之前初始化对象的属性。

class Rectangle {
  double width;
  double height;

  Rectangle(this.width, this.height);

  Rectangle.square(double side) : width = side, height = side;
}

void main() {
  var rectangle1 = Rectangle(5.0, 10.0);
  var rectangle2 = Rectangle.square(7.0);

  print(rectangle1.width);  // 输出: 5.0
  print(rectangle2.height);  // 输出: 7.0
}

类成员

字段

类的字段是类的属性,用于存储对象的数据。

class Circle {
  double radius;

  Circle(this.radius);
}
方法

类的方法是类的函数,用于定义对象的行为。

class Rectangle {
  double width;
  double height;

  Rectangle(this.width, this.height);

  double area() {
    return width * height;
  }
}

Dart和其他面向对象语言不一样,Data中没有public``private protected这些访问修饰符,但提供了一种约定来模拟私有属性和方法,以及一些限制访问的方式。

私有属性和方法的约定:

在 Dart 中,通过在标识符前加上下划线 _ 前缀,可以表示这个标识符是私有的,即只应该在当前库中访问。

class MyClass {
  int _privateVar = 42;

  void _privateMethod() {
    print("This is a private method.");
  }

  void publicMethod() {
    print("This is a public method.");
    _privateMethod();  // 可以在类内部访问私有方法
  }
}

void main() {
  var obj = MyClass();
  print(obj._privateVar);  // 私有属性可以在类外部访问,但不推荐
  obj._privateMethod();    // 私有方法可以在类外部调用,但不推荐
}

需要注意的是,虽然 Dart 不会阻止在类外部访问私有属性和方法,但强烈建议遵循私有属性和方法的约定,只在当前库中使用它们。

库级私有性:

在 Dart 中,库级私有性是指只有同一库中的代码才能访问某个标识符。通过将库的代码组织在一个 .dart 文件中,可以将标识符限制在这个库的范围内。

// my_library.dart
library my_library;

part 'my_private_part.dart';  // 使用 part 关键字引入私有部分

class MyClass {
  // ...
}
// my_private_part.dart
part of my_library;

class _MyPrivateClass {
  // ...
}

在这个例子中,_MyPrivateClass 是私有类,只能在同一个库中的代码中访问。

getter和setter

使用getter和setter来控制对类的字段的访问和修改。

class Temperature {
  double _celsius;

  Temperature(this._celsius);

  double get celsius => _celsius;
  set celsius(double value) => _celsius = value;
  
  double get fahrenheit => _celsius * 9 / 5 + 32;
  set fahrenheit(double value) => _celsius = (value - 32) * 5 / 9;
}
静态成员

在 Dart 中,static 关键字用于定义静态成员,即与类本身相关联而不是与类的实例相关联的成员。静态成员不需要创建类的实例就可以直接访问。在类级别上,static 可以用于定义静态变量、静态方法和静态常量。

以下是关于在 Dart 中使用 static 关键字的示例:

静态变量(Static Variables):

静态变量是与类相关联的属性,可以通过类名直接访问,而不需要创建类的实例。

class MyClass {
  static int count = 0;  // 静态变量

  MyClass() {
    count++;
  }
}

void main() {
  print(MyClass.count);  // 输出: 0
  var obj1 = MyClass();
  print(MyClass.count);  // 输出: 1
  var obj2 = MyClass();
  print(MyClass.count);  // 输出: 2
}
静态方法(Static Methods):

静态方法是与类相关联的方法,可以通过类名直接调用,而不需要创建类的实例。

class MathUtils {
  static int add(int a, int b) {
    return a + b;
  }
}

void main() {
  print(MathUtils.add(5, 3));  // 输出: 8
}
静态常量(Static Constants):

静态常量是在类级别上定义的常量,可以通过类名直接访问,而不需要创建类的实例。

class Constants {
  static const int maxItems = 10;  // 静态常量
}

void main() {
  print(Constants.maxItems);  // 输出: 10
}

注意:静态方法不能访问非静态成员,非静态方法可以访问静态成员。

class MyClass {
  static int staticVar = 1;
  int instanceVar = 2;

  static void staticMethod() {
    // 无法访问 this.instanceVar,因为静态方法没有实例上下文
    print(staticVar);  // 可以访问静态成员
  }

  void instanceMethod() {
    print(staticVar);     // 可以访问静态成员
    print(instanceVar);   // 可以访问实例成员
  }
}

void main() {
  MyClass.staticMethod();  // 输出: 1
  var obj = MyClass();
  obj.instanceMethod();    // 输出: 1, 2
}

总之,static 关键字在 Dart 中用于定义与类本身相关的成员,而不是与类的实例相关。静态成员可以在不创建类的实例的情况下被直接访问和调用。

对象操作符

在 Dart 中,对象操作符是一组用于操作对象的特殊符号,可以在编写代码时更方便地操作对象、访问属性和调用方法。以下是 Dart 中常见的对象操作符:

1. . 操作符

. 操作符用于访问对象的属性和方法。

class Person {
  String name = "Alice";
  void greet() {
    print("Hello, $name!");
  }
}

void main() {
  var person = Person();
  print(person.name);   // 输出: Alice
  person.greet();       // 输出: Hello, Alice!
}
2. ?. 操作符:(已经被最新的dart废弃具体请看:dart.dev/tools/diagn…)

?. 操作符用于安全地访问对象的属性和方法,如果对象为 null,则不会引发异常,而是返回 null

String getName(Person person) {
  return person?.name;
}

void main() {
  var person = Person();
  print(getName(person));  // 输出: Alice

  Person nullPerson;
  print(getName(nullPerson));  // 输出: null
}
3. .. 操作符

.. 操作符允许在同一个对象上执行多个操作,而不必重复引用对象。

class Calculator {
  double _result = 0;

  Calculator add(double value) {
    _result += value;
    return this;
  }

  Calculator multiply(double value) {
    _result *= value;
    return this;
  }

  double getResult() {
    return _result;
  }
}

void main() {
  var calc = Calculator()
    ..add(5)
    ..multiply(3);
  print(calc.getResult());  // 输出: 15.0
}
4. as 操作符

as 操作符用于类型转换,将对象转换为指定的类型。

class Animal {
  void makeSound() {
    print("Animal sound");
  }
}

class Dog extends Animal {
  void makeSound() {
    print("Dog barking");
  }
}

void main() {
  Animal animal = Dog();
  (animal as Dog).makeSound();  // 输出: Dog barking
}
5. is 操作符

is 操作符用于检查对象是否属于某个特定类型。

void printType(dynamic value) {
  if (value is String) {
    print("String");
  } else if (value is int) {
    print("Int");
  } else {
    print("Unknown");
  }
}

void main() {
  printType("Hello");  // 输出: String
  printType(42);       // 输出: Int
  printType(3.14);     // 输出: Unknown
}

继承与多态

继承

继承是一种面向对象编程的核心概念,它允许一个类(称为子类或派生类)从另一个类(称为父类或基类)继承属性和方法。继承允许重用现有类的代码,并且可以在子类中添加新的功能或修改继承的行为,但是不会继承构造函数,使用extends关键字实现继承。

class Student extends Person {
  String school;

  Student(String name, int age, this.school) : super(name, age);
}

方法重写

子类可以重写父类的方法。

class Child extends Person {
  @override
  void greet() {
    print("Hi, I'm a child.");
  }
}

抽象类

抽象类(Abstract Class)是一种不能被实例化的类,它通常用作其他类的基类,用于定义一些共享的属性和方法。抽象类可以包含抽象方法,这些方法在子类中必须被重写才能实现具体的行为。抽象类为面向对象编程中的多态性提供了基础。

定义抽象类:

使用 abstract 关键字来定义抽象类。抽象类不能被实例化,只能被用作其他类的基类。

abstract class Shape {
  double area();  // 抽象方法,没有具体实现
}
继承抽象类:

通过继承抽象类,子类可以继承抽象类的属性和方法,并且必须实现抽象类中的抽象方法。

class Circle extends Shape {
  double radius;

  Circle(this.radius);

  @override
  double area() {
    return 3.14 * radius * radius;
  }
}

在上面的例子中,Circle 类继承了 Shape 抽象类,并且必须实现 area 方法,以满足抽象类的要求。

接口与实现

在 Dart 语言中,虽然没有像某些其他编程语言那样显式的“接口”关键字,但 Dart 仍然支持接口的概念。在 Dart 中,每个类都可以被看作是一个隐式的接口,其他类可以实现这个接口以共享该类的方法签名。

类作为接口:

在 Dart 中,一个类可以被其他类实现,就好像这个类是一个接口一样。通过使用 implements 关键字,可以让一个类实现另一个类的接口。实现类必须提供接口中定义的所有方法的实现。

class Animal {
  void makeSound() {
    print("Animal sound");
  }
}

class Dog implements Animal {
  @override
  void makeSound() {
    print("Dog barking");
  }
}

void main() {
  var dog = Dog();
  dog.makeSound();  // 输出: Dog barking
}

在这个例子中,Animal 类被视为一个接口,Dog 类实现了 Animal 的接口并提供了 makeSound 方法的具体实现。

多重实现:

Dart 支持多重实现,这意味着一个类可以实现多个接口。

class Swimmer {
  void swim() {
    print("Swimming");
  }
}

class Walker {
  void walk() {
    print("Walking");
  }
}

class Duck implements Swimmer, Walker {
  @override
  void swim() {
    print("Duck swimming");
  }

  @override
  void walk() {
    print("Duck walking");
  }
}

void main() {
  var duck = Duck();
  duck.swim();  // 输出: Duck swimming
  duck.walk();  // 输出: Duck walking
}

在这个例子中,Duck 类实现了 SwimmerWalker 接口,并提供了相应的方法实现。

如果实现的类是普通类,会将普通类和抽象中的属性的方法全部需要覆写一遍。而因为抽象类可以定义抽象方法,普通类不可以,所以一般如果要实现像Java接口那样的方式,一般会使用抽象类。 所以建议使用抽象类定义接口。

Mixins

Mixin的使用

Mixin 是一种在类中重用代码的机制,它允许一个类获取另一个类的成员,而不需要继承该类。Mixins 可以在多个类之间共享代码,避免多重继承的问题,同时提供了更灵活的代码组合方式。

要使用 Mixin,可以通过 with 关键字将一个或多个 Mixin 类混合进类中。这些 Mixin 类可以提供属性、方法和其他成员。

// 定义一个 Mixin
mixin Flyable {
  void fly() {
    print("Flying...");
  }
}

class Bird with Flyable {
  String name;

  Bird(this.name);
}

class Airplane with Flyable {
  String model;

  Airplane(this.model);
}

void main() {
  var bird = Bird("Sparrow");
  bird.fly();  // 输出: Flying...

  var airplane = Airplane("Boeing 737");
  airplane.fly();  // 输出: Flying...
}

在上面的示例中,定义了一个名为 Flyable 的 Mixin,它包含一个名为 fly 的方法。然后通过 with 关键字将 Flyable Mixin 混合进了 BirdAirplane 类中。现在这两个类都拥有了 fly 方法。

需要注意以下几点关于 Mixin 的特性:

  • 类可以使用多个 Mixin,通过逗号分隔,例如 class MyClass with Mixin1, Mixin2 {}
  • Mixin 不能直接实例化,它只能通过 with 关键字混合到其他类中。
  • Mixin 可以包含属性、方法、getter 和 setter,但不能有构造函数。
  • 当多个 Mixin 拥有同名方法时,最后一个混合的 Mixin 的方法会覆盖前面的方法。

Mixin 提供了一种有效的代码重用机制,可以减少重复编写代码的情况,同时还能避免多重继承可能引发的问题。

现在有个问题:那mixins的实例类型是什么?

一个类通过使用 with 关键字引入一个或多个 Mixin,这样它就可以获得 Mixin 中定义的方法和属性。当一个类使用了一个或多个 Mixin 后,其实例类型仍然是原始类的类型,即类本身的类型。

例如,如果一个类 MyClass 使用了一个名为 MyMixin 的 Mixin:

class MyMixin {
  void doSomething() {
    print("Mixin is doing something");
  }
}

class MyClass with MyMixin {
  // ...
}

void main() {
  var obj = MyClass();
  print(obj.runtimeType);  // 输出: MyClass
}

在上面的例子中,obj 的实例类型仍然是 MyClass,尽管它使用了 MyMixin Mixin。这意味着 Mixin 并不改变实例的类型,实例仍然是基于原始类创建的。

Mixin 主要用于将特定功能注入到类中,以实现代码的复用和组合,但它并不改变类的本质或类型。

异常处理

异常类型

Dart中有许多内置的异常类型,如ExceptionError。也可以自定义异常类。

class MyException implements Exception {
  final String message;

  MyException(this.message);
}

异常捕获

使用trycatch来捕获异常。

try {
  // 可能引发异常的代码
} catch (e) {
  // 异常处理代码
}

自定义异常

可以通过抛出自定义异常来指示特定问题。

void withdrawMoney(double amount) {
  if (amount > balance) {
    throw InsufficientFundsException("余额不足");
  }
}

异步编程

Future与async/await

Future

Future表示一个可能尚未完成的操作。

Future<String> fetchUserData() {
  return Future.delayed(Duration(seconds: 2), () => "User data");
}

async与await

使用async关键字将函数标记为异步,使用await等待异步操作完成。

Future<void> fetchData() async {
  var data = await fetchUserData();
  print(data);
}

Stream

Stream基础

Stream表示一系列异步事件,可用于处理连续的数据流。

Stream<int> countStream() async* {
  for (int i = 0; i < 5; i++) {
    yield i;
  }
}

创建Stream

使用StreamController创建一个自定义的Stream

StreamController<int> controller = StreamController<int>();
Stream<int> stream = controller.stream;

Stream订阅

通过订阅,可以监听Stream中的事件。

StreamSubscription<int> subscription = stream.listen((data) {
  print(data);
});

Stream变换

可以通过mapwhere等方法对Stream进行变换。

Stream<int> transformedStream = stream.map((value) => value * 2);
Stream<int> filteredStream = stream.where((value) => value % 2 == 0);

泛型

泛型类

泛型(Generics)是一种通用的编程概念,它允许在定义类、函数或接口时使用参数类型,以便在使用时指定具体的类型。泛型使得代码更加灵活、可重用,同时提高了类型安全性。

在 Dart 中,可以使用泛型来创建具有通用性的类、函数或接口,以满足不同类型的需求,而无需为每种类型都编写不同的代码。

class Box<T> {
  T value;

  Box(this.value);
}

void main() {
  var box = Box<int>(42);
  print(box.value);  // 输出: 42

  var stringBox = Box<String>("Hello");
  print(stringBox.value);  // 输出: Hello
}

在上面的例子中,Box 是一个泛型类,它可以存储不同类型的值。

泛型函数

T getMax<T extends num>(T a, T b) {
  return a > b ? a : b;
}

void main() {
  print(getMax<int>(5, 10));    // 输出: 10
  print(getMax<double>(3.14, 2.71));  // 输出: 3.14
}

在这个例子中,getMax 是一个泛型函数,它可以比较不同类型的数值。

泛型接口

abstract class Repository<T> {
  void insert(T item);
  void update(T item);
  T getById(int id);
}

class UserRepository implements Repository<String> {
  @override
  void insert(String item) {
    print("Inserting: $item");
  }

  @override
  void update(String item) {
    print("Updating: $item");
  }

  @override
  String getById(int id) {
    return "User $id";
  }
}

void main() {
  var userRepo = UserRepository();
  userRepo.insert("Alice");  // 输出: Inserting: Alice
}

在上面的例子中,Repository 是一个泛型接口,它可以用不同的类型来实现。

类型约束

使用泛型的约束类型来限制泛型类型参数的范围,使其满足特定的条件。这种约束可以在泛型代码中使用一些特定的操作或方法,从而增加类型安全性和代码的灵活性。

1. extends 关键字

使用 extends 关键字来约束泛型类型参数为指定的类或其子类。

class Box<T extends num> {
  T value;

  Box(this.value);
}

void main() {
  var intBox = Box<int>(42);
  var doubleBox = Box<double>(3.14);
}

在这个例子中,T 必须是 num 类型或其子类。

2. implements 关键字

使用 implements 关键字来约束泛型类型参数为实现了特定接口的类。

class Repository<T extends Storable> {
  void save(T item) {
    item.store();
  }
}

abstract class Storable {
  void store();
}

class User implements Storable {
  @override
  void store() {
    print("Storing user");
  }
}

class Product implements Storable {
  @override
  void store() {
    print("Storing product");
  }
}

void main() {
  var userRepository = Repository<User>();
  var productRepository = Repository<Product>();

  userRepository.save(User());  // 输出: Storing user
  productRepository.save(Product());  // 输出: Storing product
}

在这个例子中,T 必须是实现了 Storable 接口的类。

3. 指定多个约束

可以同时指定多个约束条件,例如同时满足某个类和接口。

class MyClass<T extends SomeClass & SomeInterface> {
  // ...
}

这意味着泛型类型参数 T 必须是一个既继承自 SomeClass 类,又实现了 SomeInterface 接口的类。

库与包

库的创建

Dart代码可以组织在库中,使用library关键字创建库。

library my_library;

class MyClass {
  // ...
}

库的导入

使用import关键字引入其他库

import 'my_library.dart';

库的冲突

在项目中引入多个库(packages)时,可能会遇到库冲突的情况,即不同的库中包含相同名称的标识符(如类、函数、变量等)。这可能导致编译器无法确定使用哪个库中的标识符,从而产生冲突。

1. 使用命名前缀:

可以使用命名前缀来明确指定使用哪个库中的标识符。通过为标识符添加库名称的前缀,可以避免冲突。

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

void main() {
  var value1 = lib1.someFunction();
  var value2 = lib2.someFunction();
}
2. 只导入所需内容:

如果只需要库中的一部分内容,可以只导入所需的部分,避免将整个库引入。

import 'package:library1/library1.dart' show SomeClass;
import 'package:library2/library2.dart' hide SomeClass;

void main() {
  var instance = SomeClass();  // 仅从 library1 中引入
}
3. 优先级顺序:

如果两个库都导入了相同的标识符,而没有使用命名前缀、别名或导入部分内容,Dart 会选择优先级更高的库中的标识符。通常情况下,本地库的优先级高于依赖库。

4. 避免冲突:

在设计自己的库时,尽量避免使用与其他常用库相同的名称,以减少冲突的可能性。

延迟加载

在 Dart 中可以使用延迟加载(Deferred Loading)来在需要的时候才加载库,而不是在程序一开始就加载。这可以减少初始加载时间,只在需要的时候才加载额外的库,从而提高应用程序的性能。延迟加载通过 deferred as 关键字实现:

延迟加载的语法:

使用 deferred as 关键字来声明延迟加载的库,然后使用 loadLibrary 函数来在需要时加载该库。

import 'package:my_library/my_library.dart' deferred as mylib;

void main() async {
  print("Before loading library");
  await mylib.loadLibrary();
  print("After loading library");

  var value = mylib.someFunction();
  print(value);
}

在上述代码中,mylib 是延迟加载的库别名。通过调用 loadLibrary 函数,可以在需要时加载 my_library 库。

注意事项:
  1. 使用延迟加载时,被延迟加载的库中的内容只有在加载后才能被使用。因此,需要在加载之后才能访问库中的成员。
  2. 延迟加载库中的代码会在第一次访问库中的成员时执行。在上述示例中,调用 mylib.someFunction() 会触发库的加载和执行。
  3. 延迟加载的库不能包含顶级变量,但可以包含顶级函数。
延迟加载的应用场景:

延迟加载适用于需要按需加载额外功能的场景,特别是当这些功能在应用程序的某些分支中才会用到时。这可以减少应用程序的初始启动时间,提高性能。

需要注意的是,虽然延迟加载可以提高应用程序的性能,但也需要谨慎使用,以避免出现不必要的复杂性。延迟加载库的使用应该基于应用程序的实际需求和结构。

Pub包管理器

Pub是Dart的包管理器,用于管理和分享Dart代码。

依赖管理

pubspec.yaml文件中声明依赖项。

dependencies:
  http: ^3.0.0

常用包

许多常用的Dart库和包可以在Pub上找到,如http用于进行HTTP请求,intl用于国际化。

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

文章结束。