Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
750 views
in Technique[技术] by (71.8m points)

rust - How can I define an async method in a trait?

I have a trait that I'm using to abstract away tokio::net::TcpStream and tokio::net::UnixStream:

/// Interface for TcpStream and UnixStream.
trait TryRead {
  // overlapping the name makes it hard to work with
  fn do_try_read(&self, buf: &mut [u8]) -> Result<usize, std::io::Error>;
}

impl TryRead for TcpStream {
  fn do_try_read(&self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
      self.try_read(buf)
  }
}

The problem is that I want to abstract away pub async fn readable(&self) -> io::Result<()> in both methods but async methods cannot be implemented in traits. How can I handle this?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Currently, async fn cannot be used in traits. The reasons for this are somewhat complex, but there are plans to remove this restriction in the future. You can refer to why async fn in traits are hard for a deeper analysis of the problem.

Associated Types

In the meantime, you can use an associated type:

trait Readable {
    type Output: Future<Output = io::Result<()>>;

    fn readable(&self) -> Self::Output;
}

Concrete Future Types

When implementing this trait, you can use any type that implements Future, such as Ready from the standard library:

use std::future;

impl Readable for Reader {
    type Output = future::Ready<io::Result<()>>;
    
    fn readable(&self) -> Self::Output {
        future::ready(Ok(()))
    }
}

Dynamic Future Types

async functions return an opaque impl Future, so if you need to call one, you don't have a concrete type to set Output to. Instead, you can return an dynamically typed Future:

impl Readable for Reader {
    // or use the handy type alias from the futures crate:
    // futures::BoxFuture<'static, io::Result<()>>
    type Output = Pin<Box<dyn Future<Output = io::Result<()>>>>;
    
    fn readable(&self) -> Self::Output {
        let fut = async {
            do_stuff().await
        };
        Box::pin(fut)
    }
}

Note that using these trait methods will result in a heap allocation and dynamic dispatch per function-call. This is not a significant cost for the vast majority of applications, but is something to be considered.

Capturing References

One issue that may come up is the fact that the associated type Output does not have a lifetime, and therefore cannot capture any references:

struct Reader(String);

impl Readable for Reader {
    type Output = Pin<Box<dyn Future<Output = io::Result<()>>>>;
    
    fn readable(&self) -> Self::Output {
        let fut = async move {
            println!("{}", self.0);
            Ok(())
        };
        Box::pin(fut)
    }
}
error[E0759]: `self` has an anonymous lifetime `'_` but it needs to satisfy a `'static` lifetime requirement
  --> src/lib.rs:17:30
   |
16 |       fn readable(&self) -> Self::Output {
   |                   ----- this data with an anonymous lifetime `'_`...
17 |           let fut = async move {
   |  ______________________________^
18 | |             println!("{}", self.0);
19 | |             Ok(())
20 | |         };
   | |_________^ ...is captured here...
21 |           Box::pin(fut)
   |           ------------- ...and is required to live as long as `'static` here

Associated types on stable Rust cannot have lifetimes, so you would have to restrict the output to a boxed future that captures from self to make this possible:

trait Readable {
    // note the anonymous lifetime ('_) that refers to &self
    fn readable(&self) -> Pin<Box<dyn Future<Output = io::Result<()>> + '_>>;
}

impl Readable for Reader {
    fn readable(&self) -> Pin<Box<dyn Future<Output = io::Result<()>> + '_>> {
        let fut = async move {
            println!("{}", self.0);
            Ok(())
        };
        Box::pin(fut)
    }
}

async_trait

To avoid some of this boilerplate, you can use the async-trait crate:

#[async_trait]
trait Readable {
    fn async readable(&self) -> io::Result<()>;
}

#[async_trait]
impl Readable for Reader {
    async fn readable(&self) -> io::Result<()> {
        do_stuff().await
    }
}

async-trait transforms async methods into methods that return Pin<Box<dyn Future<Output = ...> + Send = '_>>, similar to what we wrote before, so the same points as above should be considered as well.

To avoid having Send bound placed on the async trait methods, you can invoke the async trait macro as #[async_trait(?Send)] on both the trait and the impl blocks.

Unstable Features

If you are on nightly, the story is better. You can enable the type_alias_impl_trait feature and use regular async/await syntax without boxing:

#![feature(type_alias_impl_trait)]

trait Readable {
    type Output: Future<Output = io::Result<()>>;

    fn readable(&self) -> Self::Output;
}


impl Readable for Reader {
    type Output = impl Future<Output = io::Result<()>>;
    
    fn readable(&self) -> Self::Output {
        async { ... }
    }
}

The borrowing issue still applies with the above code. However, with the unstable feature generic_associated_types, you make Output generic over a lifetime and capture self:

trait Readable {
    type Output<'a>: Future<Output = io::Result<()>>;

    fn readable(&self) -> Self::Output<'_>;
}

And the previous example compiles, with zero boxing!

struct Reader(String);

impl Readable for Reader {
    type Output<'a> = impl Future<Output = io::Result<()>> + 'a;
    
    fn readable(&self) -> Self::Output<'_> {
        let fut = async move {
            println!("{}", self.0); // we can capture self!
            Ok(())
        };
        Box::pin(fut)
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...