如何在Rust中使用泛型类型的内部可变性?

3
我希望在Rust中设计一个结构体,可以使用实现了Digest trait的对象进行构造,并通过一个方法抽象哈希行为。这里是一个简单的例子,但它不能编译:
use digest::Digest;

struct Crypto<D: Digest> {
    digest: D,
}

impl<D> Crypto<D>
where
    D: Digest,
{
    pub fn hash(&self, data: &[u8]) -> Vec<u8> {
        self.digest.chain(&data).finalize_reset().to_vec()
    }
}

这个无法编译,因为在方法签名中 self 是不可变借用的,所以无法对 self.digest 进行不可变借用。因此尝试复制它,但由于泛型 D 没有定义遵循 Copy 特性,所以失败了。
无论如何,我宁愿不复制它。我宁愿只有一个实例。我尝试过以下一些方法:
  • 将方法签名更改为使用mut self。但这会将对象的所有权移动到方法中,之后不能再次使用。

  • digest字段中包装RefMutCell,以采用内部可变性,但我无法找出正确的方法来借用可变的digest而不尝试复制该值。此外,如果可能的话,最好保持编译时的借用检查。

  • D的类型更改为返回Digest实例的函数,并在hash()方法中使用它来实例化新的摘要。但是,即使将其定义为D: Box<dyn Digest>,编译器也会抱怨必须指定与特质digest::Digest相关联的OutputSize值。因此,这似乎很具有挑战性,因为我想支持产生不同大小哈希的不同哈希算法。

我试图使用泛型来获得特质边界的编译时优势,但必须承认,在与需要可变性的对象组合时内部可变性的挑战使我受挫。非常感谢提供有关此设计难题的惯用Rust解决方案的指引。
额外奖励 - 如何避免to_vec()复制,并仅返回{{link1:finalize_reset()返回的数组}}?

3
"chain" 要求您移动 "digest",那么您计划用什么替换旧的 "digest"? - Aplet123
我可以不用 chain。但是 self.digest.update(&data); self.digest.finalize_reset().to_vec() 仍然想要将 digest 借为不可变,但是无法实现。 - theory
在摆脱了chain函数之后,您可以更新hash的方法签名,将&self替换为&mut self,这似乎符合您的所有要求,对吗? - pretzelhammer
啊,是的,我没有意识到chain想要移动digest,所以删除它并且将签名更改为mut&self确实可以解决它,只要我还将Crypto对象创建为可变的。不过最好保持内部化。 - theory
@theory 你能否澄清一下你所说的“最好保持内部”是什么意思?所有Crypto实例都保持不可变是一个强制要求吗,还是...你希望人们甚至在一个不可变的Crypto上也能调用hash函数? - pretzelhammer
是的,应该是后者。这个模块的使用者不需要关心为了进行哈希而分配和释放缓冲区,尽管我认识到update需要结构本身来维护状态。 - theory
2个回答

3

无论如何,我更愿意不复制它。我宁愿只有一个实例 [self.digest]。

问题在于self.digest.chain()会消耗(拥有)self.digest,而这是Digest::chain()合同的基本部分,您不能更改。内部可变性无法解决问题,因为这不是可变性问题,而是对象生命周期问题-您不能在对象被移动或丢弃后再使用它。

然而,您提出的将digest作为创建消息摘要的函数的想法是可行的。这需要两种通用类型:一种是摘要类型,具有Digest的trait约束,另一种是工厂类型,具有Fn() -> D的trait约束:

struct Crypto<F> {
    digest_factory: F,
}

impl<D, F> Crypto<F>
where
    D: Digest,
    F: Fn() -> D,
{
    pub fn hash(&self, data: &[u8]) -> Vec<u8> {
        (self.digest_factory)()
            .chain(&data)
            .finalize()  // use finalize as the object is not reused
            .to_vec()
    }
}

如何避免使用 to_vec() 方法复制数据,而是直接返回 finalize_reset() 方法返回的数组?
您可以让 hash() 方法返回与 finalize() 方法相同的类型,即 digest::Output<D> 类型:
pub fn hash(&self, data: &[u8]) -> digest::Output<D> {
    (self.digest_factory)()
        .chain(&data)
        .finalize()
}

1
完美,几乎就是我需要的。能够存储摘要对象以便重复使用会很好,但我可以为每个调用构建一个新的对象。我愿意为此交换,而不是将摘要输出复制到向量中。谢谢! - theory
可以调用 finalize() 而不是 finalize_reset(),因为对象永远不会被重复使用。 - theory
1
@theory 很好的观点,我已经修改了答案。我从未使用过 digest::Digest,所以我只是按照问题中的代码进行了操作。 - user4815162342

2

user4815162342的摘要工厂答案的基础上,这里提供了一种使用内部可变性的替代实现:

use digest::Digest;
use std::cell::RefCell;

struct Crypto<D: Digest> {
    digest: RefCell<D>,
}

impl<D> Crypto<D>
where
    D: Digest,
{
    pub fn hash(&self, data: &[u8]) -> Vec<u8> {
        let mut digest = self.digest.borrow_mut();
        digest.update(&data);
        digest.finalize_reset().to_vec()
    }
}

playground

程序设计相关。

啊,需要同时使用RefCell<>和Rc<>,这是我遗漏的部分。谢谢!虽然我更喜欢工厂函数来保持整洁,但我很高兴将其放在口袋里以备将来之需。 - theory
1
很好 - 如果你能摆脱chain,内部可变性确实是可能的。我没有意识到它是可选的。但为什么digest是一个Rc<RefCell<D>>?不用额外的堆分配,RefCell<D>也可以正常工作,不是吗? - user4815162342
刚刚尝试了一下,@user4815162342,使用RefCell<D>似乎可以正常工作。 - theory
@theory 在你原来的代码中可能不起作用,因为你仍然在调用 chain(),它想要消耗它 - 但是这也不适用于 Rc<RefCell<...>> - user4815162342
是的,chain() 函数消耗了结构体是解决问题的关键信息。我只是错过了它,并且没有注意到任何诊断来引导我找到它。感谢这里的答案,我感觉好多了,不再为这个问题苦恼了。 - theory
1
感谢您的评论,我已经更新了我的答案,只使用RefCellRefCell <Digest>Rc <RefCell <Digest>>之间的关键区别仅在于如果您为Crypto实现了Clone时才会发挥作用。在前一种情况下,在Crypto克隆上将克隆Digest实例,在后一种情况下,相同的Digest实例将在所有Crypto克隆之间共享,尽管在这种情况下并不重要,因为Crypto没有实现Clone,但值得一提。 - pretzelhammer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接