Day 15 - ArkTS 模块系统

目标:掌握模块导出导入、命名空间,理解 ArkTS 模块化编程模型
预计时间:1.5-2小时


课前思考

回顾前面学习的类和接口:

// 文件:Index.ets
class Calculator {
    add(a: number, b: number): number {
        return a + b;
    }
}

class Logger {
    log(message: string): void {
        console.log(message);
    }
}

class Database {
    query(sql: string): object[] {
        return [];
    }
}

思考问题:

  1. 当代码量增长到几千行时,单文件维护有什么困难?
  2. 多个类定义在同一个文件中,如何避免命名冲突?
  3. 如何复用其他文件中的工具函数或类?

第一部分:为什么需要模块

1.1 单文件的局限

问题:代码增长后的维护困难

// 所有代码挤在一个文件里...
// 工具函数(200行)
// 数据模型类(300行)
// 业务逻辑类(500行)
// UI组件类(400行)
// 总计:1400+ 行,难以定位和维护

维护痛点:

  • 查找代码困难
  • 多人协作冲突
  • 编译时间变长
  • 无法按需加载

1.2 模块化的好处

好处 说明
代码复用 写好的工具类可以在多个项目中使用
职责分离 每个文件只负责一类功能
命名隔离 不同文件的同名变量不会冲突
依赖清晰 通过导入语句明确知道依赖哪些模块

1.3 ArkTS 的模块模型

核心规则:一文件一模块

// 文件:math.ets
// 这是一个模块

// 文件:logger.ets
// 这是另一个模块

对标 C++ 的多文件编译:

C++ ArkTS
.h 头文件声明 export 导出声明
.cpp 源文件实现 .ets 文件(声明+实现)
#include "xxx.h" import { xxx } from './xxx'
#pragma once 模块单例(自动处理)
namespace namespace(概念相同)

第二部分:命名导出与导入

2.1 export 导出

问题:如何让其他文件使用本文件的类或函数?

导出变量:

// 文件:config.ets

// 导出常量
export const PI: number = 3.14159;
export const APP_NAME: string = "MyApp";
export const MAX_SIZE: number = 1024;

// 导出变量
export let debugMode: boolean = false;

export function setDebug(mode: boolean): void {
    debugMode = mode;
}

导出函数:

// 文件:utils/math.ets

// 命名导出函数
export function add(a: number, b: number): number {
    return a + b;
}

export function subtract(a: number, b: number): number {
    return a - b;
}

export function multiply(a: number, b: number): number {
    return a * b;
}

export function divide(a: number, b: number): number {
    if (b === 0) {
        throw new Error("除数不能为零");
    }
    return a / b;
}

导出类:

// 文件:models/User.ets

export class User {
    private id: number;
    private name: string;
    
    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
    }
    
    getId(): number {
        return this.id;
    }
    
    getName(): string {
        return this.name;
    }
    
    toString(): string {
        return `User(${this.id}, ${this.name})`;
    }
}

导出接口和枚举:

// 文件:types/index.ets

// 导出接口
export interface Shape {
    area(): number;
    perimeter(): number;
}

export interface Drawable {
    draw(): void;
}

// 导出枚举
export enum Color {
    RED = "#FF0000",
    GREEN = "#00FF00",
    BLUE = "#0000FF"
}

export enum Status {
    PENDING,
    PROCESSING,
    COMPLETED
}

2.2 import 导入

问题:如何使用其他文件导出的内容?

// 文件:main.ets

import { add, subtract } from './utils/math';
import { User } from './models/User';
import { PI, APP_NAME } from './config';

// 使用导入的内容
let sum: number = add(10, 20);
console.log(`结果:${sum}`);

let user: User = new User(1, "张三");
console.log(`用户:${user.toString()}`);

console.log(`应用名:${APP_NAME}`);

导入多个成员:

// 文件:geometry.ets

import { Shape, Color, Status } from './types/index';

// 使用 Shape 接口
class Circle implements Shape {
    private radius: number;
    
    constructor(radius: number) {
        this.radius = radius;
    }
    
    area(): number {
        return PI * this.radius * this.radius;
    }
    
    perimeter(): number {
        return 2 * PI * this.radius;
    }
}

// 使用枚举
let color: Color = Color.RED;
let status: Status = Status.PENDING;

2.3 重命名导入导出

问题:导入的名称和本地变量冲突怎么办?

使用 as 关键字重命名:

// 文件:math.ets
export function log(message: string): void {
    console.log(`[Math] ${message}`);
}
// 文件:logger.ets
export function log(message: string): void {
    console.log(`[Logger] ${message}`);
}
// 文件:main.ets

// 重命名导入,避免冲突
import { log as mathLog } from './math';
import { log as loggerLog } from './logger';

// 同时使用两个不同来源的 log 函数
mathLog("计算结果");
loggerLog("系统消息");

导出时重命名:

// 文件:utils.ets

function internalHelper(): void {
    console.log("内部辅助函数");
}

// 导出时重命名
export { internalHelper as helper };

对标 C++ namespace 别名:

// C++
namespace MyLongNamespaceName {
    void func() {}
}

// 创建别名
namespace Short = MyLongNamespaceName;

// 使用
Short::func();
// ArkTS
import { MyLongFunctionName as short } from './module';
short();  // 使用短名称

2.4 批量导入

问题:一个模块导出很多内容,如何一次性导入?

// 文件:utils/index.ets

export function funcA(): void { }
export function funcB(): void { }
export function funcC(): void { }
export const CONST_A: number = 1;
export const CONST_B: number = 2;
// 文件:main.ets

// 批量导入所有导出内容
import * as Utils from './utils/index';

// 通过命名空间访问
Utils.funcA();
Utils.funcB();
console.log(`常量:${Utils.CONST_A}`);

对标 C++:

// C++
using namespace std;  // 导入整个命名空间
// ArkTS
import * as Utils from './utils';  // 类似 using namespace

2.5 as 关键字

as 关键字有两个主要用途:

用途1:重命名导入/导出

// 导入时重命名,避免命名冲突
import { log as mathLog } from './math';
import { log as loggerLog } from './logger';

mathLog("计算结果");
loggerLog("系统消息");
// 导出时重命名
function internalHelper(): void { }
export { internalHelper as helper };

用途2:类型断言(Type Assertion)

问题:当你比编译器更清楚某个值的类型时

// 编译器不知道具体类型
let value: unknown = getSomeValue();

// 你确定这是 string 类型
let str: string = value as string;  // 告诉编译器:相信我,这是 string

对标 C++ 强制类型转换:

C++ ArkTS
int x = (int)y; let x = y as number;
int x = static_cast<int>(y); let x = y as number;

区别:

  • C++ 强制转换会改变运行时行为
  • ArkTS as 只是告诉编译器"我知道这是什么类型",编译后消失

警告: 滥用 as 会绕过类型检查,可能导致运行时错误。


2.6 关于 module 关键字

重要:module 关键字在 ArkTS/TypeScript 中已被废弃,现代代码中不再使用。

历史背景

早期 TypeScript 使用 module 关键字:

// 旧语法,已废弃
module Geometry {
    export class Point { }
    export class Line { }
}

现代 TypeScript/ArkTS 使用 namespace 代替:

// 新语法,推荐使用
namespace Geometry {
    export class Point { }
    export class Line { }
}

为什么废弃?

原因 说明
概念混淆 module 既指文件模块,又指命名空间,容易混淆
标准化 ES6 引入了标准的 import/export 模块系统
清晰区分 namespace 专门用于代码组织,module 专门指文件模块

现在的规范

概念 关键字 用途
文件模块 export / import 跨文件代码复用
命名空间 namespace 单文件内代码组织

你可能会在哪里看到 module

1. 类型声明文件(.d.ts)

// 声明模块,告诉编译器这个文件是什么模块
declare module "my-library" {
    export function doSomething(): void;
}

2. 配置文件中

// tsconfig.json
{
    "compilerOptions": {
        "module": "ES2020"  // 指定模块系统
    }
}

总结:在 ArkTS 代码中,你不需要使用 module 关键字。


2.7 处理大量导出的最佳实践

问题:如果一个模块导出了大量符号(如1万个),如何高效导入?

方案1:批量导入(命名空间导入)

// 导入所有导出内容到一个命名空间
import * as Utils from './utils/index';

// 通过命名空间访问
Utils.funcA();
Utils.funcB();
console.log(Utils.CONST_A);

优点: 一行代码导入所有内容
缺点: 使用时需要加前缀 Utils.


方案2:统一出口(re-export)

// utils/index.ets - 只导出常用的
export { add, sub, mul, div } from './math';
export { trim, upper } from './string';
// 其他不常用的不导出,需要时单独导入

使用者按需导入:

// 导入常用功能
import { add, trim } from './utils/index';

// 不常用的单独导入
import { complexFunction } from './utils/advanced';

方案3:模块化拆分

不要把大量符号放在一个模块里,按功能拆分:

utils/
├── math.ets      // 数学相关
├── string.ets    // 字符串相关
├── array.ets     // 数组相关
├── network.ets   // 网络相关
└── index.ets     // 统一出口

实际项目中的最佳实践

场景 推荐方案
工具库(如 lodash) 命名空间导入 import * as _ from 'lodash'
自己的业务模块 统一出口 + 按需导入
大型框架 模块化拆分,按需导入子模块

第三部分:默认导出与导入

3.1 export default

问题:一个模块主要导出的是一个类,如何简化导入?

默认导出:

// 文件:Calculator.ets

export default class Calculator {
    private value: number = 0;
    
    add(n: number): Calculator {
        this.value = this.value + n;
        return this;
    }
    
    subtract(n: number): Calculator {
        this.value = this.value - n;
        return this;
    }
    
    getValue(): number {
        return this.value;
    }
    
    reset(): void {
        this.value = 0;
    }
}

规则:

  • 每个模块最多只能有一个默认导出
  • 默认导出不需要名称(但通常会给一个)

3.2 导入默认导出

// 文件:main.ets

// 导入默认导出,可以任意命名!
import X from './Calculator';        // 命名为 X
import MyCalc from './Calculator';   // 命名为 MyCalc
import Calc from './Calculator';     // 命名为 Calc

// 甚至可以重复导入同一个模块,给同一个类取不同的名字
let calc1: X = new X();
let calc2: MyCalc = new MyCalc();
let calc3: Calc = new Calc();

// 这三个实例类型完全相同,都是 Calculator 类型
calc1.add(10);
calc2.subtract(5);
console.log(`结果:${calc3.getValue()}`);  // 5

对比命名导入:

// 命名导出 - 必须有花括号,名称必须匹配
import { Calculator } from './Calculator';

// 默认导出 - 没有花括号,可以任意命名
import Calculator from './Calculator';
import MyCalculator from './Calculator';  // 同一个模块,不同名字

关键特性:

  • 默认导出的导入名称完全自由,不需要和原类名一致
  • 可以重复导入同一个默认导出,给同一个类取不同的名字
  • 这些不同的名字指向的是同一个类型,可以互换使用

3.3 默认导出 vs 命名导出

特性 命名导出 默认导出
数量 一个模块可有多个 一个模块只能有一个
导入语法 import { A } from '...' import A from '...'
导入名称 必须匹配导出名称 可以任意命名
适用场景 工具函数库、多个相关类 单一主要功能、组件类

最佳实践:

// 推荐:工具类库使用命名导出
// 文件:math.ets
export function add(a: number, b: number): number { return a + b; }
export function sub(a: number, b: number): number { return a - b; }
export function mul(a: number, b: number): number { return a * b; }

// 推荐:单一组件使用默认导出
// 文件:DatabaseManager.ets
export default class DatabaseManager {
    // 数据库管理的主要类
}

第四部分:模块路径与加载规则

4.1 相对路径

问题:如何指定要导入的模块位置?

核心原则:以当前文件所在位置为参考,找到目标模块

假设项目结构如下:

src/
├── main.ets              ← 位置:src/
├── utils/
│   ├── math.ets          ← 位置:src/utils/
│   └── helpers/
│       └── string.ets    ← 位置:src/utils/helpers/
├── models/
│   └── User.ets          ← 位置:src/models/
└── services/
    └── api.ets           ← 位置:src/services/

从 main.ets 导入(位于 src/):

// 文件:main.ets

// 当前目录下的直接子目录
import { add } from './utils/math';

// 当前目录下的多级子目录
import { trim } from './utils/helpers/string';

// 当前目录下的其他子目录
import { User } from './models/User';
import { apiService } from './services/api';

从 utils/helpers/string.ets 导入(位于 src/utils/helpers/):

// 文件:utils/helpers/string.ets

// 上级目录(回到 utils/)
import { add } from '../math';

// 上两级目录(回到 src/),再进入其他目录
import { User } from '../../models/User';

// 上两级目录,再进入 services/
import { apiService } from '../../services/api';

路径规则总结:

路径 含义
./xxx 当前目录下的模块
./a/b/xxx 当前目录下多级子目录的模块
../xxx 上级目录下的模块
../../xxx 上两级目录下的模块

记忆口诀:

  • ./ = 往下走(进入子目录)
  • ../ = 往上走(回到父目录)
  • 每多一级 ../ = 多往上走一层

4.2 模块单例特性

问题:一个模块被多次导入,会执行几次?

// 文件:singleton.ets

console.log("模块初始化执行");

export let instanceCount: number = 0;

export function increment(): void {
    instanceCount = instanceCount + 1;
}
// 文件:a.ets
import { instanceCount, increment } from './singleton';
increment();
console.log(`A: ${instanceCount}`);  // 1
// 文件:b.ets
import { instanceCount, increment } from './singleton';
increment();
console.log(`B: ${instanceCount}`);  // 2
// 文件:main.ets
import './a';
import './b';
import { instanceCount } from './singleton';

console.log(`Main: ${instanceCount}`);  // 2
// 注意:singleton.ets 只执行了一次!

重要特性:

  • 同一模块在程序生命周期中只执行一次
  • 多次导入返回相同的实例
  • 状态在多个导入者之间共享

对标 C++:

// C++ 防止头文件重复包含
#ifndef MYHEADER_H
#define MYHEADER_H
// ... 内容
#endif

// 或者
#pragma once
// ArkTS 自动保证单例,无需手动处理

4.3 循环依赖

问题:什么是循环依赖?为什么危险?

// 文件:a.ets
import { funcB } from './b';

export function funcA(): void {
    console.log("调用 funcA");
    funcB();
}
// 文件:b.ets
import { funcA } from './a';

export function funcB(): void {
    console.log("调用 funcB");
    // funcA();  // 如果调用会导致无限循环
}

循环依赖的危害:

  • 编译/加载错误
  • 运行时未定义行为
  • 难以调试和维护

对标 C++ 头文件循环包含:

// C++ 循环包含问题
// a.h 包含 b.h
// b.h 包含 a.h
// 结果:编译错误

// 解决方案:前向声明
class B;  // 前向声明
class A {
    B* b;  // 使用指针,不依赖完整定义
};
// ArkTS 解决方案:重构代码结构
// 将共享部分提取到第三个模块

// 文件:common.ets
export interface SharedInterface {
    doSomething(): void;
}

// 文件:a.ets
import { SharedInterface } from './common';

// 文件:b.ets
import { SharedInterface } from './common';

4.4 动态导入 import()(✅ ArkTS 完全支持)

问题:如果某些模块只在特定条件下才需要,如何避免一开始就全部加载?

4.4.1 为什么需要动态导入

静态导入的局限:

// 静态导入在模块加载时就全部执行
import { HeavyModule } from './heavy-module';  // 无论用不用,都会加载
  • 增加启动时间
  • 有些模块只在特定条件下才需要
  • 大型应用中需要延迟加载非核心功能

C++ 对比:

类似 C++ 中的动态加载共享库:

// Linux
dlopen("./libmylib.so", RTLD_LAZY);

// Windows
LoadLibrary("mylib.dll");

ArkTS 的动态导入 import() 支持加载 HAP/HSP/HAR 模块、ohpm 包、Native 库。


4.4.2 基本语法

// 动态导入返回 Promise
import("./utils").then((module: ESObject) => {
  // 使用 module 中的导出
  let result: number = module.add(1, 2);
  console.log(`结果:${result}`);
});

注意: import() 返回的是一个异步操作结果。后续 Promise 章节会深入讲解,目前只需要知道 .then() 里的代码会在模块加载完成后执行。


4.4.3 使用场景

场景1:条件加载

// 根据运行时条件决定是否加载某个模块
function processData(data: object): void {
  if (needsAdvancedProcessing(data)) {
    import("./advanced-processor").then((module: ESObject) => {
      module.process(data);
    });
  } else {
    // 使用基础处理,不加载高级模块
    basicProcess(data);
  }
}

场景2:按需加载

// 大型应用中延迟加载非核心功能,提升启动速度
class Application {
  private settingsModule: ESObject | null = null;

  openSettings(): void {
    if (this.settingsModule === null) {
      import("./settings-page").then((module: ESObject) => {
        this.settingsModule = module;
        this.settingsModule.show();
      });
    } else {
      this.settingsModule.show();
    }
  }
}

4.4.4 与静态导入的对比

特性 静态 import 动态 import()
加载时机 编译时 运行时
位置要求 文件顶部 任意位置
返回值 直接使用 Promise
树摇优化 支持 不支持

4.4.5 注意事项

  1. 动态导入是异步的,不能同步获取结果
  2. 路径必须是字符串字面量(不能用变量拼接)
  3. 优先使用静态导入,仅在确实需要时才用动态导入
// ❌ 错误:不能用变量拼接路径
let moduleName: string = "./utils";
import(moduleName);  // 编译错误

// ✅ 正确:使用字符串字面量
import("./utils");   // 正确

4.5 re-export(转发导出)

问题:如何创建一个统一的模块出口?

// 文件:utils/math.ets
export function add(a: number, b: number): number { return a + b; }
export function sub(a: number, b: number): number { return a - b; }
// 文件:utils/string.ets
export function trim(s: string): string { return s.trim(); }
export function upper(s: string): string { return s.toUpperCase(); }
// 文件:utils/index.ets
// 统一出口文件

// 转发导出 math 模块的所有导出
export { add, sub } from './math';

// 转发导出 string 模块的所有导出
export { trim, upper } from './string';

// 也可以重命名后转发
export { add as addition } from './math';
// 文件:main.ets
// 只需要导入一个统一出口

import { add, sub, trim, upper } from './utils/index';

console.log(`${add(1, 2)}`);
console.log(`${trim("  hello  ")}`);

对标 C++:

// C++ 聚合头文件
// all_headers.h
#include "math.h"
#include "string.h"
#include "vector.h"
// ArkTS 聚合模块
// index.ets
export { ... } from './math';
export { ... } from './string';
export { ... } from './vector';

第五部分:命名空间

5.1 namespace 语法

问题:如何组织相关的类和函数,避免全局命名冲突?

// 文件:geometry.ets

namespace Geometry {
    export interface Point {
        x: number;
        y: number;
    }
    
    export class Rectangle {
        private x: number;
        private y: number;
        private width: number;
        private height: number;
        
        constructor(x: number, y: number, width: number, height: number) {
            this.x = x;
            this.y = y;
            this.width = width;
            this.height = height;
        }
        
        area(): number {
            return this.width * this.height;
        }
    }
    
    export class Circle {
        private center: Point;
        private radius: number;
        
        constructor(center: Point, radius: number) {
            this.center = center;
            this.radius = radius;
        }
        
        area(): number {
            return 3.14159 * this.radius * this.radius;
        }
    }
    
    export function distance(p1: Point, p2: Point): number {
        let dx: number = p2.x - p1.x;
        let dy: number = p2.y - p1.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}

// 使用命名空间
let rect: Geometry.Rectangle = new Geometry.Rectangle(0, 0, 100, 50);
console.log(`面积:${rect.area()}`);

let p1: Geometry.Point = { x: 0, y: 0 };
let p2: Geometry.Point = { x: 3, y: 4 };
console.log(`距离:${Geometry.distance(p1, p2)}`);

对标 C++ namespace:

// C++
namespace Geometry {
    struct Point {
        double x, y;
    };
    
    class Rectangle {
        // ...
    };
    
    double distance(Point p1, Point p2) {
        // ...
    }
}

// 使用
Geometry::Rectangle rect;
Geometry::Point p1;
// ArkTS - 概念完全一致!
Geometry.Rectangle
Geometry.Point

5.2 嵌套命名空间

// 文件:app.ets

namespace App {
    export namespace UI {
        export class Button {
            private label: string;
            
            constructor(label: string) {
                this.label = label;
            }
            
            render(): string {
                return `[Button: ${this.label}]`;
            }
        }
        
        export class Label {
            private text: string;
            
            constructor(text: string) {
                this.text = text;
            }
            
            render(): string {
                return `[Label: ${this.text}]`;
            }
        }
    }
    
    export namespace Data {
        export class Store {
            private data: Map<string, object> = new Map();
            
            set(key: string, value: object): void {
                this.data.set(key, value);
            }
            
            get(key: string): object | undefined {
                return this.data.get(key);
            }
        }
    }
}

// 使用嵌套命名空间
let btn: App.UI.Button = new App.UI.Button("确定");
console.log(btn.render());

let store: App.Data.Store = new App.Data.Store();
store.set("user", { name: "张三" });

对标 C++ 嵌套 namespace:

// C++17 嵌套命名空间
namespace App::UI {
    class Button { };
}

namespace App::Data {
    class Store { };
}
// ArkTS 嵌套命名空间
App.UI.Button
App.Data.Store

5.3 命名空间 vs 模块

特性 模块(Module) 命名空间(Namespace)
边界 文件边界 代码边界(可在同一文件)
加载 按需加载 编译时包含
作用 代码组织、复用 命名隔离、逻辑分组
导出 export 关键字 export 关键字
使用 import 导入 直接访问(同文件)或导入

选择建议:

// 场景1:跨文件复用 → 使用模块
// math.ets
export function add() { }

// main.ets
import { add } from './math';
// 场景2:单文件内组织 → 使用命名空间
// geometry.ets
namespace Geometry {
    export class Point { }
    export class Line { }
}

namespace MathUtils {
    export function clamp() { }
}

// 同一文件内使用
let p: Geometry.Point = new Geometry.Point();
// 场景3:大型库 → 模块 + 命名空间结合
// 文件:library.ets
export namespace Core {
    export class Application { }
}

export namespace Utils {
    export class Logger { }
}

// 其他文件
import { Core, Utils } from './library';

第六部分:小结与练习

6.1 知识点对比总结表

ArkTS 模块 C++ 多文件编译 说明
import { x } from './file' #include "file.h" 引入其他文件的声明
export public: 或全局声明 对外暴露接口
namespace namespace 命名隔离,概念完全一致
模块单例 #pragma once / include guard 自动防止重复加载
export default 无直接对应 单一主要导出
as 重命名 namespace Alias = Name 解决命名冲突
export { } from '' 聚合头文件 统一出口

6.2 使用原则

场景 推荐方案
工具函数库 命名导出 export function
单一主要类 默认导出 export default class
避免命名冲突 使用 as 重命名导入
统一模块出口 使用 export { } from '' 转发
单文件内组织 使用 namespace
大型项目结构 模块 + 命名空间结合

练习题

练习1:基础导出导入

// 1. 创建 math.ets 模块,导出以下函数:
//    - sum(a: number, b: number): number
//    - average(arr: number[]): number
//    - max(a: number, b: number): number

// 2. 在 main.ets 中导入并使用这些函数

// 3. 创建 constants.ets 模块,导出:
//    - PI: number = 3.14159
//    - E: number = 2.71828
//    - MAX_VALUE: number = 1000
点击查看答案

math.ets:

export function sum(a: number, b: number): number {
    return a + b;
}

export function average(arr: number[]): number {
    let total: number = 0;
    for (let i: number = 0; i < arr.length; i = i + 1) {
        total = total + arr[i];
    }
    return total / arr.length;
}

export function max(a: number, b: number): number {
    if (a > b) {
        return a;
    }
    return b;
}

constants.ets:

export const PI: number = 3.14159;
export const E: number = 2.71828;
export const MAX_VALUE: number = 1000;

main.ets:

import { sum, average, max } from './math';
import { PI, E, MAX_VALUE } from './constants';

console.log(`sum: ${sum(10, 20)}`);
console.log(`average: ${average([1, 2, 3, 4, 5])}`);
console.log(`max: ${max(10, 20)}`);
console.log(`PI: ${PI}`);

练习2:类与接口导出

// 1. 创建 models.ets 模块,导出:
//    - 接口 Animal { name: string; speak(): string }
//    - 类 Dog implements Animal
//    - 类 Cat implements Animal
//    - 枚举 AnimalType { DOG, CAT }

// 2. 在 main.ets 中导入并创建实例

// 3. 使用 as 重命名导入 Animal 为 IAnimal
点击查看答案

models.ets:

export interface Animal {
    name: string;
    speak(): string;
}

export class Dog implements Animal {
    name: string;
    
    constructor(name: string) {
        this.name = name;
    }
    
    speak(): string {
        return `${this.name} says: Woof!`;
    }
}

export class Cat implements Animal {
    name: string;
    
    constructor(name: string) {
        this.name = name;
    }
    
    speak(): string {
        return `${this.name} says: Meow!`;
    }
}

export enum AnimalType {
    DOG,
    CAT
}

main.ets:

import { Animal as IAnimal, Dog, Cat, AnimalType } from './models';

let dog: IAnimal = new Dog("Buddy");
let cat: IAnimal = new Cat("Kitty");

console.log(dog.speak());
console.log(cat.speak());
console.log(`Dog type: ${AnimalType.DOG}`);

练习3:命名空间

// 1. 创建 graphics.ets,定义命名空间 Graphics:
//    - 接口 Color { r: number; g: number; b: number }
//    - 类 Pixel { x: number; y: number; color: Color }
//    - 函数 createColor(r, g, b): Color

// 2. 创建嵌套命名空间 Graphics.Filter:
//    - 函数 grayscale(color: Color): Color
//    - 函数 invert(color: Color): Color

// 3. 在 main.ets 中使用
点击查看答案

graphics.ets:

namespace Graphics {
    export interface Color {
        r: number;
        g: number;
        b: number;
    }
    
    export class Pixel {
        x: number;
        y: number;
        color: Color;
        
        constructor(x: number, y: number, color: Color) {
            this.x = x;
            this.y = y;
            this.color = color;
        }
    }
    
    export function createColor(r: number, g: number, b: number): Color {
        return { r, g, b };
    }
    
    export namespace Filter {
        export function grayscale(color: Color): Color {
            let avg: number = (color.r + color.g + color.b) / 3;
            return { r: avg, g: avg, b: avg };
        }
        
        export function invert(color: Color): Color {
            return { r: 255 - color.r, g: 255 - color.g, b: 255 - color.b };
        }
    }
}

export { Graphics };

main.ets:

import { Graphics } from './graphics';

let red: Graphics.Color = Graphics.createColor(255, 0, 0);
let pixel: Graphics.Pixel = new Graphics.Pixel(10, 20, red);

let gray: Graphics.Color = Graphics.Filter.grayscale(red);
let inverted: Graphics.Color = Graphics.Filter.invert(red);

console.log(`Red: ${red.r}, ${red.g}, ${red.b}`);
console.log(`Gray: ${gray.r}, ${gray.g}, ${gray.b}`);
console.log(`Inverted: ${inverted.r}, ${inverted.g}, ${inverted.b}`);

练习4:转发导出

// 1. 创建以下文件结构:
//    utils/
//      ├── array.ets  (导出 find, filter)
//      ├── object.ets (导出 clone, merge)
//      └── index.ets  (转发导出上面所有)

// 2. 在 main.ets 中只导入 utils/index 使用所有功能
点击查看答案

utils/array.ets:

export function find<T>(arr: T[], predicate: (item: T) => boolean): T | undefined {
    for (let i: number = 0; i < arr.length; i = i + 1) {
        if (predicate(arr[i])) {
            return arr[i];
        }
    }
    return undefined;
}

export function filter<T>(arr: T[], predicate: (item: T) => boolean): T[] {
    let result: T[] = [];
    for (let i: number = 0; i < arr.length; i = i + 1) {
        if (predicate(arr[i])) {
            result.push(arr[i]);
        }
    }
    return result;
}

utils/object.ets:

export function clone<T>(obj: T): T {
    return JSON.parse(JSON.stringify(obj)) as T;
}

export function merge<T, U>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 } as T & U;
}

utils/index.ets:

export { find, filter } from './array';
export { clone, merge } from './object';

main.ets:

import { find, filter, clone, merge } from './utils/index';

let numbers: number[] = [1, 2, 3, 4, 5];
let found: number | undefined = find(numbers, (n) => n > 3);
let evens: number[] = filter(numbers, (n) => n % 2 === 0);

console.log(`Found: ${found}`);
console.log(`Evens: ${evens}`);

练习5:默认导出

// 1. 创建 Logger.ets,默认导出一个 Logger 类:
//    - 方法 log(message: string): void
//    - 方法 error(message: string): void
//    - 方法 warn(message: string): void

// 2. 在 main.ets 中导入,并命名为 MyLogger

// 3. 创建实例并调用各种方法
点击查看答案

Logger.ets:

export default class Logger {
    private prefix: string;
    
    constructor(prefix: string = "[LOG]") {
        this.prefix = prefix;
    }
    
    log(message: string): void {
        console.log(`${this.prefix} ${message}`);
    }
    
    error(message: string): void {
        console.error(`${this.prefix} [ERROR] ${message}`);
    }
    
    warn(message: string): void {
        console.warn(`${this.prefix} [WARN] ${message}`);
    }
}

main.ets:

import MyLogger from './Logger';

let logger: MyLogger = new MyLogger("[MyApp]");
logger.log("Application started");
logger.warn("Low memory");
logger.error("Connection failed");

练习6:模块单例验证

// 1. 创建 counter.ets:
//    - 导出变量 count: number = 0
//    - 导出函数 increment(): void

// 2. 创建 moduleA.ets,导入并调用 increment

// 3. 创建 moduleB.ets,导入并调用 increment

// 4. 在 main.ets 中导入 A、B 和 counter,验证 count 的值
点击查看答案

counter.ets:

console.log("Counter module initialized");

export let count: number = 0;

export function increment(): void {
    count = count + 1;
    console.log(`Count incremented to: ${count}`);
}

moduleA.ets:

import { count, increment } from './counter';

console.log("Module A: before increment");
increment();
console.log(`Module A: count = ${count}`);

moduleB.ets:

import { count, increment } from './counter';

console.log("Module B: before increment");
increment();
console.log(`Module B: count = ${count}`);

main.ets:

import './moduleA';
import './moduleB';
import { count } from './counter';

console.log(`Final count in main: ${count}`);
// 输出:2(证明模块只初始化一次,状态共享)

预期输出:

Counter module initialized
Module A: before increment
Count incremented to: 1
Module A: count = 1
Module B: before increment
Count incremented to: 2
Module B: count = 2
Final count in main: 2

练习7:综合应用

// 实现一个简单的计算器模块系统:

// 1. calculator/
//    ├── operations.ets  (加减乘除运算)
//    ├── history.ets     (计算历史记录)
//    └── index.ets       (统一出口)

// 2. 使用命名空间 Calculator 组织代码

// 3. 默认导出 Calculator 类,支持链式调用:
//    calc.add(10).multiply(2).subtract(5).result()
点击查看答案

calculator/operations.ets:

export namespace Calculator {
    export function add(a: number, b: number): number {
        return a + b;
    }
    
    export function subtract(a: number, b: number): number {
        return a - b;
    }
    
    export function multiply(a: number, b: number): number {
        return a * b;
    }
    
    export function divide(a: number, b: number): number {
        if (b === 0) {
            throw new Error("Division by zero");
        }
        return a / b;
    }
}

calculator/history.ets:

export namespace Calculator {
    export class History {
        private records: string[] = [];
        
        add(record: string): void {
            this.records.push(record);
        }
        
        getAll(): string[] {
            return this.records;
        }
        
        clear(): void {
            this.records = [];
        }
    }
}

calculator/index.ets:

export { Calculator } from './operations';
export { Calculator as CalculatorHistory } from './history';

export default class Calculator {
    private value: number = 0;
    private history: string[] = [];
    
    add(n: number): Calculator {
        this.value = this.value + n;
        this.history.push(`add ${n}`);
        return this;
    }
    
    subtract(n: number): Calculator {
        this.value = this.value - n;
        this.history.push(`subtract ${n}`);
        return this;
    }
    
    multiply(n: number): Calculator {
        this.value = this.value * n;
        this.history.push(`multiply ${n}`);
        return this;
    }
    
    divide(n: number): Calculator {
        if (n === 0) {
            throw new Error("Division by zero");
        }
        this.value = this.value / n;
        this.history.push(`divide ${n}`);
        return this;
    }
    
    result(): number {
        return this.value;
    }
    
    getHistory(): string[] {
        return this.history;
    }
}

main.ets:

import Calculator, { Calculator as CalcOps } from './calculator/index';

// 使用默认导出的 Calculator 类(链式调用)
let calc: Calculator = new Calculator();
let result: number = calc.add(10).multiply(2).subtract(5).result();
console.log(`Result: ${result}`);  // 15
console.log(`History: ${calc.getHistory()}`);

// 使用命名空间中的函数
let sum: number = CalcOps.add(5, 3);
console.log(`Sum: ${sum}`);  // 8

练习8:错误处理与模块

// 1. 创建 errors.ets 模块:
//    - 导出类 ValidationError extends Error
//    - 导出类 CalculationError extends Error
//    - 导出枚举 ErrorCode { INVALID_INPUT, DIVISION_BY_ZERO }

// 2. 创建 calculator.ets 模块,导入 errors 并在适当时候抛出异常

// 3. 在 main.ets 中导入并使用 try/catch 处理
点击查看答案

errors.ets:

export enum ErrorCode {
    INVALID_INPUT,
    DIVISION_BY_ZERO
}

export class ValidationError extends Error {
    code: ErrorCode;
    
    constructor(message: string, code: ErrorCode) {
        super(message);
        this.code = code;
    }
}

export class CalculationError extends Error {
    code: ErrorCode;
    
    constructor(message: string, code: ErrorCode) {
        super(message);
        this.code = code;
    }
}

calculator.ets:

import { ValidationError, CalculationError, ErrorCode } from './errors';

export function divide(a: number, b: number): number {
    if (b === 0) {
        throw new CalculationError("Cannot divide by zero", ErrorCode.DIVISION_BY_ZERO);
    }
    return a / b;
}

export function validatePositive(n: number): void {
    if (n < 0) {
        throw new ValidationError("Number must be positive", ErrorCode.INVALID_INPUT);
    }
}

main.ets:

import { divide, validatePositive } from './calculator';
import { ValidationError, CalculationError, ErrorCode } from './errors';

try {
    validatePositive(-5);
} catch (e) {
    if (e instanceof ValidationError) {
        console.log(`Validation error: ${e.message}, code: ${e.code}`);
    }
}

try {
    divide(10, 0);
} catch (e) {
    if (e instanceof CalculationError) {
        console.log(`Calculation error: ${e.message}, code: ${e.code}`);
    }
}

console.log("Program continues...");
posted @ 2026-04-17 10:40  thammer  阅读(60)  评论(0)    收藏  举报