Primitives

The language

The default “Hello, world!” program in Rust looks something like this:

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

If you’re already familiar with languages like C, C++, Java, etc., this sort of boilerplate should feel natural:

  • The default program entrypoint is a function called main. It takes no arguments and has no return type.

  • The body of the function is enclosed in curly braces.

  • The contents of the function are:

    • A function call to println! with a single argument, the string "Hello, world!".

      4

  • tform dependent: 32 bits on a 32-bit machine, 64 bits on a 64-bit machine, etc.)

  • f32 — float

  • f64 — double

Simple function definitions typically look like this:

fn function_name(param1: Type1, param2: Type2) -> ReturnType { // body }

where the parameter list (everything inside the round brackets (...)) and return type (everything between the closing parenthesis ) and the opening curly bracket {) is optional, depending on the needs of your function.

From here on, all of the code we’ll be discussing can go directly inside the main function body (although some things, like structs and other function definitions, can, and are usually recommended to be placed outside).

Basic bindings and numeric primitives

The first thing we’ll look at is variable bindings.

let my_variable = 10;

(In addition to being valid Rust, this is also valid JavaScript—how about that!)

Though the syntax here is pretty simple, there’s a bit more going on under the hood.

Remember how Rust is a statically-typed programming language? Well, in this case, the type is not explicit in the code, but the compiler still assigns it one. In this case, the type of this variable is i32, which is the default for otherwise unrestricted number types. i32 means “32-bit signed integer”.

As you may have guessed, there are a bunch of other number types. Generally, the names take the form of <single-letter-prefix><bit-size>.

PrefixMeaningPossible Sizes

i

Signed integer

8, 16, 32, 64, 128, size

u

Unsigned integer

8, 16, 32, 64, 128, size

f

Floating-point

32, 64

Examples:

  • i8 — signed byte

  • u64 — unsigned 64-bit integer

  • usize — unsigned word-size integer (plaMore about bindings

We have three options to explicitly indicate the type of our binding:

  • let my_variable: i32 = 10;

    This syntax declares the type of the binding, and the expression is coerced to that type if possible.

  • let my_variable = 10 as i32;

    This syntax casts the expression to the given type, which the binding then assumes.

  1. let my_variable = 10i32;

    This syntax is unique to numeric literals, and it declares the type of the expression, which the binding then assumes.

If the compiler is able to determine the appropriate type for your binding by itself, you do not need to specify it in the source code. Note that type declarations for function parameters and return types are always required (unless the function does not return anything).

Other primitives

Booleans

In addition to numeric types, Rust also has booleans:

`// This is a comment; ignored by the compiler // This type annotation is superfluous, but it is included for clarity. let happy_to_learn_rust: bool = true;

let ever_going_to_give_you_up = false;`

Characters

Single Unicode characters, delimited by single quotes ':

let currency_symbol: char = '$'; // Multi-byte characters allowed! let kanji_character: char = '字';

Strings

This can be a bit of a tricky topic in Rust, because there are a few different string types to think about. However, you usually only need to worry about two:

  • String is a heap-allocated string that can be mutated in-place. This is called an “owned string”.

  • &str is a fixed-length (sometimes stack-allocated) string that cannot be mutated. This can be called a “string slice” or “string reference”.

(Technically this is a bit of an oversimplification, but it will get you 90% of the way there. Once we’ve gone over references and ownership we can come back to this topic.)

For now, you can convert between the two string types fairly easily:

let my_str: &str = "hello"; let my_string: String = my_str.to_string(); let another_str: &str = &my_string;

Arrays

Arrays are fixed-length, homogenous collections delimited by square brackets [] with elements separated by commas ,. Note the type signature takes the form of [<element-type>; <length>].

`let my_i32_array: [u32; 4] = [1, 2, 3, 4]; let my_bool_array: [bool; 0] = []; let my_char_array: [char; 3] = ['a', 'b', 'c'];

// Error: type signature has incorrect length // let incorrect_array: [char; 100] = [];`

Tuples

Tuples are fixed-length, heterogenous collections delimited by round brackets () with elements separated by commas ,.

let my_tuple: (i32, char, [bool; 2]) = (1, 'a', [true, false]); // It's probably easier to just let the compiler compute the type let my_tuple = (1, 'a', [true, false]);

Structs

To a programmer familiar with an object-oriented or classical style of programming, structs should feel familiar. They look and often feel very similar to a class or dictionary-esque value.

First, we define the type:

struct BlogPost { title: String, text: String, author: String, timestamp: u64, }

Then, to create an instance:

let mut post = BlogPost { title: "Rust quickstart for JavaScript programmers".to_string(), text: "So, you want to learn Rust, and fast...".to_string(), author: "Jacob Lindahl".to_string(), timestamp: 1600000000000, };

Member access:

`// read println!("{}", post.title);

// mutate post.author = "John Doe".to_string();`

Now, really, structs are just a way to give nice labels to pieces of a tuple. This is evident in some of the other ways you can declare a struct type:

struct MyTupleStruct(u8); struct RGBColor(u8, u8, u8); struct RGBAColor(u8, u8, u8, u8);

If you’re familiar with the newtype pattern, this is usually how it is implemented in Rust.

If you really want, you can also create completely empty structs:

struct LiterallyNothing;

While these may not initially seem to be useful, implementing some traits on them may change that.

Enums

Rust’s enums are one of its most powerful features. Although languages like Java and TypeScript offer a primitive form of enum, Rust’s are fully-featured sum types, implemented as tagged unions.

If you’re coming from a language like Java, the most basic enum will look pretty familiar:

enum LogLevel { Error, Warn, Info, Debug, }

However, Rust is just getting started. Each variant of an enum can contain data, just like a struct or a tuple.

enum ImageFilter { InvertColors, Blur(f64), HueRotate(f64), DropShadow { x: f64, y: f64, blur: f64, color: String, }, }

The power doesn’t end there: keep reading for more!

Last updated