上下文:我正在用Rust编写一个光线跟踪器,但我一直在努力寻找一种与文件系统无关的加载场景的好方法。我使用serde,这样就不必发明自己的文件格式(尚未)。资产(图像纹理和网格数据)分别存储到场景文件中。场景文件只存储这些文件的路径。因为光线跟踪器本身应该是一个与平台无关的库(我希望能够将其编译为浏览器的WebAssembly),所以光线跟踪器本身不知道文件系统。我打算在反序列化场景时加载资源,但这现在给我带来了真实的的问题:
我需要将文件系统接口代码的实现传递给serde,我可以在Deserialize::deserialize()
中使用,但似乎没有任何简单的方法可以做到这一点。
下面是我目前的做法,分解为MCVE(使用的包是serde
和serde_json
):
库代码(lib.rs):
use std::marker::PhantomData;
use serde::{Serialize, Serializer, Deserialize, Deserializer};
pub struct Image {}
pub struct Texture<L: AssetLoader> {
path: String,
image: Image,
phantom: PhantomData<L>,
}
impl<L: AssetLoader> Serialize for Texture<L> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.path.serialize(serializer)
}
}
impl<'de, L: AssetLoader> Deserialize<'de> for Texture<L> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Texture<L>, D::Error> {
let path = String::deserialize(deserializer)?;
// This is where I'd much rather have an instance of AssetLoader
let image = L::load_image(&path);
Ok(Texture {
path,
image,
phantom: PhantomData,
})
}
}
pub trait AssetLoader {
fn load_image(path: &str) -> Image;
// load_mesh(), load_hdr(), ...
}
#[derive(Serialize, Deserialize)]
pub struct Scene<L: AssetLoader> {
textures: Vec<Texture<L>>,
// meshes, materials, lights, ...
}
平台代码(main.rs):
use serde::{Serialize, Deserialize};
use assetloader_mcve::{AssetLoader, Image, Scene};
#[derive(Serialize, Deserialize)]
struct AssetLoaderImpl {}
impl AssetLoader for AssetLoaderImpl {
fn load_image(path: &str) -> Image {
println!("Loading image: {}", path);
// Load the file from disk, the web, ...
Image {}
}
}
fn main() {
let scene_str = r#"
{
"textures": [
"texture1.jpg",
"texture2.jpg"
]
}
"#;
let scene: Scene<AssetLoaderImpl> = serde_json::from_str(scene_str).unwrap();
// ...
}
我不喜欢这种方法的原因:
AssetLoaderImpl
必须实现Serialize
和Deserialize
,即使它从未(反)序列化- 我还使用了typetag,这会导致一个编译错误,因为“还不支持泛型imps的反序列化”。
- 缓存资产将非常困难,因为我没有
AssetLoaderImpl
的示例可以将它们缓存在成员变量中 - 当
Texture
(或其他资产)嵌套得更深时,四处传递AssetLoader
类型参数会变得很笨拙 - 感觉不太对,主要是因为
PhantomData
和滥用泛型
这让我觉得我的方法不对,但我正在努力想出一个更好的解决方案。(也许是lazy_static
),但这似乎也不对。(Box<dyn AssetLoader>
可能)到serde的反序列化时,我可以在impl Deserialize for Texture
中访问。我还没有找到任何方法来做到这一点,我真的很感激,如果有人可以指出我在正确的方向。
1条答案
按热度按时间xxe27gdn1#
为了将状态传递给反序列化,你应该使用
DeserializeSeed
trait。DeserializeSeed
的文档解决了这个用例:DeserializeSeed
是Deserialize
trait的有状态形式。如果你发现自己在寻找一种将数据传递到Deserialize
impl的方法,这个trait就是实现它的方法。状态
AssetLoader
就像你说的,作为泛型参数传递
AssetLoader
意味着你不能在其中存储缓存(或其他东西)。使用DeserializeSeed
,我们可以传递AssetLoader
结构体的示例,所以让我们修改AssetLoader
的函数给予访问self
:现在我们可以修改
AssetLoaderImpl
来使用这个新定义:使用
AssetLoader
反序列化现在我们可以在反序列化过程中使用
DeserializeSeed
trait来使用AssetLoader
。(允许我们将文件系统逻辑与反序列化逻辑分开),我们仍然必须使用通用的L: AssetLoader
,但它不再必须附加到Texture
结构体(或任何包含Texture
的结构体)。一个好的模式是引入一个单独的
TextureDeserializer
类型来处理有状态的反序列化,并在该结构上实现DeserializeSeed
。我们可以设置Value
关联类型来指示反序列化应该返回Texture
。请注意,泛型
AssetLoader
不再由Texture直接使用。 我们现在必须定义
DeserializeSeed,直到
Scene的反序列化逻辑,因为我们将在整个过程中拥有
AssetLoader状态。这可能看起来非常冗长,不幸的是我们不能直接用
serde-derive派生它。但是,在我们反序列化的结构中不捆绑反序列化状态的优点远远超过了额外的冗长。 为了反序列化
Vec,我们定义了一个
TexturesDeserializer`:还有一个
SceneDeserializer
来反序列化Scene
本身:请注意,上面的这些
DeserializeSeed
定义与#[derive(Deserialize)]
生成的定义(在Scene
的情况下)以及serde
为Vec<T>
定义的定义非常相似。然而,定义这些自定义实现允许状态通过整个过程传递到Texture
的反序列化中。∮ ∮把所有的都放在一起∮
现在我们可以使用
serde_json
从我们的JSON输入进行反序列化。注意,serde_json
没有提供任何帮助方法来使用DeserializeSeed
进行反序列化(过去有discussion),所以我们必须手动使用serde_json::Deserializer
。幸运的是,它非常简单用途:现在我们可以用一个有状态的
AssetLoader
来反序列化一个Scene
。这可以很容易地扩展到包括其他资源,供Scene
的其他成员在反序列化过程中访问。最棒的是,它保持了反序列化状态与实际反序列化结构体的解耦,这意味着你不需要关心在反序列化之外使用了什么AssetLoader
。