web-dev-qa-db-fra.com

Rappels idiomatiques dans Rust

En C/C++, je faisais normalement des rappels avec un pointeur de fonction simple, en passant peut-être aussi un paramètre void* userdata. Quelque chose comme ça:

typedef void (*Callback)();

class Processor
{
public:
    void setCallback(Callback c)
    {
        mCallback = c;
    }

    void processEvents()
    {
        for (...)
        {
            ...
            mCallback();
        }
    }
private:
    Callback mCallback;
};

Quelle est la manière idiomatique de faire cela à Rust? Plus précisément, quels types ma fonction setCallback() doit-elle prendre et quel type doit être mCallback? Devrait-il prendre un Fn? Peut-être que FnMut? Est-ce que je l'enregistre Boxed? Un exemple serait incroyable.

64
Timmmm

Réponse courte: pour une flexibilité maximale, vous pouvez stocker le rappel sous forme d'objet FnMut encadré, avec le type de rappel de rappel générique sur le type de rappel. Le code correspondant est indiqué dans le dernier exemple de la réponse. Pour une explication plus détaillée, lisez la suite.

"Pointeurs de fonction": rappels en tant que fn

L'équivalent le plus proche du code C++ de la question serait de déclarer le rappel en tant que type fn. fn encapsule les fonctions définies par le mot clé fn, un peu comme les pointeurs de fonction de C++:

_type Callback = fn();

struct Processor {
    callback: Callback,
}

impl Processor {
    fn set_callback(&mut self, c: Callback) {
        self.callback = c;
    }

    fn process_events(&self) {
        (self.callback)();
    }
}

fn simple_callback() {
    println!("hello world!");
}

fn main() {
    let mut p = Processor { callback: simple_callback };
    p.process_events();         // hello world!
}
_

Ce code pourrait être étendu pour inclure un _Option<Box<Any>>_ afin de contenir les "données utilisateur" associées à la fonction. Malgré tout, ce ne serait pas idiomatique, Rust. La méthode Rust pour associer des données à une fonction consiste à les capturer dans une fermeture anonyme , comme dans le C++ moderne. Comme les fermetures ne sont pas fn, _set_callback_ devra accepter d'autres types d'objets fonction.

Rappel en tant qu'objet fonction générique

Dans Rust et C++, les fermetures portant la même signature d'appel ont des tailles différentes pour s'adapter à différentes tailles des valeurs capturées stockées dans l'objet de fermeture. De plus, chaque site de fermeture génère un type anonyme distinct qui correspond au type de l'objet de fermeture au moment de la compilation. En raison de ces contraintes, la structure ne peut pas faire référence au type de rappel par son nom ou à un alias de type.

Une façon de posséder une fermeture dans la structure sans faire référence à un type concret consiste à rendre la structure générique . La structure adaptera automatiquement sa taille et le type de rappel pour la fonction concrète ou la fermeture que vous lui transmettez:

_struct Processor<CB> where CB: FnMut() {
    callback: CB,
}

impl<CB> Processor<CB> where CB: FnMut() {
    fn set_callback(&mut self, c: CB) {
        self.callback = c;
    }

    fn process_events(&mut self) {
        (self.callback)();
    }
}

fn main() {
    let s = "world!".to_string();
    let callback = || println!("hello {}", s);
    let mut p = Processor { callback: callback };
    p.process_events();
}
_

Comme précédemment, la nouvelle définition de callback pourra accepter les fonctions de niveau supérieur définies avec fn, mais celle-ci acceptera également les fermetures en tant que || println!("hello world!"), ainsi que les fermetures qui capturent des valeurs, comme || println!("{}", somevar). Pour cette raison, la fermeture n'a pas besoin d'un argument séparé userdata; il peut simplement capturer les données de son environnement et elles seront disponibles quand il sera appelé.

Mais quel est le problème avec FnMut, pourquoi ne pas simplement utiliser Fn? Comme les fermetures contiennent des valeurs capturées, Rust leur applique les mêmes règles que celles appliquées aux autres objets conteneur. En fonction de ce que les fermetures font avec les valeurs qu’elles détiennent, elles sont regroupées en trois familles, chacune marquée d’un trait:

  • Fn sont des fermetures qui ne lisent que des données et peuvent être appelées plusieurs fois en toute sécurité, éventuellement à partir de plusieurs threads. Les deux fermetures ci-dessus sont Fn.
  • FnMut sont des fermetures qui modifient des données, par exemple. en écrivant dans une variable mut capturée. Ils peuvent également être appelés plusieurs fois, mais pas en parallèle. (L'appel d'une fermeture FnMut à partir de plusieurs threads entraînerait une course de données. Cette opération ne peut donc être effectuée qu'avec la protection d'un mutex.) L'objet de fermeture doit être déclaré mutable par l'appelant.
  • FnOnce sont des fermetures qui consomment les données capturées, par exemple. en le déplaçant vers une fonction qui les possède. Comme son nom l'indique, ceux-ci ne peuvent être appelés qu'une seule fois et l'appelant doit les posséder.

De manière quelque peu contre-intuitive, lorsque vous spécifiez un trait lié au type d'un objet qui accepte une fermeture, FnOnce est en réalité le plus permissif. Déclarer qu'un type de rappel générique doit satisfaire le trait FnOnce signifie qu'il acceptera littéralement toute fermeture. Mais cela a un prix: cela signifie que le titulaire n'est autorisé à l'appeler qu'une seule fois. Puisque process_events() peut choisir d'appeler le rappel plusieurs fois, et comme la méthode elle-même peut être appelée plusieurs fois, la prochaine limite la plus permissive est FnMut. Notez que nous devions marquer _process_events_ comme mutant self.

Rappels non génériques: objets de trait de fonction

Même si l'implémentation générique du rappel est extrêmement efficace, son interface présente de sérieuses limitations. Il faut que chaque instance Processor soit paramétrée avec un type de rappel concret, ce qui signifie qu'un seul Processor ne peut traiter qu'un seul type de rappel. Étant donné que chaque fermeture a un type distinct, le générique Processor ne peut pas gérer proc.set_callback(|| println!("hello")) suivi de proc.set_callback(|| println!("world")). L'extension de la structure pour prendre en charge deux champs de rappel nécessiterait que toute la structure soit paramétrée en deux types, ce qui deviendrait rapidement difficile à gérer à mesure que le nombre de rappels augmente. Ajouter plus de paramètres de type ne fonctionnerait pas si le nombre de rappels devait être dynamique, par exemple. pour implémenter une fonction _add_callback_ qui gère un vecteur de différents rappels.

Pour supprimer le paramètre de type, nous pouvons tirer parti de objets trait , la fonctionnalité de Rust permettant la création automatique d'interfaces dynamiques basées sur des traits. Ceci est parfois appelé effacement de type et est une technique populaire en C++ [1][2] , à ne pas confondre avec l’utilisation quelque peu différente du terme par Java et FP langages. Les lecteurs familiarisés avec C++ reconnaîtront la distinction entre une fermeture qui implémente Fn et un objet Fn trait est équivalente à la distinction entre les objets fonction généraux et les valeurs _std::function_ en C++.

Un objet de trait est créé en empruntant un objet avec l'opérateur _&_ et en le lançant ou en le contraignant avec une référence à un trait spécifique. Dans ce cas, étant donné que Processor doit posséder l’objet de rappel, nous ne pouvons pas utiliser l’emprunt, mais nous devons le stocker dans un _Box<Trait>_ (l’équivalent Rust de _std::unique_ptr_), qui est fonctionnellement équivalent à un objet trait.

Si Processor stocke Box<FnMut()>, il n'est plus nécessaire qu'il soit générique, mais la méthode _set_callback_ est maintenant générique, donc il peut correctement encadrer le callable que vous lui donnez avant de stocker la boite dans le Processor. Le rappel peut être de n'importe quel type tant qu'il ne consomme pas les valeurs capturées. _set_callback_ être générique n'entraîne pas les limitations décrites ci-dessus, car cela n'affecte pas l'interface des données stockées dans la structure.

_struct Processor {
    callback: Box<FnMut()>,
}

impl Processor {
    fn set_callback<CB: 'static + FnMut()>(&mut self, c: CB) {
        self.callback = Box::new(c);
    }

    fn process_events(&mut self) {
        (self.callback)();
    }
}

fn simple_callback() {
    println!("hello");
}

fn main() {
    let mut p = Processor { callback: Box::new(simple_callback) };
    p.process_events();
    let s = "world!".to_string();
    let callback2 = move || println!("hello {}", s);
    p.set_callback(callback2);
    p.process_events();
}
_
127
user4815162342