HelloRust

시작하기 (Intro)

Rust 소개

    println!("Hello, world!");
  • 위 코드에서 볼 수 있는 러스트의 특징
  1. 러스트 스타일은 탭이아닌 네 개의 스페이스로 들여쓰기를 한다.
  2. println!은 러스트 매크로(macro)라고 불린다. !이 보통의 함수 대신 매크로를 호출하고 있다
  3. "Hello, world!" 스트링(string)
  4. ; 으로 라인을 끝낸다.
  • 컴파일과 실행은 개별적인 단계이다 러스트 프로그램을 실행하기 전에 rustc 커맨드를 입력하고 작성한 소스코드를 넘시는 식으로 컴파일 해야한다.
rustc main.rs

컴파일을 성공적으로 마친 후, 러스트는 실헹가능한 바이너리를 출력한다.

러스트는 ahead-of-time compiled 언어이다.

이는 프로그램을 컴파일하고, 그 실행파일을 다른 이들에게 주면, 그들은 러스트를 설치하지 않고도 이를 실행할 수 있다는 의미이다.

만약 누군가에게 .rb, .py, .js파일을 준다면, 받은 사람은 각각 해당 언어의 구현체가 설치되어 있어야 한다.

하지만 루비, 파이썬, 자바스크립트 같은 동적언어는 하나의 커맨드로 프로그램을 컴파일하고 실행할 수 있다.

Cargo 소개

Cargo는 러스트의 빌드 시스템 및 패키지 매너저이다.

Cargo는 코드를 빌드하고, 의존 라이브러리를 다운로드 해주고, 그 라이브러리들을 빌드하는 등 여러분을 위한 많은 작업들을 다룬다.

terminal에서 설치여부와 버전을 확인해 보자. cargo --version

  • Cargo 프로젝트를 빌드하고 실행하기
$ cargo build

이 커맨드는 현재 디렉토리에 target/debug/hello_cargo에 실행 파일을 생성한다.

해당 파일을 직접 실행할 수 있다.

$ target/debug/hello_cargo

cargo build를 실행하면, cargo가 최상위 디렉토리에 Cargo.lock이라는 새로운 파일을 생성하도록 한다.

프로젝트가 어떤 의존성도 가지고 있지 않으므로, 파일의 내용이 얼마 없다.

$ cargo run
 Finished dev [unoptimized + debuginfo] target(s) in 0.00s
 Running `target/debug/hello_rust`

을 사용해서 실행 파일을 실행할 수 있다.

Cargo는 코드 파일들이 변경된 적이 없음을 알아내고, 따라서 해당 바이너리를 그저 실행할 뿐이다.

$ cargo run
 Compiling hello_rust v0.1.0 (/Users/wooyeonhui/develop/toy/hello_rust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/hello_rust`

변경이 있다면,Cargo는 프로젝트 실행전에 다시 빌드를 할 것이다.

$ cargo check는 코드가 컴파일되는지 빠르게 확인헤주지만 실행파일을 생성하지는 않는다.

  • 릴리즈 빌드 $ cargo build --release를 사용하면 target/debug 대신 target/release에 실행 파일을 생성한다.

  • Cargo convention rust 프로젝트가 복잡해질수록 Cargo는 큰 가치를 제공해준다.

Basic

Variables

러스트에서는 컴파일러가 변경되지 않은 값에 대한 보증을 해주고, 실제로 바뀌지 않습니다.

변수명의 접두어로 mut을 추가하는 것을 통해 가변성 변수를 선언할 수 있다.

변수와 상수 간의 차이점들

  1. 상수에 대해서는 mut를 사용하는 것이 허용되지 않는다.
  2. 상수를 사용하고자 하면 let 키워드 대신 ```const``키워드를 사용해야 하고, 값의 타입을 선언해야 한다.
  3. 상수는 어떤 영역에서도 선언될 수 있다.
  4. 상수는 오직 상수 표현식만 설정될 수 있지, 함수 호출의 결과값이나 그 외에 실행 시간에 결정되는 값이 설정될 수는 없다.
const MAX_POINTS: u32 = 100_000;

상수는 자신이 선언되어 있는 영역 내에서 프로그램이 실행되는 시간 동안 항상 유효하다.

프로그램 전체에 걸쳐 하드코드해야 하는 값을 이름지어 상수로 사용하자.

Shadowing

이전에 선언한 변수와 같은 이름의 새 변수를 선언할 수 있고, 새 변수는 이전 변수를 shadows하게 된다.

mut와 shadowing의 차이는 let 키워드를 다시 사용하여 효과적으로 새 변수를 선언하고, 값의 유형을 변경할 수 있으면서도 동일 이름을 사용할 수 있다는 점이다.

Data Types

Rust의 데이터 타입은 크게 스칼라와 컴파운드, 둘로 나눌 수 있다.

Rust는 타입이 고정된 언이이다. 이는 모든 변수의 타입이 컴파일 시에 반드시 정해져 있어야 한다는 것이다.

스칼라 타입들

스칼라는 하나의 값으로 표현되는 타입이다.

Rust는 정수형, 부동소수점 숫자, boolean, 문자 네가지 스칼라 타입을 보유하고 있다.

  1. 정수형 예제에서 u32타입인 정수형을 사용했다. 해당 타입의 선언은 부호 없는 32비트 변수임을 나타낸다.
Length Signed Unsigned
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
arch isize usize

각 부호 변수(Signed 양수음수 표현)는 -(2^(n-1))부터 2(n-1)-1 까지의 값을 포괄한다.

미부호 타입(양수표현만)은 0에서 2^n-1 까지의 값을 표현할 수 있다.

추가로, isize와 usize 타입은 당신의 프로그램이 동작하는 컴퓨터 환경이 64-bits인지 아닌지에 따라 결정된다.

34-bit 아키텍처이면 32bit를 갖게 된다.

정수표현의 리터럴은 다음 표와같이 표현하고, _를 이용해 구분을 할 수 있습니다.(자바동일)

Number literals Example
Decimal 98_222
Hex 0xff
Octal 0o77
Decimal 0b1111_0000
Decimal b'A'

확실하게 정해진 경우가 아니면 Rust의 기본 값인 i32가 일반적으로는 좋은 선택이다.

일반적으로 가장 빠르기 떄문이다. 심지어 64-bit 시스템에서도 마찬가지이다.

isize나 usize는 주로 일부 콜렉션 타입의 인덱스로 사용된다.

  1. 부동 소수점 타입 Rust는 f32, f64 두 가지 기본타입으로 32비트, 64비트의 크기를 갖는다.

기본 타입은 f64인데, 그 이유는 최신 CPU 상에서는 f64가 f32와 대략 비슷한 속도를 내면서도 더 정밀한 표현이 가능하기 떄문이다.

Rust는 일반적인 기본 수학적 연산은 모든 숫자 타입에 적용된다.

  1. Boolean 타입 Rust에서 bool로 명시된다.
fn main() {
    let t = true;
    
    let f: bool = false;
}
  1. 문자 타입 Rust의 char는 알파벳 타입이고, 스트링이 큰따옴표를 쓰는 것에 반하여 char 타입은 작은 따옴표로 쓰는 점을 주목
fn main() {
    let x = 'z';
    let z = 'Z';
    let heart_eyed_car = '😻';//머선 129
}

Rust의 char 타입은 Unicode Scalar를 표현하는 값이고 이는 ASCII 보다 많은 표현을 가능하게한다.

Unicode Scalar 값의 범위는 U+0000~ U+D7FF 이다.

이는 억양 표시가 있는 문자, 한국어/중국어/일본어 표의 문자, 이모티콘, 넓이가 0인 공백문자 모두가 char타입으로 사용할 수 있다.

컴파운드

복합 타입들은 다른 타입의 다양한 값들을 하나의 타입으로 묶을 수 있다.

Rust는 튜플과 배열 두가지 기본 타입을 갖고 있다.

  1. 튜플

튜플은 다양란 타입의 몇 개의 숫자를 집합시켜 하나의 복합 타입으로 만드는 일반적인 방법이다.

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

튜플은 단일 요소를 위한 복합계로 고려되었기에 변수 tup에는 튜플 전체가 bind된다.

개별 값을 튜플의 밖으로 뺴내오기 위해서는, 패턴 매칭을 사용해 튜플의 값을 구조해체(Destructuring) 하면 된다.

fn main() {
    let tup = (500, 6.4, 1);
    
    let (x, y, z) = tup;
    
    println!("y : {}", y)
}

패턴 매칭을 통해 destructuring에 추가로, .을 통해 Index를 접근할 수 있다.

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);
    
    let f = x.0;
    
    let s = x.1;
    
    let o = x.2;
}
  1. 배열 튜플과 다르게, 배열의 모든 요소는 모두 같은 타입이여야 한다.

Rust의 배열이 다른 언어의 배열과 다른 점은 Rust에서는 배열은 고정된 길이를 갖는다.

fn main() {
    let a = [1, 2, 3, 4, 5];
}

배열이 유용할 때는 데이터를 heap보다 stack에 할당하는 것을 원하거나, 항상 고정된 숫자의 요소를 갖는다고 확신하고 싶을 떄이다.

배열은 stack에 단일 메모리 뭉치로 할당된다. Index를 통해 배열의 요소에 접근할 수 있다.

fn main() {
    let a = [1, 2, 3, 4, 5];
    
    let first = a[0];
    let second = a[1];
}

배열의 범위를 넘은 접근을 하게되면 컴파일 시에는 아무런 에러도 발생시키지 않는다. 프로그램의 결돠는 실행 중에 에러가 발생한다.

Rust는 Index가 배열의 길이보다 작은지 확인한다.

Index가 배열의 길이보다 길면 프로그램 오류와 함께 패닉(panic)한다.

많은 저수준 언어에서 이러한 타입의 검사는 수행되지 않으며, 잘못된 Index를 제공하면 유효하지 않은 메모리에 엑세스 할 수 있다.

Rust는 메모리 접근을 허용하고 계속 진행하는 댓신 즉시 종료하여 이러한 종류의 오류로부터 사용자를 보호한다.

함수

구문과 표현식

Rust는 변수에 구문은 대입할 수 없다.

fn main() {
    let x = (let y = 6); //compile error
}

표현식은 구문의 일부분일 수 있다. 함수를 호출하는 것은 표현식, 즉 값이다.

예제같이 새로운 block {}은 표현식이다.

fn main() {
    let x = 5;
    
    let y = {
        let x = 3;
        x + 1
    };
    
    println!("y : {}", y)
}

표현식의 마지막 줄이 x + 1 세미콜론으로 끝나지 않는 점을 주목해라.

표현식의 종결은 세미콜론을 사용하지 않는다.

만약 세미콜론을 붙이면, 이는 구문으로 변경되고 반환 값이 아니게 된다.

반환값을 갖는 함수

반환되는 값을 명명해야 할 필요는 없지만, 반환값의 타입은 화살표(->) 뒤에 선언해야 한다.

Rust의 반환 값은 함수 본문의 마지막 표현식의 값과 동일하다.

return 키워드와 값을 써서 함수로부터 일찍 반환할 수 있지만, 대부분의 함수들은 암묵적으로 마지막 표현식을 반환한다.

fn five() -> i32 {
    5
}

fn main() {
    let x = five();
    println!("x : {}", x);
}

five 함수에는 함수 호출, 매크로, let 구문도 없이 그저 5 란 숫자 하나가 있다.

이는 Rust에서 완벽하게 함수로 허용된다.

fn main() {
    let x = plus_one(5);
    
    println!("x : {}", x);
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

위 예제에서 x + 1에 ;을 추가하면 어떻게 될까?

fn main() {
    let x = plus_one(5);
    
    println!("x : {}", x);
}

fn plus_one(x: i32) -> i32 {
    x + 1;
}

코드를 실행하면 다음과 같은 에러가 발생한다.

error[E0308]: mismatched types
 --> src/main.rs:7:28
  |
7 |   fn plus_one(x: i32) -> i32 {
  |  ____________________________^
8 | |     x + 1;
  | |          - help: consider removing this semicolon
9 | | }
  | |_^ expected i32, found ()
  |
  = note: expected type `i32`
             found type `()`

-> i32 붙였기때문에 반환값이 i32타입의 스칼라 정수가 와야하지만 빈 튜플로() 아무것도 반환되지 않았다는 타입 불일치 표시를 한다.

제어문