Rust

Rust 정리 (1)

2021.02.18


The Rust Programming Language - Ch 1 ~ Ch 6

 

Install / Update 등 관련 명령어

  • Installing
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
  • Updating / Uninstalling
rustup update
rustup self uninstall
  • Troubleshooting
rustc --version
cargo --version
  • Local documentation
rustup doc

 

Hello, World

Rust 파일의 확장자 : rs

// main.rs
fn main() {
  println!("Hello, world!"); // Rust 매크로를 실행하는 것.
  // 만약 함수라면 println("Hello~ ") 처럼 사용
}
rustc main.rs
# 위 명령어를 실행하면 compile된 파일을 얻을 수 있다.
# C/C++ 에서 컴파일 하는 것과 같다.(gcc | clang)
./main # Hello, world! 가 출력된다.
  • Rust Style
    • 4 space
    • ! 는 매크로를 호출하는 것
    • 문장의 마지막에 ; 사용

 

Cargo로 Rust 프로젝트 시작하기

cargo new hello_cargo # --vcs=git
ls # hello_cargo 폴더 생성됨
  • --vcs=git 옵션 추가하면 git 프로젝트화됨.(.gitignore 파일 함께 생성)

  • cargo 는 source 파일이 src 폴더 안에 있다고 생각한다.

cargo build
./target/debug/hello_cargo

위 command를 cargo run 으로 대체할 수 있다.

  • cargo check

단순히 컴파일 되는지만 확인하고, 실행파일을 생성하지 않음.(build 보다 빠름)

  • 배포용 build (최적화)
cargo build --release
./target/release/hello_cargo

 

library::Prelude

  • Rust는 std::prelude 에 해당하는 라이브러리만 자동으로 가져온다. 이 외에 라이버러리는 직접 import 해야한다.
    • 프로그램 작성에 사용되는 모든 라이브러리를 직접 import하는 것도 문제지만, 수많은 라이브러리를 자동으로 모두 import 하는 것도 문제다. 라이브러리의 특성에 맞게 prelude 를 지정하는 것이 중요하다.
use std::io;

 

Immutable Variables by default

  • 변수가 immutable 하다? = 값 수정이 불가능하다.
let mut a = String::new(); // mutable
let a = String::new(); // immutable

 

Data types

  • Integer : i8,i16, i32(default), i64, i128, isize / u8,u16, u32, u64, u128, usize

Rust에서 Integer overflow가 일어날 경우, debug 모드에서는 해당 현상을 에러로 판단하지만,

--release 모드에서는 범위가 넘어가면, 해당 data type의 최소 값으로 변경되고 panic 되지 않는다.

  • Float: f32,f64(default) / Boolean: bool / Character: char

 

Function return

fn test(x: i32) -> i32 {
  x
}
// let a = test(3); // a = 3

위 예에서는 ; 을 붙이면 compile 에러가 뜬다.

 

fn main() {
  let x = {
    let mut y=1;
    loop {
      y+=1;
      if y==3 {
        break y;
      }
    }
  }// x = 3
}

 

Iterations

  • loop
loop {
  // ...
}
  • while
let mut number = 10
while number != 0 {
  // ...
}
  • for
let a = [1,2,3,4,5];
for element in a.iter() {
  // ...
}
for number in (1..4).rev() {
  // ...
}

 

Ownership

프로그램이 메모리를 다루는 방법은 대표적으로 두가지가 있다.

  • 더 이상 사용하지 않는 메모리를 찾아 해제하거나(Garbage Collection)
  • 프로그래머가 메모리를 할당하고, 해제하는 것을 직접 하거나.

 

Rust 는 Ownership 시스템에 의해 메모리가 관리된다. 컴파일러가 컴파일시 정해진 규칙에 의해 메모리 관리에 문제가 없는지 확인한다. Ownership 개념이 프로그램의 속도에 영향을 미치거나 하지 않는다.

  • 'deep' copy를 자동으로 지원하지 않는다.

  • x = y 와 같은 상황에서, y가 포인터라면(heap에 할당된 데이터를 가리키고 있다면), x가 그 포인터가 되고, y는 더이상 유효하지 않다.(move)

    • 데이터가 stack에 저장된다면, copy 된다. (x, y 둘 다 유효)
    • String 의 경우, deep copy를 하고 싶다면, clone() 메소드를 사용한다.
  • 어떤 Type이 Copy 를 implement했다면, copy 하기 전/후 변수 둘 다 사용할 수 있다.

    • 하지만 Drop 을 implement했다면, 그럴 수 없다.

    Copy 가 implement 된 타입

    모든 정수 / 실수 타입, Boolean, char

    위 타입들만을 요소로 갖는 tuple - (i32, i32) :O / (i32, string) : X

  • 즉, heap에 할당된 변수가 복사되는 과정에서 논리적인 오류(double drop)를 방지하기 위해 ownership이 복사되는 변수로 이동한다. 함수 parameter로 전달되는 것도 복사에 해당하고, 해당 함수에서 parameter를 return해주지 않는 한, 함수 실행이 종료된 이후 시점에서 parameter로 전달된 변수에는 접근할 수 없다. (copy implemented type은 가능하다.)

    그러나, 변수의 ownership을 주고 받는 형태로 코드를 짜지 않는다.

  • Reference(&) 를 사용하면 ownership이 변경되지 않고, 값을 잠시 참조할 수 있다.

 

Slice

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5]; // Reference를 사용했기에, s의 ownership 그대로 유지
    let world = &s[6..11];
}

 

Struct

  • 코드 예시
struct User {
  username: String,
  email: String,
  sign_in_count: u64,
  active: bool,
}

let user1 = User{
  email: String::from("some@email.com"),
  username: String::from("lsb"),
  active: true,
  sign_in_count: 1,
}; // immutable

let mut user2 = User {
  email: String::from("some2@email.com"),
  username: String::from("lee"),
  ..user1
}; // mutable
user2.username = String::from("leesb");

fn build_user(email: String, username: String) -> User {
  User {
    email,
    username,
    active: true,
    sign_in_count: 1,
  }
}

let user3 = User {
 email: String::from("some3@email.com"),
  username: String::from("sb"),
  ..user2
};

Golang 에서는 새로운 객체를 생성할 때, 포인터를 공유(?) 하는 느낌이라면, Rust는 immutable한 객체를 주고받는 느낌이다.

 

Tuple Struct

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let c1 = Color(100, 100, 100);
println!("{}", c1.1);

Tuple struct는 . 을 사용하여 indexing 할 수 있다.

Unit-Like Struct

struct SomeStruct();

 

Debug trait

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {:?}", rect1); // 또는 {:#?}
}

Debug 속성(#[derive(Debug)])이 있을 때, {:?} 또는 {:#?} 으로 값을 출력할 수 있다.

 

Method

Debug trait에서 작성한 Rectangle struct에 메소드를 추가하기 위해, 다음과 같이 작성할 수 있다.

impl Rectangle {
  fn area(&self) -> u32{
    self.width * self.height
  }
  fn can_hold(&self, other: &Rectangle) -> bool {
    self.width > other.width && self.height > other.height
  }
  fn square(size: u32) -> Rectangle {
    Rectangle {
      width: size,
      height: size,
    }
  }
}

// 특정 struct에 대해 impl 여러번 작성 가능
impl Rectangle {
  fn someOtherMethod(&self) {
    // ...
  }
}

square 메소드의 경우 Rectangle::square(10); 과 같이 사용

 

Enums

  • 예시 1 - 어떤 타입이든 enum을 만들 수 있음.
enum IpAddr {
  V4(u8, u8, u8, u8),
  V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
  • 예시 2 - struct와 마찬가지로 method implement 가능.
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

impl Message {
  fn call(&self) {
    match self {
      Message::Quit => { /* ... */ },
      Message::Write(c) => println!("{}", c),
      Message::Move {x, y} => { /* ... */ },
      Message::ChangeColor(r,g,b) => { /* ... */ },
    }
  }
}

let m = Message::Write(String::from("hello"));
m.call();

 

Type Option (with match)

Rust에는 null이 없다.

그러나, Option 을 이용해서 None 을 null 개념으로 사용할 수 있다.

Option 은 prelude에 포함되어있음.

Option:: 접두어 필요없이, Some, None 사용 가능.

enum Option<T> {
    Some(T),
    None,
}
  • 예시
fn main() {
    let some_number = Some(5);
    let some_string = Some("a string");

    let absent_number: Option<i32> = None;
}

None을 사용할 때에는, 해당 변수가 어떤 타입인지를 컴파일러에게 알려줘야 타입을 추론할 수 있다.

let x = 3;
let y: Option<i32> = Some(5);
println!("{}", x+y); // error!!

위 코드가 작동하지 않는 이유는, x와 y의 타입이 다르기 때문이다.( i32 != Option<i32> )

Option<T> 에 있는 값을 사용하기 위해서는 T에 해당하는 값을 꺼내는 작업이 필요하다.

(Option 이라는 타입을 썼다는 것의 의미가, null의 경우를 대처하기 위한 것이므로.. 귀찮음을 감수해야하겠다.)

let x = 3;
let y: Option<i32> = Some(5);

// (1)
let z = &y.expect("there's value"); // None일 경우 해당 메세지로 panic 된다.
println!("{}", x + z);

// (2)
println!("{}", x + y.unwrap()); // 마찬가지로, y가 None일 경우 panic 된다.

// (3)
match y {
  Some(v) => println!("{}", v),
  None => println!("None!"),
}

// ... 등등 여러가지 방법 가능

Option<T>T 로 꺼내는 방법에는 정말 여러가지가 있다. 참고

+ match Option<T> 의 경우, SomeNone 두가지 경우가 모두 고려되지 않으면 컴파일러에서 에러임을 알려준다!

match 는 모든 경우를 다 cover 해줘야 컴파일에 성공하므로, 개발하는 입장에서 특정 경우를 빼먹지 않을 수 있다.

match에서 해당하는 경우를 다 만들 수 없는/만들지 않을 경우, _ placeholder 를 사용하면 된다.

 

if let

한 가지 경우에 대해서만 match할 경우, if let 구문을 사용하면 도움이 된다.

let some_u8_value = Some(0u8);

// (1)
match some_u8_value {
  Some(3) => println!("three"),
  _ => (),
}

// (2)
if let Some(3) = some_u8_value {
  println!("three");
}

 

출처