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 真的挺像,主要记住这几点:

  1. switch 没有 fall-through,必须显式 break
  2. 条件必须是 boolif (1) 这种写法直接报错
  3. 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 佬们真得注意 mapwhere 返回的是 Iterable,不是 List!如果需要 List,记得 .toList()

小结一下

函数这块主要记住:

  1. 命名参数 - 用 {} 包裹,required 标记必填,调用时写参数名
  2. 可选参数 - 用 [] 包裹,可以给默认值
  3. where - 就是 JS 的 filter
  4. fold - 就是 JS 的 reduce 带初始值
  5. 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()
}

⚠️ implementsextends 的区别:

  • 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. 的麻烦。

小结一下

面向对象这块内容挺多,重点记住:

  1. 构造函数语法糖 - Person(this.name, this.age)
  2. 命名构造函数 - Person.guest(),代替构造函数重载
  3. @override 必须加 - 重写父类方法时
  4. 没有 interface 关键字 - 每个类都是接口
  5. mixin + with - 实现多继承效果
  6. 下划线 = 私有 - 库级别的私有
  7. 级联运算符 .. - 链式调用神器