Lecture 7 - Variables and types
Logistics
- HW2 due next Wednesday
- I have office hours today
Circling back on comments
You definitely can make multi-line comments
#![allow(unused)] fn main() { // This is a single-line comment let age = 25; // Comments can go at the end of lines /* This is a multi-line comment Useful for longer explanations or temporarily disabling code */ /// This is a line doc comment that sits on the OUTSIDE /** Or as a block comment (note the one ending asterisk) */ fn my_function(){ //! This is a line doc comment that sits on the INSIDE /*! this is a block doc comment that sits on the inside */ } }
Learning Objectives
- Use the
mutkeyword and shadowing withletto modify variables - Declare constants using
const - Use Rust's integer types and floating-point types and understand their ranges
- Understand rust's basic types and their sizes (ints, floats,
bool,char,&str) - Use type annotation (with
let) and type conversion (as) - Work with boolean values using logical operators (
&&,||,!) - Create, access, and destructure tuples
Variables and Mutability
Variables are by default immutable!
Let's try this and then fix it.
fn main(){ let x = 3; x = x + 1; println!("{x}") }
Why can't we do this now?
fn main(){ let mut x = 3; x = 9.5; println!("{x}") }
One way to fix - Variable shadowing: new variable with the same name
fn main(){ let solution = "4"; let solution : i32 = solution.parse() .expect("Not a number!"); let solution = solution * (solution - 1) / 2; println!("solution = {}",solution); let solution = "This is a string"; println!("solution = {}", solution); }
Why does this work even though solution isn't mutable?
Variables vs Constants
Sometimes you need values that never change and are known at compile time:
#![allow(unused)] fn main() { const MAX_PLAYERS: u32 = 100; const PI: f64 = 3.14159; const GREETING: &str = "Hello, world!"; }
Constants:
- Are always immutable (no
mutallowed) - Use
constinstead oflet - Must have explicit types
- Named in
ALL_UPPERCASEby convention - Can be declared in any scope (including global)
- Must be computable at compile-time (so typically hard-coded)
When to use constants vs variables:
- Constants: Mathematical constants, configuration values, limits
- Variables: Data that might change or is computed at runtime
Types - Integers
Binary representations
Representing 13:
- In decimal (base 10): 13 = 1×10¹ + 3×10⁰
- In binary (base 2): 1101 = 1×2³ + 1×2² + 0×2¹ + 1×2⁰ = 8 + 4 + 0 + 1 = 13
For example, the number 13 in binary is 1101:
Binary: 1 1 0 1
Position: 3 2 1 0
2x^n: 8 4 2 1
Value: 8 4 0 1 → 8+4+1 = 13
T/P/S - What's the largest integer we can represent with 4 binary digits?
So what are ints, under the hood
Unsigned integers are stored in binary format.
But (signed) integers are stored in two's complement format, where:
- if the number is positive, the first bit is 0
- if the number is negative, the first bit is 1
To calculate the two's complement of a negative number, we flip all the bits and add 1.
#![allow(unused)] fn main() { // binary representation of 7 and -7 println!("{:032b}", 7); println!("{:032b}", -7); }
(Think/pair/share) Why do you think we do it this way?
Bits and bytes
- Bit: The smallest unit of data in computing - can store either 0 or 1
- Byte: A group of 8 bits - the basic addressable unit of memory
- Why 8 bits? 8 bits can represent 2⁸ = 256 different values (0-255)
- Computers typically address memory in byte-sized chunks
- (In sizes like "16 GB of RAM" GB refers to "gigaBYTES" not gigaBITS)
Integers come in all shapes and sizes
- unsigned integers:
u8,u16,u32,u64,u128,usize(architecture specific size)- from \(0\) to \(2^n-1\)
- signed integers:
i8,i16,i32(default),i64,i128,isize(architecture specific size)- from \(-2^{n-1}\) to \(2^{n-1}-1\)
These numbers (like u16) refer to bits, not bytes!
if you need to convert, use the
asoperator
i128andu128are useful for cryptography
Let's try it - min and max values of int types
#![allow(unused)] fn main() { println!("U8 min is {} max is {}", u8::MIN, u8::MAX); println!("I8 min is {} max is {}", i8::MIN, i8::MAX); println!("U16 min is {} max is {}", u16::MIN, u16::MAX); println!("I16 min is {} max is {}", i16::MIN, i16::MAX); println!("U32 min is {} max is {}", u32::MIN, u32::MAX); println!("I32 min is {} max is {}", i32::MIN, i32::MAX); println!("U64 min is {} max is {}", u64::MIN, u64::MAX); println!("I64 min is {} max is {}", i64::MIN, i64::MAX); println!("U128 min is {} max is {}", u128::MIN, u128::MAX); println!("I128 min is {} max is {}", i128::MIN, i128::MAX); println!("USIZE min is {} max is {}", usize::MIN, usize::MAX); println!("ISIZE min is {} max is {}", isize::MIN, isize::MAX); }
Different types don't play nice together
fn main(){ let x : i16 = 13; let y : i32 = -17; println!("{}", x * y); // will not work // println!("{}", (x as i32)* y); }
Be careful with math on ints
u8 is 8 bits and can store maximum value 2^8 - 1 = 255.
If we multiply: \(255*255=65025\).
How many bits do we need to store this value? We can take the log base 2 of the value.
>>> import math
>>> math.log2(255*255)
15.988706873717716
So we need 16 bits to store the product of two u8 values.
In general when we multiply two numbers of size \(n\) bits, we need \(2n\) bits to store the result.
Types - Floats
Why are they called floats?
- Two kinds:
f32andf64(default) - What do these mean?
Sizes of floats
#![allow(unused)] fn main() { println!("F32 min is {} max is {}", f32::MIN, f32::MAX); println!("F32 min is {:e} max is {:e}", f32::MIN, f32::MAX); println!("F64 min is {:e} max is {:e}", f64::MIN, f64::MAX); }
Why these sizes?
f32: 1 sign bit + 8 exponent bits + 23 significance bitsf64: 1 sign bit + 11 exponent bits + 52 significance bits
Floats and Rust's type inference system
fn main(){ let x:f32 = 4.0; let y:f32 = 4; // Will not work. It will not autoconvert for you. let z = 1.25; // won't get automatically assigned a type yet println!("{:.1}", x * z); //println!("{:.1}", (x as f64) * z); }
Formatting in println! (this didn't make it to your print-outs!)
You can control how numbers are displayed using format specifiers:
#![allow(unused)] fn main() { let total = 21.613749999999997; let big_number = 1_234_567.89; let small_number = 0.000123; let count = 42; // Float formatting println!("Default: {}", total); // Default: 21.613749999999997 println!("2 decimals: {:.2}", total); // 2 decimals: 21.61 println!("Currency: ${:.2}", price); // Currency: $19.99 // Scientific notation println!("Scientific: {:e}", big_number); // Scientific: 1.234568e6 println!("Scientific: {:.2e}", small_number); // Scientific: 1.23e-4 // Integer formatting println!("Default: {}", count); // Default: 42 println!("Width 5: {:5}", count); // Width 5: 42 println!("Zero-pad: {:05}", count); // Zero-pad: 00042 println!("Binary: {:b}", count); // Binary: 101010 println!("Hex: {:x}", count); // Hex: 2a }
Useful patterns:
{:.2}- 2 decimal places{:e}- scientific notation{:5}- fixed width 5{:b}- binary,{:x}- hexadecimal
Mini-Quiz
Take a minute to talk to a partner about what these do, then I'll call on you
cargo new my_projectcargo checkgit add .rustc hello.rsgit pullcargo run --releasegit commit -m "fix bug"
Types - Booleans (and logical operators)
booluses one byte of memory (why not one bit?)
#![allow(unused)] fn main() { let x = true; let y: bool = false; println!("{}", x && y); // logical and println!("{}", x || y); // logical or println!("{}", !y); // logical not }
Bitwise operators (just for awareness)
There are also bitwise operators that look similar to logical operators:
#![allow(unused)] fn main() { let x = true; let y: bool = false; println!("{}", x & y); // bitwise and println!("{}", x | y); // bitwise or }
But they also work on integers
fn main(){ let x = 10; let y = 7; println!("{x:04b} & {y:04b} = {:04b}", x & y); println!("{x:04b} | {y:04b} = {:04b}", x | y); // println!("{}", x && y); // println!("{}", x || y); }
So the negation of an int is...
#![allow(unused)] fn main() { let y = 7; println!("!{y:04b} = {:04b} or {0}", !y); }
Think/pair/share - What is this going to print?
#![allow(unused)] fn main() { let y:i8 = 7; println!("{:016b}", y); println!("{:016b}", !y); println!("{:016b}", -1*y); }
Types - Characters
chardefined via single quotes, uses four bytes of memory (that's how many bits?)- For a complete list of UTF-8 characters check https://www.fileformat.info/info/charset/UTF-8/list.htm
#![allow(unused)] fn main() { let x: char = 'a'; let y = '🚦'; let z = '🦕'; println!("{} {} {}", x, y, z); }
(Fun fact - try Control-Command-Space (Mac) or Windows-Key + . (Windows) to add emojis anywhere!)
Types - Strings
- A string slice (
&str) is defined via double quotes (we'll talk much more about what this means later!)
fn main() { let s1 = "Hello! How are you, 🦕?"; // type is immutable borrowed reference to a string slice: `&str` let s2 : &str = "Καλημέρα από την Βοστώνη και την DS210"; // here we make the type explicit println!("{}", s1); println!("{}\n", s2); // This doesn't work. You can't do String = &str //let s3: String = "Does this work?"; let s3: String = "Does this work?".to_string(); println!("{}", s3); let s4: String = String::from("How about this?"); println!("{}\n", s4); let s5: &str = &s3; println!("str reference to a String reference: {}\n", s5); // This won't work. // println!("{}", s1[3]); // println!("{}", s4[3]); // But you can index this way. println!("4th character of s1: {}", s1.chars().nth(3).unwrap()); println!("3rd character of s3: {}", s4.chars().nth(2).unwrap()); }
Tuples in Rust
Tuples are a general-purpose data structure that can hold multiple values of different types.
#![allow(unused)] fn main() { let mut tuple = (1, 1.1); let mut tuple2: (i32, f64) = (1, 1.1); // type annotation is optional let another = ("abc", "def", "ghi"); let yet_another: (u8, u32) = (255, 4_000_000_000); }
Accessing elements of a tuple
Rust tuples are "0-based":
#![allow(unused)] fn main() { let mut tuple = (1,1.1); println!("({}, {})", tuple.0, tuple.1); tuple.0 = 2; println!("({}, {})",tuple.0,tuple.1); println!("Tuple is {:?}", tuple); }
We can unpack a tuple by matching a pattern
#![allow(unused)] fn main() { // or pattern match and desconstruct let mut tuple = (1,1.1); let (a, b) = tuple; println!("a = {}, b = {}",a,b); }
Best Practices for tuples
When to Use Tuples:
- Small, related data: 2-4 related values
- Temporary grouping: Short-lived data combinations
- Function returns: Multiple return values
- Pattern matching: When destructuring is useful
Style Guidelines:
#![allow(unused)] fn main() { // Good: Clear, concise let (width, height) = get_dimensions(); // Good: Descriptive destructuring let (min_temp, max_temp, avg_temp) = analyze_temperatures(&data); // Avoid: Too many elements // let config = (true, false, 42, 3.14, "test", 100, false); // Hard to read // Avoid: Unclear meaning // let data = (42, 13); // What do these numbers represent? }