Intro
This is the README.md
Getting Started with Rust
Data Types
Data Types tell Rust which can of data is being specified so it knows how to work with that data. Rust has two major subsets of data types: scalar and compound.
Rust is a statically typed language, which means it needs to know the types of all variables at compile time.
Scalar Types
A scalar type represents a single value. Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters.
Integer
An integer is a number without a fractional component. E.g. 1, 2, 3, 4, 5... You can use the prefix u
for unsigned integer types (i.e. non-negative numbers don't need a sign -
). Or you can use i
for integers that could potentially be negative.
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
Signed and unsigned refer to whether it’s possible for the number to be negative—in other words, whether the number needs to have a sign with it (signed) or whether it will only ever be positive and can therefore be represented without a sign (unsigned). Each signed variant can store numbers from –(2n – 1) to 2n – 1 – 1 inclusive, where n is the number of bits that variant uses. So an i8 can store numbers from –(27) to 27 – 1, which equals –128 to 127. Unsigned variants can store numbers from 0 to 2n – 1, so a u8 can store numbers from 0 to 28 – 1, which equals 0 to 255.
Number literals | Example |
---|---|
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte (u8 only) | b'A' |
So how do you know which type of integer to use? If you’re unsure, Rust’s defaults are generally good places to start: integer types default to i32.
Floating-Point Types
Rust also has two primitive types for floating-point numbers, which are numbers with decimal points. Rust’s floating-point types are f32 and f64, which are 32 bits and 64 bits in size, respectively. The default type is f64 because on modern CPUs, it’s roughly the same speed as f32 but is capable of more precision. All floating-point types are signed.
fn main() { let x: f32 = 4.234; }
The Boolean Type
As in most other programming languages, a Boolean type in Rust has two possible values: true and false. Booleans are one byte in size.
fn main() { let t = true; let f: bool = false; }
The Character Type
Rust’s char type is the language’s most primitive alphabetic type.
fn main() { let a = 'b'; let z: char = 'Z'; }
Note that we specify char literals with single quotes, as opposed to string literals, which use double quotes.
Compund Types
Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.
The Tuple Type
A tuple is a general way of grouping together a number of values with a variety of types into one compound type. Tuples have a fixed length: once declared, they cannot grow or shrink in size. We create a tuple by writing a comma-separated list of values inside parentheses.
fn main() { let tup: (i32, f64, u8) = (1000, 3.14159, 7); let (a, b, c) = tup; println!("The value of b is: {b}"); }
It then uses a pattern with let to take tup and turn it into three separate variables, a
, b
, and c
. This is called destructuring because it breaks the single tuple into three parts.
We can also access a tuple element directly by using a period (.) followed by the index of the value we want to access.
fn main() { let tup: (i32, f64, u8) = (1000, 3.14159, 7); let one_thousand = tup.0; let text_pi = tup.1; let lucky_number_seven = tup.2; println!("{text_pi}"); }
The tuple without any values has a special name, unit.
The Array Type
Another way to have a collection of multiple values is with an array. Unlike a tuple, every element of an array must have the same type. Unlike arrays in some other languages, arrays in Rust have a fixed length.
fn main() { let a = [1, 2, 3, 4, 5]; println!("{:?}", a); }
Arrays are useful when you want your data allocated on the stack rather than the heap or when you want to ensure you always have a fixed number of elements. An array isn’t as flexible as the vector type, though. A vector is a similar collection type provided by the standard library that is allowed to grow or shrink in size. If you’re unsure whether to use an array or a vector, chances are you should use a vector. However, arrays are more useful when you know the number of elements will not need to change.
You write an array’s type using square brackets with the type of each element, a semicolon, and then the number of elements in the array.
fn main() { let a: [i32; 5] = [1, 2, 3, 4, 5]; println!("{:?}", a) }
You can also initialize an array to contain the same value for each element by specifying the initial value, followed by a semicolon, and then the length of the array in square brackets.
fn main() { let a = [5; 10]; println!("{:?}", a); }
An array is a single chunk of memory of a known, fixed size that can be allocated on the stack. You can access elements of an array using indexing,
fn main() { let a = [1, 2, 3, 4, 5]; let first = a[0]; let second = a[1]; println!("{first} then {second}"); }
Let’s see what happens if you try to access an element of an array that is past the end of the array.
fn main() { let a = [1, 2, 3, 4, 5]; let err = a[8]; }
The program resulted in a runtime error at the point of using an invalid value in the indexing operation.
Variables
Variables and Mutability
By default, variables are immutable in Rust. This means that once a value is bound to a variable name, you are no longer able to make changes to it. For example, the following will give you an error:
fn main() { // Let's assign the value of 5 to `x` let x = 5; println!("The value of x is: {x}"); // Let's try reassigning `x` to a value of 6 x = 6; println!("The value of x is: {x}"); }
These copiler errors can seem burdensome and frustrating at first, but you will soon grow to love the rich and detailed feedback of the Rust compilier. Rust's compiler even tells you how to correct the error in most instances. By reading the output of the compiler, we can see that we need to add a mut
identifier to our variable of x
. mut
will turn our variable into a mutable instance. That means we can make changes to it, but this can also be dangerous. Which is why it is has to be specified, so you are aware that you are doing something dangerous (this is a recurring theme in Rust). Let now change our x
variable to be mutable :
fn main() { // Let's assign the value of 5 to `x` let mut x = 5; println!("The value of x is: {x}"); // Let's try reassigning `x` to a value of 6 x = 6; println!("The value of x is: {x}"); }
Constants
Like immutable variables, constants are values that are bound to a name and are not allowed to change, but there are a few differences between constants and variables. You are not allowed to use mut
with constants. You also use const
keyword when declaring a constant and you need to annotate the type (i.e. if my constanct is an integer I need to put : u8
after the name of the constant). Constants can be delared in any scope, they also need to be set and cannot be the result of a runtime expression. Rust's standard naming convention for constants is all uppercase with an underscore between words.
fn main() { let FOUR_HOURS_IN_SECONDS = 4 * 60 * 60; println!("{FOUR_HOURS_IN_SECONDS}"); }
Shadowing
You can declare a new variable with the same name as a previous variable, you would then say the first variable is shadowed by the second. the second variable overshadows the first, taking any uses of the variable name to itself until either it itself is shadowed or the scope ends. Here is an example:
fn main() { // Assign 5 to `x` let x = 5; // Reassign the variable of `x` let x = x + 1; { // Assign `x` a new value to this inner scope let x = x * 2; println!("The value of x in the inner scope is: {x}"); } println!("The value of x in the outer scope is: {x}"); }
You might be curious why this ran but the previous code gave an error when we tried to reassign x
. Look close at the usage of let
in both code blocks. Shadowing is different from marking a variable as mut because we’ll get a compile-time error if we accidentally try to reassign to this variable without using the let keyword. By using let, we can perform a few transformations on a value but have the variable be immutable after those transformations have been completed. The other difference between mut and shadowing is that because we’re effectively creating a new variable when we use the let keyword again, we can change the type of the value but reuse the same name.
Let's test this out in practice. We are going to initialize spaces
as a string type then reassign it as a integer type:
fn main() { let spaces = " "; let spaces = spaces.len(); }
We don't get an error because we reinitialzied with let
. Now, let's try it without let
and read the compiler error:
fn main() { let mut spaces = " "; spaces = spaces.len(); }
Even though spaces is mutable, we tried to assign it a different type, which throws a compiler errror.
Data Types
Data Types tell Rust which can of data is being specified so it knows how to work with that data. Rust has two major subsets of data types: scalar and compound.
Rust is a statically typed language, which means it needs to know the types of all variables at compile time.
Scalar Types
A scalar type represents a single value. Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters.
Integer
An integer is a number without a fractional component. E.g. 1, 2, 3, 4, 5... You can use the prefix u
for unsigned integer types (i.e. non-negative numbers don't need a sign -
). Or you can use i
for integers that could potentially be negative.
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
Signed and unsigned refer to whether it’s possible for the number to be negative—in other words, whether the number needs to have a sign with it (signed) or whether it will only ever be positive and can therefore be represented without a sign (unsigned). Each signed variant can store numbers from –(2n – 1) to 2n – 1 – 1 inclusive, where n is the number of bits that variant uses. So an i8 can store numbers from –(27) to 27 – 1, which equals –128 to 127. Unsigned variants can store numbers from 0 to 2n – 1, so a u8 can store numbers from 0 to 28 – 1, which equals 0 to 255.
Number literals | Example |
---|---|
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte (u8 only) | b'A' |
So how do you know which type of integer to use? If you’re unsure, Rust’s defaults are generally good places to start: integer types default to i32.
Floating-Point Types
Rust also has two primitive types for floating-point numbers, which are numbers with decimal points. Rust’s floating-point types are f32 and f64, which are 32 bits and 64 bits in size, respectively. The default type is f64 because on modern CPUs, it’s roughly the same speed as f32 but is capable of more precision. All floating-point types are signed.
fn main() { let x: f32 = 4.234; }
The Boolean Type
As in most other programming languages, a Boolean type in Rust has two possible values: true and false. Booleans are one byte in size.
fn main() { let t = true; let f: bool = false; }
The Character Type
Rust’s char type is the language’s most primitive alphabetic type.
fn main() { let a = 'b'; let z: char = 'Z'; }
Note that we specify char literals with single quotes, as opposed to string literals, which use double quotes.
Compund Types
Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.
The Tuple Type
A tuple is a general way of grouping together a number of values with a variety of types into one compound type. Tuples have a fixed length: once declared, they cannot grow or shrink in size. We create a tuple by writing a comma-separated list of values inside parentheses.
fn main() { let tup: (i32, f64, u8) = (1000, 3.14159, 7); let (a, b, c) = tup; println!("The value of b is: {b}"); }
It then uses a pattern with let to take tup and turn it into three separate variables, a
, b
, and c
. This is called destructuring because it breaks the single tuple into three parts.
We can also access a tuple element directly by using a period (.) followed by the index of the value we want to access.
fn main() { let tup: (i32, f64, u8) = (1000, 3.14159, 7); let one_thousand = tup.0; let text_pi = tup.1; let lucky_number_seven = tup.2; println!("{text_pi}"); }
The tuple without any values has a special name, unit.
The Array Type
Another way to have a collection of multiple values is with an array. Unlike a tuple, every element of an array must have the same type. Unlike arrays in some other languages, arrays in Rust have a fixed length.
fn main() { let a = [1, 2, 3, 4, 5]; println!("{:?}", a); }
Arrays are useful when you want your data allocated on the stack rather than the heap or when you want to ensure you always have a fixed number of elements. An array isn’t as flexible as the vector type, though. A vector is a similar collection type provided by the standard library that is allowed to grow or shrink in size. If you’re unsure whether to use an array or a vector, chances are you should use a vector. However, arrays are more useful when you know the number of elements will not need to change.
You write an array’s type using square brackets with the type of each element, a semicolon, and then the number of elements in the array.
fn main() { let a: [i32; 5] = [1, 2, 3, 4, 5]; println!("{:?}", a) }
You can also initialize an array to contain the same value for each element by specifying the initial value, followed by a semicolon, and then the length of the array in square brackets.
fn main() { let a = [5; 10]; println!("{:?}", a); }
An array is a single chunk of memory of a known, fixed size that can be allocated on the stack. You can access elements of an array using indexing,
fn main() { let a = [1, 2, 3, 4, 5]; let first = a[0]; let second = a[1]; println!("{first} then {second}"); }
Let’s see what happens if you try to access an element of an array that is past the end of the array.
fn main() { let a = [1, 2, 3, 4, 5]; let err = a[8]; }
The program resulted in a runtime error at the point of using an invalid value in the indexing operation.
Functions
You are already familiar with functions, becuase in previous examples we have seen the use of the fn main() {}
function. The main
function is the entrypoint to many programs. You use fn
to declare new functions. Rust code uses snake case as the conventional style for function and variable names, in which all letters are lowercase and underscores separate words.
fn main() { println!("Hello, World"); another_function(); } fn another_function() { println!("another function"); }
Parameters
We can define functions to have parameters, which are special variables that are part of a function’s signature. When a function has parameters, you can provide it with concrete values for those parameters. Technically, the concrete values are called arguments, but in casual conversation, people tend to use the words parameter and argument interchangeably for either the variables in a function’s definition or the concrete values passed in when you call a function.
fn main() { another_function(5); } fn another_function(x: i32) { println!("The value of x is: {x}"); }
In function signatures, you must declare the type of each parameter. This is a deliberate decision in Rust’s design: requiring type annotations in function definitions means the compiler almost never needs you to use them elsewhere in the code to figure out what type you mean.
fn main() { print_labeled_measurement(5, 'h'); } fn print_labeled_measurement(value: i32, unit_label: char) { println!("The measurement is: {value}{unit_label}"); }
Statments and Expressions
Function bodies are made up of a series of statements optionally ending in an expression. So far, the functions we’ve covered haven’t included an ending expression, but you have seen an expression as part of a statement. Because Rust is an expression-based language, this is an important distinction to understand. Other languages don’t have the same distinctions, so let’s look at what statements and expressions are and how their differences affect the bodies of functions.
- Statements are instructions that perform some action and do not return a value.
- Expressions evaluate to a resultant value.
Function definitions are also statements and staements don't return values. Expressions evaluate to a value and make up most of the rest of the code that you’ll write in Rust.
- Calling a function is an expression.
- Calling a macro is an expression.
Expressions do not include ending semicolons. If you add a semicolon to the end of an expression, you turn it into a statement, and it will then not return a value. Keep this in mind as you explore function return values and expressions next.
Functions with Return Values
Functions can return values to the code that calls them. We don’t name return values, but we must declare their type after an arrow (->). In Rust, the return value of the function is synonymous with the value of the final expression in the block of the body of a function. You can return early from a function by using the return keyword and specifying a value, but most functions return the last expression implicitly.
fn five() -> i32 { 5 } fn main() { let x = five(); println!("The value of x is: {x}", x) }
There are no function calls, macros, or even let statements in the five function—just the number 5 by itself. That’s a perfectly valid function in Rust.
The five function has no parameters and defines the type of the return value, but the body of the function is a lonely 5 with no semicolon because it’s an expression whose value we want to return.
Control Flow
The ability to run some code depending on whether a condition is true and to run some code repeatedly while a condition is true are basic building blocks in most programming languages. The most common constructs that let you control the flow of execution of Rust code are if expressions and loops.
if
Expressions
An if expression allows you to branch your code depending on conditions. All if expressions start with the keyword if, followed by a condition.
fn main() { let x = 1; if x > 5 { println!("x is greater than five and has a value of: {x}"); } else { println!("x is less than five and has a value of: {x}"); } }
Unlike languages such as Ruby and JavaScript, Rust will not automatically try to convert non-Boolean types to a Boolean.
Handling Multiple Conditions with else if
You can use multiple conditions by combining if and else in an else if expression.
fn main() { let x = 10; if x % 4 == 0 { println!("{x} is divisible by 4"); } else if x % 3 == 0{ println!("{x} is divisible by 3"); } else { println!("{x} is not divisible by 4 or 3"); } }
Using too many else if expressions can clutter your code, so if you have more than one, you might want to refactor your code.
Using an if let
Because if is an expression, we can use it on the right side of a let statement to assign the outcome to a variable.
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of the number is: {number}"); }
Remember that blocks of code evaluate to the last expression in them, and numbers by themselves are also expressions. In this case, the value of the whole if expression depends on which block of code executes. This means the values that have the potential to be results from each arm of the if must be the same type.
Repition with Loops
It’s often useful to execute a block of code more than once. For this task, Rust provides several loops, which will run through the code inside the loop body to the end and then start immediately back at the beginning.
The following code will just give a
playground timeout
error, if you run this on your own machine, the loop will continue forever. Thanks for not breaking my server!
fn main() { loop { println!("Too infinity, and beyond!"); } }
Rust also provides a way to break out of a loop using code. You can place the break keyword within the loop to tell the program when to stop executing the loop. We can use continue, which in a loop tells the program to skip over any remaining code in this iteration of the loop and go to the next iteration.
fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!("The result is {result}"); }
Before the loop, we declare a variable named counter and initialize it to 0. Then we declare a variable named result to hold the value returned from the loop. On every iteration of the loop, we add 1 to the counter variable, and then check whether the counter is equal to 10. When it is, we use the break keyword with the value counter * 2. After the loop, we use a semicolon to end the statement that assigns the value to result.
Loop labes to Disambiguate Between Multiple Loops
If you have loops within loops, break and continue apply to the innermost loop at that point. You can optionally specify a loop label on a loop that you can then use with break or continue to specify that those keywords apply to the labeled loop instead of the innermost loop. Loop labels must begin with a single quote.
fn main() { let mut count = 0; 'counting_up: loop { println!("count = {count}"); let mut remaining = 10; loop { println!("remaining = {remaining}"); if remaining == 9 { break; } if count == 2 { break 'counting_up; } remaining -= 1; } count += 1; } println!("End count = {count}"); }
Conditional Loops with while
A program will often need to evaluate a condition within a loop. While the condition is true, the loop runs. When the condition ceases to be true, the program calls break, stopping the loop. It’s possible to implement behavior like this using a combination of loop, if, else, and break;
fn main() { let mut x = 3; while x != 0 { println!("{x}"); x -= 1; } println!("LIFTOFF!"); }
This construct eliminates a lot of nesting that would be necessary if you used loop, if, else, and break, and it’s clearer. While a condition evaluates to true, the code runs; otherwise, it exits the loop.
Looping Through a collection with for
You can choose to use the while construct to loop over the elements of a collection, such as an array.
fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { println!("The value is: {}", a[index]); index += 1; } }
However, this approach is error prone; we could cause the program to panic if the index value or test condition is incorrect. For example, if you changed the definition of the a array to have four elements but forgot to update the condition to while index < 4.
As a more concise alternative, you can use a for loop and execute some code for each item in a collection.
fn main() { let a = [10, 20, 30, 40, 50]; for i in a { println!("The value is: {i}"); } }
The safety and conciseness of for loops make them the most commonly used loop construct in Rust.
Ownership Explained
Ownership is a set of rules that govern how a Rust program manages memory. All programs have to manage the way they use a computer’s memory while running. Some languages have garbage collection that regularly looks for no-longer-used memory as the program runs; in other languages, the programmer must explicitly allocate and free the memory. Rust uses a third approach: memory is managed through a system of ownership with a set of rules that the compiler checks. If any of the rules are violated, the program won’t compile. None of the features of ownership will slow down your program while it’s running.
Ownership Rules
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
#![allow(unused)] fn main() { let s = "hello"; }
Variable Scope
- When s comes into scope, it is valid.
- It remains valid until it goes out of scope.
The String Type
The types covered previously are of a known size, can be stored on the stack and popped off the stack when their scope is over, and can be quickly and trivially copied to make a new, independent instance if another part of code needs to use the same value in a different scope. But we want to look at data that is stored on the heap and explore how Rust knows when to clean up that data, and the String type is a great example.
We’ve already seen string literals (like the variable s
above), where a string value is hardcoded into our program. String literals are convenient, but they aren’t suitable for every situation in which we may want to use text. One reason is that they’re immutable.
Rust has a second string type, String. This type manages data allocated on the heap and as such is able to store an amount of text that is unknown to us at compile time. You can create a String from a string literal using the from function:
#![allow(unused)] fn main() { let s = String::from("hello"); }
The double colon :: operator allows us to namespace this particular from function under the String type rather than using some sort of name like string_from.
#![allow(unused)] fn main() { let mut s = String::from("hello"); s.push_str(", world"); // push_str() appends a lieteral to a string println!("{s}"); }
Memory and Allocation
In the case of a string literal, we know the contents at compile time, so the text is hardcoded directly into the final executable. This is why string literals are fast and efficient. But these properties only come from the string literal’s immutability.
With the String type, in order to support a mutable, growable piece of text, we need to allocate an amount of memory on the heap, unknown at compile time, to hold the contents. This means:
- The memory must be requested from the memory allocator at runtime.
- We need a way of returning this memory to the allocator when we’re done with our String.
Rust takes a different path: the memory is automatically returned once the variable that owns it goes out of scope.
#![allow(unused)] fn main() { { let s = String::from("hello"); // s is valid from this point forward // do whatever with s } // this scope is now over and s is no longer valid }
There is a natural point at which we can return the memory our String needs to the allocator: when s
goes out of scope. When a variable goes out of scope, Rust calls a special function for us. This function is called drop
, and it’s where the author of String can put the code to return the memory. Rust calls drop automatically at the closing curly bracket.
Variables and Data Interacting with Move
A String is made up of three parts, shown on the left: a pointer to the memory that holds the contents of the string, a length, and a capacity. This group of data is stored on the stack. On the right is the memory on the heap that holds the contents.
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; }
When we assign s1
to s2
, the String data is copied, meaning we copy the pointer, the length, and the capacity that are on the stack. We do not copy the data on the heap that the pointer refers to.
The representation does not look like the picture below, which is what memory would look like if Rust instead copied the heap data as well. If Rust did this, the operation s2 = s1
could be very expensive in terms of runtime performance if the data on the heap were large.
Earlier, we said that when a variable goes out of scope, Rust automatically calls the drop function and cleans up the heap memory for that variable. But two images above shows both data pointers pointing to the same location. This is a problem: when s2
and s1
go out of scope, they will both try to free the same memory. This is known as a double free error and is one of the memory safety bugs we mentioned previously. Freeing memory twice can lead to memory corruption, which can potentially lead to security vulnerabilities. To ensure memory safety, after the line let s2 = s1;
, Rust considers s1
as no longer valid.
You should receive an error on the following code
#![allow(unused)] fn main() { let s1 = String::from("Hello"); let s2 = s1; println!("{s1}, world"); }
If you’ve heard the terms shallow copy and deep copy while working with other languages, the concept of copying the pointer, length, and capacity without copying the data probably sounds like making a shallow copy. But because Rust also invalidates the first variable, instead of being called a shallow copy, it’s known as a move. In this example, we would say that s1
was moved into s2
.
Rust will never automatically create “deep” copies of your data. Therefore, any automatic copying can be assumed to be inexpensive in terms of runtime performance.
Variables and Data Interacting with Clone
If we do want to deeply copy the heap data of the String, not just the stack data, we can use a common method called clone
.
#![allow(unused)] fn main() { let s1 = String::from("Hello"); let s2 = s1.clone(); println!("s1 = {s1}, s2 = {s2}"); }
When you see a call to `clone``, you know that some arbitrary code is being executed and that code may be expensive. It’s a visual indicator that something different is going on.
Stack Only Data: Copy
#![allow(unused)] fn main() { let x = 5; let y = x; println!("x = {x}, y = {y}"); }
But this code seems to contradict what we just learned: we don’t have a call to clone
, but x
is still valid and wasn’t moved into y
. The reason is that types such as integers that have a known size at compile time are stored entirely on the stack, so copies of the actual values are quick to make. That means there’s no reason we would want to prevent x
from being valid after we create the variable y
. In other words, there’s no difference between deep and shallow copying here, so calling clone wouldn’t do anything different from the usual shallow copying, and we can leave it out.
So, what types implement the Copy
trait? You can check the documentation for the given type to be sure, but as a general rule, any group of simple scalar values can implement Copy
, and nothing that requires allocation or is some form of resource can implement Copy
. Here are some of the types that implement Copy:
- All the integer types, such as u32.
- The Boolean type, bool, with values true and false.
- All the floating-point types, such as f64.
- The character type, char.
- Tuples, if they only contain types that also implement Copy. For example, (i32, i32) implements Copy, but (i32, String) does not.
Ownership and Functions
fn main() { let s = String::from("hello"); // s comes into scope takes_ownership(s); // s's value moves into the function... // ... and so is no longer valid here let x = 5; // x comes into scope makes_copy(x); // x would move into the function, // but i32 is Copy, so it's okay to still // use x afterward } // Here, x goes out of scope, then s. However, because s's value was moved, // nothing special happens. fn takes_ownership(some_string: String) { // some_string comes into scope println!("{some_string}"); } // Here, some_string goes out of scope and `drop` is called. The backing // memory is freed. fn makes_copy(some_integer: i32) { // some_integer comes into scope println!("{some_integer}"); } // Here, some_integer goes out of scope. Nothing special happens.
If we tried to use s after the call to takes_ownership, Rust would throw a compile-time error.
Return Values and Scope
Returning values can also transfer ownership.
fn main() { let s1 = gives_ownership(); // gives_ownership moves its return // value into s1 let s2 = String::from("hello"); // s2 comes into scope let s3 = takes_and_gives_back(s2); // s2 is moved into // takes_and_gives_back, which also // moves its return value into s3 } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing // happens. s1 goes out of scope and is dropped. fn gives_ownership() -> String { // gives_ownership will move its // return value into the function // that calls it let some_string = String::from("yours"); // some_string comes into scope some_string // some_string is returned and // moves out to the calling // function } // This function takes a String and returns a String. fn takes_and_gives_back(a_string: String) -> String { // a_string comes into // scope a_string // a_string is returned and moves out to the calling function }
While this works, taking ownership and then returning ownership with every function is a bit tedious.
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{s2}' is {len}."); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len() returns the length of a String (s, length) }
But this is too much ceremony and a lot of work for a concept that should be common. Luckily for us, Rust has a feature for using a value without transferring ownership, called references.