引言
本文接上篇:从0到1掌握Flutter(二)环境搭建与认识工程
Dart 语言基础是 Flutter 开发必须掌握的核心知识。本文将讲解变量与常量的声明、Dart 内置类型体系及其用法、运算符的应用场景三大模块。
对于具备 Java/Kotlin 背景的学习者,可以通过对比学习法快速定位知识缺口,理解语法的共性。
一、变量与常量
1.1 变量
💡 万物皆对象
在 Dart 的类型系统中,变量本质上是指对象的引用,这一设计符合面向对象语言的核心特性。
无论是自定义类型还是基本数字类型,Dart 中一切均以对象形式存在。
通过查看 int 类型的源码实现可以发现:int 本身是一个类,其实例自然也是对象:
所以基于此设计,所有未显式初始化的变量默认值均为 null,这是 Dart 统一对象模型的直接体现。
scss
int? uninitializedInt;
double? uninitializedDouble;
String? uninitializedString;
bool? uninitializedBool;
print(uninitializedInt); // null
print(uninitializedDouble); // null
print(uninitializedString); // null
print(uninitializedBool); // null
💡 类型声明
在 Dart 中有三种变量的声明方式:Object、var 与 dynamic。与 Java 的强类型约束不同,Dart 的声明方式更灵活,平衡了类型安全与开发效率。
- Object声明:
ini
Object obj = "hello";
obj = 50; // 合法但会丢失类型信息
延续了 Java 的基类传统,与 Java 一样 Object 是所有类的基类,Object 声明的变量可以是任意类型。当使用Object时,虽然支持任意赋值,但每次访问都需要显式类型转换------这种设计保留了 Java 的严谨性,却与 Dart 推崇的简洁理念存在冲突。
- var声明:
ini
var a = 1;
a = "a";//编译时报错
var b;
b = 1;
b = "a";//正确
Dart 中的 var ,与 kotlin 中的 var 类似,用于声明变量并通过初始化值自动推断类型。二者存在一下差异:
特性 | Dart 的 var | Kotlin 的 var |
---|---|---|
类型锁定 | 初始化后类型固定 | 声明时类型固定 |
默认值 | 未初始化时为 null | 必须显式初始化 |
动态类型支持 | 未初始化的 var 类似 dynamic | 需显式使用 Any 或可空类型 |
语法灵活性 | 允许延迟初始化并改变类型 | 必须立即初始化且类型不可变 |
Dart 中的 var 可以不初始化,若未初始化,变量类型默认为 dynamic,可以任意赋值,编译器不做类型检测(后续会提到)。初始化后类型锁定不可更改。简化了代码并保持编译时类型安全,适用于局部变量或类型明确的场景。
- dynamic声明:
ini
dynamic dy = 10; // 初始化为整型
dy = "text"; // 合法操作,允许运行时改变类型
dy.nonExistFunc(); // 编译通过,但运行时会抛出 NoSuchMethodError
dynamic 类型声明是 Dart 中灵活处理类型变化的特殊机制,它的作用是在保留运行时类型检查的前提下关闭静态类型检查。该类型具有三个特征:
- 动态类型转换:变量在运行时可自由转换类型,无需显式声明(如从 int 转 String)
- 编译期宽松检查:IDE 不会提供方法提示,编译器也不会验证方法是否存在
- 运行期抛出异常:虽然编译阶段放行,但执行不存在的方法时会立即抛出异常
与 Java 的 Object 类型对比时,主要差异体现在类型操作层面:Java 必须通过显式类型转换才能调用目标类型方法,而 dynamic 可以直接操作。但这种便利性会带来更高的风险,比如以下 Java 代码会在编译阶段就暴露错误:
ini
Object obj = 10;
obj = "text";
// obj.nonExistFunc(); // Java 编译报错:Object 类没有该方法
// ((String)obj).nonExistFunc(); // 强制转换后仍会在编译期报错
虽然它提供了编码灵活性,但过度使用会破坏 Dart 的类型系统保护机制。建议仅在处理 JSON 数据解析或与 JavaScript 互操作等特定场景下使用,常规业务逻辑中应优先考虑类型安全方案。
1.2 常量
💡 final 与 const
在 Dart 中,当我们确定某个变量不需要修改时,final 和 const 这两个关键字都能帮我们实现不可变性。不过它们的底层机制和应用场景有所不同,我们通过几个关键点来理解它们的差异。
先看这段基础用法示例:
ini
final userName = 'name'; // 运行时确定值
const maxCount = 100; // 编译时确定值
虽然表面上看两者用法相似,但它们的本质区别在于确定值的时机,这种差异直接影响了它们的使用场景。比如下面这个例子中:
ini
// 合法:运行时获取当前时间
final currentTime = DateTime.now().second;
// 非法:编译时无法确定具体值
// const fixedTime = DateTime.now().second;
在类成员变量使用时,要注意它们的声明方式:
arduino
class Config {
final int id; // 正确:通过构造函数初始化
static const version = 1.0; // 必须声明为静态
Config(this.id);
}
由于类的实例化是运行时行为,而 const 需要编译时确定值,因此类中的 const 常量必须通过 static 声明为类级别常量。而 final 变量则可以通过构造函数灵活初始化,每个实例可以拥有不同的 final 值。
另外,final 是运行时常量,而 const 是编译器常量,它的值在编译期就可以确定,能够让代码运行更高效。
二、内置类型
2.1 基础类型
下面来看看 Dart 语言中的基础类型设计,和 Java 直接内置 8 种基本数据类型不同,Dart 内置支持下面这些类型:
💡 Numbers
在数字类型方面,Dart 使用了一个层次分明的结构。最顶层的 num 类型作为数字类型的抽象父类,它有两个具体的子类实现:int 表示整型数值,double 则专门处理浮点数。这种继承关系意味着当我们在代码中声明一个 num 类型的变量时,实际上可以接收整数或小数两种形式的赋值。
ini
// 可以接收整型
num age = 25;
// 也可以接收浮点型
num price = 9.99;
这种设计带来的优势是:开发者既可以使用具体类型来确保数值精度,也可以通过父类 num 来编写更通用的数值处理方法。
💡 String
在 Dart 中处理字符串,有几个实用特性。创建字符串时,单引号和双引号是等效的,我们可以根据场景灵活选择,并且单引号和双引号互相嵌套可以不使用转义符号``:
ini
// 单双引号嵌套可避免转义符号的使用
var quote = 'He said "Hello!" without hesitation';
var nested = "It's a beautiful day";
当需要将变量值嵌入字符串时,Dart 提供了便捷的插值语法。注意当引用简单变量时可以直接使用$
符号,而执行表达式时则需要用花括号包裹,与 kotlin 类似:
ini
var score = 90;
var report = "Final score: $score"; // 简单变量引用
var detail = "Score status: ${score >= 60 ? 'Pass' : 'Fail'}"; // 表达式计算
字符串拼接有两种常用方式。除了传统的+
操作符,将相邻字符串写在一起也能实现自动拼接,这在格式化长字符串时特别有用:
ini
var message1 = 'Hello ' + name + '!'; // 使用加号拼接
var message2 = 'Welcome to ' 'Dart string ' 'tutorial'; // 自动拼接
处理多行文本时,三引号语法可以保留原始换行和缩进格式。这在创建格式化的长文本(如 SQL 语句或文档模板)时非常实用,与 kotlin 类似:
ini
var sql = '''
SELECT name, email
FROM users
WHERE status = 'active'
''';
当需要处理原始字符串(不解析转义字符)时,只需在引号前添加r
前缀。这在处理正则表达式或文件路径时尤为重要:
python
print(r"换行符保留:\n"); // 输出:换行符保留:\n
print("正常转义:\n"); // 输出:正常转义:\n
💡 Booleans
Dart 的布尔值与 Java 等静态类型语言相似。条件表达式必须严格返回布尔值:
go
// 基础布尔变量声明
var isLogin = true;
bool hasPermission = false;
if (isLogin) {
print('显示用户仪表盘');
} else {
print('跳转到登录页');
}
// 以下写法会导致编译错误(非布尔值不能用于条件判断)
var count = 0;
// if (count) { ... } // 错误:int 不能隐式转换为 bool
2.2 集合类型
💡 List
在 Dart 中,List 是最基础且使用频率最高的数据结构之一。我们可以将其理解为其他语言中的数组,但具备更灵活的特性:
- 创建与访问
Dart 提供了直观的字面量声明方式,用方括号包裹元素并用逗号分隔。元素的索引体系从 0 开始,与大多数编程语言一致:
scss
//使用new(new可以省去)
var list = new List.filled(1, 0);
list[0] = 2;
// 基础列表声明
var numbers = [10, 20, 30];
// 访问最后一个元素的两种方式
print(numbers[numbers.length - 1]); // 方式1:通过长度计算
print(numbers.last); // 方式2:使用内置属性(更推荐)
- 不可变列表
通过 const
关键字可以创建编译时常量列表,这种列表在内存中只会存在一份实例,且完全不可修改:
csharp
// 创建不可变列表
const immutableList = ['A', 'B', 'C'];
// 以下操作均会触发运行时错误
immutableList[0] = 'X'; // 错误:禁止修改元素
immutableList.add('D'); // 错误:禁止改变长度
- List 遍历
dart
// List 遍历
var colors = ['Red', 'Green', 'Blue'];
//增强型 for-in
for (var color in colors) {
print('颜色值: $color');
}
// 函数式 forEach
colors.asMap().forEach((index, color) {
print('索引$index -> $color');
});
💡 Map
Map 和其他语言中的 Map 类似
- 基础声明方式
我们可以用花括号 {}
直接创建 Map,键值对之间用逗号分隔:
csharp
// 创建包含国家代码的 Map
var countryCodes = {
'China': 86,
'USA': 1,
'Japan': 81
};
这种写法非常直观,就像列清单一样把键值对依次排列出来。
- 不可变 Map 的创建
当我们需要创建编译时就确定的常量 Map 时,可以像这样在花括号前添加 const 关键字:
arduino
// 定义不可修改的常量 Map
const constantMap = {
2: 'helium',
10: 'neon',
18: 'argon',
};
这个 Map 在程序运行期间将始终保持不变,任何修改它的尝试都会导致运行时错误。这种特性对于需要保证数据不被意外修改的场景非常有用。
- Map 的遍历
我们可以用两种方式查看每个键值对:
dart
// 使用 forEach 遍历
constantMap.forEach((country, code) {
print('$country 的国际区号是 $code');
});
// 或者用 for-in 遍历
for (var entry in constantMap.entries) {
print('${entry.key} -> ${entry.value}');
}
2.3 特殊类型
💡 Runes
在 Dart 中处理 Unicode 字符时,Runes 类是一个重要工具。当我们需要处理特殊字符或 32 位的 Unicode 编码时,传统的字符串表示方法可能不够用,这时就要用到 Runes 了。举个例子:
假设我们要处理一个「💡」表情符号,它使用 16 进制表示为"U+1F4A1",转为二进制是 111110010100001,这是一个 21 位的二进制数。
而 String 实际上是使用的是 UTF-16 编码,它的最小码元是 16 位,所以「💡」就需要两个码元来表示,这个时候获取字符串'💡'的长度,就会的到 2:
go
var emoji = '💡';
print(emoji.length); // 输出 2(占用2个UTF-16代码单元)
print(emoji.codeUnits); // 输出 [55357, 56399]
使用 Runes 则不会出现这个问题,Runes 使用的是 UTF-32 编码,它的最小码元是 32 位,足以表示「💡」。
scss
Runes runes = Runes('💡');
print(runes.length);// 输出 1(占用1个UTF-32代码单元)
print(runes.toList()); // 输出 [128161]
最后我们看一个组合使用 Runes 的典型场景,处理包含多种 Unicode 符号的字符串:
scss
Runes input = Runes(
'\u2665 \u{1f605} \u{1f60e} \u{1f47b} \u{1f596} \u{1f44d}');
print(String.fromCharCodes(input));
// 输出:♥ 😅 😎 👻 🖖 👍
String 与 Runes 对比
特性 | 字符串(String) | Runes |
---|---|---|
存储方式 | UTF-16 代码单元序列 | UTF-32 代码单元序列 |
长度计算 | .length 返回代码单元数量 |
.length 返回实际字符数量 |
适用场景 | 常规文本操作 | 需要精确处理 Unicode 字符的场景 |
特殊字符处理 | 可能拆分为代理对(如:'👏'.length=2) | 始终保留完整代码点(如:Runes('👏').length=1) |
💡 Symbols
Symbol 是一种特殊的标识符,来看一个具体的应用示例:
dart
void main() {
const targetSymbol = #A; // 声明编译时常量符号
print('当前符号值: $targetSymbol');
switch(targetSymbol) {
case #A: // 符号匹配判断
print("匹配到A符号");
break;
case #B:
print("匹配到B符号");
break;
}
final createdSymbol = Symbol('created'); // 通过构造函数创建符号
print(#created == createdSymbol); // 输出 true,两种创建方式等价
}
三、操作符
3.1 基础操作符
💡 赋值操作符
在 Dart 的赋值操作符中,除了 =(基础赋值)、+=(累加)、/=(除法赋值)、*=(乘法赋值)这些常规操作符之外,还有一个比较实用的空安全赋值操作符 ??=。
它的作用可以用是,当且仅当变量值为 null 时执行赋值操作:
ini
int? score; // 声明一个可空的整型变量
// 使用空安全赋值操作符的等效写法
score ??= 60; // 此时 score 的值为 60
// 传统写法需要显式判空
if (score == null) {
score = 60;
}
当第二次调用时:
scss
score ??= 90; // 由于 score 已经是 60(非空),赋值操作不会执行
print(score); // 输出仍然是 60
💡 判定操作符
Dart 提供了三个运算符确保类型安全:
- as 运算符用于显式的类型转换(类似于 Java 的强制类型转换)
- is 运算符用于检查对象是否属于特定类型(相当于 Java 的 instanceof)
- is! 则是前者的逻辑取反,判断对象是否不属于指定类型
使用方法与 kotlin 类似:
dart
void process(dynamic data) {
// 先进行类型检查
if (data is String) {
print('字符串长度:${data.length}'); // 这里会自动提升为 String 类型
}
try {
final numData = data as num; // 强制类型转换
print('数值平方:${numData * numData}');
} catch (e) {
print('类型转换失败:$e');
}
}
在这个示例中,注意几个点需要注意:
• 使用 is
进行类型检查后,Dart 会自动将变量提升为该类型,后续代码可直接使用该类型的属性和方法
• as
运算符需要谨慎使用,当转换不兼容的类型时会抛出 CastError
,因此建议先用 is
检查或配合 try-catch
3.2 特殊操作符
💡 安全操作符
在 Dart 语言中,可以使用安全操作符(?)处理空指针异常问题。
同 Kotlin 的(?)操作符一样,例如当试图访问一个可能为空的字符串长度时,传统的做法需要写大量判空代码。使用使用安全操作符(?)可以更优雅的处理类似问题:
scss
String? sb; // 声明为可空字符串类型
print(sb.length); // 触发空指针异常
print(sb?.length); // 安全访问,输出 null
💡 件表达式
Dart 有条件表达式,与 Java 与 kotlin 的条件表达式很相似。
- 三目运算符
ini
// Dart示例
var status = isPublic ? '公开' : '私密';
这个写法和 Java 的三目运算符完全一致。不过 Kotlin 需要用完整的 if 表达式来实现相同效果
- 空值合并符
ini
// 当userName为空时,使用'匿名用户'
var name = userName ?? '匿名用户';
??
操作符相当于 Kotlin 的?:
操作符。
💡 级联操作符
级联操作符(..)是一个让代码更优雅的语法糖。帮你保持对象操作连贯性的工具。
当我们需要对同一个对象进行多次操作时,它可以避免反复书写对象变量名。比如在构建字符串时,传统写法需要不断重复变量名:
arduino
final buffer = StringBuffer();
buffer.write('Hello');
buffer.write(' ');
buffer.write('Dart!');
而使用级联操作符后,代码会变得行云流水:
lua
final buffer = StringBuffer()
..write('Hello')
..write(' ')
..write('Dart!');
这种写法特别适合配置复杂对象的场景,比如构建 UI 组件时连续设置多个属性,或是处理集合元素时进行链式操作。它让代码呈现出清晰的步骤感,就像在讲述一个对象被创建的过程。
四、总结
本文系统梳理了 Flutter 开发中的基础语法,从变量声明到集合类型,从基础运算符到级联表达式。这些语法是构建应用的基石。掌握这些语法,后面我们继续学习 Dart 的方法、类。