从0到1掌握Flutter(三)Dart语法

引言

本文接上篇:从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 中灵活处理类型变化的特殊机制,它的作用是在保留运行时类型检查的前提下关闭静态类型检查。该类型具有三个特征:

  1. 动态类型转换:变量在运行时可自由转换类型,无需显式声明(如从 int 转 String)
  2. 编译期宽松检查:IDE 不会提供方法提示,编译器也不会验证方法是否存在
  3. 运行期抛出异常:虽然编译阶段放行,但执行不存在的方法时会立即抛出异常

与 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 提供了三个运算符确保类型安全:

  1. as 运算符用于显式的类型转换(类似于 Java 的强制类型转换)
  2. is 运算符用于检查对象是否属于特定类型(相当于 Java 的 instanceof)
  3. 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 的方法、类。

相关推荐
北岛贰4 小时前
爆肝两个月,我用flutter开发了一款免费音乐app
flutter·dart
亿码归一码9 小时前
【flutter】flutter 环境搭建
flutter
0^115 小时前
Flutter笔记
笔记·flutter
yuanlaile16 小时前
Flutter网页交互增强插件pulse_core_web的使用
flutter
Cao_Shixin攻城狮16 小时前
Flutter PopScope对于iOS设置canPop为false无效问题
flutter·ios·popscope
仙魁XAN1 天前
Flutter 学习之旅 之 flutter 不使用插件,实现简单自定义弹窗PopupDialog功能
flutter·提示·弹窗·toast·popupdialog
仙魁XAN1 天前
Flutter 学习之旅 之 flutter 在设备上进行 全面屏 设置/隐藏状态栏/隐藏导航栏 设置
前端·学习·flutter
一人前行1 天前
Flutter_学习记录_barcode_scan2 实现扫描条形码、二维码
flutter
顾林海1 天前
Flutter Dart 面向对象编程全面解析
android·前端·flutter