mzedeler
mzedeler

Reputation: 4371

immutable value is still being moved

I can't get this function to compile:

/// Return a String with all characters masked as '#' except the last 4.
fn maskify(cc: &str) -> String {
    let chars = cc.to_string().chars();
    chars
        .enumerate()
        .map(|(i, c)| {
            if i > chars.count() - 4 { '#' } else { c }
        })
        .collect()    
}

The current errors are:

error[E0507]: cannot move out of `chars`, a captured variable in an `FnMut` closure
 --> src/lib.rs:7:21
  |
3 |     let chars = cc.to_string().chars();
  |         ----- captured outer variable
...
7 |             if i > &chars.count() - 4 { '#' } else { c }
  |                     ^^^^^ move occurs because `chars` has type `std::str::Chars<'_>`, which does not implement the `Copy` trait

error[E0716]: temporary value dropped while borrowed
 --> src/lib.rs:3:17
  |
3 |     let chars = cc.to_string().chars();
  |                 ^^^^^^^^^^^^^^        - temporary value is freed at the end of this statement
  |                 |
  |                 creates a temporary which is freed while still in use
4 |     chars
  |     ----- borrow later used here
  |
  = note: consider using a `let` binding to create a longer lived value

error[E0382]: use of moved value: `chars`
 --> src/lib.rs:6:14
  |
3 |     let chars = cc.to_string().chars();
  |         ----- move occurs because `chars` has type `std::str::Chars<'_>`, which does not implement the `Copy` trait
4 |     chars
  |     ----- value moved here
5 |         .enumerate()
6 |         .map(|(i, c)| {
  |              ^^^^^^^^ value used here after move
7 |             if i > &chars.count() - 4 { '#' } else { c }
  |                     ----- use occurs due to use in closure

I think the source of the error is that chars is an iterator, so it mutates, making it impossible to borrow in the closure, but even if I try to declare a local variable (such as let count = chars.count()), I still get borrow errors.

I've tried dereferencing it with &, but that didn't work either.

Upvotes: 4

Views: 150

Answers (3)

Ibraheem Ahmed
Ibraheem Ahmed

Reputation: 13618

The crux of the issue here is that Char::count() consumes self. Even if you declare a local variable, you cannot use chars after you moved ownership to the count function:

fn maskify(cc: &str) {
  let chars = cc.to_string().chars();
   // ^^^^^ move occurs here
  let count = chars.count();
                 // ^^^^^^ `chars` moved because `count` consumes self
  let _ = chars.enumerate();
       // ^^^^^ value used here after move - *this is not allowed*
}

You can fix this issue by creating a new iterator and consuming that to get the count:

fn maskify(cc: &str) -> String {
    let chars = cc.chars();
    let count = cc.chars().count();
             // ^^^ create and consume a new iterator over cc
    chars
        .enumerate()
        .map(|(i, c)| {
            if i < count - 4 { '#' } else { c }
        })
        .collect()    
}

fn main() {
    assert_eq!(maskify("abcd1234"), "####1234");
}

Or you can get the length of the string with .len():

fn maskify(cc: &str) -> String {
    let chars = cc.chars();
    chars
        .enumerate()
        .map(|(i, c)| {
            if i < cc.len() - 4 { '#' } else { c }
        })
        .collect()    
}

fn main() {
    assert_eq!(maskify("abcd1234"), "####1234");
}

Note that str.len() can only handle ascii while .chars().count() can handle full utf8.

Upvotes: 4

pretzelhammer
pretzelhammer

Reputation: 15165

Two slightly different approaches you can use to implement this function depending on whether or not cc is UTF8 or ASCII. The UTF8 implementation of course works for both cases as UTF8 is a superset of ASCII.

fn maskify_utf8(cc: &str) -> String {
    let last_four = cc.chars().count().saturating_sub(4);
    cc.chars()
        .enumerate()
        .map(|(i, c)| if i < last_four { '#' } else { c })
        .collect()    
}

fn maskify_ascii(cc: &str) -> String {
    let mask_idx = cc.len().saturating_sub(4);
    format!("{0:#<1$}{2}", "#", mask_idx, &cc[mask_idx..])
}

fn main() {
    assert_eq!(maskify_utf8("🦀🦀🦀🦀1234"), "####1234");
    assert_eq!(maskify_utf8("abcd1234"), "####1234");
    assert_eq!(maskify_ascii("abcd1234"), "####1234");
}

playground

Upvotes: 3

mzedeler
mzedeler

Reputation: 4371

Thanks to @ibraheem-ahmed, I wound up with this solution:

/// Return a String with all characters masked as '#' except the last 4.
fn maskify(cc: &str) -> String {
    let leading = cc.chars().count().saturating_sub(4);
    cc
        .chars()
        .enumerate()
        .map(|(i, c)| {
            if i >= leading { c } else { '#' }
        })
        .collect()    
}

Upvotes: 0

Related Questions