Rust类型状态模式:为多个状态实现?

huangapple go评论69阅读模式
英文:

Rust typestate pattern: implement for multiple states?

问题

I have a struct with several states:

struct Select;
struct Insert;
struct Delete;
// ...more states

struct Query<T> {
    // ... some non-generic fields
    marker: PhantomData<T>
}

I have some functionality which I would like to implement for some, but not all of states. I imagine it should look something like this:

impl Query<T> for T: Select | Update | Delete {
    // implement only once
    fn foo() -> Query<T>;
}

Is this possible and if so, how?

我有一个包含多个状态的结构体:

struct Select;
struct Insert;
struct Delete;
// ...更多的状态

struct Query<T> {
    // ... 一些非泛型字段
    marker: PhantomData<T>
}

我想要为某些状态,但不是所有状态实现一些功能。我想象中应该是这样的:

impl Query<T> for T: Select | Update | Delete {
    // 只实现一次
    fn foo() -> Query<T>;
}

这是否可能,如果可能的话,如何实现?

英文:

I have a struct with several states:

struct Select;
struct Insert;
struct Delete;
// ...more states

struct Query&lt;T&gt; {
    // ... some non-generic fields
    marker: PhantomData&lt;T&gt;
}

I have some functionality which I would like to implement for some, but not all of states. I imagine it should look something like this:

impl Query&lt;T&gt; for T: Select | Update | Delete {
    // implement only once
    fn foo() -&gt; Query&lt;T&gt;;
}

Is this possible and if so, how?

答案1

得分: 2

创建一个所有所需状态都要实现的特质(仅此而已),并限制 where T: Trait

英文:

Create a trait that all wanted states implement (and nothing else), and constrain where T: Trait.

答案2

得分: 2

有两种主要方法可以做到这一点。如Chayim建议的那样,可以使用特质保护(trait guards),或者可以使用宏。让我们看看这两种解决方案分别是如何工作的以及它们的权衡。

特质保护(Trait guard)

这是一个相当简单的概念,但有微妙的差异。我们想要定义一种名为Guard的特质(trait),为某些类型实现它,然后利用通用实现。例如:

pub trait Guard {}

impl Guard for Select {}
impl Guard for Update {}
impl Guard for Delete {}

impl<T: Guard> Query<T> {
    pub fn foo() -> Query<T> {
        todo!()
    }
}

然而,这有一个重要的缺点。由于Guard是一个公共特质,如果有人为某些其他类型Other实现了它,那么impl<T: Guard>也将适用于Other类型。这可能是不希望的,因为根据您的项目要求,这可能会导致不变式(invariants)破裂。

我们可以尝试将Guard变成私有特质,但是在当前版本(rustc 1.70.02021 Edition)会产生警告,并且将在将来变成错误。

warning: private trait `Guard` in public interface (error E0445)
  --> src/lib.rs:24:1
   |
24 | impl<T: Guard> Query<T> {
   | ^^^^^^^^^^^^^^^^^^^^^^^
   |
   = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
   = note: for more information, see issue #34537 <https://github.com/rust-lang/rust/issues/34537>
   = note: `#[warn(private_in_public)]` on by default

我们可以通过使用sealed trait来解决这个问题:

mod sealed {
    pub trait Sealed {}
}

pub trait Guard: sealed::Sealed {}

impl sealed::Sealed for Select {}
impl sealed::Sealed for Update {}
impl sealed::Sealed for Delete {}

impl Guard for Select {}
impl Guard for Update {}
impl Guard for Delete {}

impl<T: Guard> Query<T> {
    pub fn foo() -> Query<T> {
        todo!()
    }
}

然而,这会导致我们必须编写的实现数量翻倍,并且会导致API略显混乱(因为我们在公共API中“泄漏”了私有seal)。这也可能导致文档变得不太可读,因为读者必须首先检查哪些类型实现了Guard

宏(Macros)

或者,您可以使用声明性宏。这将导致与您描述的非常相似的语法。

macro_rules! foo_impl {
    ($($state:ty),*) => {
        $(impl Query<$state> {
            pub fn foo() -> Query<$state> {
                todo!()
            }
        })*
    };
}

foo_impl!(Select, Update, Delete);

这有一些优点:

  • 您无需重复为类型实现特质保护。
  • 您无需担心其他人为其他类型实现特质保护。
  • 您具有更干净的API,可能还具有更可读的文档。

另一方面,如果您更喜欢特质的解决方案,仍然可以编写一个自动为您的类型实现特质和seal的宏。

macro_rules! guard_impl {
    ($($state:ty),*) => {
        $(
            impl sealed::Sealed for $state {}
            impl Guard for $state {}
        )*
    };
}

guard_impl!(Select, Update, Delete);
英文:

There are two main methods you could do that. With trait guards, as Chayim suggested, or with a macro. Let's see how each of those solutions work and what are their trade-offs.

Trait guard

This is a pretty easy concept, however it has a subtle nuances. We want to define some kind of Guard trait, implement it for some types and then leverage generic implementation. For example:

pub trait Guard {}

impl Guard for Select {}
impl Guard for Update {}
impl Guard for Delete {}

impl&lt;T: Guard&gt; Query&lt;T&gt; {
    pub fn foo() -&gt; Query&lt;T&gt; {
        todo!()
    }
}

This however has an important drawback. Since Guard is a public trait if someone would implement it for some other type Other then impl&lt;T: Guard&gt; would apply to Other type as well. This could be undesired, as depending on your project's requirements this could lead to broken invariants.

We could try making Guard a private trait, but this currently (rustc 1.70.0, 2021 Edition) results in a warning and will become an error in the future.

warning: private trait `Guard` in public interface (error E0445)
  --&gt; src/lib.rs:24:1
   |
24 | impl&lt;T: Guard&gt; Query&lt;T&gt; {
   | ^^^^^^^^^^^^^^^^^^^^^^^
   |
   = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
   = note: for more information, see issue #34537 &lt;https://github.com/rust-lang/rust/issues/34537&gt;
   = note: `#[warn(private_in_public)]` on by default

We can solve it by using a sealed trait:

mod sealed {
    pub trait Sealed {}
}

pub trait Guard: sealed::Sealed {}

impl sealed::Sealed for Select {}
impl sealed::Sealed for Update {}
impl sealed::Sealed for Delete {}

impl Guard for Select {}
impl Guard for Update {}
impl Guard for Delete {}

impl&lt;T: Guard&gt; Query&lt;T&gt; {
    pub fn foo() -&gt; Query&lt;T&gt; {
        todo!()
    }
}

This however doubles number of implementations we have to write and results in slightly uglier API (since we "leak" private seal to public API). It could also result in less readable documentation, since reader must check which types implement Guard in the first place.

Macros

Alternatively you could use a declarative macro. This would result in a very similar syntax to what you described.

macro_rules! foo_impl {
    ($($state:ty),*) =&gt; {
        $(impl Query&lt;$state&gt; {
            pub fn foo() -&gt; Query&lt;$state&gt; {
                todo!()
            }
        })*
    };
}

foo_impl!(Select, Update, Delete);

This has a couple of advantages:

  • You don't have to repeat yourself with implementing guard trait for your types.
  • You don't have to worry about someone else implementing guard trait for other types.
  • You have a cleaner API with (possibly) more readable docs.

If you on the other hand like better solution with traits you can still write a macro that would automatically implement guard and it's seal for your types.

macro_rules! guard_impl {
    ($($state:ty),*) =&gt; {
        $(
            impl sealed::Sealed for $state {}
            impl Guard for $state {}
        )*
    };
}

guard_impl!(Select, Update, Delete);

答案3

得分: 1

另一种方法是使用 duplicate crate:

// duplicate = "1.0.0"
#[duplicate::duplicate_item(
    T;
    [ Select ]; [ Update ]; [ Delete ];
)]
impl Query<T> {
    // 只需实现一次
    fn foo() -> Query<T>;
}

这与使用 trait 相比有不同的优势和劣势。

  • (优势) 使用 trait 时,如果需要引用某个使用了 T 定义的项(例如 T::my_fn()T::Type),您需要将该项添加到 trait 和所有实现中。而使用 duplicate 时,因为您使用的是类型本身,所以不需要这样做。
  • (劣势) 使用 trait 时,您可以重用它用于需要它的不同方法,包括将两个要求进行 "与" 运算(T: TraitA + TraitB)以及进行 "或" 运算的要求(impl trait TraitAB for A | BT: TraitAB)。而使用 duplicate 时,您必须每次定义方法/块时都指定所有状态,这可能导致重复。
英文:

Another way is to use the duplicate crate:

// duplicate = &quot;1.0.0&quot;
#[duplicate::duplicate_item(
    T;
    [ Select ]; [ Update ]; [ Delete ];
)]
impl Query&lt;T&gt; {
    // implement only once
    fn foo() -&gt; Query&lt;T&gt;;
}

This has different advantages/disadvantages compared to using a trait.

  • (Advantage) When using a trait, if you need to refer to some item defined with T (e.g. T::my_fn() or T::Type), you need to add that item to the trait and all implementations. Meanwhile when using duplicate, since you're using the types themselves, there's no need for that.
  • (Disadvantage) When using a trait, you can re-use it for different methods that need it, including "and"ing two requirements (T: TraitA + TraitB), as well as "or"ing requirements (impl trait TraitAB for A | B, T: TraitAB). With duplicate, you must specify all the states each time you define the method / block, which may lead to repetition.

huangapple
  • 本文由 发表于 2023年6月6日 02:08:11
  • 转载请务必保留本文链接:https://go.coder-hub.com/76408960.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定