프로를 위한 러스트 2

dante Yoon·2023년 2월 11일
0

rust

목록 보기
2/2
post-thumbnail

글을 시작하며

안녕하세요, 단테입니다. 러스트에 익숙해지기 위해 러스트의 주요 개념들을 다른 언어들과 비교하며 공부하겠습니다.

Ownership

비욘드 JS 러스트 강의에서 이야기헀지만, 러스트가 다른 언어들과 차별되는 특징으로 오너쉽이 있습니다.
러스트는 javascript, java, Go와는 다르게 가비지 콜렉터가 없습니다. 그리고 C와 다르게 메모리 크기를 명시적으로 작성해주지 않아도 됩니다. 대신 더 이상 메모리를 사용하지 않을 때 자동으로 메모리가 해제됩니다.

// Java

class User {
    private String name;

    public User(String name) {
        this.name = name;
    }
}

public static void main(String[] args) {
    String name = "User";

    User user1 = new User(name);
    User user2 = new User(name);
}

// Rust

struct User {
    name: String,
}

fn main() {
    let name = String::from("User");

    let user1 = User { name };
    let user2 = User { name }; // compile error
}

자바에서 GC는 주기적으로 객체 레퍼런스를 확인하고 User 인스턴스를 더이상 참조하지 않는 객체 레퍼런스는 자동으로 삭제됩니다. 두 인스턴스가 더 이상 사용되지 않으면 "User" 문자열도 삭제됩니다.

러스트에서 값은 동시에 여러 객체에서 소유될 수 없습니다. "User"는 user1에 의해 소유되기 때문에 두번째 패턴 바인딩 user2는 컴파일 에러를 발생합니다.

C언어에서 개발자는 매모리 할당과 해제를 잘 신경써야 하지만 러스트에서는 그럴 필요가 없어 메모리 릭에 대한 위험성을 보다 덜 염두에 두고 개발할 수 있게 합니다.

// C++

#include <string>

std::string* get_string() {
    std::string* string = new std::string("hello");

    delete string;

    return string;
}

// rust

fn get_string() -> String {
    let string = String::from("hello");

    drop(string);

    return string; // compile error!
}

러스트 컴파일러가 메모리 소유 법칙에 벗어나는 부분이 있는지 계속 지켜봄으로 인해 멀티 스레드 프로그램을 작성할 때 한 값이 다른 스레드에서 동시에 접근되는 것을 컴파일러가 방지할 수 있고 메모리 세이프한 코드를 작성할 수 있게 도와줍니다.

// C++

#include <vector>
#include <thread>

int main() {
    std::vector<std::string> list;

    auto f = [&list]() {
        for (int i = 0; i < 10000; i++) {
            list.push_back("item 123");
        }
    };

    std::thread t1(f);
    std::thread t2(f);

    t1.join();
    t2.join();
}

c++로 작성된 std::vector는 스레드 세이프하지 않지만 컴파일될 수 있고 런타임에서 에러를 발생시킬 수 있습니다. 아래의 러스트 코드에서 클로저 f는 list에 대한 오너쉽을 가지고 있고 여러 스레드에서 사용하려고 할 떄 컴파일 에러가 발생합니다.

// Rust

use std::thread;

fn main() {
    let mut list = vec![];

    let f = move || {
        for _ in 0..10000 {
            list.push("item 123");
        }
    };

    let t1 = thread::spawn(f);
    let t2 = thread::spawn(f); // compile error!

    t1.join().unwrap();
    t2.join().unwrap();
}

Strings

러스트에서 string type은 여러 가지 종류가 있습니다. str, Sring이 제일 많이 사용됩니다.
&str 타입은 string slice에 대한 reference가 필요하거나 static string이 필요할 때 사용됩니다. 다른 immutable reference와 &str은 변경될 수 없습니다. 변경할 수 있는 값을 사용하고 싶다면 String 타입을 사용해야 합니다.

// javascript

const FILE_DATE = "2020-01-01";

function printCopyright() {
  const year = FILE_DATE.substr(0, 4);
  const copyright = `(C) ${year}`;
  console.log(copyright);
}

// rust

const FILE_DATE: &str = "2020-01-01";

fn print_copyright() {
    let year: &str = &FILE_DATE[..4];
    let copyright: String = format!("(C) {}", year);
    println!("{}", copyright);
}

String 타입은 컴파일 타임에 길이를 고정하지 않기 때문에 변경 가능한 문자열이 필요할 떄 사용됩니다.

// java

public static String repeat(String s, int count) {
    StringBuilder result = new StringBuilder();
    for (int i = 0; i < count; i++) {
        result.append(s);
    }
    return result.toString();
}

// rust

fn repeat(s: &str, count: u32) -> String {
    let mut result = String::new();
    for _ in 0..count {
        result += s;
    }
    result
}

struct에서 문자열을 소유할 수 있도록 struct 필드로 보통 String타입이 사용됩니다.

data class User(
    private val source: String,
    private val name: String,
    private val address: String
) {
    companion object {
        fun fromString(string: String): User {
            val lines = string.lines()
            return User("kotlin-v1.0", lines[0],
                lines[1])
        }
    }
}

아래에서 문자열 상수를 사용하기 위해서 &'static str를 필드 타입으로 사용했습니다.
String타입 s1 변수를 &str 타입으로 변경하기 위해서 &s1을 사용합니다.
반대로 &str 타입 변수 s2를 String타입으로 변경하기 위해서 s2.to_owned()를 사용합니다. 이 메소드는 새로운 메모리에 문자열을 할당합니다.

pub struct User {
    source: &'static str,
    name: String,
    address: String,
}

impl User {
    pub fn new(s: &str) -> User {
        let mut lines = s.lines();
        User {
            source: "rust-v1.0",
            name: lines.next().unwrap().to_owned(),
            address: lines.next().unwrap().to_owned(),
        }
    }
}

Null

러스트는 null이 없습니다. 다른언어에서 None이나 Nil로 불리기도 하죠. 대신에 Option enum타입을 사용합니다. 이 타입은 Java의 Optional 타입과 유사합니다.

// java

public static Integer getYear(String date) {
    if (date.length() >= 4) {
        String s = date.substring(0, 4);
        try {
            return Integer.valueOf(s);
        } catch (NumberFormatException e) {
            return null;
        }
    } else {
        return null;
    }
}

public static void main(String[] args) {
    Integer year = getYear("2020-01-01");
    if (year != null) {
        System.out.println(year);
    }
}

러스트의 Option 타입은 러스트의 기본 라이브러리에서 제공하는 평범한 enum입니다. 이 Enum은 None과 Some 타입이 있는데요, Option을 반환할 때 null 대신 None을 사용할 수 있고 그렇지 않으면 Some(year)과 같이 Some enum으로 래핑되어야 합니다.
// rust

fn get_year(date: &str) -> Option<u32> {
    if date.len() >= 4 {
        let s = date[..4];
        match s.parse() {
            Ok(year) => Some(year),
            Err(_) => None
        }
    } else {
        None
    }
}

fn main() {
    if let Some(year) = get_year("2020-01-01") {
        println!("{}", year);
    }
}

nullish 한지 체크하기 위해서 is_none() 메소드를 사용합니다.

//java

Integer year = getYear("");
if (year == null) {
    System.err.println("Invalid date given!");
}

//rust

let year: Option<u32> = get_year("");
if year.is_none() {
    println!("Invalid date given!");
}

가끔 nullish할 때만 특정 코드를 실행시킬 수 있습니다. 다음처럼 패턴 매칭을 사용합니다.

// java

String url = "https://github.com";

String content = cache.get(url);
if (content == null) {
    content = loadUrl(url);
}

// rust

let url = "https://github.com";

let content = match cache.get(url) {
    Some(content) => content,
    None => load_url(url),
};

본 포스팅은 https://overexact.com/rust-for-professionals/ 의 콘텐츠를 기반으로 작성되었습니다.

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글