Was ist der richtige Weg, um einen Iterator (oder ein anderes Merkmal) zurückzugeben?

114

Der folgende Rust-Code wird ohne Probleme kompiliert und ausgeführt.

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

Danach habe ich so etwas ausprobiert ... aber es wurde nicht kompiliert

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

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

Das Hauptproblem ist, dass ich nicht sicher bin, welchen Rückgabetyp die Funktion to_words()haben soll. Der Compiler sagt:

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`

Was wäre der richtige Code, um dies auszuführen? .... und wo ist meine Wissenslücke?

Forgemo
quelle

Antworten:

143

Ich fand es nützlich, mich vom Compiler führen zu lassen:

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

Kompilieren ergibt:

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> {
  |                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Befolgen Sie den Vorschlag des Compilers und fügen Sie ihn als Rückgabetyp ein (mit einer kleinen Bereinigung):

use std::str;

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

Das Problem ist, dass Sie ein Merkmal nicht zurückgeben können, Iteratorda ein Merkmal keine Größe hat. Das bedeutet, dass Rust nicht weiß, wie viel Speicherplatz für den Typ reserviert werden soll. Sie können auch keinen Verweis auf eine lokale Variable zurückgeben , daher ist die Rückgabe &dyn Iteratorkein Starter.

Impl Merkmal

Ab Rust 1.26 können Sie Folgendes verwenden 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());
}

Es gibt Einschränkungen, wie dies verwendet werden kann. Sie können nur einen einzigen Typ zurückgeben (keine Bedingungen!) Und er muss für eine freie Funktion oder eine inhärente Implementierung verwendet werden.

Boxed

Wenn es Ihnen nichts ausmacht, ein wenig an Effizienz zu verlieren, können Sie Folgendes zurückgeben 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());
}

Dies ist die primäre Option, die einen dynamischen Versand ermöglicht . Das heißt, die genaue Implementierung des Codes wird zur Laufzeit und nicht zur Kompilierungszeit festgelegt. Dies bedeutet, dass dies für Fälle geeignet ist, in denen Sie mehr als einen konkreten Iteratortyp basierend auf einer Bedingung zurückgeben müssen.

Neuer Typ

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());
}

Geben Sie einen Alias ​​ein

Wie von Reem hervorgehoben

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());
}

Umgang mit Verschlüssen

Wenn sie impl Traitnicht zur Verfügung stehen, erschweren Verschlüsse die Sache. Durch Verschlüsse werden anonyme Typen erstellt, die im Rückgabetyp nicht benannt werden können:

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]>`

In bestimmten Fällen können diese Verschlüsse durch Funktionen ersetzt werden, die wie folgt benannt werden können:

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>`

Und folgen Sie den obigen Ratschlägen:

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)
}

Umgang mit Bedingungen

Wenn Sie einen Iterator unter bestimmten Bedingungen auswählen müssen, lesen Sie Bedingte Iteration über einen von mehreren möglichen Iteratoren .

Shepmaster
quelle
Danke, das hat mir sehr geholfen. Der "Trick", sich vom Compiler führen zu lassen, ist ziemlich nützlich. Ich werde ihn auf jeden Fall in Zukunft verwenden. ... und ja, das ist ernsthaft hässlich! Ich hoffe, dass RFC es zum Release Candidate schafft.
Forgemo
8
Während Wrapper-Typen typehilfreich sein können, um die Komplexität zu verbergen, finde ich es besser, stattdessen nur Aliase zu verwenden, da die Verwendung eines neuen Typs bedeutet, dass Ihr Iterator keine Merkmale implementiert, wie dies RandomAccessIteratorauch der zugrunde liegende Iterator tut.
Reem
4
Jep! Typ-Aliase unterstützen generische Parameter. Zum Beispiel machen viele Bibliotheken type LibraryResult<T> = Result<T, LibraryError>eine Annehmlichkeit ähnlich IoResult<T>, die auch nur ein Typ-Alias ​​ist.
Reem
1
Könnten Sie bitte klarstellen, warum man ein 'aLeben lang hinzufügen muss Box? Was bedeutet das? Ich dachte immer, dies sei nur für Grenzen gedacht, um zu sagen, "T kann nur von etwas abhängen, das mindestens so lange lebt wie 'a".
Torkleyy
1
@torkleyy vielleicht würde stackoverflow.com/q/27790168/155423 oder stackoverflow.com/q/27675554/155423 Ihre Frage beantworten? Wenn nicht, würde ich Sie ermutigen, nach Ihrer Frage zu suchen. Wenn Sie sie nicht finden können, stellen Sie eine neue.
Shepmaster