一、那些年我们追过的“魔法数字”

还记得刚入行时接手的一个老项目吗?代码里到处都是if (status === 1)if (status === 2)这样的判断。1代表什么?2又是什么?得翻遍三四个文件才能找到注释——如果还有注释的话。直到某天产品经理说“把状态3和状态5合并”,整个团队花了整整两天来梳理这些数字的来龙去脉。

这就是“魔法数字”的代价。而ArkTS的枚举类型,正是为了解决这个问题而生的优雅方案。


二、枚举基础:给状态一个名字

1. 最简单的枚举定义

// 以前的做法
const STATUS_LOADING = 0
const STATUS_SUCCESS = 1
const STATUS_ERROR = 2

// ArkTS枚举的做法
enum NetworkStatus {
  Loading,    // 0
  Success,    // 1  
  Error       // 2
}

看起来只是换了个写法?远不止如此。让我们看看枚举的完整生命周期:

通过

失败

开发者定义枚举

TypeScript编译器

枚举类型检查

生成运行时对象

编译错误提示

智能提示支持

反向映射能力

开发体验提升

调试便利性

2. 枚举的“超能力”

  • 自文档化NetworkStatus.Success比数字1清晰得多
  • 类型安全:编译器会阻止你传入无效值
  • 智能提示:IDE能自动补全所有枚举值
  • 集中管理:修改只需改一处,不用全局搜索替换

三、实战一波:智能家居控制面板

假设我们要开发一个智能灯控应用,灯的状态包括:关闭、开启、调光模式、色彩模式、定时关闭。

1. 传统方式(容易出错的版本)

class SmartLight {
  private state: number = 0
  
  setState(newState: number) {
    // 问题1:数字含义不明确
    // 问题2:没有范围检查
    // 问题3:后续维护困难
    this.state = newState
  }
  
  getStateDescription(): string {
    switch(this.state) {
      case 0: return "关闭"
      case 1: return "开启"
      case 2: return "调光模式"
      case 3: return "色彩模式"
      case 4: return "定时关闭"
      default: return "未知状态" // 可能永远执行不到?
    }
  }
}

// 使用时的困惑
light.setState(3) // 3是什么?得查文档
light.setState(5) // 编译通过,但运行时出错!

2. 枚举方式(优雅的版本)

// 定义灯光状态枚举
enum LightState {
  Off = "OFF",
  On = "ON",
  Dimming = "DIMMING",
  Color = "COLOR",
  ScheduledOff = "SCHEDULED_OFF"
}

// 定义灯光模式枚举(数字枚举)
enum LightMode {
  Normal,      // 0: 正常模式
  Reading,     // 1: 阅读模式  
  Movie,       // 2: 观影模式
  NightLight   // 3: 夜灯模式
}

// 定义亮度级别枚举(常量枚举)
const enum BrightnessLevel {
  Min = 10,
  Low = 25,
  Medium = 50,
  High = 75,
  Max = 100
}

class SmartLight {
  private currentState: LightState = LightState.Off
  private currentMode: LightMode = LightMode.Normal
  private brightness: BrightnessLevel = BrightnessLevel.Medium
  
  // 状态切换方法
  toggleState(newState: LightState) {
    // 编译器确保传入的是有效的LightState值
    this.currentState = newState
    
    // 根据状态执行相应操作
    switch(newState) {
      case LightState.Off:
        this.turnOff()
        break
      case LightState.On:
        this.turnOn()
        break
      case LightState.Dimming:
        this.startDimmingMode()
        break
      // ... 其他case
    }
  }
  
  // 模式切换方法
  setMode(mode: LightMode) {
    this.currentMode = mode
    
    // 可以基于枚举进行更复杂的逻辑
    if (mode === LightMode.Reading) {
      this.setBrightness(BrightnessLevel.High)
      this.setColorTemperature(4500) // 暖白色
    } else if (mode === LightMode.Movie) {
      this.setBrightness(BrightnessLevel.Low)
      this.setColorTemperature(3000) // 更暖的色调
    }
  }
  
  // 使用常量枚举作为参数
  setBrightness(level: BrightnessLevel) {
    // 因为BrightnessLevel是常量枚举,
    // 这里传入的level在编译后就是具体的数字
    this.brightness = level
    this.applyBrightness(level)
  }
  
  // 获取状态描述(不再需要switch-case)
  getStateDescription(): string {
    // 枚举值本身就是有意义的字符串
    return `当前状态: ${this.currentState}`
  }
  
  // 枚举的反向映射
  getStateByValue(value: string): LightState | undefined {
    // 可以通过值获取枚举名
    const stateName = Object.keys(LightState).find(
      key => LightState[key as keyof typeof LightState] === value
    )
    return stateName ? LightState[stateName as keyof typeof LightState] : undefined
  }
}

// 使用示例
const myLight = new SmartLight()

// 清晰、安全、易读
myLight.toggleState(LightState.On)
myLight.setMode(LightMode.Reading)
myLight.setBrightness(BrightnessLevel.High)

// 尝试传入无效值?编译器会报错!
// myLight.toggleState("INVALID_STATE") // 错误:类型不匹配

四、厉害的小技巧:枚举的“七十二变”

1. 字符串枚举的实际应用

// API接口状态码枚举
enum ApiStatusCode {
  Success = "SUCCESS",
  BadRequest = "BAD_REQUEST",
  Unauthorized = "UNAUTHORIZED",
  NotFound = "NOT_FOUND",
  ServerError = "SERVER_ERROR"
}

// 使用在API响应中
interface ApiResponse<T> {
  code: ApiStatusCode
  message: string
  data: T
}

// 类型安全的处理
function handleResponse(response: ApiResponse<any>) {
  switch(response.code) {
    case ApiStatusCode.Success:
      // 处理成功逻辑
      break
    case ApiStatusCode.Unauthorized:
      // 跳转到登录页面
      break
    // ... 其他状态码
  }
}

2. 常量枚举的性能优势

// 编译前
const enum Direction {
  Up,
  Down,
  Left,
  Right
}

function move(dir: Direction) {
  console.log(`Moving ${dir}`)
}

move(Direction.Up)

// 编译后(查看编译后的JavaScript)
function move(dir) {
  console.log(`Moving ${dir}`)
}

move(0 /* Direction.Up */) // 枚举值被内联,零运行时开销!

3. 枚举与联合类型的结合

// 定义形状类型
enum ShapeType {
  Circle = "CIRCLE",
  Rectangle = "RECTANGLE",
  Triangle = "TRIANGLE"
}

// 使用联合类型定义不同形状的数据结构
type Circle = {
  type: ShapeType.Circle
  radius: number
}

type Rectangle = {
  type: ShapeType.Rectangle
  width: number
  height: number
}

type Triangle = {
  type: ShapeType.Triangle
  base: number
  height: number
}

type Shape = Circle | Rectangle | Triangle

// 类型安全的面积计算
function calculateArea(shape: Shape): number {
  switch(shape.type) {
    case ShapeType.Circle:
      return Math.PI * shape.radius ** 2
    case ShapeType.Rectangle:
      return shape.width * shape.height
    case ShapeType.Triangle:
      return (shape.base * shape.height) / 2
    default:
      // 这里永远不会执行,但TypeScript需要这个default
      const _exhaustiveCheck: never = shape
      return _exhaustiveCheck
  }
}

五、HarmonyOS开发中的枚举实践

1. UI状态管理

// 页面加载状态
enum PageLoadState {
  Idle,          // 空闲
  Loading,       // 加载中
  Success,       // 成功
  Error,         // 错误
  Empty          // 空数据
}

@Component
struct UserListPage {
  @State loadState: PageLoadState = PageLoadState.Idle
  
  build() {
    Column() {
      // 根据状态显示不同UI
      if (this.loadState === PageLoadState.Loading) {
        LoadingIndicator()
      } else if (this.loadState === PageLoadState.Success) {
        UserList()
      } else if (this.loadState === PageLoadState.Error) {
        ErrorView(onRetry: this.loadData)
      } else if (this.loadState === PageLoadState.Empty) {
        EmptyView()
      }
    }
  }
  
  loadData() {
    this.loadState = PageLoadState.Loading
    
    // 模拟网络请求
    setTimeout(() => {
      this.loadState = PageLoadState.Success
    }, 1000)
  }
}

2. 权限管理

// 应用权限枚举
enum AppPermission {
  Camera = "camera",
  Location = "location",
  Storage = "storage",
  Microphone = "microphone",
  Contacts = "contacts"
}

// 权限检查函数
async function checkPermission(permission: AppPermission): Promise<boolean> {
  try {
    const result = await abilityAccessCtrl.requestPermissionsFromUser(
      [permission]
    )
    return result[0] === 0 // 0表示授权成功
  } catch (error) {
    console.error(`权限检查失败: ${permission}`, error)
    return false
  }
}

// 使用示例
const hasCameraAccess = await checkPermission(AppPermission.Camera)
if (hasCameraAccess) {
  // 打开相机
}

3. 路由管理

// 应用路由枚举
enum AppRoute {
  Home = "/home",
  Profile = "/profile",
  Settings = "/settings",
  Details = "/details/:id"
}

// 路由跳转函数
function navigateTo(route: AppRoute, params?: Record<string, any>) {
  let url = route
  
  // 处理参数
  if (params) {
    Object.keys(params).forEach(key => {
      url = url.replace(`:${key}`, params[key])
    })
  }
  
  router.pushUrl({ url })
}

// 使用示例
navigateTo(AppRoute.Details, { id: "123" })

六、枚举的最佳实践和小小陷阱

1. 该用枚举的时机

  • 有限的、已知的选项集合(状态、类型、模式等)
  • 需要编译时检查的场景
  • 代码需要自文档化的地方
  • 选项可能在未来扩展的情况

2. 不该用枚举的时机

  • 选项数量非常多(考虑用配置对象)
  • 选项需要动态生成(考虑用Map或普通对象)
  • 对性能要求极高(考虑用常量或数字字面量)

3. 常见陷阱与解决方案

// 陷阱1:数字枚举的隐式递增
enum ProblematicEnum {
  A, // 0
  B = 2,
  C  // 3?不,是3!因为B=2,C自动变成3
}

// 解决方案:显式赋值
enum SafeEnum {
  A = 0,
  B = 2,
  C = 3 // 明确赋值
}

// 陷阱2:枚举的运行时对象
enum MyEnum { A, B }
console.log(MyEnum) // 输出: {0: "A", 1: "B", A: 0, B: 1}

// 如果不需要反向映射,用常量枚举
const enum ConstEnum { A, B }
// console.log(ConstEnum) // 错误!常量枚举在编译时被消除

七、总结一下下哈:枚举,不只是语法糖

用了这么多年枚举,我最大的体会是:好的枚举设计,能让代码自己讲故事

看看这段代码:

// 没有枚举
if (user.status === 3 && order.type === 7) {
  // ... 神秘的逻辑
}

// 使用枚举
if (user.status === UserStatus.Active && order.type === OrderType.Refund) {
  // 哦!这是在处理活跃用户的退款订单
}

枚举的价值,体现在三个方面:

第一,它是代码的翻译官。把计算机理解的数字,翻译成人类能懂的语义。

第二,它是类型的守门员。在编译阶段就把错误拒之门外,而不是等到运行时才暴露问题。

第三,它是重构的安全网。当需要修改状态值时,只需改枚举定义,编译器会告诉你所有需要更新的地方。

在HarmonyOS开发中,枚举更是如鱼得水。从UI状态到权限管理,从路由配置到API设计,枚举都能让代码更健壮、更易维护。

下次写代码时,当你又想用数字1、2、3表示状态时,不妨停下来想一想:三个月后的我,还能看懂这些数字的意思吗?六个月后接手的新同事,需要花多少时间才能理解这段逻辑?

给状态一个名字吧。用枚举,让代码说人话。


最后的小贴士

  • 优先使用字符串枚举,除非有明确的性能需求
  • 常量枚举能提升性能,但会失去运行时访问能力
  • 枚举成员名用大写,这是TypeScript社区的约定俗成
  • 为枚举写注释,特别是当枚举值有特殊含义时

记住,好的代码不是写给机器看的,而是写给人看的——包括未来的自己。枚举,就是让代码更友好的重要工具之一。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐