一、本课目标
掌握 map_state 的用法——当子组件需要展示的数据不是状态中的某个字段,而是从状态计算出来的派生值时,lens 无能为力,map_state 才是正确的工具。
二、问题场景:lens 的边界在哪里?
回顾第17课的 lens,它的第二个参数(lens 闭包)必须返回父状态中某个字段的 &mut 引用:
lens(
|data: &mut i32| counter_view(data),
|data: &mut AppState| &mut data.count, // 只能返回字段的 &mut 引用
)
这在"子组件需要直接编辑某个字段"时完美工作。但考虑这个场景:
struct AppState {
celsius: f64,
}
你想在界面上同时显示摄氏度和华氏度。摄氏度是字段,可以直接用 lens 取。但华氏度不是字段——它是 celsius * 9.0 / 5.0 + 32.0 的计算结果。
你试着用 lens:
lens(
|data: &mut f64| label(format!("华氏: {:.1}°F", data)),
|data: &mut AppState| {
let fahrenheit = data.celsius * 9.0 / 5.0 + 32.0;
&mut fahrenheit // ❌ 编译错误!返回了局部变量的引用
},
)
编译器报错:returns a reference to data owned by the current function。这是第17课练习三已经讲过的问题——lens 闭包不能返回临时值的引用。
lens 的设计初衷是字段访问,不是数据变换。当子组件需要的数据需要经过计算才能得出时,lens 就到极限了。
map_state 就是为这个场景而生的。
三、map_state 的核心思想
map_state 和 lens 的根本区别在于映射方向:
lens——从父状态中取出一个字段的 &mut 引用,交给子组件。子组件可以直接修改这个字段。是双向的:读和写都通过同一个引用。
map_state——从父状态中计算出一个值,交给子组件。子组件只能读取这个值,不能修改它。是单向的:只能读,不能写。
用一句话概括:lens 用于"编辑字段",map_state 用于"展示派生数据"。
四、完整示例:温度转换器
4.1 Cargo.toml
[package]
name = "xilem-map-state-demo"
version = "0.1.0"
edition = "2024"
[dependencies]
xilem = "0.4"
winit = "0.30"
4.2 main.rs
use winit::error::EventLoopError;
use xilem::view::{Axis, flex, label, text_button, lens, map_state};
use xilem::{EventLoop, WidgetView, WindowOptions, Xilem};
#[derive(Default)]
struct AppState {
celsius: f64,
}
// —- 子组件 1:摄氏度编辑器(用 lens,编辑真实字段)—-
fn celsius_editor(data: &mut f64) -> impl WidgetView<f64> + use<> {
flex(Axis::Horizontal, (
label(format!("{:.1}°C", data)),
text_button("+1°C", |data: &mut f64| *data += 1.0),
text_button("-1°C", |data: &mut f64| *data -= 1.0),
))
}
// —- 子组件 2:华氏度显示器(用 map_state,只读派生值)—-
fn fahrenheit_display(data: &f64) -> impl WidgetView<()> + use<> {
label(format!("{:.1}°F", data))
}
// —- 根视图 —-
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
flex(Axis::Vertical, (
// lens:从 AppState 取出 &mut f64(celsius 字段),可编辑
lens(
|data: &mut f64| celsius_editor(data),
|data: &mut AppState| &mut data.celsius,
),
// map_state:从 AppState 计算出 f64(华氏度),只读
map_state(
|data: &f64| fahrenheit_display(data),
|data: &AppState| data.celsius * 9.0 / 5.0 + 32.0,
),
// 根视图自己的控件
text_button("设为 100°C", |data: &mut AppState| {
data.celsius = 100.0;
}),
text_button("设为 0°C", |data: &mut AppState| {
data.celsius = 0.0;
}),
))
}
fn main() -> Result<(), EventLoopError> {
let app = Xilem::new_simple(
AppState::default(),
app_logic,
WindowOptions::new("温度转换器"),
);
app.run_in(EventLoop::with_user_event())?;
Ok(())
}
五、逐行拆解
5.1 状态定义
#[derive(Default)]
struct AppState {
celsius: f64,
}
只有一个字段 celsius。华氏度不是字段,它是从 celsius 计算出来的。
5.2 摄氏度编辑器(lens 子组件)
fn celsius_editor(data: &mut f64) -> impl WidgetView<f64> + use<> {
flex(Axis::Horizontal, (
label(format!("{:.1}°C", data)),
text_button("+1°C", |data: &mut f64| *data += 1.0),
text_button("-1°C", |data: &mut f64| *data -= 1.0),
))
}
参数是 &mut f64——它可以读取和修改摄氏度。按钮的闭包通过 *data += 1.0 直接修改字段值。这是标准的 lens 用法:子组件拥有对真实字段的可变访问权。
5.3 华氏度显示器(map_state 子组件)
fn fahrenheit_display(data: &f64) -> impl WidgetView<()> + use<> {
label(format!("{:.1}°F", data))
}
注意参数类型:&f64,不是 &mut f64。它是不可变引用——只能读,不能写。这个组件的职责就是"显示一个数值",它不需要也不应该修改这个值。
5.4 根视图:lens 和 map_state 的配合
fn app_logic(data: &mut AppState) -> impl WidgetView<AppState> + use<> {
flex(Axis::Vertical, (
// lens:取出 &mut f64,可编辑
lens(
|data: &mut f64| celsius_editor(data),
|data: &mut AppState| &mut data.celsius,
),
// map_state:计算出 f64,只读
map_state(
|data: &f64| fahrenheit_display(data),
|data: &AppState| data.celsius * 9.0 / 5.0 + 32.0,
),
text_button("设为 100°C", |data: &mut AppState| {
data.celsius = 100.0;
}),
text_button("设为 0°C", |data: &mut AppState| {
data.celsius = 0.0;
}),
))
}
map_state 接收两个参数:
第一个参数:子 View 函数。它接收 &子状态类型(不可变引用),返回一个 WidgetView。
第二个参数:映射闭包。它接收 &父状态(不可变引用),返回一个owned 值(注意不是引用)。这个闭包可以包含任意计算逻辑——乘法、除法、字符串拼接、条件判断,什么都行。
对比 lens 的第二个参数:
// lens:返回 &mut 引用,只能指向已有字段
|data: &mut AppState| &mut data.celsius
// map_state:返回 owned 值,可以是任意计算结果
|data: &AppState| data.celsius * 9.0 / 5.0 + 32.0
六、数据流走一遍
- lens 取出 &mut data.celsius(值为 0.0)→ celsius_editor 显示 “0.0°C” + 两个按钮
- map_state 计算 0.0 * 9.0 / 5.0 + 32.0 = 32.0 → fahrenheit_display 显示 “32.0°F”
关键点:华氏度的更新不需要任何额外代码。只要你修改了 celsius,map_state 的映射闭包就会在下一次 app_logic 执行时自动重新计算。
七、运行
cargo run
弹出一个窗口,上方显示摄氏度数值和"+1°C""-1°C"按钮,中间显示华氏度数值(自动换算),下方是"设为 100°C"和"设为 0°C"快捷按钮。
点击"+1°C",摄氏度加一,华氏度自动更新。点击"设为 100°C",摄氏度变为 100.0,华氏度自动变为 212.0。
八、map_state 的底层原理
map_state 在 xilem_core 中对应的 View 类型是 MapState。
pub struct MapState<C, F> {
child: C,
map_fn: F,
}
- C 是子 View 的类型
- F 是映射闭包的类型
当框架调用 build 或 rebuild 时,MapState View 会:
对比 Lens 的行为:
核心差异在于引用的可变性:Lens 给子 View 的是 &mut,MapState 给子 View 的是 &。这决定了子 View 能不能修改数据。
九、lens vs map_state 选型指南
用 lens 的场景——子组件需要编辑状态中的某个字段。典型例子:输入框绑定到 name 字段、计数器绑定到 count 字段、开关绑定到 is_enabled 字段。
用 map_state 的场景——子组件需要展示一个从状态计算出来的值。典型例子:摄氏度→华氏度换算、总价=单价×数量、进度百分比=已完成/总数×100。
判断标准:问自己一个问题——"子组件需要修改这个值吗?"如果需要,用 lens;如果只需要展示,用 map_state。
能不能用 map_state 替代 lens? 不能。map_state 给子组件的是 &(不可变引用),子组件无法修改数据。如果你的子组件有按钮、输入框等交互控件需要修改状态,必须用 lens。
能不能用 lens 替代 map_state? 不能。lens 闭包必须返回字段的 &mut 引用,不能返回计算结果。如果你需要的值不是某个字段的直接引用,而是经过计算的,lens 做不到。
十、练习
练习一:填空题——补全 map_state 调用
以下代码中有一个购物车状态,需要在界面上显示商品数量和小计金额。小计金额是派生数据(price × quantity)。请补全 app_logic 中的 map_state 调用。
use xilem::view::{Axis, flex, label, text_button, lens, map_state};
use xilem::{WidgetView};
#[derive(Default)]
struct Cart {
price: f64,
quantity: u32,
}
fn quantity_editor(data: &mut u32) -> impl WidgetView<u32> + use<> {
flex(Axis::Horizontal, (
label(format!("数量: {}", data)),
text_button("+1", |data: &mut u32| *data += 1),
text_button("-1", |data: &mut u32| {
if *data > 0 { *data -= 1; }
}),
))
}
fn subtotal_display(data: &f64) -> impl WidgetView<()> + use<> {
label(format!("小计: {:.2} 元", data))
}
fn app_logic(data: &mut Cart) -> impl WidgetView<Cart> + use<> {
flex(Axis::Vertical, (
// 填空①:用 lens 把 quantity_editor 连接到 data.quantity
lens(______, ______),
// 填空②:用 map_state 计算小计(price × quantity),传给 subtotal_display
map_state(______, ______),
))
}
要求:
- 填空①:写出 lens 的两个参数
- 填空②:写出 map_state 的两个参数(第二个参数是一个计算表达式)
练习二:改造题——BMI 计算器
要求:
struct BmiState {
height_cm: f64, // 身高(厘米)
weight_kg: f64, // 体重(公斤)
}
// 你的代码:实现 Default、height_editor、weight_editor、bmi_display、app_logic、main
练习三:思考题——map_state 的子组件能不能有按钮?
假设你写了这样的代码:
fn editable_display(data: &f64) -> impl WidgetView<()> + use<> {
flex(Axis::Horizontal, (
label(format!("值: {}", data)),
text_button("+1", |data: &mut f64| *data += 1.0), // ❌
))
}
问题:
练习四:改造题——多派生值 + 嵌套计算
要求:
struct OrderState {
unit_price: f64,
quantity: u32,
discount_rate: f64, // 折扣率,0.0 到 1.0
}
- 原价小计 = unit_price × quantity
- 折扣金额 = 原价小计 × (1.0 – discount_rate)
- 实付金额 = 原价小计 × discount_rate
提示:你需要三个 map_state 调用,每个计算一个派生值。
// 你的代码:实现 Default、各子组件、app_logic、main
练习答案与知识要点
练习一答案
fn app_logic(data: &mut Cart) -> impl WidgetView<Cart> + use<> {
flex(Axis::Vertical, (
// 填空①
lens(
|data: &mut u32| quantity_editor(data),
|data: &mut Cart| &mut data.quantity,
),
// 填空②
map_state(
|data: &f64| subtotal_display(data),
|data: &Cart| data.price * data.quantity as f64,
),
))
}
解读:lens 的第二个参数返回 &mut data.quantity——这是字段的直接引用,子组件可以编辑它。map_state 的第二个参数返回 data.price * data.quantity as f64——这是一个计算结果(owned f64),不是任何字段的引用。注意 data.quantity as f64 的类型转换:quantity 是 u32,price 是 f64,相乘前需要统一类型。map_state 的映射闭包接收 &Cart(不可变引用),所以它只能读取字段,不能修改。
练习二答案
use winit::error::EventLoopError;
use xilem::view::{Axis, flex, label, text_button, lens, map_state};
use xilem::{EventLoop, WidgetView, WindowOptions, Xilem};
struct BmiState {
height_cm: f64,
weight_kg: f64,
}
impl Default for BmiState {
fn default() -> Self {
BmiState {
height_cm: 170.0,
weight_kg: 65.0,
}
}
}
fn height_editor(data: &mut f64) -> impl WidgetView<f64> + use<> {
flex(Axis::Horizontal, (
label(format!("身高: {:.1} cm", data)),
text_button("+1", |data: &mut f64| *data += 1.0),
text_button("-1", |data: &mut f64| *data -= 1.0),
))
}
fn weight_editor(data: &mut f64) -> impl WidgetView<f64> + use<> {
flex(Axis::Horizontal, (
label(format!("体重: {:.1} kg", data)),
text_button("+0.5", |data: &mut f64| *data += 0.5),
text_button("-0.5", |data: &mut f64| *data -= 0.5),
))
}
fn bmi_display(data: &f64) -> impl WidgetView<()> + use<> {
let category = if *data < 18.5 {
"偏瘦"
} else if *data < 24.0 {
"正常"
} else if *data < 28.0 {
"偏胖"
} else {
"肥胖"
};
label(format!("BMI: {:.1} ({})", data, category))
}
fn app_logic(data: &mut BmiState) -> impl WidgetView<BmiState> + use<> {
flex(Axis::Vertical, (
lens(
|data: &mut f64| height_editor(data),
|data: &mut BmiState| &mut data.height_cm,
),
lens(
|data: &mut f64| weight_editor(data),
|data: &mut BmiState| &mut data.weight_kg,
),
map_state(
|data: &f64| bmi_display(data),
|data: &BmiState| {
let height_m = data.height_cm / 100.0;
data.weight_kg / (height_m * height_m)
},
),
))
}
fn main() -> Result<(), EventLoopError> {
let app = Xilem::new_simple(
BmiState::default(),
app_logic,
WindowOptions::new("BMI 计算器"),
);
app.run_in(EventLoop::with_user_event())?;
Ok(())
}
解读:这道题考察的是"在 map_state 映射闭包中做多步计算"。BMI 的计算需要先把身高从厘米转换为米(data.height_cm / 100.0),再套用公式 weight / (height_m²)。这个计算过程涉及多步运算,但 map_state 的映射闭包可以包含任意复杂的逻辑——它不受"只能返回字段引用"的限制。
bmi_display 子组件内部根据 BMI 值判断分类(偏瘦/正常/偏胖/肥胖),这个判断是在子组件内部完成的,不是在映射闭包中。映射闭包只负责计算 BMI 数值,分类逻辑属于视图层的展示职责。
练习三答案
不能编译。
编译器会报错,大意是:text_button 的闭包要求 FnMut(&mut f64),但 data 的类型是 &f64(不可变引用),不能隐转换为 &mut f64。
原因:map_state 给子组件传递的是 &f64(不可变引用)。子组件的所有闭包都只能拿到 &f64,不可能拿到 &mut f64。而 text_button 的回调需要 FnMut(&mut State) 类型的闭包——它需要可变引用来修改状态。&f64 无法提供 &mut f64,所以编译失败。
这是设计上的刻意限制:map_state 的语义就是"只读映射"。如果你需要一个能编辑的值,应该用 lens 把它绑定到真实字段上。
如果你确实需要一个"能编辑派生值"的组件,正确的做法是:不要编辑派生值本身,而是编辑产生这个派生值的源字段。比如在温度转换器的例子中,不要试图直接编辑华氏度,而是编辑摄氏度——华氏度会自动跟着变。这就是单向数据流的核心思想:派生数据永远是源数据的"影子",你只需要修改源头。
练习四答案
use winit::error::EventLoopError;
use xilem::view::{Axis, flex, label, text_button, lens, map_state};
use xilem::{EventLoop, WidgetView, WindowOptions, Xilem};
struct OrderState {
unit_price: f64,
quantity: u32,
discount_rate: f64,
}
impl Default for OrderState {
fn default() -> Self {
OrderState {
unit_price: 100.0,
quantity: 1,
discount_rate: 1.0,
}
}
}
fn price_editor(data: &mut f64) -> impl WidgetView<f64> + use<> {
flex(Axis::Horizontal, (
label(format!("单价: {:.0} 元", data)),
text_button("+10", |data: &mut f64| *data += 10.0),
text_button("-10", |data: &mut f64| {
if *data >= 10.0 { *data -= 10.0; }
}),
))
}
fn quantity_editor(data: &mut u32) -> impl WidgetView<u32> + use<> {
flex(Axis::Horizontal, (
label(format!("数量: {}", data)),
text_button("+1", |data: &mut u32| *data += 1),
text_button("-1", |data: &mut u32| {
if *data > 1 { *data -= 1; }
}),
))
}
fn discount_editor(data: &mut f64) -> impl WidgetView<f64> + use<> {
let rate_text = format!("{:.0}折", data * 10.0);
flex(Axis::Horizontal, (
label(format!("折扣: {}", rate_text)),
text_button("9折", |data: &mut f64| *data = 0.9),
text_button("8折", |data: &mut f64| *data = 0.8),
text_button("恢复原价", |data: &mut f64| *data = 1.0),
))
}
fn money_display(data: &f64) -> impl WidgetView<()> + use<> {
label(format!("{:.2} 元", data))
}
fn app_logic(data: &mut OrderState) -> impl WidgetView<OrderState> + use<> {
flex(Axis::Vertical, (
// 三个 lens:编辑真实字段
lens(
|data: &mut f64| price_editor(data),
|data: &mut OrderState| &mut data.unit_price,
),
lens(
|data: &mut u32| quantity_editor(data),
|data: &mut OrderState| &mut data.quantity,
),
lens(
|data: &mut f64| discount_editor(data),
|data: &mut OrderState| &mut data.discount_rate,
),
// 三个 map_state:展示派生值
label("—- 计算结果 —-"),
map_state(
|data: &f64| {
flex(Axis::Horizontal, (
label("原价小计: "),
label(format!("{:.2} 元", data)),
))
},
|data: &OrderState| data.unit_price * data.quantity as f64,
),
map_state(
|data: &f64| {
flex(Axis::Horizontal, (
label("折扣金额: "),
label(format!("{:.2} 元", data)),
))
},
|data: &OrderState| {
let subtotal = data.unit_price * data.quantity as f64;
subtotal * (1.0 – data.discount_rate)
},
),
map_state(
|data: &f64| {
flex(Axis::Horizontal, (
label("实付金额: "),
label(format!("{:.2} 元", data)),
))
},
|data: &OrderState| {
let subtotal = data.unit_price * data.quantity as f64;
subtotal * data.discount_rate
},
),
))
}
fn main() -> Result<(), EventLoopError> {
let app = Xilem::new_simple(
OrderState::default(),
app_logic,
WindowOptions::new("订单计算器"),
);
app.run_in(EventLoop::with_user_event())?;
Ok(())
}
解读:这道题考察的是"多个 lens + 多个 map_state 混合使用"的模式。三个字段(unit_price、quantity、discount_rate)各用一个 lens 连接编辑器,三个派生值(原价小计、折扣金额、实付金额)各用一个 map_state 连接显示器。
注意三个 map_state 的映射闭包都读取了多个字段(unit_price、quantity、discount_rate),但它们都是只读的(&OrderState),不会互相冲突。这是 map_state 的优势:多个 map_state 可以同时读取同一个父状态的不同部分,甚至同一部分,因为它们拿到的都是不可变引用,不存在借用冲突。
如果你用 lens 来做同样的事,就会遇到借用检查器的问题——你不能同时从 &mut OrderState 中取出多个 &mut 引用给不同的 lens。但 map_state 只需要 &,多个 & 可以共存,所以没有这个问题。
知识要点
要点一:lens 和 map_state 的分工
lens 用于"编辑字段"——子组件需要 &mut 引用来修改状态中的某个字段。map_state 用于"展示派生数据"——子组件只需要 & 引用来读取一个计算结果。两者不是替代关系,而是互补关系:一个处理写入,一个处理读取。
要点二:map_state 映射闭包返回 owned 值
map_state 的第二个参数(映射闭包)接收 &父状态,返回一个owned 值(如 f64、String、bool)。这个值可以是任意计算的结果——乘法、除法、条件判断、字符串拼接,什么都行。这是它和 lens 的根本区别:lens 闭包必须返回字段的 &mut 引用,map_state 闭包返回的是计算出来的 owned 值。
要点三:map_state 的子组件只能读不能写
map_state 给子组件传递的是 &子状态类型(不可变引用)。子组件内部的所有闭包都只能拿到不可变引用,无法修改数据。这是设计上的刻意限制——派生数据不应该被直接编辑,而应该通过修改源字段来间接更新。
要点四:多个 map_state 可以共存
因为 map_state 的映射闭包只需要 &父状态(不可变引用),多个 map_state 可以同时读取同一个父状态,不存在借用冲突。而多个 lens 同时从 &mut 父状态 中取出 &mut 引用时,需要确保它们取的是不同的字段(否则会产生多个可变借用,编译器会报错)。
要点五:单向数据流的核心思想
派生数据永远是源数据的"影子"。你不应该直接编辑派生值(也编辑不了——map_state 禁止了这种操作),而应该编辑产生派生值的源字段。源字段一变,app_logic 重新执行,map_state 的映射闭包自动重新计算,派生值自动更新。整个过程不需要任何手动同步代码。
下一课可以讲 memoize——当状态很大、重建 View Tree 很昂贵时,用 memoize 跳过不必要的重建,只在实际变化时才重新计算。要继续吗?



