ArkTS 开发者必看!泛型 + 空安全实战:告别重复代码,杜绝空指针崩溃(附完整案例)

79 阅读7分钟

在鸿蒙应用开发中,ArkTS 的泛型与 空安全是解决 “代码冗余” 和 “空指针 bug” 的两大核心技能。不少开发者在写自定义组件、处理集合数据时,总为 “多类型适配” 重复写代码;运行时又常因 “空值没处理” 导致 App 崩溃 —— 其实掌握这两个特性,就能轻松避开这些坑。本文结合实战案例,从原文核心内容出发,教你用泛型实现 “一套逻辑适配多类型”,用空安全从编译层杜绝空指针,新手也能快速上手!

一、ArkTS 泛型:一次编码,多类型复用(彻底告别重复代码)

1.1 先搞懂:泛型到底是什么?

泛型的本质是类型参数化—— 就像函数传数值参数一样,把 “数据类型” 当作参数传给类或方法。比如集合里限定只存字符串,还是数字,都靠泛型来定。

它的核心作用有 3 个,也是开发者最需要的:

  • 保安全:强制集合 / 数据结构里的类型统一,不会出现 “字符串和数字混存” 的情况;
  • 省代码:一套逻辑适配多种类型,不用为 String、Number、自定义类(比如 Student)重复写相似代码;
  • 免强转:编译时就确定类型,不用手动做类型转换,减少冗余和出错概率。

1.2 踩过的坑:非泛型代码有多麻烦?

栈是 “先进后出” 的结构,比如存字符串的栈,代码是这样的:

// 非泛型栈:只能存字符串
export class StringStack {
  private index: number = -1; // 栈顶指针(空栈时为-1)
  private data: string[] = []; // 只存字符串的数组

  // 入栈:只能传字符串
  push(item: string): void {
    this.index++;
    this.data[this.index] = item;
  }

  // 出栈:只能返回字符串
  pop(): string {
    const item = this.data[this.index];
    this.index--;
    return item;
  }

  // 取栈顶元素
  peek(): string {
    return this.data[this.index];
  }

  // 看栈里有多少元素
  size(): number {
    return this.index + 1;
  }
}

问题来了: 如果想存数字(比如 12、22),就得再写一个NumberStack,代码逻辑完全一样,只把string改成number;要是想存自定义的 “学生”“食物” 对象,又得再写 N 个类似的类 —— 重复代码一大堆,维护起来超麻烦!

1.3 泛型改造:一套代码适配所有类型

给类加个 “类型参数”,用<T>表示(T 是占位符,也能叫其他名字),把原来固定的类型(比如 string)换成 T。改造后的泛型栈是这样的:

// 泛型栈:T是类型参数,传什么类型就适配什么
export class GenericStack<T> {
  private index: number = -1;
  private data: T[] = []; // 数组类型跟着T变

  // 入栈:参数类型是T
  push(item: T): void {
    this.index++;
    this.data[this.index] = item;
  }

  // 出栈:返回值类型是T
  pop(): T {
    const item = this.data[this.index];
    this.index--;
    return item;
  }

  peek(): T {
    return this.data[this.index];
  }

  size(): number {
    return this.index + 1;
  }
}

怎么用?指定类型就行! 不管是存字符串、数字,还是自定义对象,只要传<类型>,一套代码全搞定:

// 1. 存字符串
const strStack = new GenericStack<string>();
strStack.push("我");
strStack.push("爱");
strStack.push("ArkTS");
console.log(strStack.pop()); // 输出:ArkTS(先进后出)

// 2. 存数字
const numStack = new GenericStack<number>();
numStack.push(12);
numStack.push(22);
console.log(numStack.peek()); // 输出:22(只看栈顶)

// 3. 存自定义学生对象
class Student {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
const studentStack = new GenericStack<Student>();
studentStack.push(new Student("小明"));
console.log(studentStack.size()); // 输出:1

在这里插入图片描述

1.4 泛型约束:给类型加 “规则”(不是什么类型都能传)

有时候我们需要限制 “能传的类型”,比如只允许传 “食物相关的类”。这时候用extends做泛型约束就行。

比如先定义一个Food基类,再让泛型栈只接受Food及其子类:

// 基类:食物
export class Food {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// 子类:鱼、猪肉
export class Fish extends Food {}
export class Pig extends Food {}

// 泛型约束:T必须是Food或它的子类
export class FoodStack<T extends Food> {
  private data: T[] = [];

  push(item: T): void {
    this.data.push(item);
  }

  getItems(): T[] {
    return this.data;
  }
}

// 正确:传Food子类(Fish)
const fishStack = new FoodStack<Fish>();
fishStack.push(new Fish("鲈鱼"));

// 错误:传string,不符合约束
const errorStack = new FoodStack<string>(); // 编译报错:string不满足T extends Food

1.5 泛型方法:抽取重复逻辑(不用写多个相似方法)

除了泛型类,我们还有 “泛型方法”—— 比如 “取数组最后一个元素”,不用为 string 数组、number 数组各写一个方法,用泛型方法一次搞定:

@Entry
@Component
struct demo {
  build() {
    Column(){
      Button("测试泛型")
        .onClick(()=>{
          // 调用时不用显式写<T>,编译器自动推断
          const strArr = ["a", "b", "c"];
          console.log(getLastElement(strArr)); // 输出:c(自动认T为string)

          const numArr = [10, 20, 30];
          console.log(getLastElement(numArr).toString()); // 输出:30(自动认T为number)
        })
    }
  }
}
// 泛型方法:T由传入的数组类型自动判断
function getLastElement<T>(arr: T[]): T {
  return arr[arr.length - 1];
}

在这里插入图片描述

二、ArkTS 空安全:从编译层杜绝空指针

2.1 核心原则:默认 “不可为空”

ArkTS 里,没明确说 “可空” 的变量,赋值为 null 会直接报错。比如下面这样写,编译都通不过:

// 错误:默认不可为空,不能赋值null
let name: string = null; // 编译报错:null不能赋值给string

// 错误:没初始化的变量不能用
let age: number;
console.log(age); // 编译报错:变量没赋值就用

这样设计的目的很简单:让开发者在写代码时就处理空值,避免运行时突然崩溃。

2.2 解决方案 1:联合类型(允许变量为空)

如果变量确实需要 “可能为空”(比如用户还没输入用户名),用类型 | null的联合类型,明确告诉编译器 “这个变量可空”。用的时候要判断空值,避免风险:

// 明确可空:string或null
let username: String | null= null;

// 使用前判断空值
if (username !== null) {
     console.log(`用户名长度:${String(username).length.toString()}`); // 安全:此时username非空
   } else {
     console.log("请输入用户名");
   }
// 后续赋值(非空、空都可以)
username = "鸿蒙开发者";
console.log(username.length.toString()); // 输出:5

在这里插入图片描述

2.3 解决方案 2:非空断言(告诉编译器 “我保证非空”)

有时候我们确定 “变量此时一定非空”,但编译器没看出来,这时候用!(非空断言)就行。比如这样:

// 可空变量
let score: number | null = 95;

// 错误:编译器不确定score非空
let doubleScore: number = score * 2; // 编译报错

// 正确:用!断言“score非空”
let doubleScore: number = score! * 2;
console.log(doubleScore.toString()); // 输出:190

*注意:这是 “开发者的承诺”,如果实际是 null,运行时还是会报错,别滥用!

2.4 解决方案 3:空值合并(空值时用默认值)

要是变量为空,想给个默认值,用??(空值合并运算符)特别方便 —— 左边为空就取右边,不为空就取左边,比 if-else 简洁多了:

class User {
  // 可空昵称
  nickname: string | null = null;

  // 获取昵称:空值时返回“匿名用户”
  getNickname(): string {
    return this.nickname ?? "匿名用户";
  }
}

const user1 = new User();
console.log(user1.getNickname()); // 输出:匿名用户(nickname为空)

const user2 = new User();
user2.nickname = "小洪";
console.log(user2.getNickname()); // 输出:小洪(nickname非空)

2.5 解决方案 4:可选链(安全访问关联对象)

如果访问 “对象的属性 / 方法” 时,对象可能为空,用?.(可选链)就行。用 “学生有狗” 的案例说明:

// 狗类
class Dog {
  eat(): void {
    console.log("狗吃骨头");
  }
}

// 学生类:dog是可选属性(可能没有)
class Student {
  name: string;
  dog?: Dog; // ?: 表示“可选”,可能为undefined

  constructor(name: string) {
    this.name = name;
  }
}

// 测试1:学生没有狗(dog为空)
const student1 = new Student("张三");
student1.dog?.eat(); // 安全:dog为空,不执行eat(),不报错

// 测试2:学生有狗
const student2 = new Student("李四");
student2.dog = new Dog();
student2.dog?.eat(); // 输出:狗吃骨头

在这里插入图片描述

三、总结:泛型 + 空安全实战建议

泛型怎么用?

  • 写自定义集合(栈、列表)→ 用泛型类;
  • 多类型方法重复→ 用泛型方法;
  • 限制类型范围→ 用extends做泛型约束。

空安全怎么选

  • 允许空值 + 需判断→ 联合类型(| null);
  • 确定非空→ 非空断言(!,谨慎用);
  • 空值要默认值→ 空值合并(??);
  • 访问关联对象→ 可选链(?.)。

掌握这两个特性,不仅能减少重复代码、避免空指针 bug,还能让你的 ArkTS 代码更简洁、更健壮。

如果今天的内容帮你解决了实际问题,欢迎分享给身边同样在学鸿蒙的朋友,如果还有疑问,也可以在评论区留言,一起解锁 ArkTS 的更多核心能力~


想入门鸿蒙开发又怕花冤枉钱?别错过!现在能免费系统学 —— 从 ArkTS 面向对象核心的类和对象、继承多态,到吃透鸿蒙开发关键技能,还能冲刺鸿蒙基础 + 高级开发者证书,更惊喜的是考证成功还送好礼!快加入我的鸿蒙班,一起从入门到精通,班级链接:点击免费进入