3.4 计算属性

> **本节目标**:完整掌握 `#[computed]` 宏的依赖追踪、缓存机制、使用场景与最佳实践。

3.4.1 计算属性的定义

计算属性是基于其他字段自动计算的派生值。它有两个核心特性:

1. **自动依赖追踪**:自动追踪计算过程中访问的字段 2. **结果缓存**:依赖字段未变化时,不重新计算,直接返回缓存值

#[derive(IModel)]
#[component]
pub struct TodoViewModel {
    pub todos: Vec<TodoItem>,
}

impl TodoViewModel {
    #[computed]
    pub fn completed_count(&self) -> usize {
        // 自动追踪依赖:self.todos
        self.todos.iter().filter(|t| t.done).count()
    }
}
<!-- 像访问字段一样访问计算属性 -->
<p>已完成: {completed_count}</p>

3.4.2 为什么需要计算属性

问题:在插值中写复杂表达式

<!-- ❌ 每次重绘都重新计算 -->
<p>已完成: {todos.iter().filter(|t| t.done).count()}</p>
<p>待办: {todos.iter().filter(|t| !t.done).count()}</p>
<p>进度: {todos.iter().filter(|t| t.done).count() as f64 / todos.len() as f64 * 100.0}%</p>

三个插值都遍历 `todos`,重复计算。

解决:用计算属性缓存

#[computed]
pub fn completed_count(&self) -> usize {
    self.todos.iter().filter(|t| t.done).count()
}

#[computed]
pub fn pending_count(&self) -> usize {
    self.todos.len() - self.completed_count()
}

#[computed]
pub fn progress(&self) -> f64 {
    if self.todos.is_empty() {
        0.0
    } else {
        self.completed_count() as f64 / self.todos.len() as f64 * 100.0
    }
}
<!-- ✅ 计算属性缓存,多个插值共享结果 -->
<p>已完成: {completed_count}</p>
<p>待办: {pending_count}</p>
<p>进度: {progress}%</p>

3.4.3 依赖追踪机制

`#[computed]` 在编译期分析方法体,自动识别访问的 `self` 字段:

#[computed]
pub fn full_name(&self) -> SharedString {
    // 访问了 self.first_name 和 self.last_name
    format!("{} {}", self.first_name, self.last_name).into()
}

编译期生成的代码(简化示意):

pub fn full_name(&self) -> SharedString {
    // 检查缓存:如果 first_name 和 last_name 未变化,返回缓存
    if self.computed_cache.full_name.is_valid(&[&self.first_name, &self.last_name]) {
        return self.computed_cache.full_name.value.clone();
    }

    // 重新计算
    let result = format!("{} {}", self.first_name, self.last_name).into();

    // 更新缓存
    self.computed_cache.full_name.update(result.clone(), &[&self.first_name, &self.last_name]);

    result
}

依赖的类型

计算属性可以依赖:

- **字段**:`self.field_name` - **其他计算属性**:`self.other_computed()` - **字段方法**:`self.todos.len()`(依赖 `todos`)

#[computed]
pub fn summary(&self) -> SharedString {
    // 依赖 todos(通过 len())和 completed_count(计算属性)
    format!("共 {} 项,已完成 {} 项", self.todos.len(), self.completed_count()).into()
}

3.4.4 计算属性的缓存

缓存的有效性

缓存基于依赖字段的"版本号"。每次 `cx.notify()` 后,相关字段的版本号递增,缓存失效。

字段版本号:
  todos: v3
  completed_count 缓存依赖 todos:v3

调用 cx.notify() 修改 todos:
  todos: v4
  completed_count 缓存失效(todos 版本变化)

下次访问 completed_count:
  重新计算,更新缓存为 todos:v4

缓存的生命周期

缓存与 ViewModel 实例绑定,ViewModel 销毁时缓存自动释放。

3.4.5 计算属性的使用场景

场景一:派生显示值

#[computed]
pub fn display_status(&self) -> SharedString {
    match self.status {
        Status::Loading => "加载中...".into(),
        Status::Success => "加载成功".into(),
        Status::Error(ref msg) => format!("错误: {}", msg).into(),
    }
}
<p>{display_status}</p>

场景二:过滤列表

#[computed]
pub fn pending_todos(&self) -> Vec<&TodoItem> {
    self.todos.iter().filter(|t| !t.done).collect()
}
<ul>
    <li each={todo in pending_todos} key={todo.id}>{todo.text}</li>
</ul>

场景三:格式化输出

#[computed]
pub fn formatted_price(&self) -> SharedString {
    format!("¥{:.2}", self.price).into()
}

#[computed]
pub fn formatted_date(&self) -> SharedString {
    self.timestamp.format("%Y-%m-%d %H:%M").to_string().into()
}
<p>价格: {formatted_price}</p>
<p>时间: {formatted_date}</p>

场景四:条件判断

#[computed]
pub fn can_submit(&self) -> bool {
    !self.user_name.is_empty() && !self.password.is_empty() && !self.is_submitting
}
<button disabled={!can_submit} on-click={submit}>提交</button>

场景五:聚合统计

#[computed]
pub fn total_price(&self) -> f64 {
    self.cart.iter().map(|item| item.price * item.quantity as f64).sum()
}

#[computed]
pub fn total_count(&self) -> usize {
    self.cart.iter().map(|item| item.quantity).sum()
}
<p>共 {total_count} 件商品,总计 {total_price}</p>

3.4.6 计算属性的规则

规则一:必须是只读方法

计算属性不能修改 `self`:

#[computed]
pub fn bad_computed(&mut self) -> i32 {  // ❌ 不能是 &mut self
    self.count += 1;
    self.count
}

#[computed]
pub fn good_computed(&self) -> i32 {  // ✅ 必须是 &self
    self.count + 1
}

规则二:不能有参数

#[computed]
pub fn with_param(&self, x: i32) -> i32 {  // ❌ 不能有参数
    self.count + x
}

如果需要参数化计算,应该写成普通方法,在插值中调用:

<p>{calculate_with(5)}</p>

但这样会失去缓存。更好的做法是为每个常用参数组合定义计算属性:

#[computed]
pub fn count_plus_5(&self) -> i32 {
    self.count + 5
}

规则三:返回值必须可克隆

计算属性的返回值会被缓存,因此必须实现 `Clone`:

#[computed]
pub fn items_ref(&self) -> &Vec<Item> {  // ❌ 返回引用无法缓存
    &self.items
}

#[computed]
pub fn items_count(&self) -> usize {  // ✅ usize 实现 Clone
    self.items.len()
}

3.4.7 计算属性的依赖链

计算属性可以依赖其他计算属性,形成依赖链:

#[computed]
pub fn completed_count(&self) -> usize {
    self.todos.iter().filter(|t| t.done).count()
}

#[computed]
pub fn pending_count(&self) -> usize {
    self.todos.len() - self.completed_count()  // 依赖 completed_count
}

#[computed]
pub fn progress_percent(&self) -> f64 {
    if self.todos.is_empty() {
        0.0
    } else {
        self.completed_count() as f64 / self.todos.len() as f64 * 100.0
    }
}

依赖链的缓存传播:

todos 变化
    ↓
completed_count 缓存失效
    ↓
pending_count 缓存失效(依赖 completed_count)
    ↓
progress_percent 缓存失效(依赖 completed_count)

3.4.8 计算属性 vs 普通方法

| 特性 | 计算属性 `#[computed]` | 普通方法 | | ------ | ------------------- | --------------- | | 缓存 | ✅ 自动缓存 | ❌ 每次调用都执行 | | 依赖追踪 | ✅ 自动 | ❌ 无 | | 参数 | ❌ 无参数 | ✅ 可以有参数 | | 修改 self | ❌ 只读 | ✅ 可以修改(需 `&mut self`) | | 调用方式 | `{name}`(像字段) | `{name(args)}`(像方法) |

选择建议

- **纯计算、依赖明确**:用 `#[computed]` - **需要参数**:用普通方法 - **需要修改状态**:用 `#[command]` - **简单的一次性计算**:直接在插值中写表达式

3.4.9 调试计算属性

查看依赖

用 `cargo rml-expand` 查看生成的代码,可以看到计算属性的依赖列表:

cargo rml-expand views::todo

输出会包含:

// #[computed] completed_count
// dependencies: [todos]
pub fn completed_count(&self) -> usize {
    self.todos.iter().filter(|t| t.done).count()
}

验证缓存

在计算属性中加日志,验证是否被缓存:

#[computed]
pub fn expensive_calc(&self) -> f64 {
    log::info!("expensive_calc 被调用");
    // ... 复杂计算
}

如果日志只在依赖变化时打印,说明缓存生效。

3.4.10 计算属性的最佳实践

1. 保持计算属性纯函数

// ✅ 纯函数:只读 self,无副作用
#[computed]
pub fn display_name(&self) -> SharedString {
    format!("{} ({})", self.name, self.id).into()
}

// ❌ 有副作用:修改了其他状态
#[computed]
pub fn bad_computed(&self) -> i32 {
    self.cache.hit_count += 1;  // 副作用!
    self.count + 1
}

2. 避免过深的依赖链

// ❌ 依赖链过深,缓存失效传播成本高
#[computed] pub fn a(&self) -> i32 { self.x + 1 }
#[computed] pub fn b(&self) -> i32 { self.a() + 1 }
#[computed] pub fn c(&self) -> i32 { self.b() + 1 }
#[computed] pub fn d(&self) -> i32 { self.c() + 1 }

// ✅ 直接计算,减少中间层
#[computed]
pub fn d(&self) -> i32 {
    self.x + 4
}

3. 计算属性命名用名词

// ✅ 名词:像字段
#[computed] pub fn completed_count(&self) -> usize { ... }
#[computed] pub fn display_name(&self) -> SharedString { ... }

// ❌ 动词:像方法
#[computed] pub fn calculate_count(&self) -> usize { ... }
#[computed] pub fn get_name(&self) -> SharedString { ... }

3.4.11 小结

计算属性是 RML 绑定系统的核心能力:

- **自动依赖追踪**:编译期分析方法体,识别依赖字段 - **结果缓存**:依赖未变化时直接返回缓存 - **像字段访问**:在 `.rml` 中用 `{name}` 访问,无需括号 - **只读纯函数**:不修改 `self`,无副作用

最佳实践:**任何在 `.rml` 中出现的复杂表达式,都应提取为计算属性**。这既提升性能(缓存),又提升可读性(命名)。

下一节 → [3.5 值转换器](./converter.md)