1
votes

I am new to Rust and am trying to learn the idiomatic way to work with the borrow checker.

I'm trying to write a simple function that takes in a slice (generic over the data type) and returns the last element, in such a way that I can mutate the slice afterward. The naive implementation gives me an error:

fn last_element<T>(list: &[T]) -> T {
    list[list.len() - 1]
}

fn main() {
    let mut slice = [1, 2, 3, 4, 5];
    let x = last_element(&slice);
    println!("{}", x);

    // I want to be able to mutate slice after extracting last element
    slice[2] = 17;
}

The error is cannot move out of type[T], a non-copy slice. I see that one workaround is to have the function return a reference:

fn last_element<T>(list: &[T]) -> &T {
    &list[list.len() - 1]
}

fn main() {
    let mut slice = [1, 2, 3, 4, 5];
    let x = last_element(&slice);
    println!("{}", x);
    // I want to be able to mutate slice after extracting last element
    slice[2] = 17;
}

But then I get an error for slice[2] = 17 because the slice was borrowed when x was assigned. I do want to be able to mutate after calling last_element. The one workaround I found was to dereference x, which I believe consumes the borrow:

fn last_element<T>(list: &[T]) -> &T {
    &list[list.len() - 1]
}

fn main() {
    let mut slice = [1, 2, 3, 4, 5];
    let x = *last_element(&slice);
    println!("{}", x);
    // I want to be able to mutate slice after extracting last element
    slice[2] = 17;
}

Is this the most idiomatic way to accomplish the aim of being able to get the last element of a slice and still mutate the slice afterward? Or should I never even be doing the mutation afterward if I am writing good code?

1

1 Answers

4
votes

If you are working with simple types, such as i32 and other small, fixed-sized structures, these types usually implement Copy. This trait is a marker which tells the compiler to copy values in memory in situations where they would otherwise be moved. In fact, this is what the error message means when it refers to a non-copy slice: if the elements don't implement Copy then it would have to move them and that would leave the slice in an invalid state, which is not allowed.

If you constrain your function to expect only Copy types, then it will happily copy those values for you:

fn last_element<T: Copy>(list: &[T]) -> T {
    list[list.len() - 1]
}

If the type of your elements could be more complex, and doesn't implement Copy, then you can constrain the function wider, to any type that implements Clone, and then call clone() on the element before it is returned:

fn last_element<T: Clone>(list: &[T]) -> T {
    list[list.len() - 1].clone()
}

Cloning is generally a heavier operation than copying, since it is usually implemented on per-field basis in Rust code, while Copy is a lower-level operation on raw memory.

A final option would be to just return a reference to the last element in the first place. The caller of the function can then decide what to do with it. If it's a Copy type (like numbers) then dereferencing it will copy it:

fn last_element<T>(list: &[T]) -> &T {
    &list[list.len() - 1]
}

fn main() {
    let mut slice = [1, 2, 3, 4, 5];
    let x = *last_element(&slice);
}

If the elements were Clone but not Copy then, instead of dereferencing, you can explicitly clone it at that point instead:

let x = last_element(&slice).clone();