【问题标题】:How can I use internal mutability with generic type in Rust?如何在 Rust 中使用泛型类型的内部可变性?
【发布时间】:2021-04-23 06:37:15
【问题描述】:

我想在 Rust 中设计一个结构,它可以用实现 Digest 特征的对象构造,并抽象出方法背后的哈希行为。这是一个无法编译的简单示例:

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 中,以努力采用internal mutability,但我无法找到正确的方法,然后在没有尝试的情况下可变地借用digest复制值。此外,如果可能,希望在编译时保留借用检查。

  • D 的类型更改为返回Digest 实例的函数,并使用它在hash() 方法内实例化一个新摘要。但是,即使我将其定义为D: Box&lt;dyn Digest&gt;,编译器也会抱怨the value of the associated type OutputSize (from trait digest::Digest) must be specified。所以这看起来很有挑战性,因为我想支持不同的哈希算法,这些算法会产生不同大小的哈希值。

我试图使用泛型来获得 trait bound 的编译时优势,但不得不承认,在与行为需要可变性的对象组合时,内部可变性的挑战阻碍了我。非常感谢针对这一设计挑战的惯用 Rust 解决方案的指针。

奖励 — 我如何避免 to_vec() 复制而只返回 the array returned by finalize_reset()

【问题讨论】:

  • chain 要求你移动digest,那么你打算用什么来替换旧的digest
  • 我可以取消chain。但是self.digest.update(&amp;data); self.digest.finalize_reset().to_vec() 还是想借用digest 作为不可变的,不能。
  • 去掉chain 函数后,您可以更新hash 的方法签名以采用&amp;mut self 而不是&amp;self,这似乎满足您的所有要求,不是吗?
  • 啊,是的,我没有意识到 chain 想要移动 digest,所以删除它 将签名更改为 mut &amp;self 确实可以解决它,只要我还将 Crypto 对象创建为可变的。不过,将其保留在内部会很好。
  • @theory 您能否澄清一下“将其保留在内部”是什么意思?是所有Crypto 实例保持不可变的强烈要求,还是......您希望人们即使在不可变的Crypto 上也能够调用hash

标签: generics rust traits composition interior-mutability


【解决方案1】:

要添加到user4815162342's digest factory answer,这里有一个使用内部可变性的替代实现:

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,这是我缺少的一点。谢谢!不过,我认为我更喜欢工厂功能来保持整体整洁。不过,很高兴把它放在我的口袋里,以备将来使用。
  • 很好——如果你能摆脱chain,内部可变性确实是可能的,我没有意识到这是可选的。但是为什么digestRc&lt;RefCell&lt;D&gt;&gt;?如果没有额外的堆分配,RefCell&lt;D&gt; 是否也能正常工作?
  • 刚刚试过了,@user4815162342,它似乎确实可以与RefCell&lt;D&gt; 一起工作。
  • @theory 它可能在您的原始代码中不起作用,因为您仍在调用想要使用它的 chain() - 但这也不适用于 Rc&lt;RefCell&lt;...&gt;&gt;
  • 感谢 cmets,我已将答案更新为仅使用 RefCellRefCell&lt;Digest&gt;Rc&lt;RefCell&lt;Digest&gt;&gt; 之间的主要区别只有在您为 Crypto 实现 Clone 时才会发挥作用。在前一种情况下,Digest 实例将被克隆到Crypto 克隆上,而在后一种情况下,相同的Digest 实例将在所有Crypto 克隆之间共享,这在这种情况下并不重要,因为Crypto 不'不实现 Clone 但我猜值得一提。
【解决方案2】:

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

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

不过,您让digest 成为创建摘要的函数的想法应该可行。它将需要两种泛型类型,一种用于摘要类型,特征绑定为Digest,另一种用于工厂,特征绑定为Fn() -&gt; D

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&lt;D&gt; 相同的类型:

pub fn hash(&self, data: &[u8]) -> digest::Output<D> {
    (self.digest_factory)()
        .chain(&data)
        .finalize()
}

【讨论】:

  • 完美,几乎正是我所需要的。能够存储一个摘要对象以重复使用会很好,但我可以为每次调用构建一个新对象。我将用它来交换不将摘要输出复制到向量。谢谢!
  • 可以调用finalize()而不是finalize_reset(),因为该对象永远不会被重用。
  • @theory 好点,我现在修改了答案。我从未使用过digest::Digest,所以我只是使用问题中的代码。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-01
  • 1970-01-01
  • 2023-03-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多