Day 08 - 函数进阶与阶段总结

目标:掌握函数重载、递归函数、typeof 类型守卫,完成 Phase 02 阶段总结
预计时间:1.5-2小时
前置知识:Day 06 函数基础、Day 07 函数类型与回调


第一部分:函数重载

1.1 为什么需要函数重载

在实际开发中,我们经常需要一个函数处理不同类型的输入。例如,一个格式化函数可能需要处理数字、字符串或布尔值。

// 不用重载:需要写多个函数,函数名难以管理
function formatNumber(value: number): string {
    return value.toFixed(2);
}

function formatString(value: string): string {
    return value.toUpperCase();
}

function formatBoolean(value: boolean): string {
    return value ? "是" : "否";
}

// 使用重载:一个函数名,多种调用方式
// 见下文...

C/C++对比:
C++ 支持真正的函数重载,编译器会根据参数类型自动选择对应的函数实现:

// C++ 函数重载
std::string format(double value) { return std::to_string(value); }
std::string format(const std::string& value) { return value; }
std::string format(bool value) { return value ? "true" : "false"; }

// 编译器自动选择
format(3.14);        // 调用 format(double)
format("hello");     // 调用 format(const std::string&)

1.2 ArkTS 重载语法

ArkTS 的函数重载与 C++ 有本质区别:不是写多个同名函数,而是写多个重载签名 + 一个统一实现

// 重载签名(只有声明,没有实现)
function format(value: number): string;
function format(value: string): string;
function format(value: boolean): string;

// 实现签名(必须兼容所有重载签名)
function format(value: number | string | boolean): string {
    if (typeof value === 'number') {
        return value.toFixed(2);
    } else if (typeof value === 'string') {
        return value.toUpperCase();
    } else {
        return value ? "是" : "否";
    }
}

// 使用
let r1: string = format(3.14159);     // "3.14"
let r2: string = format("hello");     // "HELLO"
let r3: string = format(true);        // "是"

关键要点:

  1. 重载签名可以有多个,每个定义一种调用方式
  2. 实现签名只有一个,参数类型必须是所有重载签名的联合类型
  3. 实现签名内部用 typeof 判断实际类型,执行不同逻辑

1.3 参数类型重载

根据参数类型的不同执行不同逻辑:

// 处理设备ID:可以是数字ID或设备名称
function findDevice(id: number): string;
function findDevice(name: string): string;

function findDevice(param: number | string): string {
    if (typeof param === 'number') {
        return `查找ID为 ${param} 的设备`;
    } else {
        return `查找名称为 "${param}" 的设备`;
    }
}

console.log(findDevice(1001));           // "查找ID为 1001 的设备"
console.log(findDevice("客厅灯"));        // "查找名称为 "客厅灯" 的设备"

另一个例子 - 计算面积:

// 计算不同形状的面积
function calculateArea(radius: number): number;                    // 圆
function calculateArea(width: number, height: number): number;     // 矩形

function calculateArea(a: number, b?: number): number {
    if (b === undefined) {
        // 圆形:π * r²
        return 3.14159 * a * a;
    } else {
        // 矩形:宽 * 高
        return a * b;
    }
}

console.log(`圆面积: ${calculateArea(5)}`);           // 78.54
console.log(`矩形面积: ${calculateArea(4, 6)}`);      // 24

1.4 参数数量重载

根据参数个数的不同执行不同逻辑:

// 创建配置:不同数量的参数
function createConfig(name: string): string;
function createConfig(name: string, value: number): string;
function createConfig(name: string, value: number, unit: string): string;

function createConfig(name: string, value?: number, unit?: string): string {
    if (value === undefined) {
        return `配置项: ${name}`;
    } else if (unit === undefined) {
        return `配置项: ${name} = ${value}`;
    } else {
        return `配置项: ${name} = ${value} ${unit}`;
    }
}

console.log(createConfig("温度阈值"));                    // "配置项: 温度阈值"
console.log(createConfig("亮度", 80));                    // "配置项: 亮度 = 80"
console.log(createConfig("电压", 220, "V"));              // "配置项: 电压 = 220 V"

1.5 重载的注意事项

1. 实现签名必须兼容所有重载签名

// ❌ 错误:实现签名参数类型不包含所有重载签名的情况
function process(x: number): void;
function process(x: string): void;
function process(x: number): void {  // 错误!缺少 string 的处理
    // ...
}

// ✅ 正确:实现签名使用联合类型
function process(x: number): void;
function process(x: string): void;
function process(x: number | string): void {
    // ...
}

2. 重载签名顺序很重要

编译器会按顺序匹配重载签名,更具体的签名应该放在前面:

// ❌ 错误:通用签名在前,具体签名无法匹配
function handle(x: number | string): void;
function handle(x: number): void;  // 这行永远不会被匹配到

// ✅ 正确:具体签名在前
function handle(x: number): void;
function handle(x: string): void;
function handle(x: number | string): void {
    // ...
}

3. 返回值类型也要匹配

// 重载签名声明的返回值类型,实现签名必须满足
function getValue(id: number): number;
function getValue(name: string): string;

function getValue(param: number | string): number | string {
    if (typeof param === 'number') {
        return param * 10;  // 返回 number
    } else {
        return `Value of ${param}`;  // 返回 string
    }
}

C/C++对比:

  • C++ 重载是编译时决议,每个重载有独立的函数体
  • ArkTS 重载是运行时判断,只有一个函数体,内部用条件分支处理
  • ArkTS 重载更像 C 语言中用 void* 或联合体模拟的重载,但类型安全

第二部分:递归函数

2.1 递归的基本概念

递归是函数调用自身的编程技巧。作为有 14 年 C/C++ 经验的开发者,你对递归一定很熟悉。

递归函数必须包含两个要素:

  1. 终止条件(Base Case):防止无限递归
  2. 递归步骤(Recursive Step):向终止条件靠近
// 经典例子:计算阶乘
// n! = n * (n-1) * (n-2) * ... * 1
// 递归定义:n! = n * (n-1)!, 且 0! = 1, 1! = 1

function factorial(n: number): number {
    // 终止条件
    if (n <= 1) {
        return 1;
    }
    // 递归步骤
    return n * factorial(n - 1);
}

console.log(`5! = ${factorial(5)}`);   // 120
console.log(`0! = ${factorial(0)}`);   // 1

C/C++对比:

// C++ 实现完全相同
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

2.2 经典案例

案例1:斐波那契数列

// 斐波那契数列:0, 1, 1, 2, 3, 5, 8, 13, 21...
// F(0) = 0, F(1) = 1
// F(n) = F(n-1) + F(n-2)

function fibonacci(n: number): number {
    if (n === 0) {
        return 0;
    }
    if (n === 1) {
        return 1;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}

console.log(`F(10) = ${fibonacci(10)}`);   // 55
console.log(`F(20) = ${fibonacci(20)}`);   // 6765

案例2:计算幂次

// 计算 base 的 exponent 次幂
function power(base: number, exponent: number): number {
    // 终止条件:任何数的0次方都是1
    if (exponent === 0) {
        return 1;
    }
    // 负指数处理
    if (exponent < 0) {
        return 1 / power(base, -exponent);
    }
    // 递归步骤
    return base * power(base, exponent - 1);
}

console.log(`2^10 = ${power(2, 10)}`);     // 1024
console.log(`5^0 = ${power(5, 0)}`);       // 1
console.log(`2^-3 = ${power(2, -3)}`);     // 0.125

2.3 递归处理数组

案例1:递归求和

// 计算数组所有元素的和
function sumArray(arr: number[]): number {
    // 终止条件:空数组
    if (arr.length === 0) {
        return 0;
    }
    // 取第一个元素 + 剩余元素的和
    return arr[0] + sumArray(arr.slice(1));
}

let numbers: number[] = [1, 2, 3, 4, 5];
console.log(`数组求和: ${sumArray(numbers)}`);   // 15

案例2:递归查找最大值

// 递归查找数组中的最大值
function findMax(arr: number[]): number {
    // 终止条件:只有一个元素
    if (arr.length === 1) {
        return arr[0];
    }
    // 比较第一个元素和剩余元素的最大值
    let subMax: number = findMax(arr.slice(1));
    return arr[0] > subMax ? arr[0] : subMax;
}

let values: number[] = [23, 56, 12, 89, 34];
console.log(`最大值: ${findMax(values)}`);   // 89

案例3:递归查找元素

// 递归查找元素在数组中的索引,找不到返回 -1
function findIndex(arr: number[], target: number): number {
    return findIndexHelper(arr, target, 0);
}

function findIndexHelper(arr: number[], target: number, index: number): number {
    // 终止条件1:遍历完数组
    if (index >= arr.length) {
        return -1;
    }
    // 终止条件2:找到目标
    if (arr[index] === target) {
        return index;
    }
    // 递归步骤:检查下一个位置
    return findIndexHelper(arr, target, index + 1);
}

let data: number[] = [10, 20, 30, 40, 50];
console.log(`30 的索引: ${findIndex(data, 30)}`);   // 2
console.log(`99 的索引: ${findIndex(data, 99)}`);   // -1

2.4 递归注意事项

1. 终止条件的重要性

没有终止条件会导致无限递归,最终栈溢出:

// ❌ 错误:缺少终止条件
function badRecursive(n: number): number {
    return n + badRecursive(n - 1);  // 永远不会停止!
}

// ✅ 正确:有终止条件
function goodRecursive(n: number): number {
    if (n <= 0) {
        return 0;  // 终止条件
    }
    return n + goodRecursive(n - 1);
}

2. 栈溢出风险

每次递归调用都会消耗栈空间,递归深度过大时会导致栈溢出:

// 危险:计算大数的斐波那契会导致栈溢出
// fibonacci(10000);  // 可能栈溢出

// 优化方案1:使用尾递归(ArkTS引擎可能不优化,但代码更清晰)
function factorialTail(n: number, acc: number = 1): number {
    if (n <= 1) {
        return acc;
    }
    return factorialTail(n - 1, n * acc);
}

// 优化方案2:使用循环(推荐用于大数)
function factorialIterative(n: number): number {
    let result: number = 1;
    for (let i: number = 2; i <= n; i++) {
        result = result * i;
    }
    return result;
}

3. 递归深度建议

  • 一般递归深度建议控制在 1000 以内
  • 对于大数据量处理,优先考虑循环而非递归
  • 树形结构遍历适合用递归,因为深度通常可控

C/C++对比:

  • C/C++ 和 ArkTS 都有栈溢出风险
  • C++ 编译器可能对尾递归进行优化,ArkTS 引擎优化不确定
  • 嵌入式开发中,递归使用要格外谨慎(栈空间有限)

第三部分:typeof 类型守卫

3.1 问题引入

当函数参数是联合类型时,如何在函数体内安全地处理不同类型的值?

// 联合类型参数
function processValue(value: number | string): string {
    // ❌ 错误:无法确定 value 是 number 还是 string
    // return value.toFixed(2);  // 编译错误:string 没有 toFixed 方法
    
    // ❌ 错误:同样的问题
    // return value.toUpperCase();  // 编译错误:number 没有 toUpperCase 方法
    
    return "unknown";
}

我们需要一种机制在运行时判断类型,并帮助编译器收窄类型范围。


3.2 typeof 判断

ArkTS 提供 typeof 运算符来检测值的类型:

function processValue(value: number | string): string {
    if (typeof value === 'number') {
        // 在这个代码块中,编译器知道 value 是 number 类型
        return `数字: ${value.toFixed(2)}`;
    } else {
        // 在这个代码块中,编译器知道 value 是 string 类型
        return `字符串: ${value.toUpperCase()}`;
    }
}

console.log(processValue(3.14159));   // "数字: 3.14"
console.log(processValue("hello"));   // "字符串: HELLO"

typeof 可检测的类型:

  • "string" - 字符串
  • "number" - 数字
  • "boolean" - 布尔值
  • "undefined" - 未定义
  • "object" - 对象(注意:null 也是 object)
  • "function" - 函数

3.3 实际应用

应用1:格式化输出函数

function formatOutput(value: number | string | boolean): string {
    if (typeof value === 'number') {
        return `数值: ${value}`;
    } else if (typeof value === 'string') {
        return `文本: "${value}"`;
    } else {
        return `状态: ${value ? "开启" : "关闭"}`;
    }
}

console.log(formatOutput(42));        // "数值: 42"
console.log(formatOutput("设备A"));   // "文本: "设备A""
console.log(formatOutput(true));      // "状态: 开启"

应用2:安全计算函数

function safeAdd(a: number | string, b: number | string): string {
    if (typeof a === 'number' && typeof b === 'number') {
        // 两个都是数字,执行加法
        return `结果: ${a + b}`;
    } else {
        // 至少有一个是字符串,执行字符串拼接
        let strA: string = typeof a === 'number' ? a.toString() : a;
        let strB: string = typeof b === 'number' ? b.toString() : b;
        return `拼接: ${strA}${strB}`;
    }
}

console.log(safeAdd(10, 20));           // "结果: 30"
console.log(safeAdd("Hello", "World")); // "拼接: HelloWorld"
console.log(safeAdd(10, "个设备"));      // "拼接: 10个设备"

应用3:处理可选参数

function configureDevice(deviceId: number, name: string, extra?: number | string): void {
    console.log(`配置设备 ${deviceId}: ${name}`);
    
    if (extra !== undefined) {
        if (typeof extra === 'number') {
            console.log(`数值参数: ${extra}`);
        } else {
            console.log(`字符串参数: ${extra}`);
        }
    }
}

configureDevice(1001, "温度传感器");
configureDevice(1002, "湿度传感器", 5000);       // 数值参数
configureDevice(1003, "压力传感器", "kPa");      // 字符串参数

3.4 重要限制

typeof 用于运行时判断 vs 类型查询

ArkTS 中 typeof 有两种完全不同的用途,必须严格区分:

// ✅ 用途1:运行时类型判断(函数参数或局部变量均可)
function handleParam(value: number | string): void {
    if (typeof value === 'number') {
        console.log(`数字: ${value.toFixed(2)}`);
    } else {
        console.log(`字符串: ${value.toUpperCase()}`);
    }
}

// ✅ 局部变量同样可以使用 typeof 运行时判断
function handleLocal(): void {
    let value: number | string = 42;
    if (typeof value === 'number') {
        console.log(`数字: ${value}`);
    } else {
        console.log(`字符串: ${value}`);
    }
}

// ❌ 用途2:类型查询(ArkTS 禁止)
let x: number = 10;
type T = typeof x;  // 编译错误!ArkTS 禁止 typeof 用于类型查询

两种用途的本质区别:

用途 语法形式 ArkTS 是否支持
运行时类型判断 if (typeof x === 'number') ✅ 支持
编译期类型查询 type T = typeof x ❌ 禁止

C/C++对比:

  • C/C++ 没有 typeof 类型守卫的概念
  • C++ 用函数重载或模板处理多类型
  • C 语言通常用宏或 void* 配合类型标记处理

第四部分:阶段二总结

4.1 函数知识全景图

Day 06 - Day 08 三天所学内容的关系:

                    函数基础(Day 06)
                         │
        ┌────────────────┼────────────────┐
        │                │                │
   函数定义          参数类型          箭头函数
   function         必需/可选/默认       () => {}
        │                │                │
        └────────────────┼────────────────┘
                         │
              函数类型与回调(Day 07)
                         │
        ┌────────────────┼────────────────┐
        │                │                │
   函数类型          回调函数          高阶函数
   (x)=>T           作为参数          返回函数
        │                │                │
        └────────────────┼────────────────┘
                         │
              函数进阶(Day 08)
                         │
        ┌────────────────┼────────────────┐
        │                │                │
   函数重载          递归函数          类型守卫
   多签名+实现       自调用            typeof

4.2 关键概念对比表

参数类型对比:

类型 语法 调用时可省略 未提供时的值
必需参数 x: number -
可选参数 x?: number undefined
默认参数 x: number = 10 默认值
剩余参数 ...x: number[] []

函数形式对比:

特性 函数声明 箭头函数
语法 function foo() {} () => {}
适合场景 独立函数、递归 回调、简短函数
代码量 较多 简洁
可读性 清晰 需要适应

重载实现对比:

特性 C++ ArkTS
实现方式 多个函数体 一个函数体 + 条件分支
决议时机 编译时 运行时
性能 无运行时开销 有类型判断开销
灵活性 签名必须不同 可用联合类型统一处理

4.3 从 C/C++ 到 ArkTS 的函数差异总结

概念 C/C++ ArkTS 注意事项
函数声明 int add(int a, int b) function add(a: number, b: number): number 参数名在前,类型在后
无返回值 void func() function func(): void 语法类似
默认参数 int timeout = 5000 timeout: number = 5000 语法类似
函数指针 std::function / 函数指针 函数类型 / 箭头函数 ArkTS 更简洁
Lambda [](int x) { return x * 2; } (x: number) => x * 2 语法更短
函数重载 多个函数定义 多个签名 + 一个实现 实现方式完全不同
递归 支持 支持 都要注意栈溢出
类型判断 typeid / dynamic_cast typeof ArkTS 更简单

最重要的三点差异:

  1. 函数是第一类公民:ArkTS 中函数可以像变量一样赋值、传递、返回,比 C/C++ 的函数指针更自然

  2. 函数重载机制不同:C++ 是编译时多态,多个函数体;ArkTS 是运行时判断,一个函数体内部用 typeof 分支处理

  3. 类型守卫:ArkTS 的 typeof 可以在运行时收窄联合类型,C/C++ 需要依赖虚函数或 RTTI


第五部分:综合练习

练习 1:函数重载

编写一个 convertTemperature 函数,支持以下重载:

  • 接收摄氏度(number),返回华氏度(number)
  • 接收华氏度(number)和一个标识字符串 "F",返回摄氏度(number)

转换公式:

  • 摄氏度转华氏度:F = C × 9/5 + 32
  • 华氏度转摄氏度:C = (F - 32) × 5/9
参考答案
// 摄氏度转华氏度
function convertTemperature(celsius: number): number;
// 华氏度转摄氏度
function convertTemperature(fahrenheit: number, unit: string): number;

function convertTemperature(value: number, unit?: string): number {
    if (unit === undefined) {
        // 摄氏度转华氏度
        return value * 9 / 5 + 32;
    } else {
        // 华氏度转摄氏度
        return (value - 32) * 5 / 9;
    }
}

console.log(`25°C = ${convertTemperature(25)}°F`);        // 77
console.log(`77°F = ${convertTemperature(77, "F")}°C`);   // 25

练习 2:递归函数

2.1 实现 sumDigits 函数,递归计算一个正整数各位数字之和。

示例:sumDigits(12345) 返回 15(1+2+3+4+5)

2.2 实现 reverseString 函数,递归反转字符串。

示例:reverseString("hello") 返回 "olleh"

参考答案
// 2.1 各位数字之和
function sumDigits(n: number): number {
    if (n < 10) {
        return n;
    }
    return n % 10 + sumDigits(Math.floor(n / 10));
}

console.log(`sumDigits(12345) = ${sumDigits(12345)}`);  // 15

// 2.2 递归反转字符串
function reverseString(str: string): string {
    if (str.length <= 1) {
        return str;
    }
    return reverseString(str.substring(1)) + str.charAt(0);
}

console.log(`reverseString("hello") = ${reverseString("hello")}`);  // olleh

练习 3:typeof 类型守卫

编写 processSensorData 函数,参数为 data: number | number[] | string

  • 如果是 number,返回 "单个读数: X"
  • 如果是 number[],返回 "批量读数: X个数据"
  • 如果是 string,返回 "状态信息: X"
参考答案
function processSensorData(data: number | number[] | string): string {
    if (typeof data === 'number') {
        return `单个读数: ${data}`;
    } else if (typeof data === 'string') {
        return `状态信息: ${data}`;
    } else {
        // 到这里 data 一定是 number[]
        return `批量读数: ${data.length}个数据`;
    }
}

console.log(processSensorData(25.5));                    // "单个读数: 25.5"
console.log(processSensorData([20, 21, 22, 23]));        // "批量读数: 4个数据"
console.log(processSensorData("传感器正常"));             // "状态信息: 传感器正常"

练习 4:综合应用

实现一个 calculate 函数,支持以下重载:

  • calculate(a: number, b: number): number - 返回两数之和
  • calculate(a: number, b: number, operation: string): number - 根据 operation 执行不同运算

operation 可以是:

  • "add" - 加法
  • "subtract" - 减法
  • "multiply" - 乘法
  • "divide" - 除法(注意除零检查)
参考答案
// 两数相加
function calculate(a: number, b: number): number;
// 指定运算
function calculate(a: number, b: number, operation: string): number;

function calculate(a: number, b: number, operation?: string): number {
    let op: string = operation ?? "add";

    if (op === "add") {
        return a + b;
    } else if (op === "subtract") {
        return a - b;
    } else if (op === "multiply") {
        return a * b;
    } else if (op === "divide") {
        if (b === 0) {
            return 0;
        }
        return a / b;
    } else {
        return 0;
    }
}

console.log(`10 + 5 = ${calculate(10, 5)}`);
console.log(`10 - 5 = ${calculate(10, 5, "subtract")}`);
console.log(`10 * 5 = ${calculate(10, 5, "multiply")}`);
console.log(`10 / 5 = ${calculate(10, 5, "divide")}`);
console.log(`10 / 0 = ${calculate(10, 0, "divide")}`);

练习 5-8:更多练习

练习 5:编写递归函数 countDown,从 n 倒数到 0,每步打印当前数字。

练习 6:编写函数 formatDeviceInfo,参数为 id: number | string,用重载实现:

  • number 类型:返回 "设备ID: X"
  • string 类型:返回 "设备名称: X"

练习 7:编写 safeDivide 函数,参数为 a: number | string, b: number | string,使用 typeof 判断类型,如果都是数字则返回除法结果,否则返回错误信息字符串。

练习 8:实现递归函数 arraySum,计算多维数组(number[][])中所有数字的和。


附录:ArkTS 函数速查表

// 函数声明
function add(a: number, b: number): number {
    return a + b;
}

// 箭头函数
let add = (a: number, b: number): number => a + b;

// 可选参数
function greet(name: string, greeting?: string): void {
    let msg: string = greeting ?? "Hello";
    console.log(`${msg}, ${name}`);
}

// 默认参数
function power(base: number, exponent: number = 2): number {
    return Math.pow(base, exponent);
}

// 剩余参数
function sum(...numbers: number[]): number {
    let total: number = 0;
    for (let i: number = 0; i < numbers.length; i++) {
        total += numbers[i];
    }
    return total;
}

// 函数重载
function process(x: number): number;
function process(x: string): string;
function process(x: number | string): number | string {
    if (typeof x === 'number') {
        return x * 2;
    } else {
        return x.toUpperCase();
    }
}

// typeof 类型守卫
function handle(value: number | string): void {
    if (typeof value === 'number') {
        console.log(`数字: ${value}`);
    } else {
        console.log(`字符串: ${value}`);
    }
}

// 递归函数
function factorial(n: number): number {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
posted @ 2026-04-13 11:39  thammer  阅读(6)  评论(0)    收藏  举报