rust programming

Collections and Strings in Rust

Collections are the most common form of data storage in data structures. The Rust standard library provides a rich set of collection types to help developers handle data structure operations.

Vectors

A vector is a single data structure that can store multiple values, storing values of the same type linearly in memory.

Vectors are linear structures, represented as Vec<T> in Rust.

Vectors are used similarly to lists. We can create vectors of a specific type like this:

let vector: Vec<i32> = Vec::new(); // Create an empty vector of type i32
let vector = vec![1, 2, 4, 8];     // Create a vector from an array

When working with linear structures, we often need to append elements. Since appending is essentially the same as pushing to a stack, vectors only have a push method for adding single elements:

fn main() {
    let mut vector = vec![1, 2, 4, 8];
    vector.push(16);
    vector.push(32);
    vector.push(64);
    println!("{:?}", vector);
}

Output:

[1, 2, 4, 8, 16, 32, 64]

The append method is used to concatenate one vector to the end of another:

fn main() {
    let mut v1: Vec<i32> = vec![1, 2, 4, 8];
    let mut v2: Vec<i32> = vec![16, 32, 64];
    v1.append(&mut v2);
    println!("{:?}", v1);
}

Output:

[1, 2, 4, 8, 16, 32, 64]

The get method is used to retrieve values from a vector:

fn main() {
    let mut v = vec![1, 2, 4, 8];
    println!("{}", match v.get(0) {
        Some(value) => value.to_string(),
        None => "None".to_string()
    });
}

Output:

1

Since a vector's length cannot be logically inferred, the get method cannot guarantee it will always return a value. Therefore, it returns an Option enum that might be empty.

This is a safe way to access values, though it can be verbose. If you can guarantee that the index won't exceed the vector's bounds, you can use array indexing syntax:

fn main() {
    let v = vec![1, 2, 4, 8];
    println!("{}", v[1]);
}

Output:

2

However, trying to access v[4] would result in an error.

Iterating over a vector:

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{}", i);
    }
}

Output:

100
32
57

If you need to modify values during iteration:

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}

Strings

The String class has been used extensively up to this point, so many of its methods are already familiar to readers. This chapter mainly introduces string methods and UTF-8 properties.

Creating a new string:

let string = String::new();

Converting basic types to strings:

let one = 1.to_string();         // Integer to string
let float = 1.3.to_string();     // Float to string
let slice = "slice".to_string(); // String slice to string

Strings containing UTF-8 characters:

let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");

String appending:

let mut s = String::from("run");
s.push_str("oob"); // Append string slice
s.push('!');       // Append character

Concatenating strings with +:

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;

This syntax can also include string slices:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = s1 + "-" + &s2 + "-" + &s3;

Using the format! macro:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{}-{}-{}", s1, s2, s3);

String length:

let s = "hello";
let len = s.len();

Here len is 5.

let s = "你好";
let len = s.len();

Here len is 6 because Chinese characters are UTF-8 encoded, with each character being 3 bytes. However, Rust supports UTF-8 character objects, so if you want to count characters, you can first convert the string to a character collection:

let s = "hello你好";
let len = s.chars().count();

Here len is 7 because there are 7 characters in total. Counting characters is much slower than counting bytes.

Iterating over a string:

fn main() {
    let s = String::from("hello中文");
    for c in s.chars() {
        println!("{}", c);
    }
}

Output:

h
e
l
l
o

Getting a single character from a string:

fn main() {
    let s = String::from("EN中文");
    let a = s.chars().nth(2);
    println!("{:?}", a);
}

Output:

Some('中')

Note: The nth function is a method for taking a value from an iterator. Don't use it during iteration because UTF-8 characters can have different lengths!

To get a substring:

fn main() {
    let s = String::from("EN中文");
    let sub = &s[0..2];
    println!("{}", sub);
}

Output:

EN

But be careful as this usage might split a UTF-8 character! That would cause an error:

fn main() {
    let s = String::from("EN中文");
    let sub = &s[0..3];
    println!("{}", sub);
}

Output:

thread 'main' panicked at 'byte index 3 is not a char boundary; it is inside '中' (bytes 2..5) of `EN中文`', src\libcore\str\mod.rs:2069:5 
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

Hash Maps

Maps are widely used in other languages. The most commonly used is the key-value hash map.

Creating a new hash map:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();

    map.insert("color", "red");
    map.insert("size", "10 m^2");

    println!("{}", map.get("color").unwrap());
}

Note: We didn't declare the hash map's generic types because of Rust's type inference mechanism.

Output:

red

The insert and get methods are the most commonly used methods for maps.

Hash maps support iteration:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();

    map.insert("color", "red");
    map.insert("size", "10 m^2");

    for p in map.iter() {
        println!("{:?}", p);
    }
}