Flutter面试:Dart基础1

221 阅读7分钟

文章目录

Dart中dynamic,var,Object三者的区别?

一句话总结

  • dynamic:动态类型变量。编译时不进行类型检查,可动态赋值任意类型。运行时才会校验,可能导致运行时错误。
  • var:不是类型,而是关键字。一旦推断为某类型,就不能再赋其他类型的值。适合局部变量声明。
  • Object :所有类型都继承自Object,编译时严格检查;要用更具体的方法需要 is/as 进行显式的向下转型才能用。

代码演示

dynamic:动态类型变量

dynamic value = "dart";
value = 3.14; // 合法:可改为double类型
value.toUpperCase(); // 编译不报错,运行时错误:double无该方法
value.toString(); // 合法:double有该方法
value.unknownMethod(); // 编译不报错,运行时抛出错误

var:类型推断变量

var x = 'hi';  // 推断为 String
// x = 42;        // ❌ 编译期报错:不能把 int 赋给 String
x = 'hello';

var y;         // 没有初始化器 => 推断为 dynamic
y = 'hi';      // ✅
y = 42;        // ✅

Object:所有类型的根类

Object obj = "flutter";
// obj.length; // 编译错误:Object无length属性
(obj as String).length; // 合法:通过类型转换后可调用String方法
obj = 123; // 合法:int是Object的子类
obj.toString(); // 合法:Object有该方法


Dart中const和final的区别?

一句话总结

  • const:编译期常量,在编译时就必须能确定值,且不可修改。
  • final:运行时常量,只能赋值一次。值在运行时确定,之后不可再修改。

代码演示

const:编译时常量
void main() {
  // 合法:字面量是编译时常量
  const maxCount = 100;
  const pi = 3.14159;
  const greeting = "Hello";

  // 合法:const变量可由其他const变量计算而来
  const double maxRadius = maxCount * pi;

  // 非法:运行时才能确定的值不能用于const
  // const currentTime = DateTime.now(); // 编译错误
}

// 类中使用:需声明为static const(类级别的编译时常量)
class Config {
  static const apiUrl = "https://api.example.com";
}
final:运行时常量
void main() {
  // 局部变量
  final name = "Flutter"; // 类型推断为String
  // name = "Dart"; // 编译错误:final变量不可重新赋值

  // 运行时确定值(合法)
  final currentTime = DateTime.now(); // 每次运行时值不同
}

// 类成员
class Person {
  final String id;
  // 可在构造函数中初始化
  Person(this.id);
}

进阶用法:

const的不可变性是深度的,而final仅保证变量引用不可变,不限制对象内部状态:

// final变量:引用不可变,但对象内容可修改
final list = [1, 2, 3];
list.add(4); // 合法:list引用未变,仅修改内部元素
// list = [4, 5, 6]; // 非法:不能重新赋值

// const变量:引用和对象内容均不可修改
const constList = [1, 2, 3];
// constList.add(4); // 编译错误:const对象不可修改
// constList = [4, 5, 6]; // 编译错误:不能重新赋值

const变量在内存中只有一份实例,相同值的const变量会共享同一内存地址。‌而final变量每次都会创建新实例,即使值相同

const a = [1, 2];
const b = [1, 2];
print(identical(a, b)); // true,相同实例
final c = [1, 2];
final d = [1, 2];
print(identical(c, d)); // false,不同实例
const修饰的构造函数:
  1. 必须用于创建不可变对象,即类的所有实例变量必须是final类型
  2. 构造函数体必须为空(使用;结束,不能有{}
  3. 如果类有父类,父类也必须有常量构造函数
  4. 实例化时必须使用const关键字(除非在常量上下文中)
  5. 内部引用对象的类也要const 构造

正确示例:

// 正确的常量构造函数示例
class ImmutablePoint {
  // 所有实例变量必须是final
  final int x;
  final int y;

  // 常量构造函数,使用const修饰且没有函数体
  const ImmutablePoint(this.x, this.y);

  @override
  String toString() => 'Point($x, $y)';
}

// 继承情况下的常量构造函数
class Immutable3DPoint extends ImmutablePoint {
  final int z;

  // 父类必须有常量构造函数,且这里必须调用父类的常量构造函数
  const Immutable3DPoint(int x, int y, this.z) : super(x, y);

  @override
  String toString() => 'Point($x, $y, $z)';
}

void main() {
  // 使用const创建实例
  const point1 = ImmutablePoint(1, 2);
  const point2 = ImmutablePoint(1, 2);
  
  // 相同参数的const实例是同一个对象
  print(point1 == point2); // 输出: true
  
  // 在常量上下文中可以省略const
  const points = [
    ImmutablePoint(0, 0), // 等价于 const ImmutablePoint(0, 0)
    ImmutablePoint(10, 20), // 等价于 const ImmutablePoint(10, 20)
  ];
  
  // 3D点示例
  const point3D = Immutable3DPoint(1, 2, 3);
  print(point3D); // 输出: Point(1, 2, 3)
}

错误示例:

// 错误示例1:存在非final变量
class BadPoint1 {
  int x; // 错误:不是final变量
  final int y;

  const BadPoint1(this.x, this.y); // 编译错误
}

// 错误示例2:有构造函数体
class BadPoint2 {
  final int x;
  final int y;

  // 错误:常量构造函数不能有函数体
  const BadPoint2(this.x, this.y) {
    print('Creating point');
  }
}

// 错误示例3:父类没有常量构造函数
class Parent {
  final int value;
  Parent(this.value); // 不是常量构造函数
}

class Child extends Parent {
  // 错误:父类没有常量构造函数
  const Child(int value) : super(value);
}

// 错误示例4
class Point {
  final int x;
  final int y;

  // 常量构造函数
  const Point(this.x, this.y);
}
// 虽然调用了常量构造函数,但未用 const 修饰,创建的是普通对象
final p1 = Point(1, 2); // 应该用 const Point(1, 2)
final p2 = Point(1, 2); // 相同参数,但未用 const

// 因为省略了 const,它们是不同的对象(== 为 false)
print(p1 == p2); // 输出 false(不符合常量语义的预期)

// 正确做法:显式使用 const
const p3 = Point(1, 2);
const p4 = Point(1, 2);
print(p3 == p4); // 输出 true(符合常量语义)


// 错误示例5:Address 类没有 const 构造函数(仅普通构造)
class Address {
  final String city;
  final String street;

  // 普通构造函数(无 const)
  Address(this.city, this.street);
}

class User {
  final String name;
  final Address address;

  const User(this.name, this.address); // 常量构造函数
}

const user = User(
  'Charlie',
  Address('Guangzhou', 'River Road'), // 编译错误:Address 没有 const 构造
);
finalconst 谁性能更好?
  • const 更好,因为对象在编译期就分配好,运行时不再重复创建。
  • Flutter 中 const Widget 会被缓存重用,减少构建时对象创建,优化性能。而 final Widget 每次 build 都要新建。

const 一定比 final 更好吗? 不完全。const 只能用于编译期可确定的值,场景有限。final 更灵活,适合依赖运行时的数据。



Dart中..是什么?...是什么?

一句话总结

  • ..:级联操作符是允许对同一个对象连续连续调用多个方法或访问属性, 无需重复引用对象本身,简化链式操作。
  • ...:展开操作符将一个可迭代对象(如 List、Set、Map)的元素 “展开” 并插入到另一个集合中,简化集合的合并或初始化。

代码演示

..:级联操作符
class Person {
  String name = '';
  int age = 0;
  
  void sayHello() => print("Hello, I'm $name");
  void setAge(int a) => age = a;
}

void main() {
  // 普通写法:多次引用对象
  final person1 = Person();
  person1.name = "Alice";
  person1.setAge(25);
  person1.sayHello();
  
  // 级联操作写法:简化为链式调用
  final person2 = Person()
    ..name = "Bob"    // 设置属性
    ..setAge(30)      // 调用方法
    ..sayHello();     // 继续调用方法
  
  // 嵌套级联(对对象的属性进行级联)
  final Map<String, dynamic> config = {};
  config
    ..["host"] = "api.example.com"
    ..["port"] = 8080
    ..["headers"] = {}
      ..["headers"]["Content-Type"] = "application/json";
}
...:展开操作符
void main() {
  final list1 = [1, 2, 3];
  final list2 = [4, 5, 6];
  
  // 合并两个列表(普通写法)
  final combined1 = List.from(list1)..addAll(list2);
  
  // 展开操作符写法(更简洁)
  final combined2 = [...list1, ...list2]; // [1, 2, 3, 4, 5, 6]
  
  // 混合元素和展开
  final combined3 = [0, ...list1, 7, ...list2]; // [0, 1, 2, 3, 7, 4, 5, 6]
  
  // 用于Set去重(Set本身去重特性+展开)
  final set1 = {1, 2, 3};
  final set2 = {3, 4, 5};
  final combinedSet = {...set1, ...set2}; // {1, 2, 3, 4, 5}
  
  // 空安全处理:可空迭代对象使用...?
  List<int>? nullableList = null;
  final safeList = [0, ...?nullableList, 6]; // [0, 6](若nullableList为null则忽略)
}


Dart中如何给String扩展一个方法?

一句话总结

  • Dart 的 Extension Methods 特性允许你向现有的类(甚至是来自外部库的类)添加新功能,而无需修改原始类的代码或创建子类。Dart 2.7 及以上版本引入的特性,它是编译期静态分发,没有运行时开销。

代码演示

extension:扩展方法
// 定义String的扩展
extension StringExtensions on String {
  // 扩展方法1:判断字符串是否为数字
  bool isNumber() {
    if (isEmpty) return false;
    return double.tryParse(this) != null;
  }

  // 扩展方法2:判断字符串是否包含指定列表中的任意元素
  bool containsAny(List<String> substrings) {
    for (final sub in substrings) {
      if (contains(sub)) return true;
    }
    return false;
  }

  // 将字符串转换为指定类型的列表(示例:按分隔符拆分后转换为int)
  List<T>? toList<T>(T Function(String) converter, {String separator = ","}) {
    try {
      return split(separator).map((s) => converter(s.trim())).toList();
    } catch (e) {
      return null; // 转换失败返回null
    }
  }
}

// 扩展1
extension StringExt1 on String {
  String format() => "Ext1: $this";
}

// 扩展2(与扩展1方法名冲突)
extension StringExt2 on String {
  String format() => "Ext2: $this";
}

// 使用扩展方法
void main() {
  final str1 = "123";
  print(str1.isNumber()); // 输出: true

  final str2 = "hello world";
  print(str2.containsAny(["hello", "dart"])); // 输出: true

  final str3 = "1,2,3,4";
  final numbers = str3.toList<int>((s) => int.parse(s));
  print(numbers); // 输出: [1, 2, 3, 4]

  final str4 = "test";
  // 直接调用会报错(方法名冲突)
  // print(str4.format());

  // 显式指定扩展名称调用
  print(StringExt1(str4).format()); // 输出: "Ext1: test"
  print(StringExt2(str4).format()); // 输出: "Ext2: test"
}


Dart中async和await?

一句话总结

  • asyncawait是处理异步操作的核心语法糖,它们让异步代码的写法更接近同步代码,大幅提升可读性,避免回调地狱(多层then嵌套)。

核心概念

async:标记异步函数
  • 作用:声明一个函数为异步函数,使其返回值自动包装为Future(即使函数实际返回非Future类型)。
  • 特性
    • 异步函数内部可以使用await关键字(非异步函数中不能使用await)。
    • 若函数无返回值,实际返回Future<void>;若返回值为T类型,实际返回Future<T>
await:暂停等待异步结果
  • 作用:在异步函数中,暂停代码执行,等待Future完成并获取其结果后再继续执行。
  • 特性
    • 仅能在async标记的函数内部使用。
    • await会将Future<T>转换为T类型(获取异步结果的 "解包" 操作)。
    • 等待期间不会阻塞当前线程(Dart 是单线程事件循环模型,会切换执行其他任务)。

Dart 的 async/await 并不是多线程,而是基于 单线程事件循环(Event Loop)+ Future 微任务队列 实现的。

  • await 并不会阻塞线程,只是让函数把后续代码注册为一个回调,等 Future 完成后再继续。
  • Dart VM 的事件循环模型类似 JavaScript:
    • Microtask Queue(微任务队列,优先级高,包括Future.then的回调函数,async/await的回调函数.)
    • Event Queue(事件队列,处理 IO/Timer/消息)

所以 await 本质是:语法糖包装了 then 回调

代码演示

// 模拟异步请求
Future<String> fetchUser() async {
  await Future.delayed(Duration(seconds: 1));
  return "UserA";
}

Future<String> fetchOrders(String user) async {
  await Future.delayed(Duration(seconds: 1));
  return "Orders_of_$user";
}

Future<String> fetchOrderDetail(String orders) async {
  await Future.delayed(Duration(seconds: 1));
  return "Detail_of_$orders";
}

void main() async {
  print("=== 使用 then 的写法 ===");
  fetchUser().then((user) {
    return fetchOrders(user).then((orders) {
      return fetchOrderDetail(orders).then((detail) {
        print("最终结果: $detail");
      });
    });
  });

  // 分隔开
  await Future.delayed(Duration(seconds: 4));

  print("\n=== 使用 async/await 的写法 ===");
  final user = await fetchUser();
  final orders = await fetchOrders(user);
  final detail = await fetchOrderDetail(orders);
  print("最终结果: $detail");
}

优先使用async/await而非then:复杂异步逻辑中,async/await的线性代码结构更易维护。

void main() {
  print('1: Main starts');
  fetchUserOrder(); // 调用异步函数
  print('4: Main ends');
}

Future<void> fetchUserOrder() async {
  print('2: Fetching user order...');
  // 模拟网络请求
  final order = await Future.delayed(Duration(seconds: 2), () => 'Large Latte');
  print('5: Your order is: $order');
}

// 输出顺序:
// 1: Main starts
// 2: Fetching user order...
// 4: Main ends
// (等待约2秒后)
// 5: Your order is: Large Latte

流程解析:

  1. main() 开始执行,打印 1
  2. 调用 fetchUserOrder(),它是一个 async 函数。
  3. 进入 fetchUserOrder(),同步执行代码直到 await,打印 2
  4. 遇到 awaitfetchUserOrder() 函数在此暂停立即返回一个未完成的 Future<void>  给调用者。
  5. main() 函数继续同步执行,打印 4,然后结束。此时 main() 函数执行完毕。
  6. Event Loop 现在空闲了。大约 2 秒后,Future.delayed 完成,它将 'Large Latte' 作为结果,并将其回调任务(即 print('5: Your order is: $order'))放入 Microtask Queue(对于 Future,通常是微任务队列)。
  7. Event Loop 从微任务队列中取出这个回调任务并执行它,打印 5

这个过程完美展示了  “暂停而不阻塞”  的含义。