Rust Programming :The Complete Developor‘s Guide--06 Fundamentals

Equality & Ordering Comparing Enums

Comparison Operators

  • Enums can be compared using equality operators
  • Derivable traits enable enums to be compared
    • PartialEq
      • Provides equality
    • PartialOrd
      • Provides ordering: greater/less than
  • PartialOrd requires PartialEq to be implemented
  • Usually don’t need to manually implement

Example PartialEq

#[derive(PartialEq)]
enum Floor {
    ClientServices,
    Marketing,
    Ops,
}

let first = Floor::ClientServices;
let second = Floor::Marketing;
if first == second {
    //...
}

Example PartialOrd

#[derive(PartialEq, PartialOrd)]
enum Floor {
    ClientServices,
    Marketing,
    Ops,
}

fn is_below(this:& Floor, other: &Floor) -> bool {
    this < other
}

Example PartialOrd w/Variant Data

#[derive(PartialEq, PartialOrd)]
enum Tax {
    Flat(f64),
    None,
    Percentage(f64),
}

fn smallest_amount(tax: Tax, other: Tax) -> Tax {
    if tax < other {
        tax
    } else {
        other
    }
}

fn main() {
    // Flat is always be less than None
    let no_tax = Tax::None;
    let flat_tax = Tax::Flat(5.5);
    let ret = smallest_amount(no_tax, flat_tax);
    match ret {
        Tax::None => println!("Tax::None < Tax::Flat(5.5)"),
        Tax::Flat(_x) => println!("Tax::None > Tax::Flat(5.5)"),
        _ => (),
    }

    // Flat is always be less than Percentage
    let flat_tax = Tax::Flat(4.0);
    let percent = Tax::Percentage(1.0);
    let ret = smallest_amount(flat_tax, percent);
    match ret {
        Tax::Flat(_x) => println!("Tax::Flat(4.0) < Tax::Percentage(1.0)"),
        Tax::Percentage(_x) => println!("Tax::Flat(4.0) > Tax::Percentage(1.0))"),
        _ => (),
    }

    // compare the actual variance
    let low = Tax::Flat(5.5);
    let high = Tax::Flat(8.0);
    let ret = smallest_amount(low, high);
    match ret {
        Tax::Flat(x) => {
            if x == 5.5 {
                println!("Tax::Flat(5.5) < Tax::Flat(8.0)")
            } else if x == 8.0 {
                println!("Tax::Flat(5.5) > Tax::Flat(8.0)")
            }
        }
        _ => (),
    }
}

result

Tax::None > Tax::Flat(5.5)
Tax::Flat(4.0) < Tax::Percentage(1.0)
Tax::Flat(5.5) < Tax::Flat(8.0)

Recap

  • Enums can be ssorted and compared
    • PartialOrd and PartialEq implementation required
  • These traits can be used with derive
  • Ordering respects enum variant order in the code
    • Variant data will only be considered for ordering if both enumerations are the same variant
  • Manual implementation almost never needed for enums

Equality & Odering Comparing Structs

Comparison Operators

  • Structs can be compared using equality operator
  • Detivable traits enable structs to be compared
    • PartialEq
      • Provides equality
    • PartialOrd
      • Provides ordering: greater/less than
  • PartialOrd requires PartialEq to be implemented

Example PartialEq
#[derive(PartialEq)]
struct User {
id: i32,
name: String,
}

// when use PartialEq
// all the struct field should be the same
let a = User {id:1, name:“a”.to_owned()};
let b = User {id:2, name:“b”.to_owned()};
if a == b {
//…
}

**Example PartialOrd**      
#[derive(PartialEq, PartialOrd)]
struct User {
    id: i32,
    name: String,
}

// when use PartialOrd
// only the first field of the structure is compared
let a = User {id:1, name:"a".to_owned()};
let b = User {id:2, name:"b".to_owned()};
if a < b {
    // just compare id
    //...
}

PartialOrd

  • PartialOrd only considers the first struct fiels
  • Manual implementation needed to compare other fileds
    • Always ensure PartialOrd and PartialEq are consistent
PartialOrd Manual implementation

Example01

use std::cmp::Ordering;

#[derive(PartialEq)]
struct User {
    id: i32,
    name: String,
}

impl PartialOrd for User {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        if self.name < other.name {
            Some(Ordering::Less)
        } else if self.name > other.name {
            Some(Ordering::Greater)
        } else {
            Some(Ordering::Equal)
        }
    }
}
fn main() {
    let a = User {
        id: 1,
        name: "a".to_owned(),
    };
    let b = User {
        id: 2,
        name: "b".to_owned(),
    };
    
    if a < b {
        println!("a < b");
    }
}

Example02

use std::cmp::Ordering;

#[derive(PartialEq)]
struct User {
    id: i32,
    name: String,
}

impl PartialOrd for User {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.name.cmp(&other.name))
    }
}
fn main() {
    let a = User {
        id: 1,
        name: "a".to_owned(),
    };
    let b = User {
        id: 2,
        name: "b".to_owned(),
    };

    if a < b {
        println!("a < b");
    }
}
  • .cmp() is made available through #[derive(Ord)]
    • Automatically derived on primitive types

Recap

  • Structs can be sorted and compared
    • PartialOrd and PartialEq implementation required
    • Ord implementation optional
  • These traits can be used with derive
  • Ordering respects only the first struct field when derived
    • Manual implementation required to order on different fields
    • Ordering and equality must remain consistent when implementing manually

Stdlib Operator Overloading

Operator Overloading

  • Operators can be overloaded for structs and enums
  • Trait implementation required
  • All overloadable operators are available in std::ops mudule
  • Behavior should be consistent with the meaning of the operator
    • Adding should make something larger
    • Substracting should make it smaller, etc
Example Add
use std::ops::Add;

struct Speed(u32);

impl Add<Self> for Speed {
    type Output = Self;
    fn add(self, rhs: Self) -> Self::Output {
        Speed(self.0 + rhs.0)
    }
}

fn main() {
    let fast = Speed(5) + Speed(3);
}
Example Add w/Different Output
use std::ops::Add;

struct Letter(char);

impl Add<Self> for Letter {
    type Output = String;
    fn add(self, rhs: Self) -> Self::Output {
        format!("{}{}", self.0, rhs.0)
    }
}

fn main() {
    println!("{}", Letter('h') + Letter('i'));
}

Common Operators

ops::Add    +    lhs + rhs
ops::Sub    -    lhs - rhs
ops::Mul    *    lhs * rhs
ops::Div    /    lhs / rhs
ops::Rem    %    lhs % rhs
ops::Not    !    !item
ops::Neg    -    -item

Index Operator

use std::ops::Add;
use std::ops::Index;

enum Temp {
    Current,
    Max,
    Min,
}

struct Hvac {
    current_temp: i16,
    max_temp: i16,
    min_temp: i16,
}

impl Index<Temp> for Hvac {
    type Output = i16;
    fn index(&self, index: Temp) -> &Self::Output {
        match index {
            Temp::Current => &self.current_temp,
            Temp::Max => &self.max_temp,
            Temp::Min => &self.min_temp,
        }
    }
}

fn main() {
    let env = Hvac {
        current_temp: 30,
        max_temp: 60,
        min_temp: 0,
    };

    let current = env[Temp::Current];

    println!("current={}", current);
}

Recap

  • Operators are overloaded via traits
    • Listing of traits is in std::ops module
  • Input type can be specified with generic parameter
  • Output type can be specified with the Output associated type alias
  • Behavior should remain consistent with operator purpose

Iterators Implementing Iterator

Iterator

  • Iterator is provided by the Iteratortrait
    • Only one function to be implemented
    • Provides for…in syntax
    • Access to all iterator adapters
      • map, take, filter, etc
  • Can be implemented for any structure

Iterator Trait

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}
Example Iterator
struct Odd {
    number: isize,
    max: isize,
}

impl Odd {
    fn new(max: isize) -> Self {
        Self { number: -1, max }
    }
}

impl Iterator for Odd {
    type Item = isize;
    fn next(&mut self) -> Option<Self::Item> {
        self.number += 2;
        if self.number <= self.max {
            Some(self.number)
        } else {
            None
        }
    }
}

fn main() {
    let mut odds = Odd::new(7);
    println!("{:?}", odds.next()); // Some(1)
    println!("{:?}", odds.next()); // Some(3)
    println!("{:?}", odds.next()); // Some(5)
    println!("{:?}", odds.next()); // Some(7)
    println!("{:?}", odds.next()); // None
    // for..in
    // i 3 5 7
    for odd in odds{
        println!("odd: {:?}", odd); 
    }
    // adapter
    // 2 4 6 8
    let mut evens = Odd::new(8);
    for e in evens.map(|odd| odd + 1) {
        println!("even: {}", e);
    }
}

Recap

  • Implementing Iterator provides access tp for…in syntax and iterator adapters
    • Set the output type using the Item associated type as part of the Iterator trait
    • Return Some when data is available and None when data are no more items to iterate
  • Data structure must:
    • Be mutable
    • Have a field to track iteration

Iterators Implementing IntoIterator Using An Existing Collection

Iterator Trait

  • By default requires mutable access to structure
    • Inconvenient
    • Not always possible
    • Mutation nut always needed
  • Solution:
    • Implement IntoIterator trait & call .iter() on inner collection
      • Vector, HashMap

IntoIterator Trait

  • Yields an Iterator (yield items/values)
    • Implementation details determine how items are accessed
      • Borrow, mutable, move

IntoIterator Trait

trait IntoIterator {
    type Item;
    type IntoIter;
    fn into_iter(self) -> Self::IntoIter;
}
Move
struct Friends {
    names: Vec<String>,
}

impl IntoIterator for Friends {
    type Item = String;
    type IntoIter = std::vec::IntoIter<Self::Item>;
    fn into_iter(self) -> Self::IntoIter {
        self.names.into_iter()
    }
}

fn main() {
    let names = vec!["Albert".to_owned(), "Sara".to_owned()];
    let mut friends = Friends { names };
    // friends` moved due to this implicit call to `.into_it
    for f in friends {
        println!("{:?}", f);
    }

    //! value used here after move
    // for f in friends {
    //     println!("{:?}", f);
    // }
}
Borrow
struct Friends {
    names: Vec<String>,
}

impl<'a> IntoIterator for &'a Friends {
    type Item = &'a String;
    type IntoIter = std::slice::Iter<'a, String>;
    fn into_iter(self) -> Self::IntoIter {
        self.names.iter()
    }
}

fn main() {
    let names = vec!["Albert".to_owned(), "Sara".to_owned()];
    let mut friends = Friends { names };    
    for f in &friends {
        println!("{:?}", f);
    }

    for f in &friends {
        println!("{:?}", f);
    }
}

Mutable Borrow

struct Friends {
    names: Vec<String>,
}

impl<'a> IntoIterator for &'a mut Friends {
    type Item = &'a mut String;
    type IntoIter = std::slice::IterMut<'a, String>;
    fn into_iter(self) -> Self::IntoIter {
        self.names.iter_mut()
    }
}

fn main() {
    let names = vec!["Albert".to_owned(), "Sara".to_owned()];
    let mut friends = Friends { names };
    for f in &mut friends {
        *f = "Frank".to_string();
        println!("{:?}", f);
    }
}

Iter Methods

  • Convention for exposing iteration is to provide up to two methods:
    • .iter()
      • Iteration over borrowed values
    • .iter_mut()
      • Iteration over borrowed mutable values
  • Implement these by simple calling into_iter() after implementing the IntoIterator trait
  • These are optional, but allow for easy combinator usage without the for loop

Recap

  • IntoIterator trait yields iterators
    • Allows control over borrows & mutability
  • Implementation of IntoIterator requires:
    • An Item type -yielded value
    • An IntoIter - mutable struct which tracks iteration progress / proxy to data structure
  • The IntoIter type can be retrieved from the documention on your inner collection

Demo Implementing IntoIterator

use std::collections::HashMap;

#[derive(Debug, Hash, Eq, PartialEq)]
enum Fruit {
    Apple,
    Banana,
    Orange,
}

struct FruitStand {
    fruits: HashMap<Fruit, u32>,
}

impl IntoIterator for FruitStand {
    type Item = (Fruit, u32);
    type IntoIter = std::collections::hash_map::IntoIter<Fruit, u32>;
    fn into_iter(self) -> Self::IntoIter {
        self.fruits.into_iter()
    }
}
// borrow
impl<'a> IntoIterator for &'a FruitStand {
    type Item = (&'a Fruit, &'a u32);
    type IntoIter = std::collections::hash_map::Iter<'a, Fruit, u32>;
    fn into_iter(self) -> Self::IntoIter {
        self.fruits.iter()
    }
}
// mut borrow
impl<'a> IntoIterator for &'a mut FruitStand {
    type Item = (&'a Fruit, &'a mut u32);
    type IntoIter = std::collections::hash_map::IterMut<'a, Fruit, u32>;
    fn into_iter(self) -> Self::IntoIter {
        self.fruits.iter_mut()
    }
}

fn main() {
    let mut fruits = HashMap::new();
    fruits.insert(Fruit::Banana, 5);
    fruits.insert(Fruit::Apple, 2);
    fruits.insert(Fruit::Orange, 6);

    let store = FruitStand{fruits};
    for (fruit, stock) in store.into_iter() {
        println!("{:?} {:?}", fruit, stock);
    }
}

Iterator Implementing IntoIterator Using a Custom Iterator

Mini Iterator Review

  • Iterator trait allows iteration over a collection
    • Yield items
    • Struct must be mutable & contain iteration state information
  • IntoIterator trait defines a proxy struct & determines how data is accessed
    • Move, borrow, mutation

Problem

  • Implementing IntoIterator allows control of the iteration, but …
    • We aren’t using an existing collection to store data
      • No .iter() or .into_iter()
    • We don’t want to pollute our data structure with iteration information

Solution

  • Make an intermediary struct
    • Implement Iterator
      • Mutable, handles iteration state
  • Implement IntoIterator on data struct
    • Combined with the intermediary struct will allow iteration

Setup

struct Color {
    r: u8,
    g: u8,
    b: u8,
}

struct ColorIntoIter {
    color: Color,
    pos: u8,
}

struct ColorIter<'a> {
    color: &'a Color,
    pos: u8,
}

Review Iterator Trait

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

Impl Iterator - Move

/// Impl Iterator - Move
impl Iterator for ColorIntoIter {
    type Item = u8;
    fn next(&mut self) -> Option<Self::Item> {
        let next = match self.pos {
            0 => Some(self.color.r),
            1 => Some(self.color.g),
            2 => Some(self.color.b),
            _ => None,
        };

        self.pos += 1;
        next
    }
}

/// Impl IntoIterator - Move
impl IntoIterator for Color {
    type Item = u8;
    type IntoIter = ColorIntoIter;
    fn into_iter(self) -> Self::IntoIter {
        Self::IntoIter {
            color: self,
            pos: 0,
        }
    }
}

Impl Iterator - Borrow

impl<'a> Iterator for ColorIter<'a> {
    type Item = u8;
    fn next(&mut self) -> Option<Self::Item> {
        let next = match self.pos {
            0 => Some(self.color.r),
            1 => Some(self.color.g),
            2 => Some(self.color.b),
            _ => None,
        };

        self.pos += 1;
        next
    }
}

Impl IntoIterator - Borrow

impl<'a> IntoIterator for &'a Color {
    type Item = u8;
    type IntoIter = ColorIter<'a>;
    fn into_iter(self) -> Self::IntoIter {
        Self::IntoIter {
            color: &self,
            pos: 0,
        }
    }
}

Notes

  • Non-trivial to implement mutable iteration using IntoIterator
    • Collect mutable references into a Vector and return it
    • Use unsafe to bypasss compiler checks
  • Prefer using existing .iter() methods on structures when possible
    • Vectors, HashMaps, etc
    • Easier to work with, covers most cases

Recap

  • Custom iteration requires a dedicated iteration struct for each type of data handling mechanism
    • Move, borrow
  • Prefer using the .iter() methods on existing collections if possible

stdlib MaCros

assert

let a = 1;
let b = 2;
assert!(a == b, "{} ne {}", a, b);
assert_eq(a, b, "values should be equal");
assert_ne(a, b, "values should not be equal");

dbg

#[derive(Debug)]
enum RoomType {
    Bedroom,
    Kitchen,
}

#[derive(Debug)]
struct Room {
    dimensions: (usize, usize),
    kind: RoomType,
}

fn main() {
    let kitchen = Room {
        dimensions: (20, 20),
        kind: RoomType::Kitchen,
    };

    dbg!(&kitchen);
}

format

let h = "hello";
let w = "World";
let greet: String = format!("{}, {}", h, w);
println!("{}", greet);

include_str

Data file path is relative to the source file

let msg = include_str!("../msg.txt");
println!("{}", msg);

include_bytes

Data is saved as an array of bytes(u8)

let bytes = include_bytes!("image.png");

env

Include string data at compile time, based on environment variable

let config_1 = env!("CONFIG_1");
//error: environment variable `CONFIG_1` not defined
// --> src/main.rs:3:20
//  |
//  |     let config_1 = env!("CONFIG_1");
//  |                    ^^^^^^^^^^^^^^^^
//  |
//  = note: this error originates in the macro `env` (in Nightly builds, run with -Z macro-backtrace for more info)

todo / unimplemented

  • todo!
    • Incomplete code sections, with intent to implement
  • unimplemented!
    • Incomplete code sections, with no intent to implement
  • Program will panic when line is executed
    • todo!(“taking a vacation”);
    • unimplemented!(“nobody wants this”);

unreachable

  • Indicates that some code should never be executed
    • Useful as both a debugging tool and to ease working with match arms
  • Will panic at runtime if the macro is executed
fn main() {
    let number = 12;
    let max_5 = {
        if number > 5 {
            5
        } else {
            number
        }
    };

    match max_5 {
        n @ 0..=5 => println!("n = {}", n),
        _ => unreachable!("n > 5. this is a bug"),
    }
}

Recap

  • assert is used to confirm if something is true
  • dbg can be used to inspect values while coding
  • format provides string interpolation
  • include_str & include_bytes copy data from a file into the compiled binary
  • env copies an environment variable into the binary
  • todo indicates unfinished code
  • unimplemented indicates code that will not be finished
  • unreachable indicates code that should never execute

stdlib Managing Integer Overvflow

Overflow

  • Primitive integers can overflow when they reach their limits
    • In debug mode: panic
    • In release mode: wrap
  • Wrapping may or may not be desired
  • Functions exist to handle these sifuations
    • Communicate intent
    • Reduce bugs

About Functions

  • Defined on interger types
    • 8, 16, 32, 64, 128 bit signed & unsigned
  • Typical operations such as addition, division, multiplication each have functions to handle overflow
    • Each function has different behavior when overflow occurs

Overflow Functions

  • checked_* Option
    • Returns an option
      let n: Option<u32> = 0u32.checked_sub(1);       // None
      let n: Option<u32> = u32::MAX.checked_add(1);   // None
      let n: Option<u32> = 9_u32.checked_add(1);      // Some(10)
      
  • overflowing_* (i32, bool)
    • Indicates whether overflow occurred
      // (4294967295, true)
      let n:(u32, bool) = 0u32.overflowing_sub(1);
      // (6, false)
      let n:(u32, bool) = 5u32.overflowing_add(1);
      
  • saturating_*
    • Limits the value to the min or max of the type
      // 0
      let n: u32 = 0_u32.saturating_sub(9001);
      // 4294967295
      let n: u32 = u32::MAX.saturating_add(u32::MAX);
      
  • wrapping_*
    • Wraps on overflow (default)
      // 4294967295
      let n: u32 = 1_u32.wrapping_sub(2);
      // 0
      let n: u32 = u32::MAX.wrapping_add(1);
      

Recap

  • Arithmetic overflow will panic in debug builds & overflow in release builds
  • Overflow functions exist to handle these situations in different ways
  • When performing arithmetic on numbers at the extremes, prefer using an overflow function to reduce bugs

Fundamentals Turbofish

What is a turbofish

  • Sometimes the compiler cannot determine the type of same data
  • A few options are available when this happens:
    • Type annotations
    • Turbofish

Type Annotations Review

let numbers: Vec<u32> = vec![1, 2, 3];
let numbers: Vec<_> = vec![1, 2, 3];
let odds: Vec<_> = numbers.iter().filter(|n| **n % 2 == 1).collect();

Syntax

ident::<type>
::<>

When Turbofish Can Be Used

  • Any item having a generic parameter
pub fn collect<B>(self) -> B
    collect::<>()

Recap

  • Turbofishs is a way to specify a type when working with generics
    • Only needed if the compiler cannot determing the type being used
  • Usually optional; type annotations suffices

Fundamentals Loop Labels

Loop Labels

  • Loops can be annotated with a label for control flow
  • Allows changing flow control to an outer loop
    • break
    • continue
  • Useful when working with nested loops

Syntax

'ident: loop {}
'ident: for x in y {}
'ident: while true {}
/// ident is the name give to the loop

Example break

fn main() {
    let matrix = [
        [2, 4, 6],
        [8, 9, 10],
        [12, 14, 16]
    ];
    'rows: for row in matrix.iter() {
        'cols: for col in row {
            if col % 2 == 1 {
                println!("odd: {}", col);
                break 'rows;
            }

            println!("{}", col);
        }
    }
}
/// 2, 4, 6, 8, odd:9

Example continue

type UserInput<'a> = Result<&'a str, String>;
'menu: loop {
    println!("menu");
    'input: loop {
        let user_input: UserInput = Ok("next");
        match user_input {
            Ok(input) => break 'menu,
            Err(_) => {
                println!("try again");
                continue 'input;
            }
        }
    }
}

Recap

  • Loop labels can be applied to any type of loop
    • loop, while, for
  • Control can be directed to outer loops using loop labels
    • Break will exit the specified loop
    • Continue will exectue the specified loop
      • 'ident: loop {}

Fundamentals Loop Expresions

  • Loops are expresions
    • Values can be returned from loops
    • Can only be used with loop
  • break optionally return a value

Sytax

let value = 5;
let result: usize = 'ident: loop {
    break value;
    break 'ident value;
};
// result == 5

Example

let value: usize = loop {
    if let Ok(input) = get_input() {
        match input.parse::<usize>() {
            Ok(value) => break value,
            Err(e) => continue,
        }
    }
};

Example Loop Labels

let nums = vec![1, 2, 3, 4, 5, 6, 7, 8];
let div_by_three: Option<usize> = 'outer: loop {
    for n in nums {
        if n % 3 == 0 {
            break 'outer Some(n);
        }
    }

    break None;
};

Recap

  • break can return a value to a loop expression
    • Only valid on loop (not while or for)
  • Can alse break to a loop label with a value
    let data = loop {
        break 'label value;
    };
    

Fundamentals Struct Update Syntax

Struct Instantiation

  • Structs may have many fields to set during creation
    • Lots of code
  • Default can be used to set the default values
    • Sometimes one or two fields may need to have non-default values
      • Possible mutability, lots of boilerplate

Setup

struct Particle {
    color: (u8, u8, u8),
    alpha: u8,
    size: (u32, u32),
    position: (i32, i32),
    velocity: i32,
    direction: f32,
}

impl Default for Particle {
    fn default() -> Self {
        Self {
            color: (255, 0, 255),
            alpha: 255,
            size: (100, 100),
            position: (0, 0),
            velocity: 0,
            direction: 0.0,
        }
    }
}

Without Struct Update

let mut particle = Particle::default();
particle.alpha = 127;
let particle = particle;
println!("{:#?}", particle);

Struct Update w/Other Struct

let red_particle = Particle {
    color: (255, 0, 0),
    ..Particle::default()
};

let faset_particle = Particle {
    velocity: 10,
    ..red_particle
};

Recap

  • Struct update syntax allows structs to be easily instantiated
  • Can be used with
    • default
    • Another struct of the same type
      let s = Struct{
          field: value,s
          ..Struct::default()
      };
      

Fundamentals Escape Sequences & Raw Strings

Escape Sequences

  • Not always possible or convenient to include certain characters in a string
    • Quotes, newlines, tabs, Unicode
  • Escap sequences allow inclusion of any type of character

Example

let msg = "Hello\nWorld!";
let msg = "Hello\tWorld!";
let msg = "Left\\Right!";
let msg = "Over\"there\"";
let smiley = "\u{1f642}"; // 🙂

Raw Strings

  • Escape sequences are disbaled
  • Clearer code when multiple special characters are needed
let msg = r"Hello
World";
let msg = r"Hello     world";
let msg = r"left\right";
let msg = r#"Over "there""#;
let msg = r##"Over #"#there#"#"##;
let smiley = r"🙂";

Recap

  • Escape sequences are a way to include special characters in strings
  • Raw strings allow insertion of any characters without using escapes
    • Newlines in raw string will include indents
    • Surround raw strings with hashes(#) if you need to include double quotes
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值