Tengo este código (área de juegos):

use std::sync::Arc;

pub trait Messenger : Sync + Send {
    fn send_embed<F: FnOnce(String) -> String>(&self, u64, &str, f: F)
        -> Option<u64> where Self: Sync + Send;
}

struct MyMessenger {
    prefix: String,
}
impl MyMessenger {
    fn new(s: &str) -> MyMessenger {
        MyMessenger { prefix: s.to_owned(), }
    }
}
impl Messenger for MyMessenger {
    fn send_embed<F: FnOnce(String) -> String>(&self, channel_id: u64, text: &str, f: F) -> Option<u64> {
        println!("Trying to send embed: chid={}, text=\"{}\"", channel_id, text);
        None
    }

}

struct Bot {
    messenger: Arc<Messenger>,
}
impl Bot {
    fn new() -> Bot {
        Bot {
            messenger: Arc::new(MyMessenger::new("HELLO")),
        }
    }
}

fn main() {
    let b = Bot::new();
}

Quería hacer un objeto polimórfico (rasgo Messenger y una de las implementaciones polimórficas es MyMessenger). Pero cuando intento compilarlo tengo un error:

error[E0038]: the trait `Messenger` cannot be made into an object
  --> <anon>:25:5
   |
25 |     messenger: Arc<Messenger>,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Messenger` cannot be made into an object
   |
   = note: method `send_embed` has generic type parameters

He descubierto que debo requerir Sized en este caso, pero esto no lo resuelve. Si cambio mi método send_embed a lo siguiente:

fn send_embed<F: FnOnce(String) -> String>(&self, u64, &str, f: F)
    -> Option<u64> where Self: Sized + Sync + Send;

Luego se compila con éxito pero:

  1. ¿Por qué necesitamos Sized aquí? Esto viola el polimorfismo si no podemos usar este método desde un objeto rasgo.
  2. De hecho, no podemos usar este método desde Arc<Messenger> entonces:

    fn main() {
        let b = Bot::new();
        b.messenger.send_embed(0u64, "ABRACADABRA", |s| s);
    }
    

    Da:

    error[E0277]: the trait bound `Messenger + 'static: std::marker::Sized` is not satisfied
      --> <anon>:37:17
       |
    37 |     b.messenger.send_embed(0u64, "ABRACADABRA", |s| s);
       |                 ^^^^^^^^^^ the trait `std::marker::Sized` is not implemented for `Messenger + 'static`
       |
       = note: `Messenger + 'static` does not have a constant size known at compile-time
    

Estoy totalmente atrapado aquí. No tengo idea de cómo usar el polimorfismo con un método genérico en un rasgo. ¿Hay alguna manera?

12
Victor Polevoy 6 mar. 2017 a las 10:38

2 respuestas

La mejor respuesta

El despacho dinámico (es decir, llamar a métodos a través de objetos de rasgos) funciona llamando a través de una tabla v (es decir, utilizando un puntero de función), ya que no sabe en tiempo de compilación qué función será.

Pero si su función es genérica, debe compilarse de manera diferente (monomorfizada) para cada instancia de F que realmente se utiliza. Lo que significa que tendrá una copia diferente de send_embed para cada tipo de cierre diferente con el que se llama. Cada cierre es de un tipo diferente.

Estos dos modelos son incompatibles: no puede tener un puntero de función que funcione con diferentes tipos.

Sin embargo, puede cambiar el método para usar un objeto rasgo también en lugar de ser genérico en tiempo de compilación:

pub trait Messenger : Sync + Send {
    fn send_embed(&self, u64, &str, f: &Fn(String) -> String)
        -> Option<u64> where Self: Sync + Send;
}

(Zona de juegos)

En lugar de un send_embed diferente para cada tipo que puede ser Fn(String) -> String, ahora acepta una referencia de objeto de rasgo. (También puede usar un Box<Fn()> o similar). Debe usar Fn o FnMut y no FnOnce, ya que este último toma self por valor, es decir, tampoco es seguro para los objetos (la persona que llama no sabe qué tamaño para pasar como el parámetro de cierre self).

Todavía puede llamar a send_embed con una función de cierre / lambda, pero solo debe ser por referencia, de esta manera:

self.messenger.send_embed(0, "abc", &|x| x);

He actualizado el área de juegos para incluir un ejemplo de llamar a send_embed directamente con un cierre referenciado, así como la ruta indirecta a través de un contenedor genérico en Bot.

16
Shepmaster 6 mar. 2017 a las 15:48

No se puede hacer un método genérico object-safe, porque no puede implementar una vtable con él. La respuesta de @ ChrisEmerson explicó en detalle por qué.

En su caso, puede hacer send_embed rasgo-objeto, haciendo que f tome un rasgo-objeto en lugar de un parámetro genérico. Si su función acepta un f: F where F: Fn(X) -> Y, puede hacer que acepte f: &Fn(X) -> Y, de manera similar para FnMut f: &mut FnMut(X) -> Y. FnOnce es más complicado ya que Rust no admite el movimiento de tipos sin tamaño, pero podría intentar encajonarlo:

//           ↓ no generic          ↓~~~~~~~~~~~~~~~~~~~~~~~~~~~~ box the closure
fn send_embed(&self, u64, &str, f: Box<FnOnce(String) -> String>) -> Option<u64> 
    where Self: Sync + Send
{
    f("hello".to_string());
    None
}

b.messenger.send_embed(1, "234", Box::new(|a| a));
// note: does not work.

Sin embargo, a partir de Rust 1.17.0 no puede boxear un FnOnce y llamarlo, debe usar FnBox:

#![feature(fnbox)]
use std::boxed::FnBox;

//                                     ↓~~~~
fn send_embed(&self, u64, &str, f: Box<FnBox(String) -> String>) -> Option<u64> 
    where Self: Sync + Send 
{
    f("hello".to_string());
    None
}

b.messenger.send_embed(1, "234", Box::new(|a| a));

Si no desea utilizar la función inestable, puede utilizar la caja boxfnonce como solución alternativa :

extern crate boxfnonce;
use boxfnonce::BoxFnOnce;

fn send_embed(&self, u64, &str, f: BoxFnOnce<(String,), String>) -> Option<u64> 
    where Self: Sync + Send 
{
    f.call("hello".to_string());
    None
}

b.messenger.send_embed(1, "234", BoxFnOnce::from(|a| a));
5
Community 23 may. 2017 a las 12:32