Dart 入门随笔:从零开始的优雅之旅
一、初见 Dart:为什么是它?
“写这篇随笔的初衷,是想记录下我与 Dart 的第二次相遇…” 第一次相遇好像在两三年前了
那为啥又捡起 Dart 呢?说起来也简单——部门 Flutter 这边缺人,招又招不到,反倒是前端人满为患。季度绩效聊的时候领导问了一圈谁愿意转方向,结果就我一个举了手。说实话我对 Flutter 印象还行,好几年前私下捣鼓过一点,这次干脆趁机会往移动端试试水。再说了,现在前端这行情懂的都懂… 咳咳,那就让一个 JS 半吊子来记录 Dart 入门吧。
二、温暖的开端:Hello World 与变量
本人目前精通JavaScript、Java、Go、Python、Rust、C#编程语言
void main(){
print('Hello World and Dart!');
}
写到这里,大家已经精通Dart咯!!(ps:开玩笑啦)
var、final、const 的爱恨情仇
var: 核心是做类型推断,而不是“我想变就变”
void main(){
var name = 'Flutter';
name = 123; // ❌ 编译错误,type 'int' is not a subtype of type 'String'
// Good: 右侧类型非常明确
var items = <String>[];
// Bad: 右侧是一个复杂的函数返回值,显式声明类型更利于阅读
var result = calculateComplexData();
// 推荐写法:
DataResult result = calculateComplexData();
}
final: 运行时不可变。意味着它可以在运行时被赋值一次,之后引用不可改。 const: 编译期不可变。意味着它的值在编译时就必须确定,且在内存中是“规范化的”(相同值的 const 对象共享内存)。
跟 js 还是有所出入的,强类型语言就是好!
void main(){
final list1 = [1, 2, 3];
const list2 = [1, 2, 3];
list1.add(4); // ✅ 合法!list1 的引用没变,只是内容变了
list2.add(4); // ❌ 非法!const 对象完全不可变
}
类型
num
// num 是 int 和 double 的父类
num x = 10; // 可以是 int
x = 10.5; // 也可以是 double
// int 整数
int count = 42;
int bigNumber = 1_000_000; // 下划线分隔(Dart 2.6+)
// double 浮点数
double pi = 3.14159;
double scientific = 1.42e5; // 科学计数法
// 📌 类型转换
int a = 10;
double b = a.toDouble(); // int → double
int c = b.toInt(); // double → int(截断)
String
String s1 = 'Hello';
// 字符串插值(类似模板字符串)
String name = 'Alice';
int age = 25;
String greeting = 'Hello, $name! You are $age years old.';
String expression = '2 + 2 = ${2 + 2}'; // 复杂表达式用 {}
// 多行字符串
String multiline = '''
这是一个
多行字符串
''';
// 原始字符串(不转义)
String raw = r'C:\Users\name\file.txt';
String path = r'$name 不会被插值';
// 常用方法
String text = 'Hello, Dart!';
text.toLowerCase(); // ' hello, dart! '
text.toUpperCase(); // ' HELLO, DART! '
text.length; // 16
// 字符串遍历
for (var char in 'Hello'.runes) {
print(String.fromCharCode(char));
}
bool
bool isValid = true;
bool isEmpty = false;
// ⚠️ Dart 是强类型,不存在隐式转换 ~
// JavaScript: if (1) { } // true
// Dart: if (1) { } // ❌ 错误:条件必须是 bool
// 📌 正确做法
if (isValid) { }
if (count > 0) { }
if (name.isNotEmpty) { } // ⚠️ 不是 if (name) { }
可空类型
// 默认不可空
String name = 'Alice';
// name = null; // ❌ 错误
// 可空类型(加 ? 后缀)
String? nickname;
nickname = null; // ✅ 正确
nickname = 'Bob'; // ✅ 正确
空值处理
String? name;
// 1. if 检查
if (name != null) {
print(name.length);
}
// 2. ?. 安全调用(类似可选链)
print(name?.length); // null(如果 name 是 null)
print(name?.toUpperCase()); // null
// 3. ?? 空值合并(类似 ?? 运算符)
String displayName = name ?? 'Guest';
// 4. ??= 空值赋值
name ??= 'Default'; // 如果 name 是 null,则赋值
// 5. ! 非空断言(类似 TypeScript 的 !)
// ⚠️ 确定不为空时使用,否则抛出异常
print(name!.length);
三、流程控制:代码的节奏感
流程控制这块,说实话跟 JS 大同小异,但还是有几个坑值得说道说道。
if-else:老朋友了
int score = 75;
if (score >= 90) {
print('优秀');
} else if (score >= 60) {
print('及格');
} else {
print('下次加油');
}
这块没啥好说的,该咋写咋写。
switch-case:没有 fall-through!
这个要注意,Dart 的 switch 不会自动往下穿透,每个 case 必须有 break(或者 return/throw/continue)。
String grade = 'A';
switch (grade) {
case 'A':
print('优秀');
break; // 必须写!不写编译器直接报错
case 'B':
print('良好');
break;
default:
print('其他');
}
💡 JS 老司机注意:JS 里 case 不加 break 会继续执行下一个 case(著名的 fall-through 坑),Dart 直接把这个坑填了,不写 break 编译都不让你过。
循环:for / while / for-in
// 经典 for
for (int i = 0; i < 5; i++) {
print(i);
}
// for-in(遍历集合超方便)
var list = ['苹果', '香蕉', '橘子'];
for (var fruit in list) {
print(fruit);
}
// while
int i = 0;
while (i < 3) {
print(i);
i++;
}
循环控制:break 和 continue
// break:直接跳出
for (int i = 0; i < 10; i++) {
if (i == 5) break;
print(i); // 0, 1, 2, 3, 4
}
// continue:跳过本次
for (int i = 0; i < 5; i++) {
if (i == 2) continue;
print(i); // 0, 1, 3, 4
}
标签:跳出多层循环
有时候需要从内层循环直接跳到外层,可以用标签:
outer: // 这是标签
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1) {
break outer; // 直接跳出外层循环
}
print('$i, $j');
}
}
说实话这个功能我用得不多,但面试可能会问…
assert:开发阶段的”安全带”
void setAge(int age) {
assert(age >= 0, '年龄不能是负数吧?');
// ...
}
⚠️ assert 只在调试模式生效,生产环境会被直接忽略。适合用来做开发时的”防御性检查”,比如参数校验、前置条件之类。
小结一下
流程控制这块 Dart 和 JS 真的挺像,主要记住这几点:
- switch 没有 fall-through,必须显式 break
- 条件必须是 bool,
if (1)这种写法直接报错 - assert 只在调试生效,别当业务逻辑用
四、函数:Dart 的一等公民
函数这块 Dart 和 JS 还是有不少区别的,尤其是参数的处理。
基本函数定义
// 标准写法
int add(int a, int b) {
return a + b;
}
// 箭头函数(单行表达式)
int add2(int a, int b) => a + b;
// 返回类型可以省略(但不推荐)
add3(a, b) => a + b;
💡 箭头函数
=> expr是{ return expr; }的简写,注意和 JS 的=>区分,JS 那边是真正的函数,这边更像是语法糖。
参数的”花样”:可选参数 vs 命名参数
这块是 Dart 的特色,JS 开发者需要适应一下。
1. 位置参数(必填,和 JS 一样)
String greet(String name, int age) {
return '你好 $name,你 $age 岁了';
}
greet('小明', 18); // ✅
greet('小明'); // ❌ 编译错误:少参数
2. 可选位置参数 []
// 用 [] 包起来的参数是可选的
String greet(String name, [int? age]) {
if (age != null) {
return '你好 $name,你 $age 岁了';
}
return '你好 $name';
}
greet('小明'); // ✅ 你好 小明
greet('小明', 18); // ✅ 你好 小明,你 18 岁了
3. 可选参数带默认值
String greet(String name, [int age = 18]) {
return '你好 $name,你 $age 岁了';
}
greet('小明'); // 你好 小明,你 18 岁了(用了默认值)
4. 命名参数 {} 给个夯
强烈推荐用来提高代码可读性:
// 用 {} 包起来的是命名参数
void createUser({
required String name, // required 表示必填
required String email,
int age = 18, // 没有 required 就是可选的,可以给默认值
}) {
print('创建用户: $name, $email, $age');
}
// 调用的时候必须写参数名
createUser(
name: '小明',
email: 'xiaoming@example.com',
); // ✅ age 用了默认值 18
createUser(
name: '小红',
email: 'xiaohong@example.com',
age: 20,
); // ✅
createUser('小明', 'xxx@qq.com'); // ❌ 编译错误: 必须用命名方式传参
💡 命名参数的好处是可读性超强,尤其是参数多的时候。你回头看 JS 的函数调用
createUser('小明', 'xxx', 20, '北京', true),谁知道每个参数是啥意思?Dart 这边一目了然。
5. 混合使用
// 位置参数 + 命名参数
void printInfo(String name, {int? age, String? city}) {
print('姓名: $name, 年龄: $age, 城市: $city');
}
printInfo('小明'); // ✅
printInfo('小明', age: 18); // ✅
printInfo('小明', city: '北京', age: 18); // ✅ 命名参数顺序随意
匿名函数和闭包
这块和 JS 基本一致:
// 匿名函数赋值给变量
var multiply = (int a, int b) => a * b;
print(multiply(3, 4)); // 12
// 当参数传递
var list = [1, 2, 3, 4, 5];
list.forEach((n) => print(n * 2)); // 2, 4, 6, 8, 10
// 闭包
Function makeCounter() {
int count = 0;
return () {
count++;
return count;
};
}
var counter = makeCounter();
print(counter()); // 1
print(counter()); // 2
print(counter()); // 3
闭包这块懂的都懂,和 JS 一模一样。
高阶函数:map / where / reduce
Dart 的数组方法名字和 JS 略有不同:
var numbers = [1, 2, 3, 4, 5];
// map - 映射(名字一样)
var doubled = numbers.map((n) => n * 2).toList();
print(doubled); // [2, 4, 6, 8, 10]
// where - 过滤(JS 叫 filter)
var evens = numbers.where((n) => n % 2 == 0).toList();
print(evens); // [2, 4]
// reduce - 归约(名字一样)
var sum = numbers.reduce((acc, n) => acc + n);
print(sum); // 15
// fold - 带初始值的 reduce(JS 的 reduce 第二个参数)
var product = numbers.fold<int>(1, (acc, n) => acc * n);
print(product); // 120
// every - 每个都满足
var allPositive = numbers.every((n) => n > 0);
print(allPositive); // true
// any - 有任意一个满足
var hasEven = numbers.any((n) => n % 2 == 0);
print(hasEven); // true
⚠️ 这个 js 佬们真得注意
map和where返回的是Iterable,不是List!如果需要 List,记得.toList()。
小结一下
函数这块主要记住:
- 命名参数 - 用
{}包裹,required标记必填,调用时写参数名 - 可选参数 - 用
[]包裹,可以给默认值 - where - 就是 JS 的 filter
- fold - 就是 JS 的 reduce 带初始值
- map/where 返回 Iterable - 需要
.toList()转成 List
五、面向对象:Dart 的骨血
Dart 是一门纯正的面向对象语言,连数字、函数都是对象。这块内容比较多,咱们拆开来讲。
类的基本写法
class Person {
// 属性(也叫实例变量)
String name;
int age;
// 构造函数(这种写法叫"语法糖",自动赋值)
Person(this.name, this.age);
// 方法
void sayHello() {
print('我是 $name,今年 $age 岁');
}
}
// 使用
var person = Person('小明', 18);
person.sayHello(); // 我是小明,今年18岁
💡 对比 JS/TS:Dart 没有
constructor关键字,构造函数直接用类名。Person(this.name, this.age)这行代码相当于 TS 里的constructor(public name: string, public age: number)。
构造函数的几种写法
1. 默认构造函数
class Person {
String name;
int age;
Person(this.name, this.age);
}
2. 命名构造函数
Dart 不支持构造函数重载(同名不同参数),但可以用”命名构造函数”:
class Person {
String name;
int age;
Person(this.name, this.age);
// 命名构造函数:Person.guest()
Person.guest() : name = '游客', age = 0;
// 命名构造函数:Person.fromMap()
Person.fromMap(Map<String, dynamic> map)
: name = map['name'],
age = map['age'];
}
var p1 = Person('小明', 18);
var p2 = Person.guest(); // name='游客', age=0
var p3 = Person.fromMap({'name': '小红', 'age': 20});
💡 这点要习惯一下,Dart 用命名构造函数代替了重载。
3. 初始化列表
class Rectangle {
final double width;
final double height;
final double area;
// 初始化列表在构造函数体执行前运行
Rectangle(this.width, this.height) : area = width * height;
}
var rect = Rectangle(3, 4);
print(rect.area); // 12.0
初始化列表适合用来给 final 变量赋值,或者做参数校验:
class Person {
final String name;
final int age;
Person(this.name, this.age)
: assert(age >= 0, '年龄不能为负'); // 初始化时校验
}
getter 和 setter
有时候属性需要计算得出,或者赋值时需要校验:
class Rectangle {
double _width; // 下划线开头表示私有
double _height;
Rectangle(this._width, this._height);
// getter:用 get 关键字
double get area => _width * _height;
double get width => _width;
// setter:用 set 关键字
set width(double value) {
if (value > 0) {
_width = value;
}
}
}
var rect = Rectangle(3, 4);
print(rect.area); // 12.0(像访问属性一样调用 getter)
rect.width = 5; // 调用 setter
print(rect.area); // 20.0
💡 getter/setter 的调用方式和普通属性一模一样,外部根本不知道是计算属性还是存储属性。这是封装的艺术。
继承:extends 和 super
class Animal {
String name;
Animal(this.name);
void eat() {
print('$name 在吃东西');
}
}
class Dog extends Animal {
String breed; // 品种
// 调用父类构造函数
Dog(String name, this.breed) : super(name);
// 子类自己的方法
void bark() {
print('$name 在汪汪叫');
}
// 重写父类方法
@override
void eat() {
super.eat(); // 先调用父类的 eat
print('$name 吃得很香');
}
}
var dog = Dog('旺财', '金毛');
dog.eat(); // 旺财 在吃东西 \n 旺财 吃得很香
dog.bark(); // 旺财 在汪汪叫
💡
@override注解是强制的,重写父类方法必须加,不加会有警告。这点比 TS 更严格。
抽象类:abstract
// 抽象类不能实例化
abstract class Animal {
String name;
// 抽象方法:没有实现
void makeSound();
// 具体方法
void sleep() {
print('$name 在睡觉');
}
}
class Cat extends Animal {
Cat(String name) {
this.name = name;
}
@override
void makeSound() {
print('$name 喵喵叫');
}
}
// var animal = Animal(); // ❌ 抽象类不能实例化
var cat = Cat('咪咪'); // ✅
cat.makeSound(); // 咪咪 喵喵叫
接口:implements
Dart 没有 interface 关键字!每个类都隐式定义了一个接口:
class Flyable {
void fly() {
print('在飞');
}
}
// implements:实现接口(必须重新实现所有成员)
class Bird implements Flyable {
@override
void fly() {
print('鸟在飞');
}
}
// extends:继承实现(可以复用代码)
class Plane extends Flyable {
// 不重写也行,直接用父类的 fly()
}
⚠️
implements和extends的区别:
extends:继承实现,可以复用代码implements:实现接口,必须重新实现所有成员
多实现
abstract class Flyable {
void fly();
}
abstract class Swimmable {
void swim();
}
abstract class Runnable {
void run();
}
// 实现多个接口
class Duck implements Flyable, Swimmable, Runnable {
@override
void fly() => print('鸭子飞');
@override
void swim() => print('鸭子游');
@override
void run() => print('鸭子跑');
}
Mixin:多继承的替代方案
Dart 是单继承,但可以用 Mixin 实现”多继承”的效果:
mixin Flying {
void fly() => print('在飞');
}
mixin Swimming {
void swim() => print('在游泳');
}
mixin Running {
void run() => print('在跑');
}
// with 关键字使用 mixin
class Duck with Flying, Swimming, Running {
void quack() => print('嘎嘎叫');
}
var duck = Duck();
duck.fly(); // 在飞
duck.swim(); // 在游泳
duck.run(); // 在跑
duck.quack(); // 嘎嘎叫
💡 Mixin 和
implements的区别:Mixin 是复用代码,implements 是实现接口。
私有成员:下划线
Dart 没有 public/private/protected 关键字,用下划线表示私有:
class Person {
String name; // 公有
String _secret; // 私有(库级别,同一个文件可以访问)
Person(this.name, this._secret);
}
⚠️ 注意:Dart 的私有是库级别的,不是类级别。同一个文件里的其他类可以访问
_secret。
级联运算符:..
这个是 Dart 特色,JS 没有:
class Person {
String name = '';
int age = 0;
void sayHello() => print('我是 $name');
}
var person = Person()
..name = '小明'
..age = 18
..sayHello();
// 等价于
var person2 = Person();
person2.name = '小明';
person2.age = 18;
person2.sayHello();
链式调用,省去了写一堆 person. 的麻烦。
小结一下
面向对象这块内容挺多,重点记住:
- 构造函数语法糖 -
Person(this.name, this.age) - 命名构造函数 -
Person.guest(),代替构造函数重载 - @override 必须加 - 重写父类方法时
- 没有 interface 关键字 - 每个类都是接口
- mixin + with - 实现多继承效果
- 下划线 = 私有 - 库级别的私有
- 级联运算符
..- 链式调用神器