다가오는 AI 시대.. 조만간 프로그래머는 "대체"된다.. 염병! 지금까지 해온 몸값 부풀리기 전략이 통하지 않게 된다니!! 어떡하지? 땅을 파자니 힘이 없고 빌어 먹자니 부끄럽구나. 옳지 지금이라도 '상대적으로 새롭고 + 조금 어렵다고 하는데 + 조만간 뜬다고들 하는' 다른 무언가를 얼른 배워야겠다!! 그래서 정보 비대칭성에 의한 상대적 희소가치를 확보해야지!!! 기왕이면 PHP라는 고수준 언어로만 밥벌이를 해온 설움을 해소할 저수준 언어 하나 골라서...
그래서 이런 책을 오프라인 서점에서 사서 Rust 공부를 하고 있다. 이하는 이 책을 스터디하는 과정에서 나오는 '나중에 분명 또 잊어버려서 다시 확인하고 싶을 게 뻔한' 내용들의 메모.
러스트 파일명과 프로젝트명은 snake_case로 작성. cargo new foo_bar
막상 다른 챕터에서는 slug-case 사용한다. -_-;;
들여쓰기 규칙은 4 spaces.
fmt와 clippy는 VS Code에서 지정해두는 것이 좋음.
if 및 for 조건식에 괄호 쓰지 않음.
- 자꾸 까먹기 때문에 적어둔다.
- 언어 디자인 측면에선 이게 옳은 거 같다. 사실 이런 통제 조건문들은 불리언으로 평가되기만 하면 되므로 간단할수록 좋으며 간단할 필요가 있다. 근데 PHP 같은 것의 경우 조건문 자리에 괄호가 필수다 보니, 더 복잡한 조건문이 필요해질 때 괄호를 남발해서 "해결"하는 함정에 빠지는 경우가 종종 있다.
따옴표 사용: 의도적으로 char를 표현할 때는 ' 사용. 그밖의 &str은 " 사용.
콘솔 출력: println!() 매크로로.
#[derive(Debug)]걸렸는지 확인하기.
때때로 함수/메소드가 반환할 형태를 지정 가능.
String::parse<i32>() 하면 (가능할 경우) i32가 나오고,
String::parse<u32>() 하면 (가능할 경우) u32가 나온다.
- "Rust는 일반적으로 ... 최종 자료형을 유추할 수 있으나 ... 언제든 명시적으로 지정할 수 있습니다."
::<T>연산자는 '터보피쉬'라고 부름.
범위축약: a..b는 b-1까지의 범위임.
a..=b로 적어주면 b까지의 범위가 됨.k: &str에 대하여, k == k[0..k.len()] == k[0..]'를 붙여서 뭔가의 별칭으로 쓸 수 있다.
특히 루프에 이름 지정하여 탈출 가능.
// 별로 안좋은 예제지만...
'foo for i in 2..=9 {
'bar for j in 2..=9 {
if i >= 7 {
println!("6단까지만 외웠어요");
break 'foo;
}
println!("{} x {} = {}", i, j, i * j);
}
}
null 처리: 일단 Result::ok()로 시작하기. Result<T, E>를 Option<T>로 변환한다.
"러스트는 값이 없을 가능성이 있다면 반드시
Option타입을 사용해야 합니다. 반대로Option타입이 아니라면 반드시 값이 있습니다."
Struct: 우리말로는 "구조체".
변수명이 구조체 내부 필드명과 같을 때(만)는 다음과 같이 초기화 가능.
let foo = "abc";
let bar = "def";
let FB1 = FooBar { foo, bar };
열거형을 콘솔에 찍으려면 #[derive(Debug)] 추가하고 println!("enum: {:?}", my_enum) 형식으로 출력.
std::iter::map() 같은 데서 쓰는 클로저 구문은 다음과 같음. ref
let cubes = numbers.iter().map(|x| x ** 3);
구조체에는 연관 함수라는 것이 있음. self를 인자로 받지 않는 것.
반대로, 구조체의 "메소드"는 첫번째 인자가
self여야 한다. 아마도&self또는&mut self일 것.
신택스 슈거 하나: 블록 끝에서 뭔가를 리턴한다면 return과 ;을 생략 가능하다.
// 둘은 같다
fn foo1() {
return bar;
}
fn foo2() {
bar
}
책은 '참조된 값을 인자로 받는 함수' 정의하는 방법을 가르쳐주는 식으로 소유권 얘기를 시작한다. (좋은 도입 같다. 확실히 이거는 다른 언어에서도 비슷하게 한다.)
// PHP의 경우 함수 인자에서만 & 키워드가 등장한다.
function append_bar(&$str) {
$str .= "bar";
}
$x = "Allahu Ak";
append_bar($x); // 그러다 보니 여기서 $x가 가공되고 있음을 눈치채기 어려운 게 사실이다.
echo $x;
// 러스트의 경우 '변수값이 어디서 (안) 가공되느냐?'가 중대한 관심사다.
fn append_bar(str: &mut String) {
str.push_str("bar");
}
let mut x = String::from("Allahu Ak");
append_bar(&mut x); // 그래서 이런 곳에서 x가 가공되고 있음을 알 수 있게 했다.
println!("{}", x);
&변수명은 불변, &mut 변수명은 가변.fn foo(numbers: &[f64])fn foo(numbers: &Vec<f64>)& 키워드를 쓰지 않더라도 소유권이 이동하지 않고, 대신 "분신"이 복사되는 방식으로 대입된다.f64 는 copy semantics를 따르는 타입. 아무래도 원시자료형이니까.Vec<T>는 move semantics를 따르는 타입. 아무래도 소유권, 내용 등이 함부로 바뀌면 곤란하니까.루프되는 자료는 무조건 어딘가에서 빌려오는 자료임.
// 현실적으로 이런 루프는 극히 드물게만 존재한다.
// (e.g. `main` 끝에서 마지막으로 사용)
for a in b { ... }
// 보통 이렇게 된다.
// 왜냐면 이 루프 이후로도 b를 쓸 일이 있기 때문.
for a in &b { ... }
// 또는 이렇게 된다.
for a in b.iter_mut() {
// 여기서 a는 &mut T이다.
}
목록을 통째로 불변 참조한다는 개념이다. 나쁜 PHP와 비교해 보면, 이게 왜 좋은지 알 수 있다.
// 모든 게 가능한 PHP에서는 이런 것도 가능하다. foreach ($a as $b => $c) { $a[$b . '1'] = $c + 1; // $c가 문자열이냐 숫자냐에 따라 난리굿판이 벌어진다. unset($a[$b]); // 언어에 따라서는 이런 짓은 상상조차 못할 일이다. } // 여기쯤에서 $a는 정체불명의 무언가가 되어 있다.
"일부 트레이트는 조건이 맞으면 타입 선언 앞에
#[derive()]어트리뷰트를 사용해 자동으로 구현할 수 있습니다."
어트리뷰트는 붙이는 순서가 중요함. Clap 크레이트로 예를 들자면 다음과 같다.
// 작동함
#[derive(Parser)]
#[clap(version = "1.0")]
struct App { ... }
// 작동 안함
#[clap(version = "1.0")] // 이 안에 derive가 포함돼 있기 때문에 "derive가 도입되기도 전에 여기서 먼저 사용되고 있음" 오류가 뜨고 컴파일 안된다.
#[derive(Parser)]
struct App { ... }
뭔가가 반환한 Result를 사용하지 않을 거면 .unwrap() 해줘야 한다.
let foo: ABC = bar.unwrap()serde::Serialize 파생을 공유하는 것들끼리 이 방식으로 캐스팅 가능하다.파일 핸들링은 자주 보게 될 테니 PHP와 비교해 보도록 하자.
// PHP는 내장함수 및 기본 익스텐션의 함수형 API를 사용.
// "파일 핸들러" 개념은 비전공자들에게는 머리 터지는 것이므로, PHP는 그런 바탕 원리는 몰라도 되게 숨겨놨다.
$f = fopen("foo.csv", 'wb+');
fputcsv($f, ["a", "b", "cde"]);
fclose($f);
이상의 '핸들 획득', '핸들로 쓰기', '핸들 닫기' 3단계를 러스트 3줄에 억지로 우겨넣자면 다음과 같음.
let file = std::fs::OpenOptions::new().write(true).append(true).open("foo.csv");
let mut writer = csv::Writer::from_writer(&file); writer.write_record(["a", "b", "cde"]).unwrap();
writer.flush().unwrap();
모듈 구조는 다음과 같음.
src/모듈명/mod.rs ---> "모듈명" 모듈의 메인
src/모듈명/foobar.rs ---> "모듈명" 모듈의 서브모듈 foobar의 메인
pub mod foobar;= "foobar의 내용을{}로 감싸서 여기 불러올 것이다"- 뭐 이것저것 쓰다 보면 싫어도 이해하게 되겠지.
함수를 use할 때는 "필수 한 단계 위의 모듈까지" 지정하는 것이 관행이다.
- "그밖의 요소는 전체 경로로 가져오는 방법이 일반적입니다."
- 뭐 rust-analyzer가 알아서 해주겠지.
테스트 케이스들은 mod tests로 묶는 것이 관행이다.
#[cfg(test)] = "테스트할 때만 컴파일하고 그 외에는 무시"
#[rstest]로 "파라미터화 테스트" 작성시 함수/메소드 인자 앞에 #[case] 어트리뷰트를 붙여야 함.
함수, 클로저 및 함수 포인터 매개변수는 외부 속성을 허용합니다. 문서
HashMap 타입 정의에는 &가 안 들어간다.
HashMap<String, f32>와 HashMap<String, &f32>는 다름. HashMap<&String, f32>는 더더욱 말이 안 됨.해시맵 값을 조작하는 예제는 다음이 가장 간결하니 외울 것.
let value: f32 = hashmap.entry(key).or_insert(0.0);
*value -= 45; // 맨앞의 *에 주의할 것
Docker로 프로젝트 시작하는 건 다음과 같이.
# -u 옵션 주지 않으면 프로젝트 소유자가 root가 됨
docker run --rm -it -u "$(id -u):$(id -g)" -v "$(pwd):/app" -w /app rust cargo new foobar
cargo 자체를 alias 등록해두는 것이 가능하다.
alias cargo='docker run --rm -it -u "$(id -u):$(id -g)" -v "$(pwd):/app" -w /app rust cargo'
암튼 그렇게 새 프로젝트 띄우고 나서, 다음 템플릿을 .devcontainer/devcontainer.json에 저장하고 컨테이너 띄우면 끝.
{
"name": "Rust devcontainer template", // 이름만 바꾼다
"image": "mcr.microsoft.com/devcontainers/rust:2-1-trixie", // 공식 이미지
"customizations": {
"vscode": {
"extensions": [
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml"
],
// 위의 customizations.vscode.extensions 에 의해 확장이 설치되므로,
// 아래의 cutomizations.vscode.settings 에서 "rust-analyzer.*" 설정이 유효해진다.
"settings": {
// 혹시 "작업 영역 설정 JSON"에 뭔가 넣어놨던 게 있다면 여기로 옮긴다
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.formatOnSave": true
},
"rust-analyzer.checkOnSave": true,
"rust-analyzer.check.command": "clippy"
}
}
}
}