请教 Rust 大佬们一个关于 Generic 函数中使用 serde 反序列化的问题。

2022-05-21 11:21:02 +08:00
 chuanqirenwu

背景

一个 WebSocket 客户端,当接收到服务端推送的各种类型的消息后,调用用户传入的回调函数处理消息。

假设只有一种消息,容易写出一个非 Generic 的版本:

use serde::Deserialize;
use serde_json;

#[derive(Deserialize, Debug)]
struct Ping<'a> {
    #[serde(borrow)]
    ping: &'a str,
}

fn handle<Callback: Fn(Ping)>(callback: Callback) {
  	// msg 可以是从 WebSocket 连接获取的消息,示例为了简单起见,直接写死。
    let msg = format!("{{\"ping\":\"{}\"}}", "abcdefg");
    let res: Ping = serde_json::from_str(&msg).unwrap();
    callback(res);
}

fn main() {
    let callback = |p: Ping| println!("{:?}", p);
    handle(callback);
    // 输出结果:Ping { ping: "abcdefg" }
}

基于此,为了让回调函数接收的参数类型由用户决定,我把 handle 写成一个 Generic 方法。

use serde::Deserialize;
use serde_json;

#[derive(Deserialize, Debug)]
struct Ping<'a> {
    #[serde(borrow)]
    ping: &'a str,
}

fn handle<'de, T: Deserialize<'de>, Callback: Fn(T)>(callback: Callback) {
    let msg = format!("{{\"ping\":\"{}\"}}", "abcdefg");
    let res: T = serde_json::from_str(&msg).unwrap();
    callback(res);
}

fn main() {
    let callback = |p: Ping| println!("{:?}", p);
    handle(callback);
}

这种写法报编译错误:

10 | fn handle<'de, T: Deserialize<'de>, Callback: Fn(T)>(callback: Callback) {
   |           --- lifetime `'de` defined here
11 |     let msg = format!("{{\"ping\":\"{}\"}}", "abcdefg");
12 |     let res: T = serde_json::from_str(&msg).unwrap();
   |                  ---------------------^^^^-
   |                  |                    |
   |                  |                    borrowed value does not live long enough
   |                  argument requires that `msg` is borrowed for `'de`
13 |     callback(res);
14 | }
   | - `msg` dropped here while still borrowed

从编译器的角度可以理解为什么报错,T 应该具有生命周期 'de,但是 &msg 因为是局部变量,生命周期短于 'de。然而从开发者的角度来说,代码足以保证不存在失效的引用。

问题:

  1. 是否有保持接口设计不变的情况下,解决编译的问题?
  2. 如果是因为 Rust 或者 serde 库的限制,这种 pattern 是不可行的,那么有没有其他比较好的 pattern 绕过这个问题呢?

参考的资料:

Deserializer lifetimes · Serde

[help] Generic deserialize wrapper · Issue #450 · serde-rs/json (github.com)

rust - Lifetime error with Serde in a Generic Function - Stack Overflow

PS:

参考资料给出的解决方案都是建议将 T 约束为 DeserializeOwned,但这种方式就无法实现零 copy 的反序列化,带来一定的性能损失。

1498 次点击
所在节点    Rust
7 条回复
hsfzxjy
2022-05-21 13:34:51 +08:00
可以用 struct 来约束被 borrow 的 String 和 deserialize 出来的对象的 lifetime 关系。

https://gist.github.com/rust-play/37e864f6c06fe0a346b24151ed7fa7b4

```
use serde::Deserialize;
use serde_json;
use std::marker::PhantomData;

#[derive(Deserialize, Debug)]
struct Ping<'a> {
#[serde(borrow)]
ping: &'a str,
}

struct Handler<T> {
data: String,
_marker: PhantomData<T>,
}

impl<'de, T: Deserialize<'de>> Handler<T> {
fn new(data: String) -> Self {
Self {
data,
_marker: PhantomData::default(),
}
}
fn handle(&'de self, callback: impl FnOnce(T)) {
let res: T = serde_json::from_str(&self.data).unwrap();
callback(res);
}
}

fn main() {
let callback = |p: Ping| println!("{:?}", p);
Handler::<Ping>::new(format!("{{\"ping\":\"{}\"}}", "abcdefg")).handle(callback);
}
```
hsfzxjy
2022-05-21 13:40:08 +08:00
脑抽了。。其实只要把 String 作为入参,并声明好 lifetime 约束就行了

https://gist.github.com/rust-play/c929dd0bfcf378abb0ba1668589e02af
chuanqirenwu
2022-05-21 14:27:53 +08:00
> 脑抽了。。其实只要把 String 作为入参,并声明好 lifetime 约束就行了
@hsfzxjy 谢谢,这种方式似乎不太可行,请看这个: https://gist.github.com/d52e5de95efe63d43ba7b7a8d45b4c1e

现有的接口设计下,`msg: &'a str` 这个参数恐怕无法从外部传入。
hsfzxjy
2022-05-21 14:48:07 +08:00
@chuanqirenwu #3 那就套一层函数调用呗,21 行开始那里改成调用 handle(&self.content, callback)
chuanqirenwu
2022-05-21 14:51:20 +08:00
@hsfzxjy 尝试过,生命周期问题还是存在,另外还多了个 callback 被 move 的问题。
hsfzxjy
2022-05-21 14:51:38 +08:00
或者你 fn run() 的签名变一下

pub fn run<'a: 'de, 'de, T: Deserialize<'de>, Callback: Fn(T)>(&'a self, callback: Callback)

原理是一样的
chuanqirenwu
2022-05-21 15:04:40 +08:00
@hsfzxjy sorry ,楼上说的方法原理不太明白,我试了一下似乎还是报同样的错误。您能说的详细一点吗?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/854315

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX