【问题标题】:Curiously recurring generic trait pattern: overflow evaluating the requirement奇怪地重复出现的通用特征模式:溢出评估需求
【发布时间】:2018-11-12 10:21:31
【问题描述】:

我正在尝试用一堆字段实现一个通用结构,其中每个字段类型都应该知道整个结构的确切类型。这是一种策略模式。

pub struct Example<S: Strategy<Example<S, D>>, D> {
    pub s: S,
    pub a: S::Associated,
    pub data: D,
}
pub trait Strategy<T> {
    type Associated;
    fn run(&self, &T);
}
pub trait HasData {
    type Data;
    fn data(&self) -> &Self::Data;
}

impl<S: Strategy<Self>, D> Example<S, D> {
//               ^^^^
// the complex code in this impl is the actual meat of the library:
    pub fn do_it(&self) {
        self.s.run(self); // using the Strategy trait
    }
}
impl<S: Strategy<Self>, D> HasData for Example<S, D> {
    type Data = D;
    fn data(&self) -> &D {
        &self.data
    }
}

然后我打算从上述“库”中实例化泛型:

pub struct ExampleStrat;
pub struct ExampleData;

impl<E: HasData<Data = ExampleData>> Strategy<E> for ExampleStrat {
    type Associated = ();
    fn run(&self, e: &E) {
        let _ = e.data();
        // uses ExampleData here
    }
}
let example = Example {
    s: ExampleStrat,
    a: (),
    data: ExampleData,
};
example.do_it();

在我的实际代码中,我有很多不同的“策略”和多个数据字段,所以Example 类型有一个令人印象深刻的泛型列表,如果库用户不需要,我很高兴明确说明它们(或至少不经常),而可以只使用 HasData 特征(及其关联类型,而不是泛型类型参数)。

如果struct Example&lt;S, D&gt; 中没有类型绑定,这实际上会(令人惊讶地)正常工作,比我最初预期的要好得多(在fighting with Self in the struct bounds 之后)。但是,当结构只应该与受约束的类型一起使用时,建议使用duplicate the impl trait bounds on the struct,而在我的情况下,我实际上需要它们能够将Associated 类型用于@ 987654336@字段。

现在编译器在抱怨

error[E0275]: overflow evaluating the requirement `main::ExampleStrat: Strategy<Example<main::ExampleStrat, main::ExampleData>>`
  --> src/main.rs:42:9
   |
42 |         a: (),
   |         ^^^^^
   |
   = note: required because of the requirements on the impl of `HasData` for `Example<main::ExampleStrat, main::ExampleData>`
   = note: required because of the requirements on the impl of `Strategy<Example<main::ExampleStrat, main::ExampleData>>` for `main::ExampleStrat`

我该如何解决这个问题? 我是在尝试做一些不可能的事情,是我做错了,还是它应该是可能的,但我成为a compiler bug 的牺牲品?我的完整设计有缺陷吗?

【问题讨论】:

  • 一般来说,将特征边界放在结构或特征的类型参数上并不是一个好习惯。如果您将它们移至 impl,那么您将更清楚地看到为什么会发生循环依赖。
  • @PeterHall 我确实看到了循环依赖,但我不明白为什么它无法解决。
  • @PeterHall this (without using the associated type) 不做同样的事情吗?
  • 是的。但是您在主结构中丢失了泛型类型。大概您不想将其硬编码为()

标签: compiler-errors rust traits type-constraints


【解决方案1】:

首先,如果您避免在结构和特征的定义上设置特征界限,一切都会变得更加清晰。当事情变得复杂时,至少从同一个方向解决约束。

pub struct Example<S, D, A> {
    pub s: S,
    pub a: A,
    pub data: D,
}

pub trait Strategy<T> {
    type Associated;
    fn run(&self, &T);
}

pub trait HasData {
    type Data;
    fn data(&self) -> &Self::Data;
}

impl<S, D, A> Example<S, D, A>
where
    S: Strategy<Self, Associated = A>,
{
    pub fn do_it(&self) {
        self.s.run(self);
    }
}

impl<S, D, A> HasData for Example<S, D, A>
where
    S: Strategy<Self, Associated = A>,
{
    type Data = D;
    fn data(&self) -> &D {
        &self.data
    }
}

您对 ExampleStratStrategy 实现如下所示:

impl<E: HasData<Data = ExampleData>> Strategy<E> for ExampleStrat {
    type Associated = ();
     // ...
}

这意味着您正在为所有可能的限定类型E 定义它。类型检查器现在只能查看 trait 边界,这些边界也是通用的,并且只能用其他特性表示,它们相互用作边界,因此类型检查器进入一个循环。通过给它一个你知道的具体类型来将一个块放入循环中。

pub struct ExampleStrat;
pub struct ExampleData;

impl Strategy<Example<ExampleStrat, ExampleData, ()>> for ExampleStrat {
    type Associated = ();
    fn run(&self, e: &Example<ExampleStrat, ExampleData, ()>) {
        let _ = e.data();
        // uses ExampleData here
    }
}

fn main() {
    let example = Example {
        s: ExampleStrat,
        a: (),
        data: ExampleData,
    };
    example.do_it();
}

【讨论】:

  • 谢谢,我明白了。这就是我最初的想法,但我的库应该使用多种策略(不仅仅是Strategy trait),并且可能有多种实现(不仅仅是ExampleStrat 用于每种策略),这样main 中的Example 就可以从实现中任意组合。所以ExampleStrat 的定义看起来更像impl&lt;A, B, C, D, E, …&gt; Strategy&lt;Example&lt;A, B, C, ExampleStrat, D, E, …&gt;&gt; for ExampleStrat,这就是我想避免使用trait 的原因。这不可能吗?
  • 这只是说ExampleStrat 仅在与ExampleExampleData 一起使用时才是有效的Strategy。您可以为Example 创建其他策略,您可以为ExampleStrat 声明Strategy 的不同impls,因此它可以与其他目标一起使用。
  • 总体而言,您似乎正在尝试直接从 Java 或 C++ 移植 OO 模式。通常这不会产生很好的惯用 Rust。
  • 是的,这正是我想说的,但如果可能的话,不要在每个impl 上都加载泛型参数 A-Z。我知道我的 ExampleStrat 策略实现仅适用于 Example 类型,但无论其类型参数如何,或多或少“任何”Example 类型。
  • 我想也许我应该问一个单独的问题,并且只留下关于错误消息的问题。
【解决方案2】:

如果以下implStrategy 的特征,那么它可能被参数化为错误的东西。 (我将忽略此答案的关联类型,因为该示例未使用它。)

impl<E: HasData<Data = ExampleData>> Strategy<E> for ExampleStrat {
    fn run(&self, e: &E) {
        let _ = e.data();
        // uses ExampleData here
    }
}

您可以改为参数化 Strategy 而非 D -- 打破 impl 依赖循环 -- 并仅参数化 run 方法而不是 E

pub trait Strategy<D> {
    fn run(&self, &impl HasData<Data = D>);
}

impl Strategy<ExampleData> for ExampleStrat {
    fn run(&self, e: &impl HasData<Data = ExampleData>) {
        let _ = e.data();
        // uses ExampleData here
    }
}

fn run&lt;E: HasData&lt;Data = ExampleData&gt;&gt;(&amp;self, e: &amp;E) 是定义run 的另一种方式,同样用于此目的。 Here is a full example.

这种方法的一个潜在缺点是不能通过Strategy trait 对象调用run,因为对于实现HasData 的任何类型都必须对其进行单态化。但是HasData trait 在这个impl 中似乎没有多大作用:它唯一能做的就是返回一个内部引用,一旦你有了它,再使用它就没有意义了。也许run 可以参考&amp;D

pub trait Strategy<D> {
    fn run(&self, &D);
}

impl Strategy<ExampleData> for ExampleStrat {
    fn run(&self, _: &ExampleData) {
        // uses ExampleData here
    }
}

可以肯定的是,现在您必须在 do_it 中调用 self.s.run(self.data()),但这并不会使您失去与原始版本相比的灵活性,在原始版本中,如果它正常工作¹,您只能使用 Strategy&lt;E&gt;::run 调用&amp;E 类型的参数。

事实上,整个HasData trait 对我来说似乎是不必要的:它总是由实现调用它的同一类型实现,所以除了传递self 而不是self.data 的小便利之外,它没有提升do_it 方法内部的抽象级别。所以在我看来,delete HasData entirelyExample 知道如何使用正确的参考来调用Strategy::run 是一样的;无论如何,它必须。 (不过,我也有可能只是缺乏想象力。)

这些解决方案中的任何一个都应该处理将关联类型添加到Strategy,但在不知道如何使用的情况下,很难确定。

¹它可以在编译器的未来版本中工作,具有足够智能的类型检查。

【讨论】:

  • "HasData trait 在这个 impl 中似乎没有多大作用——是的,我已经为这个问题简化了它。它在Example 结构中包含许多getter 和setter,不同的Strategy 实现使用它们的不同子集。我会编写 Strategy 特征,以便 run 直接采用 Example&lt;…&gt;((可变)引用),不便是这需要每个 impl Strategy 列出无数类型参数及其界限。 (部分问题可以通过RFC2089 解决)
  • @Bergi 我想可能还有更多。但这可能是最好硬着头皮写下界限的情况。
猜你喜欢
  • 1970-01-01
  • 2019-10-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多