안녕하세요, 단테입니다. 러스트에 익숙해지기 위해 러스트의 주요 개념들을 다른 언어들과 비교하며 공부하겠습니다.
비욘드 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();
}
러스트에서 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이 없습니다. 다른언어에서 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/ 의 콘텐츠를 기반으로 작성되었습니다.