Contents


Beginner's Guide to Rust

Get to know Rust

A compiled alternative to C and C++

Comments

Content series:

This content is part # of # in the series: Beginner's Guide to Rust

Stay tuned for additional content in this series.

This content is part of the series:Beginner's Guide to Rust

Stay tuned for additional content in this series.

If you have ever written software, you have undoubtedly asked yourself, "What language should I write this in?" It's a valid question. Does your code need to be as fast as possible? Will it be running on the Web? Will the code be on the back end or the front? All languages have their niches, and Rust is no different.

Rust is a statically typed compiled language that fills the roles that most users use C or C++ for. Unlike C or C++, though, Rust also encroaches on territory that C# and the Java™ language have dominated for much of this century: Rust is a language that is memory safe and operating system agnostic, meaning that it can run on any computer. Essentially, you get all the speed and low-level benefits of a systems language without the pesky garbage collection of those latter languages I mentioned. Excited? Yeah, me, too. Welcome to Rust!

Note: All the code and commands I demonstrate in this article I wrote on a computer running Linux®, but they are easily translatable to both macOS® and Windows®.

Installing and running Rust

Most beginners' guides dive right into the code, but I think it's better to know first how to compile and run the software before you start development. The Rust installation notes describe the steps necessary for installation, regardless of your operating system.

Next, I set up a project in which my examples will reside. I could make the directory structure of the project myself, but I would much rather use the powerful package manager that comes with Rust, called Cargo. Reading through the Cargo documentation, it’s easy to use Cargo to make a new executable binary program. Simply issue this command in the terminal:

$ cd ~/Documents
$ cargo new rusty_start –bin

The command cargo new rusty_start tells Cargo to create a new program directory structure named rusty_start. The --bin option tells Cargo that this program will be an executable binary. If you leave this option off, Cargo will create a library, instead. Essentially, replacing the file main.rs with lib.rs is simple. Looking at the new rusty_start directory in the tree program, I see the following:

$ cd rusty_start
$ tree .
.
??? Cargo.toml
??? src
    ??? main.rs

Perfect! What is the Cargo.toml file, though? It's what Cargo uses to build the program. Right now, it just describes the authors of the program and the program's dependencies. For this example, I am the only author, and no outside packages are needed.

Note in main.rs that Cargo also generated some code:

fn main() {
    println!("Hello, world!");
}

This is how Rust defines its main function. For now, just know that this function prints to the terminal. The question now is, How do I run this code? It’s actually simple (Listing 1).

Listing 1. Hello World! in Rust
$ cargo build
    Compiling …
     Finished …
$ cargo run
     Finished …
      Running …
Hello, world!

That was certainly painless compared with ; java or setting up CMake. To make things even easier, I could have executed cargo run, a command that both compiles and executes.

With Rust installed and running, I can move on to the good stuff.

Rust's core functionality

This section covers two topics that will make it far easier to understand the examples in later sections: binding variables and functions.

Caught in a bind

Unlike in the prebuilt Hello, World! program, I will eventually need to use variables in my Rust code. To bind values to a name, I use the let keyword in my main.rs file:

fn main() {
    let x = 5;
}

In this snippet, I'm binding the value 5 to the variable x. But, what about the type? Didn’t I say earlier that Rust is a statically typed language? Well, another fun fact about Rust is that it can infer the type of the variable from the value at compile time—a great nicety that isn't paid for at runtime. If I wanted to explicitly declare the type—say, a 32-bit signed integer—I could easily do that:

let x: i32 = 5;

You can also choose from several primitive types, which I cover in the next section.

Before I move on to functions, though, I want to demonstrate a fun and useful aspect of Rust’s let bindings using patterns:

let (x, y) = (5, ‘6’); // types (i32, char)

Here, I'm binding the integer 5 to the variable x and the character 6 to the variable y. This trick is extremely useful, and you can find patterns throughout the Rust community (the members of which are called Rustaceans). Patterns can do much more, but for the sake of simplicity, I'll leave it at this example.

By default, everything in Rust is immutable, meaning that you cannot change things later. For example, if I compile this command:

let x: i32 = 5;
x = 6;

The compiler yells at me, saying that x is not mutable. So, to change this variable, I must use the mut keyword:

let mut x: i32 = 5;
x = 6;

Now, everything is back to normal!

The last aspect of variables that I touch on is scoping. Scoping encompasses the idea that variables live only within the given set of curly braces ({}) in which they were instantiated. Access to variables outside their scope leads to compilation failure. Consider the code in Listing 2.

Listing 2. Scoping variables
fn main() {
    let x: i32 = 5; // x lives in this scope

    {
        let y: i32 = 4; // y only live in this scope.
    }
  
    println!(“x: {}, y: {}, x, y); // y is not in this scope
}

It's important to understand scoping when it comes to the ownership and borrowing properties of Rust. Now, on to functions!

What's your function?

Variables are great and all, but they're ultimately useless without their accompanying partners, functions. Every program has at least one function, and in Rust, it's the main function. I can define a new function, however, by using the fn keyword:

fn foo() {
}

What about taking arguments, you ask? Easy:

fn print_value(value: i32) {
    println!(“The value given was: {}”, value);
}

This function takes an integer argument and prints its value. Listing 3 shows how I can use it in main.rs.

Listing 3. Printing an argument's value
fn main() {
    print_value(17);
}

fn print_value(value: i32) {
    println!(“The value given was: {}”, value);
}

Just like variables you assign with let, arguments follow the pattern of a name followed by a semicolon (;), followed by a type. For multiple arguments, you simply separate variables with commas (,):

fn print_values(value_1: i32, value_2: i32) {
    println!(“Values given were: {} and {}”, value_1, value_2);
}

Now that I can make functions with and without arguments, I want to try to make a function that returns a value. How about one that increases an integer by one?

fn increase_by_one(value: i32) -> i32 {
    value += 1
}

In Rust, functions return only one value, and that type is declared after an arrow (->). Where am I returning the i32, though? Fun fact: In Rust, it's common practice to return a value by leaving off the semicolon. (The language has a return keyword, but it's not often used.) If I wanted to use the return keyword, I could write a function like this:

fn divmod(x: i32, y: i32) -> (i32, i32) {
    return (x / y, x % y);
}

There it is! The basics of binding variables and writing functions. As I mentioned, Rust has plenty of types to work with. I provide a rundown in the next section.

Am I your type?

Like every other programming language, Rust has a type system. I briefly introduce these types in the sections that follow.

Booleans

There isn’t much to say about the Boolean type. Rust has a built-in Boolean type—bool—with two values: true and false. Here's an example:

let x = true;
let y: bool = !x; // y == false

Chars

Like bool, Rust has a type for single Unicode scalar values: char. Unlike most languages, however, char is not a type to stuff numeric values into. Chars are initialized with single quotation marks, as shown here:

let x = ‘x’;
let y: char = ‘y’;

Strings

Rust is a special breed that has two string types: str and String. There’s nothing terribly interesting about str because it's "unsized," but if I throw an ampersand (&) in front of it, &str type becomes quite useful. In short, it uses the reference of str—a topic I cover in the next section. String is the heap-allocated, object version of &str.

First, let me dig into &str. This type is called a string slice. It is a fixed-sized, immutable sequence of UTF-8 bytes. I can bind this type to a variable by using double quotation marks:

let x = “Hello, World!”;
let y: &str = “Isn’t it a wonderful life?!”;

For String objects, which can be mutable, I can initialize the type with the String::from constructor:

let x = String::from(“Hello, World”);
let y: String = String::from(“Isn’t it a wonderful life?!”);

Numbers

Number types in Rust come in all signs and sizes. Here's the current list:

  • i8
  • i16
  • i32
  • i64
  • u8
  • u16
  • u32
  • u64
  • isize
  • usize
  • f32
  • f64

The types prefixed with an i indicate a signed integer, meaning that the number can be negative. The types prefixed with a u are unsigned integers. The types prefixed with an f are floats. The preceding numbers indicate the byte size of the type, and the preceding word size indicate the system's pointer size, which is used to access elements of sequential arrays.

Arrays

Speaking of arrays, I want to talk about how Rustaceans represent sequences of data. The basic strategy is it to use an array, such as this:

let x = [1, 2, 3];
let y: [i32; 3] = [4, 5, 6];

When explicitly declaring the type of an array, the formula is [T; N], where T is the type of the element and N is the number of elements. Arrays in Rust are like arrays other languages, accessing elements with brackets ([]), but they also have built-in functions such as len (the number of elements in the array):

let x = [1, 2, 3];
let s: usize = x.len(); // s == 3
let e = x[1]; // e is an i32, and e == 2;

Now, although arrays seem like fun, they are not often used. Most of the Rust community uses vectors, instead. Vectors, unlike arrays, allocate their data on the heap, much like the difference between &str and String. You create these types by using the macro vec!, and they have the type Vec<T>, where T is the type of the elements contained:

let x = vec![1, 2, 3];
let y: Vec<i32> = [4, 5, 6];

Tuples

Moving on to the last basic Rust type, a tuple is an ordered, immutable list of objects. Their benefit is that the types do not have to be uniform:

let x = (5, ‘6’);
let y: (i32, char) = (7, ‘8’);

These types, unlike arrays, must be indexed a bit differently:

let x = (5, ‘6’);
let y: i32 = x.0; // y == 5
let z: char = x.1; // z == ‘6’

If I want to make a tuple of a single value, I add a comma after the first element. Otherwise, the parenthesis is ignored:

let x: (bool) = (true,); x == (true)
let y: bool = (false); y == false

Flow control

It would be difficult to write and program in Rust if it didn't come with a handy set of control keywords. Here are two of the big ones.

If

Rust is like most other programming languages, so the example of Rust’s if statement in Listing 4 should be familiar.

Listing 4. An if statement in Rust
let x = ‘5’;

if x == ‘5’ {
    println!(“X is the char ‘5’”);
}

This can also be extended with the other keywords else if and else:

let x = ‘5’;

if x == ‘5’ {
    println!(“X is the char ‘5’!”);
} else if x == ‘6’ {
    println!(“X is the char ‘6’!”);
} else {
    println!(“I don’t know what X is.”);
}

You can also use if blocks like ternary expressions:

let x = ‘5’;
let y = if x == ‘5’ { 5 } else if x == ‘6’ { 6 } else { -1 };
// y == 5

This code works because the information to the left of the equal sign (=) is an expression that returns a value, much like a function. Notice the missing semicolons on the integers in the if blocks.

Loops

Again, Rust wouldn't be much of a language without a way to loop. The language offers three methods for looping: loop, for, and while. I start with loop, which simply loops forever:

loop {
    println!(“Looping for eternity!”);
}

If you're like me, you don't always like to loop forever. Fortunately, Rust provides a common while loop, which breaks on a given condition (Listing 5).

Listing 5. A while loop in Rust
let mut x = 0;

while x != 5 {
    println!(“x: {}”, x);
    x += 1; // this is equal to x = x + 1;
}

Listing 6 shows how to use loop with the break keyword, which stops the loop at that point.

Listing 6. Creating a loop with the break keyword in Rust
let mut x = 0;

loop {
    if x == 5 {
        break;
    }

    println!(“x: {}”, x);
    x += 1;
}

Finally, Rust has the for loop. Unlike the others, this loop is not like most other C-style languages, which use the form:

for(int x = 0; x < 5; x++) {
    printf(“%d\n”, x);
}

Instead, Rust has the form in Listing 7.

Listing 7. A typical Rust for loop (option 1)
for x in 0..5 {
    println!(“x: {}”, x);
}

Or, more generally (Listing 8) . . .

Listing 8. A typical Rust for loop (option 2)
for var in expression {
    …
}

. . . where expression can covert into an iterator. In the example in Listing 7, 0..5 converts into an iterator that starts at the first number and ends—but doesn't include—the second number. 0..5 is called a range. Rust doesn't conform to the standard C-style format so that it can avoid common mistakes when handling each element of the loop. Plus, it allows for Python-style iteration through objects like vectors, such as in Listing 9.

Listing 9. Iteration in Rust
let x = vec![0, 1, 2, 3, 4];

for element in &x {
    println!(“element: {}”, element);
}

// prints out the elements in x: 0, 1, 2, 3, 4

The &x means that the for loop is "borrowing" the vector, which is a concept I cover in the next section.

Borrowing from an owner

Hands down, borrowing is the most important section of this article and Rust generally. Coincidentally, borrowing is also the concept that frustrates new users the most. This system is what allows Rust to be as fast as C but much, much safer.

Ownership

In Rust, every variable has ownership of the value to which it's bound. When the variable goes out of scope, Rust cleans up that resource. For example:

fn foo() {
    let x = String::from(“Hello, World!”);
}

When x is created in the scope of the function foo, the assigned data in the string is allocated in the heap. Now, when the function ends, x will go out of scope, and Rust will clean up the related string data, even the bytes in the heap. I will no longer have to live in the pain of C/C++, remembering to include free or delete.

Another great way to view ownership in action is with move semantics. In addition to every variable having ownership of its bound value, Rust ensures that every resource is bound to a single value. In other words, if a resource is reassigned from one variable to another, the first variable is no longer valid (Listing 10).

Listing 10. Move semantics in Rust
fn main() {
    let x = String::from(“Hello, World!”);
    
    println!(“{}”, x); // x is valid here

    let y = x; // the resource assigned to x is move to y
    
    println!(“{}”, x);
}

If I run this code, the program will fail, and the compiler will notify me that I tried to use the variable x, which is moved to variable y. This behavior applies to functions, as well (Listing 11).

Listing 11. Move semantics in Rust functions
fn main() {
    let x = vec![1, 2, 3];

    take_ownership(x);

    println!(“{:?}”, x); // ignore the ‘:?’ for now.
                         // it just prints the full vector.
}

fn take_ownership(v: Vec<i32>) {
   println!(“I took this data: {:?}”, v);
}

This program will fail again, and the compiler will tell me that I tried to use the variable x, which is moved to variable v in the function take_ownership. I can fix this by using the construction in Listing 12.

Listing 12. Fixing the compiler error
fn main() {
    let x = vec![1, 2, 3];

    let x = print_vector(x);

    println!(“{:?}”, x);
}

fn print_vector(v: Vec<i32>) -> Vec<i32> {
   println!(“I took this data: {:?} , and returned it”, v);
   v
}

But, this seems silly, doesn't it? What if I wanted to sum the elements in the vector and return that value? Would I really have to return a tuple of the original vector and its sum? No: That's what borrowing is for. Before I move onto that topic, though, remember these three rules to ownership:

  • Each value in Rust has a variable that's called its owner.
  • There can be only one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

References and borrowing

Now that we have a clear definition of what ownership is, I'll dig into another, equally important concept: borrowing. Rust has three rules to borrowing resources:

  • No borrow can outlive the original owner's scope.
  • You can have as many immutable borrows, or references, to a resource at any given time.
  • You can have a single mutable borrow, or reference, to a resource at any given time.

Notice that rules 2 and 3 are mutually exclusive.

At the end of the subsection Ownership, I was left with the nasty conundrum with object ownership and functions shown in Listing 13.

Listing 13. The object ownership conundrum
fn main() {
    let x = vec![1, 2, 3];

    let x = print_vector(x);

    println!(“{:?}”, x);
}

fn print_vector(v: &Vec<i32>) -> Vec<i32> {
   println!(“I took this data: {:?} , and returned it”, v);
   v
}

I had to habitually return the original resource to the original owner. This functionality is not idiomatic Rust. Instead I can use the borrow system with references to achieve the same goal but without the hideous returns. (Listing 14).

Listing 14. Borrowing with references
fn main() {
    let x = vec![1, 2, 3];

    print_vector(&x);

    println!(“{:?}”, x);
}

fn print_vector(v: &Vec<i32>) {
   println!(“I borrowed this data: {:?}”, v);
}

The program in Listing 14 compiles just fine. By prefixing an ampersand to x when its passed to print_vector and to print_vector’s argument, I solved a huge issue. Idiomatically, I would say this as print_vector borrows x. Now, what if I want to write a function that pushes the value 5 to x? First, I'll have to make m mutable. (Listing 15).

Listing 15. Making m mutable
fn main() {
    let mut x = vec![1, 2, 3];

    push_five(&x);

    println!(“{:?}”, x);
}

fn push_five(v: &Vec<i32>) {
    v.push(5);
}

What gives? The build failed, and the compiler is yelling at me! Well, it's because the argument to print_five is not mutable. It's just an immutable reference to the resource. Let me change that (Listing 16).

Listing 16. Fixing the mutable build
fn main() {
    let mut x = vec![1, 2, 3];

    push_five(&x);

    println!(“{:?}”, x);
}

fn push_five(v: &mut Vec<i32>) {
    v.push(5);
}

It’s still mad?! That’s right: I'm only passing an immutable reference to x to print_five. Let me change that line:

push_five(&mut x);

There it is. The compiler is finally happy. Now, looking at the three rules, let me see if I can break them. Here's how I break rule 1 (Listing 17).

Listing 17. Breaking Rust rule 1
fn main() {
    let x = 4;
    let mut x_ref: &i32 = &x;

    // this works, and prints “x_ref: 4”
    println!(“x_ref: {}”, x_ref);

    {
        let y = 5;
        x_ref = &y;
    }

    // this, however, will fail, according to rule 1!
    println!(“x_ref: {}”, x_ref);
}

The compiler is telling me that the borrowed value y does not live long enough for x_ref to be valid, which is great because I have made many of these errors in C without even knowing it! Now, onto breaking rule 2 and 3 mutual exclusivity (Listing 18).

Listing 18. Breaking Rust rule 2
fn main() {
    let mut x = 4; // needs to be mut to borrow mutable.
    let x_ref_1 = &x;
    let x_ref_2: &i32 = &x; // this is fine because we can have 
                            // multiple immutable references.

    let x_mut_ref = &mut x; // this is not fine because it
                            // breaks the rule 2 and 3 mutual
                            // exclusivity.
}

Success! The compiler is warning me that I cannot mutably borrow x while also immutably borrowing it. Finally, I want to break rule 3 (Listing 19).

Listing 19. Breaking Rust rule 3
fn main() {
    let mut x = 4;
    let x_mut_ref_1 = &x;
    let x_mut_ref_2: &mut i32 = &x;
}

Huzzah! I broke it again. It sure seems like these three rules are strongly enforced—and for a reason. Having multiple variables with the ability to change the resource through the reference can lead to bug city! Rust truly is fun, but it's safe, too.

I object!

In this section, I move on to creating objects in Rust. In this object-oriented world, it seems necessary.

Structs

Unlike most languages, Rust doesn't have classes. Instead, it has structures that only hold data. Here's a typical structure in Rust:

struct Circle {
    center: (i32, i32),
    radius: u32,
}

Breaking this structure down, I use the keyword struct to declare a custom structure type. When adding member variables, I use the key–value syntax with the form variable_name: type. I separate multiple member variables with commas. Finally, no semicolons are needed to terminate the declaration, much like functions. Now, that I have this structure, Listing 20 shows how I would use it.

Listing 20. The structure syntax in Rust
fn main() {
    let c_1 = Circle {
        center: (0, 0),
        radius: 1,
    };

    let c_2: Circle = Circle {
        center: (-1, 1),
        radius: 2,
    };

    println!(“c_1’s radius is {}”, c_1.radius);
}

struct Circle {
    center: (i32, i32),
    radius: u32,
}

First, the impl keyword signals to Rust that it must associate the following functions with the given type—in this case, Circle. Next, in Rust it's idiomatic to name constructors "new." This new function is called an associated function. In other words, the type is used to call the function, like Circle::new in Listing 20.

With that done, how about some methods? Take a look at Listing 21.

Listing 21. Example methods in Rust
fn main() {
    let mut c = Circle::new((0, 0), 1);

    println!(“The circle’s area is {}”, c.area());
    println!(“The circles location is {:?}”, c.center);
    
    c.move_to((-1, 1));
    
    println!(“The circles location is {:?}”, c.center);}

struct Circle {
    center: (i32, i32),
    radius: u32,
}

impl Circle {
    fn new(center: (i32, i32), radius: u32) -> Circle {
        Circle {
            center: center,
            radius: radius,
        }
    }
    
    fn area(&self) -> f64 {
        // converting self.radius, a u32, to an f64.
        let f_radius = self.radius as f64;
        
        f_radius * f_radius * 3.14159
    }
    
    fn move_to(&mut self, new_center: (i32, i32)) {
        self.center = new_center;
    }
}

Adding methods seems straightforward, but what are these new self arguments? This argument is passed to the method by the dot syntax (shown in Listing 21 in the main function), and its value is the object that called the method.

Methods in Rust have three unique variables: self, &self, &mut self. Remembering all that business about ownership and borrowing, these variables, respectively, move object ownership to the method, immutably borrow an object, and mutably borrow an object.

Now I have a structure with methods and a constructor that I can use like the basic object found in other programming languages. There's just one last type to cover: enum.

Enums

An enum is a type that represents varying state. Many C/C++ users might recognize this ideal because it's heavily used to indicate a specific state of a program. Here's how to create this type using the enum keyword:

enum Color {
    Red,
    Green,
    Blue,
}

This looks oddly like the declaration of a struct but without assigning a type to the variants. With this new enum, I can assign a variable of the type shown in Listing 22.

Listing 22. Assigning variables
fn main() {
    let red = Color::Red;
    let blue: Color = Color::Blue;
}

enum Color {
    Red,
    Green,
    Blue,
}

Now, Rust has done some unique work with the enum type, like adding associated data to the variants, but that's outside the scope of this article.

Miscellaneous functionality

So far, I've covered all the necessities anyone could need to start a Rust project. This section touches on some niceties to help smooth out the edges.

Match

match is a keyword that often comes up when looking at programs written in Rust. Some might see it as a simple switch/case replacement or that it's nothing an if/else block can't handle, but they would be wrong. if/else blocks can get wildly out of hand as conditions become complex, and the switch/case blocks found in C and C++ are riddled with errors (if the default case is forgotten). Listing 23 provides a simple example of match.

Listing 23. A simple example of match
fn main() {
    let x = 5;
    
    match x {
        1 => println!(“Matched to 1!”),
        2 => println!(“Matched to 2!”),
        3 => println!(“Matched to 3!”),
        4 => println!(“Matched to 4!”),
        5 => println!(“Matched to 5!”),
        6 => println!(“Matched to 6!”),
        _ => println!(“Matched to some other number!”),
    };
}

Here's how it works: match takes an expression, like x, and finds the branch that matches the value. Each "arm" of the match statement takes on the form value => expression, separated by colons. When the proper arm is found, the branch's expression is executed. The _ value is Rust's syntax for "anything else" or "default."

A special aspect of match is that it forces you to exhaustively cover every possible outcome of the given expression to be matched. In other words, if I were to remove the _ branch, the build would fail and the compiler would notify me that not all possible values of i32 were dealt with (that is, values from -2,147,483,648 to 2,147,483,647).

Another fun use of match is its use with enum, shown in Listing 24.

Listing 24. Using match with enum
fn main() {
    let color = Color::Red;

    match color {
        Color::Red => println!(“Got a red color!”),
        Color::Green => println!(“Got a green color!”),
        Color::Blue => println!(“Got a blue color!”),
    };
}

enum Color {
    Red,
    Green,
    Blue,
}

Finally, because match is an expression, I can use it to assign variables, much like the if block I used earlier as a ternary expression (Listing 25).

Listing 25. Using match to assign variables
let x = 5;
    
let s = match x {
    1 => “Matched to 1!”,
    2 => “Matched to 2!”,
    3 => “Matched to 3!”,
    4 => “Matched to 4!”,
    5 => “Matched to 5!”,
    6 => “Matched to 6!”,
    _ => “Matched to some other number!”,
};

println!(“{}”, s);

Modules

You might be alarmed that I haven't mentioned namespaces. That's because in Rust there are none. Instead, Rust uses modules, created with the mod keyword. You define modules as shown in Listing 26.

Listing 26. Defining modules
fn main() {
    let c = Circle {
        center: (0, 0),
        radius: 1,
    };
}

mod shapes {
    struct Circle {
        center: (i32, i32),
        radius: u32,
    }
}

Notice that I put my previously defined Circle structure in the mod shapes block. This means that Circle is now under that namespace, which is why the build fails when I compile the program. The compiler is warning me that the type Circle is not defined in the scope of main. I fix that by using the module's name followed by double colons (Listing 27).

Listing 27. Defining Circle
fn main() {
    let c = shapes::Circle {
        center: (0, 0),
        radius: 1,
    };
}

mod shapes {
    struct Circle {
        center: (i32, i32),
        radius: u32,
    }
}

The build is still failing, but this time the compiler is telling me that the Circle structure is private. This is standard in Rust because all types, variables, and functions in a module are private by default. To turn this behavior off, I have to use the pub keyword. Listing 28 shows the fix to the Circle declaration.

Listing 28. Fixing the Circle declaration
mod shapes {
    pub struct Circle {
        center: (i32, i32),
        radius: u32,
    }
}

That code fixed the Circle privacy issue, but now I'm getting warnings that the member variables center and radius are private. I'll fix that now:

mod shapes {
    pub struct Circle {
        pub center: (i32, i32),
        pub radius: u32,
    }
}

Finally, the program compiles. But, that begs the question, Do I really want the module in the main.rs file? The answer is no: I want it in a separate file. I'm going to create a new shapes.rs file in the same src folder that holds main.rs. In this file, I put this new declaration of Circle. Listing 29 shows the new files and their contents.

Listing 29. The new shapes.rs
main.rs:
fn main() {
    let c = shapes::Circle {
        center: (0, 0),
        radius: 1,
    };
}

shapes.rs:
pub struct Circle {
    pub center: (i32, i32),
    pub radius: u32,
}

Okay, now that I've moved the shapes contents to shapes.rs, how do I access Circle? Easy: you can use the mod keyword to find files of a given name and use them as a namespace. Listing 30 shows the contents of my new main.rs file.

Listing 30. The contents of main.rs
mod shapes;

fn main() {
    let c = shapes::Circle {
        center: (0, 0),
        radius: 1,
    };
}

Basically, that mod statement signals Rust to look for either a shapes.rs file or a shapes folder containing a mod.rs file. If I didn't want to constantly use shapes::Circle, I can use a little trick with Rust: the use keyword. Listing 31 shows how this works.

Listing 31. The use keyword
mod shapes;

use shapes::Circle;

fn main() {
    let c = Circle {
        center: (0, 0),
        radius: 1,
    };
}

Now, I can just use Circle as if I declared the structure in this scope. You can see a more advanced use in Listing 32, where I want to use a hashmap (located in Rust's std::collections module).

Listing 32. Using a hashmap
use std::collections::HashMap;

fn main() {
    let mut counter = HashMap::new(); // HashMap<char, i32>

    counter.insert(‘a’, 1);
}

Whew! Almost done. The last thing to cover is how you can use third-party packages, or as Rust calls them, Crates.

Crates

Now that I know how to create number variables that I set myself, what if I wanted a variable that was random? Well, looking through the Rust documentation, I couldn't find a standard implementation of random variables. So, I'm going to have use a third-party library. A quick Google search yields Crate rand.

First, I have to change my Cargo.toml file to the code in Listing 33.

Listing 33. Cargo.toml modified for rand
[package]
name = "rusty_start"
version = "0.1.0"
authors = ["Dylan Hicks <dirtgrub.dylanhicks@gmail.com>"]

[dependencies]
rand = "0.4"

By adding rand = "0.4" to my dependencies, I'm telling Cargo to search for the crate by this name and version and compile it into my binary. In my main.rs file, I use the extern crate keywords:

extern crate rand:

fn main() {
    let x = 5;
}

I can now use the rand crate to generate a random number with the help of the use keyword, as shown in Listing 34.

Listing 34. Generating a random number with rand crate
extern crate rand:

use rand::Rng;

fn main() { 
    let mut rng = rand::thread_rng(); // random number generator
    let x = rng.get::<i32>(); // ignore the ::<i32>
                              // this just tells the rng to make
                              // an i32.

    println!(“x: {}”, x);
}

Final thoughts

I can't believe I made it! But, the truth is, I didn't even scrape the surface of what Rust is. For anyone starting a new project that requires a systems language or if the talk of Java or C# is in the air, please consider Rust as an alternative. Rust is fast, fun, easy to read, and safer than any other language out there, eliminating as much human error as possible. To learn more, visit https://doc.rust-lang.org/book and read both books Mozilla has written about its great language.


Downloadable resources


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Open source
ArticleID=1060799
ArticleTitle=Beginner's Guide to Rust: Get to know Rust
publish-date=05072018