<Flutter学习>1,Dart语言入门

181 阅读13分钟

Dart 是一门由 Google 开发的、为客户端优化的编程语言,它的目标是让你能在任何平台上构建快速、美观的用户界面。无论你是想开发移动应用 (Flutter)、Web 应用,还是后端服务,Dart 都能助你一臂之力。

本教程是为那些已经具备一些编程基础的同学设计的。如果你了解变量、循环、函数等基本概念,那么学习 Dart 将会非常顺利。

建议学习资源:

在学习过程中,我强烈建议大家参考 Dart 官方的中文文档,特别是 "语言概览" 或 "Language Tour" 部分,那里有更详尽的解释和示例:

好了,让我们开始吧!


目录

  1. Dart 语言基础变量 (Variables)
  2. 数据类型 (Data Types)
  3. 控制流语句 (Control Flow Statements)
  4. 函数 (Functions)
  5. 面向对象基础 (Object-Oriented Programming - 初步了解)
  6. Null Safety (空安全)
  7. 异步编程 (Asynchronous Programming)
  8. Mixins (混入)
  9. 扩展方法 (Extension Methods)
  10. 错误处理 (Error Handling)

1. Dart 语言基础变量 (Variables)

在 Dart 中,变量是存储数据的容器。声明变量时,你可以使用 var 关键字,Dart 会自动推断其类型。如果你希望明确指定类型,也可以直接使用类型名。

  • var: 编译器会自动推断变量的类型。一旦类型被确定,就不能再改变为其他类型。
  • final: 用于声明一个只能被赋值一次的变量。一旦赋值后,其值不能再改变(运行时常量)。
  • const: 用于声明一个编译时常量。它的值在编译时就必须确定。const 变量本身也是 final 的。
void main() {
  // 使用 var 声明变量,Dart 会推断其类型
  var name = 'Dart'; // 推断为 String
  var year = 2024;   // 推断为 int
  var pi = 3.14;     // 推断为 double

  // 明确指定类型
  String language = 'Dart';
  int version = 3;

  print('Hello, $name! Version: $version, Year: $year, PI: $pi, Language: $language');

  // final 变量
  final String courseName = 'Dart Basics';
  // courseName = 'Advanced Dart'; // 错误!final 变量不能再次赋值

  // const 变量
  const double gravity = 9.8;
  // const currentTime = DateTime.now(); // 错误!DateTime.now() 不是编译时常量
  // gravity = 9.81; // 错误!const 变量不能再次赋值

  print('Course: $courseName, Gravity: $gravity m/s^2');
}

重点:

  • finalconst 都表示不可变,但 const 是编译时常量,其值在编译期间就确定了。如果一个 const 变量的值依赖于运行时计算,那么它不能被声明为 const
  • 类型一旦确定(无论是显式声明还是 var 推断),就不能再赋给不同类型的值。

2. 数据类型 (Data Types)

Dart 是一门强类型语言,但由于类型推断的存在,书写起来可以像动态语言一样简洁。以下是 Dart 中最常用的内置数据类型:

  • 数字 (Numbers):

    • int: 整数值,大小没有限制(取决于内存)。

    • double: 双精度浮点数值,遵循 IEEE 754 标准。 intdouble 都是 num 类型的子类。

    int score = 100;
    double percentage = 99.5;
    num anyNumber = 1;      // 可以是 int
    anyNumber = 1.5;    // 也可以是 double
    
    print('Score: $score, Percentage: $percentage%, Any Number: $anyNumber');
    
  • 字符串 (Strings):

    • String: 表示一系列 UTF-16 编码的字符。

    • 可以使用单引号 '...' 或双引号 "..." 创建字符串。

    • 使用三个单引号或三个双引号可以创建多行字符串。

    • 字符串插值:使用 ${expression} 将表达式的值嵌入字符串中。如果表达式只是一个标识符,可以省略花括号:$identifier

    String greeting = 'Hello';
    String target = "World";
    String message = '$greeting, $target!'; // 字符串插值
    String multiLine = """
    这是
    一个多行
    字符串。
    """;
    
    print(message);
    print(multiLine);
    print('The sum of 5 and 3 is ${5 + 3}.');
    
  • 布尔值 (Booleans):

    • bool: 表示布尔值,只有两个可能的值:truefalse

    bool isLoading = true;
    bool isFinished = false;
    
    if (isLoading) {
      print('Still loading...');
    } else {
      print('Finished!');
    }
    
  • 列表 (Lists, 也称为 Arrays):

    • List: Dart 中的有序集合,类似于其他语言中的数组。

    • 列表的索引从 0 开始。

    List<String> fruits = ['Apple', 'Banana', 'Orange'];
    var numbers = [1, 2, 3, 4, 5]; // 推断为 List<int>
    
    print('First fruit: ${fruits[0]}');
    fruits.add('Mango');
    print('All fruits: $fruits');
    print('Number of fruits: ${fruits.length}');
    
    // 创建一个固定长度的列表,所有元素默认为 null (如果类型可空)
    var fixedList = List<int?>.filled(3, null);
    print('Fixed list: $fixedList');
    fixedList[0] = 10;
    // fixedList.add(4); // 错误!固定长度列表不能添加元素
    
    // 使用 List.generate 创建列表
    var generatedList = List<int>.generate(5, (index) => index * index);
    print('Generated list (squares): $generatedList'); // [0, 1, 4, 9, 16]
    
  • 哈希表 (Maps):

    • Map: 键值对的集合。键和值可以是任何类型的对象。每个键必须是唯一的。

    • 也常被称为字典 (dictionary) 或哈希 (hash)。

    Map<String, String> capitals = {
      'USA': 'Washington D.C.',
      'China': 'Beijing',
      'Japan': 'Tokyo'
    };
    var studentScores = {
      'Alice': 90, // 推断为 Map<String, int>
      'Bob': 85
    };
    
    print('Capital of USA: ${capitals['USA']}');
    studentScores['Charlie'] = 95; // 添加新的键值对
    print('Student scores: $studentScores');
    print('Is Alice in scores? ${studentScores.containsKey('Alice')}');
    
  • (其他类型如 Runes, Symbols):

    • Runes: 用于表示字符串中的 UTF-32 编码字符。通常在需要处理特殊字符(如表情符号)时使用。
    • Symbols: Symbol 对象表示 Dart 程序中声明的运算符或标识符。你可能不会经常直接使用它们,但它们在某些反射相关的 API 中非常重要。
    • 这两个类型我们暂时不深入,同学们可以在进阶学习中了解。

3. 控制流语句 (Control Flow Statements)

控制流语句用于根据条件执行不同的代码块或重复执行某段代码。

  • ifelse:

    int age = 18;
    if (age >= 18) {
      print('Adult');
    } else if (age >= 13) {
      print('Teenager');
    } else {
      print('Child');
    }
    

    注意: Dart 中的条件必须是布尔值,不像某些语言那样可以将数字(如 0 或 1)视为布尔值。

  • for 循环:

    • 标准 for 循环:

      for (int i = 0; i < 5; i++) {
        print('Number: $i');
      }
      
    • for-in 循环:用于遍历可迭代对象(如 ListSet)的元素。

      List<String> colors = ['Red', 'Green', 'Blue'];
      for (String color in colors) {
        print('Color: $color');
      }
      
      Map<String, int> scores = {'Math': 90, 'English': 85};
      for (var key in scores.keys) {
          print('$key: ${scores[key]}');
      }
      for (var value in scores.values) {
          print('Score: $value');
      }
      for (var entry in scores.entries) {
          print('${entry.key}: ${entry.value}');
      }
      

      我们也可以使用 forEach 方法,它通常与匿名函数(后面会讲到)结合使用:

      colors.forEach((color) {
        print('Color from forEach: $color');
      });
      
  • whiledo-while 循环:

    • while 循环:先判断条件,条件为真则执行循环体。

      int count = 0;
      while (count < 3) {
        print('While count: $count');
        count++;
      }
      
    • do-while 循环:先执行一次循环体,然后再判断条件,条件为真则继续执行。

      int num = 0;
      do {
        print('Do-while num: $num');
        num++;
      } while (num < 3);
      
  • breakcontinue:

    • break: 立即跳出整个循环。

    • continue: 跳过当前迭代中 continue 之后的代码,直接开始下一次迭代。

    for (int i = 0; i < 10; i++) {
      if (i == 5) {
        break; // 当 i 等于 5 时,跳出循环
      }
      if (i % 2 != 0) {
        continue; // 如果 i 是奇数,跳过本次迭代的 print
      }
      print('Even number: $i');
    }
    
  • switchcase:

    • switch 语句比较一个整数、字符串或编译时常量与 case 子句中的值。

    • 每个非空的 case 子句通常以 break 语句结束。也可以使用 continue (配合标签), throwreturn

    • 如果没有任何 case 匹配,则执行 default 子句(如果存在)。

    • Dart 2.19 之后,switch 支持更强大的模式匹配功能,但这里我们先看基础用法。

    String command = 'OPEN';
    switch (command) {
      case 'OPEN':
        print('Opening file...');
        break; // 非常重要,否则会 "fall-through" 到下一个 case (在 Dart 中,除非 case 为空,否则不允许隐式 fall-through)
      case 'CLOSE':
        print('Closing file...');
        break;
      case 'SAVE':
        print('Saving file...');
        break;
      default:
        print('Unknown command.');
    }
    
    // Dart 的 switch 不允许隐式 "fall-through"
    // 如果你想让一个 case 执行完后继续执行下一个 case 的代码,需要显式使用标签和 continue (较少用)
    // 或者如果 case 的代码块为空,则会自动 fall-through 到下一个非空 case
    int value = 1;
    switch (value) {
        case 0:
        case 1: // value 为 0 或 1 都会执行这里的代码
            print("Value is 0 or 1");
            break;
        case 2:
            print("Value is 2");
            break;
        default:
            print("Other value");
    }
    

4. 函数 (Functions)

函数是一段可重复使用的代码块,用于执行特定的任务。Dart 是一门真正的面向对象语言,所以即使是函数也是对象,其类型是 Function

  • 函数的定义与调用:

    // 定义一个函数,指定返回类型 (void 表示不返回任何值)
    void greet(String name) {
      print('Hello, $name!');
    }
    
    // 定义一个带返回值的函数
    int add(int a, int b) {
      return a + b;
    }
    
    void main() {
      greet('Alice'); // 调用 greet 函数
      int result = add(5, 3); // 调用 add 函数并获取返回值
      print('Sum: $result');
    }
    
  • 函数参数:

    Dart 提供了灵活的参数定义方式:

    • 必选参数 (Required Positional Parameters): 参数必须按顺序传递。

      void describe(String name, int age) {
        print('$name is $age years old.');
      }
      // describe('Bob'); // 错误,缺少 age 参数
      // describe(30, 'Bob'); // 错误,参数顺序和类型不匹配
      describe('Bob', 30);
      
    • 可选命名参数 (Optional Named Parameters):

      使用 {} 包裹参数。调用时通过 parameterName: value 的形式指定。

      默认情况下,命名参数是可选的,如果不传递,它们的值为 null(除非它们是不可空类型且没有默认值,这时必须提供或标记为 required)。

      你可以使用 required 关键字使命名参数变为必传。

      void printInfo({String? name, int? age}) { // name 和 age 可空
        print('Name: ${name ?? "N/A"}, Age: ${age ?? "N/A"}');
      }
      
      void printUserDetails({required String username, String? email}) {
          print('Username: $username, Email: ${email ?? "No email"}');
      }
      
      void main() {
        printInfo(name: 'Charlie', age: 25);
        printInfo(age: 30); // name 会是 null
        printInfo();       // name 和 age 都会是 null
      
        printUserDetails(username: 'dave');
        printUserDetails(username: 'eve', email: 'eve@example.com');
        // printUserDetails(email: 'test@example.com'); // 错误,username 是 required 的
      }
      
    • 可选位置参数 (Optional Positional Parameters):

      使用 [] 包裹参数。调用时按顺序传递,可以省略。

      String say(String from, String msg, [String? device]) {
        var result = '$from says $msg';
        if (device != null) {
          result = '$result with $device';
        }
        return result;
      }
      
      void main() {
        print(say('John', 'Hello')); // device 为 null
        print(say('Jane', 'Hi', 'iPhone'));
      }
      
    • 默认参数值 (Default Parameter Values):

      可选参数(命名或位置)都可以有默认值,使用 = 指定。默认值必须是编译时常量。

      如果参数是可空的,并且没有提供值,它将是 null。如果它有默认值,则会使用默认值。

      void setVolume(int volume, {int min = 0, int max = 100}) {
        print('Setting volume to $volume, min: $min, max: $max');
      }
      
      void enableFlags({bool bold = false, bool hidden = false}) {
        print('Bold: $bold, Hidden: $hidden');
      }
      
      void main() {
        setVolume(50); // min: 0, max: 100 (使用默认值)
        setVolume(70, max: 120); // min: 0 (使用默认值), max: 120
      
        enableFlags(bold: true); // bold: true, hidden: false (使用默认值)
      }
      

      注意: 必选参数不能有默认值。

  • 返回值 (return):

    • 所有函数都有返回值。如果未指定 return 语句,或者 return;,则函数隐式返回 null
    • 如果函数声明了返回类型(如 int),则必须返回该类型的值(或 null,如果返回类型可空 int?)。
    • 如果函数声明为 void,它不能有 return <value>; 语句,但可以有 return;
  • 匿名函数 (Anonymous Functions / Lambdas / Closures):

    • 没有名字的函数。通常用于传递给其他函数或赋值给变量。

    • 语法: (parameterList) { statements; }(parameterList) => expression

    • 闭包 (Closure):匿名函数可以捕获其词法作用域中的变量(即使函数在其原始作用域之外执行)。

    void main() {
      var fruits = ['apple', 'banana', 'orange'];
    
      // 使用匿名函数作为 forEach 的参数
      fruits.forEach((fruit) {
        print(fruit.toUpperCase());
      });
    
      // 将匿名函数赋值给变量
      var multiply = (int a, int b) {
        return a * b;
      };
      print('Product: ${multiply(4, 5)}');
    
      // 闭包示例
      Function makeAdder(int addBy) {
        return (int i) => addBy + i; // 这个匿名函数捕获了 addBy
      }
    
      var add2 = makeAdder(2); // addBy is 2
      var add4 = makeAdder(4); // addBy is 4
    
      print(add2(3)); // 5 (2 + 3)
      print(add4(3)); // 7 (4 + 3)
    }
    
  • 箭头函数 (=>):

    • 如果函数体只包含一个表达式,可以使用箭头语法,也称为 "fat arrow" 语法。

    • => expression; 等价于 { return expression; }

    int subtract(int a, int b) => a - b; // 等价于 { return a - b; }
    
    void printMessage(String message) => print('Message: $message'); // 返回 void (隐式返回 null)
    
    void main() {
      print('Difference: ${subtract(10, 4)}');
      printMessage('Dart is concise!');
    }
    
  • 词法作用域 (Lexical Scope):

    • Dart 是一种词法作用域语言,这意味着变量的作用域在代码编写时就已静态确定,而不是在运行时确定。

    • 内层作用域可以访问外层作用域的变量,但反之不行。

    • 花括号 {} 定义了一个新的作用域。

    String topLevelVariable = 'Top';
    
    void main() {
      String mainVariable = 'Main';
    
      void innerFunction() {
        String innerVariable = 'Inner';
        print(topLevelVariable); // 可以访问
        print(mainVariable);     // 可以访问
        print(innerVariable);    // 可以访问
      }
    
      innerFunction();
      // print(innerVariable); // 错误!innerVariable 在 innerFunction 作用域内
    }
    

5. 面向对象基础 (Object-Oriented Programming - 初步了解)

Dart 是一门纯粹的面向对象语言,每个值都是一个对象,包括数字、函数和 null。所有的对象都继承自 Object 类。

  • 类 (Classes) 与对象 (Objects):

    • 类 (class) 是创建对象的蓝图或模板。它定义了对象的属性(实例变量)和行为(方法)。

    • 对象 (Object) 是类的一个实例。

    // 定义一个类
    class Dog {
      String name; // 实例变量
      int age;     // 实例变量
    
      // 构造函数 (后面详细讲)
      Dog(this.name, this.age);
    
      // 方法
      void bark() {
        print('$name says Woof!');
      }
    
      void displayAge() {
        print('$name is $age years old.');
      }
    }
    
    void main() {
      // 创建 Dog 类的对象 (实例化)
      // 'new' 关键字在 Dart 2 中是可选的
      var myDog = Dog('Buddy', 3);
      Dog anotherDog = Dog('Lucy', 5);
    
      myDog.bark(); // 调用对象的方法
      myDog.displayAge();
    
      anotherDog.bark();
      anotherDog.displayAge();
    
      print(myDog.name); // 访问对象的实例变量
    }
    
  • 构造函数 (Constructors):

    • 构造函数是用于创建和初始化对象的特殊方法。它的名称与类名相同。

    • 默认构造函数 (Default Constructor):

      如果你不声明构造函数,Dart 会提供一个默认的无参构造函数(前提是父类也有可访问的无参构造函数)。

      最常见的构造函数形式是直接初始化实例变量,可以使用 this. 语法糖:

      class Point {
        double x;
        double y;
      
        // 语法糖:直接将参数赋值给同名实例变量
        Point(this.x, this.y);
      }
      // 等价于:
      // class Point {
      //   double x;
      //   double y;
      //   Point(double x, double y) {
      //     this.x = x;
      //     this.y = y;
      //   }
      // }
      
      void main() {
        var p1 = Point(10.0, 20.0);
        print('Point: (${p1.x}, ${p1.y})');
      }
      
    • 命名构造函数 (Named Constructors):

      一个类可以有多个命名构造函数,以提供不同的对象创建方式。

      语法:ClassName.identifierName()

      class Rectangle {
        double width;
        double height;
      
        // 主构造函数
        Rectangle(this.width, this.height);
      
        // 命名构造函数:创建一个正方形
        Rectangle.square(double side)
            : width = side,
              height = side; // 初始化列表,在构造函数体执行前赋值
      
        // 命名构造函数:从一个 Map 创建
        Rectangle.fromMap(Map<String, double> map)
            : width = map['width']!,  // 使用 ! 断言 map['width'] 不为 null (后面讲 Null Safety)
              height = map['height']!;
      
        double get area => width * height;
      }
      
      void main() {
        var rect1 = Rectangle(10, 20);
        var square = Rectangle.square(15);
        var rectFromMap = Rectangle.fromMap({'width': 5, 'height': 8});
      
        print('Rect1 area: ${rect1.area}');
        print('Square area: ${square.area}');
        print('RectFromMap area: ${rectFromMap.area}');
      }
      
    • 工厂构造函数 (Factory Constructors - 初步了解):

      使用 factory 关键字声明。工厂构造函数不总是创建其类的新实例。例如,它可以返回一个缓存中的实例,或者返回一个子类型的实例。

      工厂构造函数内部不能使用 this 关键字访问实例成员,因为它可能不创建新实例。

      class Logger {
        final String name;
        static final Map<String, Logger> _cache = <String, Logger>{};
      
        // 私有命名构造函数,防止外部直接实例化
        Logger._internal(this.name);
      
        // 工厂构造函数
        factory Logger(String name) {
          if (_cache.containsKey(name)) {
            return _cache[name]!;
          } else {
            final logger = Logger._internal(name);
            _cache[name] = logger;
            return logger;
          }
        }
      
        void log(String msg) {
          print('$name: $msg');
        }
      }
      
      void main() {
        var logger1 = Logger('UI');
        var logger2 = Logger('Network');
        var logger3 = Logger('UI'); // 会从缓存中返回 logger1
      
        logger1.log('Button clicked');
        logger2.log('Request sent');
        logger3.log('Dialog shown');
      
        print(identical(logger1, logger3)); // true,它们是同一个对象
      }
      

      工厂构造函数是一个比较高级的概念,我们先初步了解即可。

  • 方法 (Methods) 和实例变量 (Instance Variables):

    • 实例变量 (Instance Variables): 类中定义的变量,属于类的每个实例。

    • 方法 (Methods): 类中定义的函数,用于操作对象的实例变量或执行其他操作。方法可以访问 this 和实例变量。

    • Getter 和 Setter: 特殊的方法,用于读取和写入对象的属性。可以使用 getset 关键字定义。

    class Circle {
      double radius;
    
      Circle(this.radius);
    
      // Getter for diameter
      double get diameter => radius * 2;
    
      // Setter for diameter (updates radius)
      set diameter(double newDiameter) {
        if (newDiameter > 0) {
          radius = newDiameter / 2;
        }
      }
    
      // Method
      double calculateArea() {
        return 3.14159 * radius * radius;
      }
    }
    
    void main() {
      var c = Circle(5.0);
      print('Radius: ${c.radius}');
      print('Area: ${c.calculateArea()}');
      print('Diameter (getter): ${c.diameter}'); // 使用 getter
    
      c.diameter = 12; // 使用 setter
      print('New Radius after setting diameter: ${c.radius}');
      print('New Area: ${c.calculateArea()}');
    }
    
  • this 关键字:

    • 在类的方法或构造函数中,this 关键字指向当前对象的实例。

    • 通常在参数名与实例变量名冲突时使用,或者在需要将当前实例传递给其他方法时使用。

    class MyClass {
      String name;
    
      MyClass(String name) {
        this.name = name; // this.name 是实例变量, name 是参数
      }
    
      void printName() {
        print(this.name); // this 可选,因为没有歧义
      }
    
      MyClass getSelf() {
          return this; // 返回当前实例
      }
    }
    
  • 继承 (Inheritance) - (extends):

    • 继承允许一个类(子类或派生类)获取另一个类(父类或基类)的属性和方法。

    • 使用 extends 关键字实现继承。

    • 子类可以重写父类的方法,添加新的方法和属性。

    • Dart 是单继承语言,即一个类只能直接继承一个父类。

    class Animal {
      String name;
      Animal(this.name);
    
      void makeSound() {
        print('Animal sound');
      }
    }
    
    class Cat extends Animal {
      // 子类构造函数必须调用父类的构造函数
      // 使用 super(...) 来调用父类构造函数
      Cat(String name) : super(name);
    
      // 覆盖父类的方法
      @override // @override 注解表示这个方法是重写父类的方法
      void makeSound() {
        print('$name says Meow!');
      }
    
      void purr() {
        print('$name is purring.');
      }
    }
    
    void main() {
      var animal = Animal('Generic Animal');
      var cat = Cat('Whiskers');
    
      animal.makeSound(); // Animal sound
      cat.makeSound();    // Whiskers says Meow! (调用的是 Cat 类重写的方法)
      cat.purr();         // Whiskers is purring.
      // animal.purr();   // 错误!Animal 类没有 purr 方法
    }
    
  • 覆盖成员 (@override):

    • 当子类提供一个与父类中同名、同参数的方法时,称为方法覆盖(或重写)。
    • 使用 @override 注解是一个好习惯,它可以让编译器检查你是否真的覆盖了一个父类成员,并且能更清晰地表达你的意图。
  • super 关键字 (初步了解):

    • 用于调用父类的构造函数或方法。

    • super(): 调用父类的无名构造函数。

    • super.namedConstructor(): 调用父类的命名构造函数。

    • super.methodName(): 调用父类的方法。

    class Vehicle {
      String model;
      Vehicle(this.model) {
        print('Vehicle constructor: $model');
      }
      void start() {
        print('$model Vehicle started.');
      }
    }
    
    class Car extends Vehicle {
      String color;
      // 调用父类的构造函数,并初始化自己的成员
      Car(String model, this.color) : super(model) {
          print('Car constructor: $model, Color: $color');
      }
    
      @override
      void start() {
        super.start(); // 调用父类的 start 方法
        print('Car ($model, $color) specific start sequence.');
      }
    }
    
    void main() {
        var myCar = Car('SedanX', 'Red');
        myCar.start();
        // 输出:
        // Vehicle constructor: SedanX
        // Car constructor: SedanX, Color: Red
        // SedanX Vehicle started.
        // Car (SedanX, Red) specific start sequence.
    }
    
  • (抽象类 abstract class 和接口 implements - 可作为进阶了解):

    • 抽象类 (abstract class): 不能被实例化的类,通常用作基类,可以包含抽象方法(没有实现的方法,子类必须实现)。
    • 接口 (implements): Dart 没有专门的 interface 关键字。任何类都可以作为接口。当一个类 implements 另一个类(接口)时,它必须实现该接口的所有方法和 getter(除非实现类本身是抽象的)。
    • 这两个概念在构建大型、模块化应用时非常重要,我们将在进阶课程中详细讨论。

6. Null Safety (空安全)

Null Safety 是 Dart 语言的一个重要特性,旨在通过在类型系统中区分可空类型和不可空类型,来帮助开发者在编译时就发现潜在的空引用错误 (Null Pointer Exceptions)。

  • 理解 Null Safety 的重要性:

    空引用是许多编程语言中常见的错误来源。Null Safety 通过强制你在编码时就考虑变量是否可能为 null,从而减少运行时因空引用导致的程序崩溃。

  • 可空类型 (?):

    • 默认情况下,所有类型都是不可空的。如果你想让一个变量可以持有 null 值,你需要在类型声明后面加上 ?

    int a = 5; // 不可空,a 不能是 null
    // a = null; // 编译错误
    
    String? name = 'Alice'; // 可空,name 可以是 'Alice' 或 null
    name = null; // 合法
    
    // 如果你尝试访问可空类型的方法或属性,而不先检查它是否为 null,编译器会报错
    // print(name.length); // 错误!name 可能为 null
    
  • 非空断言 (!):

    • 如果你非常确定一个可空表达式的值在运行时不会是 null,你可以使用后缀操作符 ! 来断言它非空。

    • 警告: 如果你在一个值为 null 的表达式上使用 !,你的程序会在运行时抛出异常。所以请谨慎使用。

    String? getNullableString() {
      return "Hello"; // 或者 return null;
    }
    
    void main() {
      String? maybeString = getNullableString();
      if (maybeString != null) {
        // 在检查后,编译器知道 maybeString 在这个分支里不为 null (类型提升)
        print(maybeString.length);
      }
    
      // 如果你非常确定它不为 null (例如,基于某些外部逻辑)
      String notNullString = maybeString!; // 断言 maybeString 不为 null
      print(notNullString.length); // 如果 maybeString 实际为 null,这里会抛出运行时异常
    }
    
  • 类型提升 (Type Promotion):

    • Dart 的流程分析器 (flow analyzer) 非常智能。如果你对一个可空变量进行了 null 检查 (例如 if (variable != null)), 在该检查的作用域内,编译器会自动将该变量提升 (promote) 为其对应的不可空类型。

    void printStringLength(String? str) {
      if (str != null) {
        // 在这个代码块中,str 被提升为 String (不可空)
        print('Length: ${str.length}');
      } else {
        print('String is null.');
      }
      // print(str.length); // 错误!在这里 str 仍然是 String?
    }
    
  • late 关键字:

    • late 关键字有几个用途,主要用于处理非空变量的延迟初始化:

      1. 声明一个非空变量,但它的初始化会延迟到首次使用时才进行。 如果在初始化前访问它,会抛出运行时错误。

      2. 用于懒加载实例变量。

    class MyService {
      late String _data; // 声明一个非空的 _data,但延迟初始化
    
      void _initializeData() {
        print("Initializing data...");
        _data = "Fetched Data";
      }
    
      String getData() {
        // 假设我们在这里确保了 _initializeData 会被调用,或者 _data 会被赋值
        if (!this::_data.isInitialized) { // 检查 late 变量是否已初始化 (需要 Dart 2.12+)
             _initializeData();
        }
        return _data;
      }
    }
    
    // 另一个例子: 懒加载
    class HeavyComputation {
        late String result = _compute(); // result 会在首次访问时计算
    
        String _compute() {
            print("Performing heavy computation...");
            // 模拟耗时操作
            for(int i=0; i<1000000000; i++) {}
            return "Computation Done";
        }
    }
    
    void main() {
      MyService service = MyService();
      // print(service._data); // 如果 _initializeData 还没被调用,这里会抛出 LateInitializationError
      print(service.getData()); // getData 内部会确保初始化
    
      HeavyComputation hc = HeavyComputation();
      print("HeavyComputation instance created.");
      // _compute() 还没有执行
      print(hc.result); // 此时 _compute() 执行,然后返回结果
      print(hc.result); // 再次访问,直接返回已计算的结果,_compute() 不会再次执行
    }
    

    late 关键字对于处理那些你知道在运行时首次使用前一定会被初始化的非空变量非常有用,例如 Flutter 中的 initState


7. 异步编程 (Asynchronous Programming)

现代应用经常需要执行一些耗时操作,比如网络请求、文件读写等。如果这些操作在主线程同步执行,会导致用户界面卡顿。异步编程允许这些操作在后台执行,完成后再通知主线程。

  • Future 对象:

    • Future<T> 对象代表一个异步操作的最终结果,这个结果的类型是 T

    • 一个 Future 会在某个时刻完成,并提供一个值(如果操作成功)或一个错误(如果操作失败)。

    • 你可以使用 .then() 来注册一个回调,当 Future 完成时,该回调会被执行。使用 .catchError() 来处理可能发生的错误。

    Future<String> fetchData() {
      // 模拟一个网络请求,2秒后返回数据
      return Future.delayed(Duration(seconds: 2), () {
        // return 'Data fetched successfully!';
        throw Exception('Failed to fetch data!'); // 模拟错误
      });
    }
    
    void main() {
      print('Fetching data...');
      fetchData().then((value) {
        print(value); // Future 成功完成时执行
      }).catchError((error) {
        print('Error: $error'); // Future 失败时执行
      }).whenComplete(() {
        print('Data fetching operation complete.'); // 无论成功或失败都会执行
      });
      print('Doing other work while data is being fetched...');
    }
    
  • asyncawait

    • asyncawait 是 Dart 提供的用于简化异步代码书写的关键字,让异步代码看起来更像同步代码。

    • async: 将一个函数标记为异步函数。异步函数的返回类型通常是 Future<T>。如果异步函数不返回任何有意义的值,其返回类型是 Future<void>

    • await: 只能在 async 函数内部使用。它会暂停当前 async 函数的执行,等待其后的 Future 完成。一旦 Future 完成,await 会返回 Future 的结果 (如果是错误则会抛出)。

    Future<String> downloadFile(String url) {
      print('Starting download: $url');
      return Future.delayed(Duration(seconds: 3), () {
        if (url.isEmpty) {
          throw Exception('URL cannot be empty');
        }
        return 'Content of $url';
      });
    }
    
    // 使用 async 和 await
    Future<void> processDownloads() async {
      print('Process started.');
      try {
        String file1Content = await downloadFile('http://example.com/file1.txt');
        print('File 1 downloaded: ${file1Content.length} bytes');
    
        String file2Content = await downloadFile('http://example.com/file2.txt');
        print('File 2 downloaded: ${file2Content.length} bytes');
    
        // String file3Content = await downloadFile(''); // 模拟一个错误
        // print('File 3 downloaded: ${file3Content.length} bytes');
    
      } catch (e) {
        print('An error occurred during download: $e');
      } finally {
        print('All download attempts finished.');
      }
    }
    
    void main() {
      print('Main function started.');
      processDownloads(); // 调用异步函数
      print('Main function continues execution...'); // 这会先于 processDownloads 内部的 await 后的代码执行
    }
    

    使用 async/await 可以让异步代码的逻辑更清晰,更容易理解和维护。

  • (Streams - 可作为进阶了解):

    • Stream 是一种处理异步事件序列的方式。与 Future 代表单个异步结果不同,Stream 可以随着时间的推移发出零个或多个值或错误。
    • 例如,用户输入事件、文件持续读取、WebSocket 数据流等都可以用 Stream 来表示。
    • 你可以使用 await for 循环来监听 Stream 发出的事件,或者使用 listen() 方法。
    • Stream 是响应式编程中的一个核心概念,我们会在进阶时详细学习。

8. Mixins (混入)

Mixins 是一种在多个类层次结构中复用类代码的方式。当你想为一个类添加某些行为,但又不想通过继承(因为 Dart 是单继承)或者不想让这个行为成为类定义的核心部分时,Mixin 非常有用。

  • 使用 mixin关键字定义 Mixin:

    Mixin 本身不能被实例化,也不能有构造函数。

    mixin Piloting {
      int astronauts = 1;
    
      void pilot() {
        print('Glide through the stars');
      }
    }
    
    mixin FuelSystem {
      void fillFuel() {
        print('Filling fuel tank...');
      }
      void checkFuelLevel() {
        print('Fuel level is good.');
      }
    }
    
  • 使用 with 关键字将 Mixin 应用到类:

    class Spacecraft {
      String name;
      Spacecraft(this.name);
    
      void launch() {
        print('$name launching...');
      }
    }
    
    // Spaceship 类继承自 Spacecraft,并混入了 Piloting 和 FuelSystem 的行为
    class Spaceship extends Spacecraft with Piloting, FuelSystem {
      Spaceship(String name) : super(name);
    
      void fly() {
        print('$name with $astronauts astronauts is flying!');
        pilot(); // 来自 Piloting mixin
        checkFuelLevel(); // 来自 FuelSystem mixin
      }
    }
    
    class OrbitalModule with FuelSystem { // 也可以不继承任何类,直接使用 mixin
        void dock() {
            print("Docking module");
            fillFuel();
        }
    }
    
    void main() {
      var enterprise = Spaceship('Enterprise');
      enterprise.launch();
      enterprise.fillFuel();
      enterprise.fly();
    
      var fuelPod = OrbitalModule();
      fuelPod.dock();
    }
    
  • Mixin 的作用和使用场景 (代码复用):

    • 代码复用: Mixin 的核心目的是共享行为。你可以定义一组通用的方法和属性,然后将它们混入到需要的类中。

    • 避免继承的限制: Dart 是单继承的,Mixin 提供了一种“横向”组合行为的方式,而不是“纵向”的继承。

    • 特定能力的赋予: 例如,你可以创建一个 Serializable mixin 来为类添加序列化/反序列化的能力,一个 Loggable mixin 添加日志记录能力等。

    • Mixin 可以通过 on 关键字来限制它可以被混入的类的类型(即要求宿主类是某个特定类或其子类)。这允许 Mixin 调用宿主类定义的方法。

    abstract class Performer {
        void performAction();
    }
    
    mixin Musical on Performer { // 这个 Mixin 只能被 Performer 或其子类使用
      void playInstrument() {
        print('Playing music...');
        performAction(); // 可以调用 Performer 中定义的方法
      }
    }
    
    class Dancer extends Performer {
      @override
      void performAction() {
        print('Dancing gracefully!');
      }
    }
    
    class MusicianDancer extends Dancer with Musical {
      // MusicianDancer 继承自 Dancer (Dancer 是 Performer), 所以可以混入 Musical
    }
    
    void main() {
      var md = MusicianDancer();
      md.playInstrument();
      // 输出:
      // Playing music...
      // Dancing gracefully!
    }
    

9. 扩展方法 (Extension Methods)

扩展方法允许你为现有的库(甚至是你不拥有的库,比如 Dart 核心库中的类)添加新的功能,而无需更改库的源代码或创建子类。

  • extension 关键字的用法:

    语法:extension on { // members }

    是可选的,但如果省略,就无法在导入时显式控制其可见性 (show/hide)。

    // 为 String 类添加一个将字符串转换为整数的扩展方法
    extension StringParsing on String {
      int? toIntOrNull() {
        return int.tryParse(this); // 'this' 指向 String 实例本身
      }
    
      String capitalizeFirstLetter() {
        if (this.isEmpty) return this;
        return '${this[0].toUpperCase()}${this.substring(1)}';
      }
    }
    
    // 也可以不给扩展命名 (但不推荐,除非在同一个库中使用)
    // extension on List<int> {
    //   int sum() {
    //     return this.fold(0, (prev, element) => prev + element);
    //   }
    // }
    
    
    void main() {
      String numberStr = '123';
      String text = "hello world";
      String empty = "";
    
      int? parsedNum = numberStr.toIntOrNull();
      print('Parsed number: $parsedNum'); // Parsed number: 123
    
      String? invalidStr = 'abc';
      print('Parsed invalid: ${invalidStr.toIntOrNull()}'); // Parsed invalid: null
    
      print('Capitalized: ${text.capitalizeFirstLetter()}'); // Capitalized: Hello world
      print('Capitalized empty: "${empty.capitalizeFirstLetter()}"'); // Capitalized empty: ""
    
      List<int> numbers = [1, 2, 3, 4, 5];
      // 如果上面那个匿名的 List 扩展被定义了,可以这样调用:
      // print('Sum of numbers: ${numbers.sum()}');
    }
    
  • 使用场景:

    • 当你觉得某个类缺少一个有用的方法时,可以为其添加。
    • 提高代码的可读性,使调用更自然。
    • 例如,为 DateTime 添加格式化方法,为 List 添加更复杂的集合操作等。
    • 注意: 扩展方法是静态解析的。这意味着它们是根据接收者的静态类型来调用的,而不是动态类型。如果一个 dynamic 类型的变量恰好持有一个 String,你不能直接调用为 String 定义的扩展方法,除非你先将其转换为 String

10. 错误处理 (Error Handling)

在程序执行过程中,可能会发生各种预料之外的情况,比如文件未找到、网络连接失败、无效输入等。错误处理机制允许你优雅地捕获和处理这些异常情况,防止程序崩溃。

  • try-catch 语句:捕获和处理异常

    • 将可能抛出异常的代码块放在 try 块中。

    • 使用 catch 块来捕获并处理异常。

    void main() {
      try {
        int result = 10 ~/ 0; // 整数除法,除以0会抛出 IntegerDivisionByZeroException
        print('Result: $result'); // 这行不会执行
      } catch (e) {
        // 'e' 是捕获到的异常对象
        print('An error occurred: $e');
      }
      print('Program continues...');
    }
    

    你还可以获取堆栈跟踪信息(StackTrace),这对于调试非常有用:

    try {
      // ... some code that might throw ...
      throw FormatException('Invalid format');
    } catch (e, s) { // s 是 StackTrace 对象
      print('Exception: $e');
      print('Stack trace:\n$s');
    }
    
  • on 关键字:捕获特定类型的异常

    • 如果你想针对不同类型的异常执行不同的处理逻辑,可以使用 on 关键字。

    void main() {
      String input = "abc";
      try {
        // int value = int.parse(input); // FormatException
        // print(value);
        var list = <int>[];
        print(list[0]); // RangeError
      } on FormatException catch (e) {
        print('Caught a FormatException: $e');
        print('Input string "$input" is not a valid integer.');
      } on RangeError catch (e) {
        print('Caught a RangeError: $e');
        print('Accessing an invalid index.');
      } catch (e) {
        // 通用的 catch 块,捕获其他未被 on 子句捕获的异常
        print('Caught some other exception: $e');
      }
    }
    

    on 关键字可以不带 catch (e),如果你不需要异常对象本身。

  • finally 关键字:确保某些代码无论是否发生异常都会执行

    • finally 块中的代码总是在 try 块执行完毕后执行,无论是否发生异常,也无论异常是否被捕获。

    • 通常用于释放资源,如关闭文件、取消网络连接等。

    void main() {
      try {
        print('Trying to perform an operation...');
        // throw Exception('Something went wrong!');
        print('Operation successful.');
      } catch (e) {
        print('Caught exception: $e');
      } finally {
        print('Finally block executed. Cleaning up resources...');
      }
      print('After try-catch-finally.');
    }
    
  • throw 关键字:抛出异常

    • 你可以使用 throw 关键字显式地抛出一个异常。你可以抛出预定义的异常(如 FormatException, ArgumentError),或者自定义异常类。

    class InsufficientFundsException implements Exception {
      final String message;
      InsufficientFundsException(this.message);
    
      @override
      String toString() => 'InsufficientFundsException: $message';
    }
    
    void withdraw(double amount, double balance) {
      if (amount <= 0) {
        throw ArgumentError('Amount to withdraw must be positive.');
      }
      if (amount > balance) {
        throw InsufficientFundsException('Cannot withdraw $amount. Balance is only $balance.');
      }
      print('Withdrawing $amount...');
      // ... 实际的取款逻辑 ...
    }
    
    void main() {
      try {
        withdraw(200, 100);
      } on ArgumentError catch (e) {
        print('Argument error: $e');
      } on InsufficientFundsException catch (e) {
        print('Funds error: $e');
      } catch (e) {
        print('Unknown error: $e');
      }
    }
    

我们已经完成了 Dart 语言基础核心概念的学习。从变量、数据类型到控制流、函数,再到面向对象编程的初步认识、空安全、异步编程、Mixin、扩展方法以及错误处理,这些都是构建 Dart 应用的基石。

当然,Dart 的世界远不止这些。还有很多高级特性和库等待我们去探索,比如集合的深入使用、Stream 的高级操作、Dart 的并发模型 (Isolates)、元数据 (Annotations) 等等。