How is ownership when doing join on a Vec<&str>


#1

Hi,

Someone please explain me what is wrong in this code

https://bitbucket.org/hongquan/rust-vect-join-lose-ownership/src/master/src/main.rs

The idea is I have a Vec<&str>. I replace the first element with a string from other source, then I do join(",") on the Vec.

The compiler says that

… dropped here while still borrowed

I paste code here if you don’t want to go outside:

extern crate chrono;
use chrono::{UTC, TimeZone};

fn process_line(line: String) -> Option<String> {
        let mut columns: Vec<&str> = line.split(',').collect();
        let timestamp = columns[0].parse::<i64>();
        if timestamp.is_err() {
            return None;
        }
        let datatime = UTC.timestamp(timestamp.unwrap(), 0).to_string();
        columns[0] = datatime.as_str();
        Some(columns.join(","))
    }

fn main() {
    // Convert the timestamp in first column to datetime string
    let s = String::from("1489544029,20");
    process_line(s).unwrap();
}

And here the compilation error:

warning: unused manifest key: package.published
   Compiling ali v0.1.0 (file:///home/quan/Works/Test/Rust/ali)
error: `datatime` does not live long enough
  --> src/main.rs:13:2
   |
11 | 		columns[0] = datatime.as_str();
   | 		             -------- borrow occurs here
12 | 		Some(columns.join(","))
13 | 	}
   | 	^ `datatime` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

error: aborting due to previous error

error: Could not compile `ali`.

#2

What happens is that you are storing datatime.as_str() into columns. As datatime is owned string, and it’s deallocated before columns, you would end up with a state where columns contains an invalid pointer after datatime is deallocated, but columns isn’t.

The solution is to move declaration of datatime to be deallocated after columns.

fn process_line(line: String) -> Option<String> {
    let datatime;
    let mut columns: Vec<&str> = line.split(',').collect();
    let timestamp = columns[0].parse::<i64>();
    if timestamp.is_err() {
        return None;
    }
    datatime = UTC.timestamp(timestamp.unwrap(), 0).to_string();
    columns[0] = datatime.as_str();
    Some(columns.join(","))
}

#3

I believe the problem is that both datatime and columns are owned, and columns is dropped after datatime, so for a short moment columns would contain an invalid reference.

I would change it to not go through columns, but instead populate the new String directly first with the transformed first column, then pushing in the rest.

That way should also make it possible to simply iterate over the split values, and not require the intermediate Vec at all.


#4

Thank you all, now I understand. I try to move declaration of datatime to be above column and it works.

populate the new String directly first with the transformed first column, then pushing in the rest.

Can you please give me an example. I think to have “first colum” and the rest, we still have to split the line to a Vec?


#5

Something like this:

fn process_line(line: String) -> Option<String> {

    // make an iterator but don't collect
    let mut columns = line.split(',');

    // take the first item and see if its a timestamp
    let timestamp = match columns.next() {
        Some(value) => match value.parse::<i64>() {
            Ok(ts) => ts,
            Err(_) => return None,
        },
        None => return None,
    };

    // make a string containing only the first value
    let mut output = UTC.timestamp(timestamp, 0).to_string();

    // push the rest directly from the splitting iterator
    for item in columns {
        output.push(',');
        output.push_str(item);
    }
    
    Some(output)
}

This could probably be shortened, but I kept it a bit more explicit.

Edit: Forgot the separating commas
Edit 2: Push the separator as char, no need for it to be a &str


#6

Here’s a shorter version, although I don’t necessarily think it’s more readable or better unless line/word count is the only criteria :slight_smile:

let mut columns = line.split(',');
columns.nth(0).and_then(|s| s.parse::<i64>().ok())
             .map(|i| UTC.timestamp(i, 0).to_string())
             .map(|s| columns.nth(0).map(|x| [&s, x].join(",")).unwrap_or(s))