Quelle est la manière correcte de renvoyer un Iterator (ou tout autre trait)?

114

Le code Rust suivant se compile et s'exécute sans aucun problème.

fn main() {
    let text = "abc";
    println!("{}", text.split(' ').take(2).count());
}

Après cela, j'ai essayé quelque chose comme ça ... mais ça n'a pas été compilé

fn main() {
    let text = "word1 word2 word3";
    println!("{}", to_words(text).take(2).count());
}

fn to_words(text: &str) -> &Iterator<Item = &str> {
    &(text.split(' '))
}

Le problème principal est que je ne suis pas sûr du type de retour que la fonction to_words()devrait avoir. Le compilateur dit:

error[E0599]: no method named `count` found for type `std::iter::Take<std::iter::Iterator<Item=&str>>` in the current scope
 --> src/main.rs:3:43
  |
3 |     println!("{}", to_words(text).take(2).count());
  |                                           ^^^^^
  |
  = note: the method `count` exists but the following trait bounds were not satisfied:
          `std::iter::Iterator<Item=&str> : std::marker::Sized`
          `std::iter::Take<std::iter::Iterator<Item=&str>> : std::iter::Iterator`

Quel serait le code correct pour exécuter cette opération? .... et où est mon manque de connaissances?

oublier
la source

Réponses:

143

J'ai trouvé utile de laisser le compilateur me guider:

fn to_words(text: &str) { // Note no return type
    text.split(' ')
}

La compilation donne:

error[E0308]: mismatched types
 --> src/lib.rs:5:5
  |
5 |     text.split(' ')
  |     ^^^^^^^^^^^^^^^ expected (), found struct `std::str::Split`
  |
  = note: expected type `()`
             found type `std::str::Split<'_, char>`
help: try adding a semicolon
  |
5 |     text.split(' ');
  |                    ^
help: try adding a return type
  |
3 | fn to_words(text: &str) -> std::str::Split<'_, char> {
  |                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

En suivant la suggestion du compilateur et en copiant-collant cela comme type de retour (avec un peu de nettoyage):

use std::str;

fn to_words(text: &str) -> str::Split<'_, char> {
    text.split(' ')
}

Le problème est que vous ne pouvez pas renvoyer un trait comme Iteratorparce qu'un trait n'a pas de taille. Cela signifie que Rust ne sait pas combien d'espace à allouer pour le type. Vous ne pouvez pas non plus renvoyer une référence à une variable locale , donc le retour &dyn Iteratorest un non-starter.

Trait d'impl

À partir de Rust 1.26, vous pouvez utiliser impl trait:

fn to_words<'a>(text: &'a str) -> impl Iterator<Item = &'a str> {
    text.split(' ')
}

fn main() {
    let text = "word1 word2 word3";
    println!("{}", to_words(text).take(2).count());
}

Il existe des restrictions sur la manière dont cela peut être utilisé. Vous ne pouvez renvoyer qu'un seul type (pas de condition!) Et il doit être utilisé sur une fonction libre ou une implémentation inhérente.

En boîte

Si cela ne vous dérange pas de perdre un peu d'efficacité, vous pouvez renvoyer un Box<dyn Iterator>:

fn to_words<'a>(text: &'a str) -> Box<dyn Iterator<Item = &'a str> + 'a> {
    Box::new(text.split(' '))
}

fn main() {
    let text = "word1 word2 word3";
    println!("{}", to_words(text).take(2).count());
}

Il s'agit de la principale option qui permet une répartition dynamique . Autrement dit, l'implémentation exacte du code est décidée au moment de l'exécution, plutôt qu'au moment de la compilation. Cela signifie que cela convient aux cas où vous devez renvoyer plus d'un type concret d'itérateur en fonction d'une condition.

Nouveau genre

use std::str;

struct Wrapper<'a>(str::Split<'a, char>);

impl<'a> Iterator for Wrapper<'a> {
    type Item = &'a str;

    fn next(&mut self) -> Option<&'a str> {
        self.0.next()
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        self.0.size_hint()
    }
}

fn to_words(text: &str) -> Wrapper<'_> {
    Wrapper(text.split(' '))
}

fn main() {
    let text = "word1 word2 word3";
    println!("{}", to_words(text).take(2).count());
}

Tapez un alias

Comme l'a souligné reem

use std::str;

type MyIter<'a> = str::Split<'a, char>;

fn to_words(text: &str) -> MyIter<'_> {
    text.split(' ')
}

fn main() {
    let text = "word1 word2 word3";
    println!("{}", to_words(text).take(2).count());
}

Faire face aux fermetures

Lorsqu'elles impl Traitne sont pas disponibles, les fermetures compliquent les choses. Les fermetures créent des types anonymes et ceux-ci ne peuvent pas être nommés dans le type de retour:

fn odd_numbers() -> () {
    (0..100).filter(|&v| v % 2 != 0)
}
found type `std::iter::Filter<std::ops::Range<{integer}>, [closure@src/lib.rs:4:21: 4:36]>`

Dans certains cas, ces fermetures peuvent être remplacées par des fonctions, qui peuvent être nommées:

fn odd_numbers() -> () {
    fn f(&v: &i32) -> bool {
        v % 2 != 0
    }
    (0..100).filter(f as fn(v: &i32) -> bool)
}
found type `std::iter::Filter<std::ops::Range<i32>, for<'r> fn(&'r i32) -> bool>`

Et en suivant les conseils ci-dessus:

use std::{iter::Filter, ops::Range};

type Odds = Filter<Range<i32>, fn(&i32) -> bool>;

fn odd_numbers() -> Odds {
    fn f(&v: &i32) -> bool {
        v % 2 != 0
    }
    (0..100).filter(f as fn(v: &i32) -> bool)
}

Gérer les conditions

Si vous avez besoin de choisir conditionnellement un itérateur, reportez-vous à Itération conditionnelle sur l'un des itérateurs possibles .

Shepmaster
la source
Merci, cela m'a beaucoup aidé. Le "truc" pour laisser le compilateur vous guider est assez utile, je vais certainement l'utiliser dans le futur. ... et oui, c'est vraiment moche! J'espère que RFC parviendra à la version candidate.
Forgemo
8
Bien que les types de wrapper puissent être agréables pour masquer la complexité, je trouve qu'il est préférable d'utiliser simplement des typealias à la place, car l'utilisation d'un nouveau type signifie que votre Iterator n'implémentera pas de traits comme RandomAccessIteratormême si l'itérateur sous-jacent le fait.
reem
4
Ouaip! Les alias de type prennent en charge les paramètres génériques. Par exemple, de nombreuses bibliothèques font type LibraryResult<T> = Result<T, LibraryError>comme une commodité similaire à IoResult<T>, qui est également juste un alias de type.
reem
1
Pourriez-vous s'il vous plaît clarifier pourquoi il faut ajouter une 'avie à Box? Qu'est-ce que ça veut dire? J'ai toujours pensé que c'était pour les limites seulement, pour dire "T ne peut dépendre que de quelque chose vivant au moins aussi longtemps que 'a".
torkleyy
1
@torkleyy peut-être que stackoverflow.com/q/27790168/155423 ou stackoverflow.com/q/27675554/155423 répondrait à votre question? Sinon, je vous encourage à rechercher votre question et si vous ne la trouvez pas, posez-en une nouvelle.
Shepmaster