[Rust] Cheatsheet

iguigu·2022년 5월 7일
1

Life time

  • second borrowing 등이 일어났을 경우 컴파일러는 해당 레퍼런스에 대해 어디까지 유효한지 체크하는 것이 불가능
  • 컴파일러에게 레퍼런스가 어디까지 유효한지를 알려주는 장치
  • life time을 설정해준다고 해서 실제 레퍼런스의 life time이 바뀌는 것은 아님, 단순히 컴파일러에게 알려주기 위한 용도
  • 자세한 내용은 링크 참조

Vector Macro

  • 기본적으로 vec은 어떤 타입이 들어갈지 모르기 때문에, 데이터를 집어 넣어줘야 컴파일이 됨
  • vec을 초기화하고 데이터를 집어넣는데 번거로운 코드를 매크로를 통해 쉽게 해결 가능
let name1="harvey";
let name2="mike";

vec![name1,name2];

from, into

  • 타입을 바꾸는 것
  • 어떤 타입 간에 변경이 가능한지를 확인하기 위해서는 Docs의 trait implementation을 참고하면 됨
let my_name = String::from("harvey"); // &str -> String 
let my_city:String = "Seoul".into(); // &str -> String

tuple

let random_tuple = ("aaa",1,vec!['a'])
// random_tuple.0, random_tuple.1, random_tuple.2로 접근 가능

attribute

  • #[ATTR] : inner attribute
  • #[ATTR] : outer attribute
    • Debug : struct 등에서 struct의 멤버를 println 에 넣을 경우 디버깅 가능하게 해줌

enum

  • use Enum::* 을 통해 어느 부분에서든 import 하는 것이 가능함
  • enum은 default가 0부터며 값이 지정된다면 그 이후 값은 +1된 값을 가짐
  • struct는 and, enum은 or 에 가까움
  • enum이 function을 가지는 경우
enum Number{
	U32(u32),
    I32(i32)
}
fn get_number(input: i32) -> Number{
	let number = match input.is_positive(){
    	true => Number::U32(input as u32),
        false => Number::I32(input)
    };
    number
}
fn main(){
	let my_vec = vec![get_number(-800),get_number(3)];
	for item in my_vec{
    	match item{
        	Number::U32(number) => println!("U32 {}", number),
            Number::I32(number) => println!("I32 {}", number),
        }
    }
}

loop

  • loop 는 while로 변경 가능
  • 제일 많이 쓰는 loop는 for loop임
fn main(){
    let mut counter = 0;
    let mut counter2 = 0;
    
    'first_loop:loop{
        counter +=1 ;
        println!("The counter is now : {}", counter);
        if counter > 9 {
            'second_loop:loop{
                println!("The second counter is now : {}", counter2);
                counter2 +=1;
                if counter2 == 4 {
                    break 'first_loop; // break는 하나의 loop만 탈출 가능하기에 탈출 희망 지점 설정
                }
            }
        }
    }
}

impl

#[derive(Debug)]
struct Animal{
    age : u8,
    animal_type : AnimalType
}

#[derive(Debug)]
enum AnimalType{
    Cat,
    Dog
}

impl Animal{ // impl 은 동일한 struct에 대해 여러개 만들 수 있음
    fn new_cat(age:u8) -> Self{ // function signature(Self = Animal))
        Self{
            age : age,
            animal_type:AnimalType::Cat
        }
    }
    fn print(&self) { // reference to self
        println!("I am a : {:?}",self);
    }
    fn change_to_dog(&mut self){
        self.animal_type = AnimalType::Dog;
    }
}

fn main(){
    let mut my_animal=Animal::new_cat(19);
    my_animal.print(); //Animal::print(&my_animal); 과 동일함, syntactic sugar
    my_animal.change_to_dog();
    my_animal.print();
    
    // output
    // I am a : Animal { age: 19, animal_type: Cat }
    // I am a : Animal { age: 19, animal_type: Dog }
}

generic type

  • T, U는 원하는 것으로 바꿀수도 있음
  • where를 사용하여 더 깔끔하게 표현 가능
use std::cmp::PartialOrd;
use std::fmt::Display;

fn compare_and_print<T: Display, U: Display + PartialOrd>(statement: T, num1: U, num2: U) 
//or where T: Display, U: Display + PartialOrd,
{
    // T는 프린트, U는 프린트 + 비교
    println!(
        "{}! is {} greater than {}? {}",
        statement,
        num1,
        num2,
        num1 > num2
    )
}
fn main() {
    compare_and_print("Listen up!", 9, 8)
}

Result and Option

definition

enum Option<T> {
  Some(T), // existence
  None,    // non-existence
}

enum Result<T, E> {
  Ok(T),   // success
  Err(E),  // failure
}

Result

  • Result는 enum type으로 Result<T,E>에서 T와 E는 각각 generic이며, success와 failure를 표현함
    • Ok(T) : a Result container which has succeeded, containing T
    • Err(E) : a Result container which has failed, containing E
  • Ok를 unwrap()하면 안의 값이 안전하게 나오고, Err를 unwrap()하면 panic이 발생함
let good_result: Result<i32, i32> = Ok(10);
let bad_result: Result<i32, i32> = Err(10);

// The `is_ok` and `is_err` methods do what they say.
assert!(good_result.is_ok() && !good_result.is_err());
assert!(bad_result.is_err() && !bad_result.is_ok());

// `map` consumes the `Result` and produces another.
let good_result: Result<i32, i32> = good_result.map(|i| i + 1);
let bad_result: Result<i32, i32> = bad_result.map(|i| i - 1);

// Use `and_then` to continue the computation.
let good_result: Result<bool, i32> = good_result.and_then(|i| Ok(i == 11));

// Use `or_else` to handle the error.
let bad_result: Result<i32, i32> = bad_result.or_else(|i| Ok(i + 20));

// Consume the result and return the contents with `unwrap`.
let final_awesome_result = good_result.unwrap();

Option

  • Rust에서는 nil이나 null의 개념이 없는 대신 Option 타입이 존재함
  • 이는 컨테이너 타입 안에 존재하거나 존재하지 않는다는 개념임
    • Some(<wrapped-value>) : Some 은 내부의 value를 wrap하며 unwrap을 통해 액세스가 가능함
    • None : None
  • panic을 회피하고 안전하게 동작하는데 큰 도움을 줌
  • if let 문법을 이용하여 match 에서 None을 작성하는 번거로움 해소 가능
// use std::Option::*; 생략 가능 std::prelude 덕분
fn take_fifth(value:Vec<i32>)->Option<i32>{
    if value.len() <5 {
        None // Option::None 
    }else{
        Some(value[4])
    }
    
}
fn main() {
    let new_vec = vec![1,2];
    let index = take_fifth(new_vec);
    match index{
        Some(number) => println!("I got a number : {}", number),
        None => println!("There was nothing inside"),
    }
}

dyn

  • dyn 키워드는 Trait과 연관된 매서드 콜이 동적으로 dispatch 되는 것을 강조하는데 사용 됨
  • trait을 이와 같은 방법으로 사용하기 위해선 object safe 해야함
    • object safe trait이란
      • return 타입이 Self가 아님
      • generic type parameter가 없어야 함
  • generic parameter나 impl Trait와 달리, 컴파일러는 전달 받은 concrete type을 알지 못함 (type has been erased)
  • dyn Trait 레퍼런스는 2개의 포인터를 가짐
    • 하나는 data (e.g., instance of struct)
    • 다른 하나는 map of method (function pointer known as virtual method table or vtable)
  • 런타임에서 매서드가 dyn Trait에서 호출될 필요가 있을때, vtable은 function pointer를 가져오고 호출됨
  • Trade-off
    • 이와 같은 indirection 은 추가적인 runtime cost를 발생시킴
    • dynamic dispatch에 의해 호출된 매서드는 컴파일러에 의해 inline되지 못함
    • 하지만 dyn Traitimpl Trait이나 generic parameter보다 더 적은 코드를 필요로함 (method가 각각의 concrete type에 대해 중복되지 않기 때문에)

smart pointer

Box<T>

  • 데이터를 stack이 아닌 heap에 저장할 수 있도록 해줌
  • 스택에는 힙 데이터를 가리키는 포인터가 존재함
  • Box는 스택을 힙에 저장하는 것 이외에 성능적 오버헤드가 없음 (일반적으로 스택이 좋음)
  • 다음과 같은 상황에서 자주 사용
    • 컴파일 타임에 크기를 알 수 없는 타입을 갖고, 정확한 사이즈를 알 필요가 있는 타입의 값 이용
    • 큰 사이즈의 데이터를 소유권 이동 시 데이터 카피가 되지 않을 것을 원할 때
    • 어떤 값을 소유하고 해당 값의 타입을 알기보다는 특정 trait을 구현한 타입임만을 신경쓸 때
  • 관련 글 포스트

Deref trait

  • Deref trait을 이용하여 참조자를 통해 데이터 접근이 가능
  • 기본적으로 integer와 &integer 다른 타입이기 때문에 비교가 불가능하여 *를 통해 참조자를 따라가 해당 참조자가 가리키는 값을 얻어야 함
fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

// 만약 assert_eq!(5, y);로 할 경우
error[E0277]: the trait bound `{integer}: std::cmp::PartialEq<&{integer}>` is
not satisfied
 --> src/main.rs:6:5
  |
6 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^^ can't compare `{integer}` with `&{integer}`
  |
  = help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for
  `{integer}`
  • Box<T> 또한 참조자처럼 사용 가능
fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
  • Deref 트레잇을 구현하여 임의의 타입을 참조자처럼 다루기가 가능
use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}
  • *y는 러스트 내부적으로 *(y.deref())와 같이 동작하게 됨
  • Deref가 가변 참조자에 대한 오버라이딩 시 다음과 같이 동작
    • T: Deref<Target=U> 일 때 &T 에서 &U로
    • T: DerefMut<Target=U> 일 때 &mut T 에서 &mut U로
    • T: Deref<Target=U> 일 때 &mut T 에서 &U로
  • 자세한 내용은 여기

Drop trait

  • Drop trait은 메모리 정리 코드를 실행 시킴
  • value가 scope 밖으로 벗어날려고 할 때 발생되는 일을 커스터마이징 가능
  • Drop 트레잇은 대부분 스마트 포인터를 구현할 때 사용됨
    • Box<T>는 박스가 가리키고 있는 힙 상의 공간을 할당해제 하기 위해 Drop 사용
  • 일부 언어에서 메모리 해제를 매번 할당할 때 마다 해야하는데, 이와 달리 러스트는 특정 타입에 대해 자동으로 해결될 수 있게 해줌
struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer { data: String::from("my stuff") };
    let d = CustomSmartPointer { data: String::from("other stuff") };
    println!("CustomSmartPointers created.");
}

// 결과
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`! // 스코프 탈출
Dropping CustomSmartPointer with data `my stuff`!    // 스코프 탈출
  • 자동적으로 호출되는 drop 기능을 비활성화 할 필요는 보통 없지만 가끔 일찍 호출하길 원할 수 있음
  • 이럴 경우 std::mem::drop을 이용하여 값을 일찍 버리는 것이 가능함
// Drop 트레잇의 drop 이용
fn main() {
    let c = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");
    c.drop();
    println!("CustomSmartPointer dropped before the end of main.");
}


error[E0040]: explicit use of destructor method
  --> src/main.rs:14:7
   |
14 |     c.drop();
   |       ^^^^ explicit destructor calls not allowed
   
--------------------------------------------------------------------   
   
// std::mem::drop 이용
fn main() {
    let c = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
}

CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.

Rc<T>, 참조 카운팅 스마트 포인터

  • 하나의 값을 여러 소유자가 가질 수가 있음
    • 그래프 데이터 구조에서 여러 에지가 동일한 노드를 가리키는 것이 가능
    • 해당 노드는 개념적으로 해당 노드를 가리키는 에지들의 소유가 됨
    • 에지가 연결되어 있는 한 해당 노드가 해제되어선 안 됨
  • 복수 소유권을 가능하게 하기 위해 러스트는 Rc<T>(참조 카운팅)를 사용 함
  • 프로그램의 여러 부분에서 읽을 데이터를 힙에 할당하고 싶고, 어떤 부분이 그 데이터를 마지막에 이용하게 되는지를 컴파일 타임에 알 수 없으면 Rc<T>를 이용함
  • Rc<T>는 오직 단일 스레드에서만 사용가능 함. 다중 스레드에서는 다르게 동작
// Box<T> 이용 시에 a 노드에 대한 소유권이 호용되지 않음
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let a = Cons(5,
        Box::new(Cons(10,
            Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}


error[E0382]: use of moved value: `a`
  --> src/main.rs:13:30
   |
12 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
13 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move
   |
   = note: move occurs because `a` has type `List`, which does not implement
   the `Copy` trait
   
------------------------------------------------------------
//  Rc::clone을 호출하여 a의 `Rc<List>`에 대한 참조인자를 넘김
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}
  • Rc<T> 의 클론 생성은 참조 카운트를 증가 시킴
fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}


// 결과
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

RefCell<T>

  • interior mutability (내부 가변성)은 어떤 데이터에 대해 불변 참조자가 있더라도 데이터를 변형할 수 있게하는 디자인 패턴
  • 기본적으로는 borrow rule에 의해 허용되지 않음. 이를 위해 데이터 구조내에서 unsafe 코드를 사용함

순환참조

  • 러스트는 뜻하지 않게 해제되지 않는 메모리를 발생시키는 것을 컴파일적으로 어렵게 하지만, 불가능한 것은 아님
  • 순환 참조를 통해 메모리 릭을 발생시킬 수 있음
  • Rc<T>RefCell<T>를 활용하여 메모리릭을 발생시킬 수도 있음
  • 기본적으로 순환 고리 안에서는 참조 카운트가 0이 되지 않는다느 점을 이용
use std::rc::Rc;
use std::cell::RefCell;
use List::{Cons, Nil};

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match *self {
            Cons(_, ref item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

    println!("a initial rc count = {}", Rc::strong_count(&a));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    println!("a rc count after changing a = {}", Rc::strong_count(&a));

    // Uncomment the next line to see that we have a cycle;
    // it will overflow the stack
    // println!("a next item = {:?}", a.tail());
}

// 결과
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2

  • 순환 참조를 방지하기 위해 Rc<T>Weak<T>로 바꿀 수 있음
profile
2929

0개의 댓글