Rust의 가장 미묘한 구문
(zkrising.com)Rust의 let
과 const
-
let
은 새로운 변수를 선언하는 데 사용됨-
let PAT = EXPR;
형태로, 보기보단 더 강력함 - 패턴 매칭과 결합하여 편리한 기능을 제공
-
let (a, b) = (5, 10);
-
let maybe_string: Option<String> = ..;
-
let Some(value) = maybe_string else { panic!("die horribly")};
-
-
-
const
는 컴파일 타임에 계산되어 컴파일된 코드에 직접 포함되는 상수-
const MY_VAR: &str = "heyyyyyyyy man";
const SECRET: i32 = 0x1234;
-
const IDENT: TYPE = EXPR;
형태로, 타입을 명시해야 하며 패턴을 사용할 수 없음
-
헷갈리게 하는 것
-
const
는 선언 순서에 상관없이 사용 가능함 (hoisting)
// X가 Y 뒤에 정의되어 있어도 컴파일됨
const Y: i32 = X + X;
const X: i32 = 5;
- 함수 내부에서도 선언 가능하며, 그 상태에서도 호이스팅도 가능
fn oh_boy() -> i32 {
return X;
const X: i32 = 5;
// ^ 컴파일 되며 동작함. 워닝 없음!
}
- 자바스크립트 출신으로 이제 막 Rust를 배우는 프로그래머와 함께 작업하는 경우, 이 기능은 그들을 당황하게 만들 수 있는 훌륭한 기능임
- 훌륭한 기능의 무해한 결과인데, 이제 해로운 결과를 작성해보기로 함
Rust의 Match
// let PAT = EXPR;
let x = 5;
// 이 경우, `x`는 패턴임. `5`를 `x`에 넣을 수 있는지 확인함
// 이 패턴은 항상 매치됨 -- 항상 5를 `x`라는 변수에 넣을 수 있음
// 모든 패턴이 반드시 매치될 필요는 없음. 예를 들어:
let (5, x) = (a, b);
// 여기서 표현식은 a == 5인 경우에만 패턴과 "매치"
//
// 이를 "반박 가능한(refutable)" 패턴이라고 함
//
// `let` 선언에서, 반박 가능한 패턴은 "거부된(refused)" 경우를 처리해야 함:
let (5, x) = (a, b) else { panic!() };
//
// ...그렇지 않으면 "조건부로 존재하는(conditionally existing)" 변수를 갖게 될 수 있는데, 이는 좋지 않음
- 그럼
match
에 대해 알아봅시다. match는 무엇일까요?
// match는 패턴과 매치될 경우 수행할 작업의 목록
//
// match EXPR {
// PAT => EXPR
// PAT => EXPR
// ..
// }
match (a, b) {
(5, x) => {
// 만약 (a,b)가 (5,x)와 매치되면, 이 블록이 실행됨
},
(x, 5) => {
// 같은 방식으로: 만약 (a,b)가 (x, 5)와 매치되면..
},
(x, y) => {
// 그리고 이것은 "모든 것을 잡아내는" 패턴으로, let (x,y) = (a,b)가 동작하는 방식과 같음
}
}
고통을 줘 봅시다
- 사람들을 혼란스럽게 하는 것도 재미있지만, 완전한 불행과 실제 버그를 야기하는 것은 어떨까?
- 내가 보기엔 이것이 Rust의 가장 미묘한 문법임:
- 이 글에서 가장 흥미로운 한 줄 : Rust의 가장 미묘한 문법은 상수 자체가 패턴이라는 것
- 이 문법은 매칭 주변에 몇 가지 좋은 ergonomic을 추가함:
let input: i32 = ..;
const GOOD: i32 = 1;
const BAD: i32 = 2;
match input {
// 이것은 input == GOOD인지 확인. 왜냐하면 GOOD은 상수이기 때문
GOOD => println!("input was 1"),
// 이것은 input == BAD인지 확인. 왜냐하면 BAD는 상수이기 때문.
BAD => println!("input was 2"),
// 이것은 otherwise = input으로 정의하고, 항상 매치됨...
otherwise => println!("input was {otherwise}"),
}
그러나 상수를 대문자로 쓰는 것은 단순히 관례일 뿐. 그렇게 하지 말라는 컴파일러 경고일 뿐임.
const good: i32 = 1;
const bad: i32 = 2;
match input {
// 음...
good => {},
bad => {},
otherwise => {},
}
이제 우리는 동일해 보이는 세 개의 분기를 가지고 있지만, 그것들이 하는 일은 해당 이름의 상수가 존재하는지에 따라 달라짐!
더 나빠져 봅시다. 아래에선 어떤 일이 일어날까?
const GOOD: i32 = 1;
match input {
// 오타...
GOD => println!("input was 1"),
otherwise => println!("input was not 1")
}
여기서는 컴파일러 경고가 나타나겠지만, 이 코드는 항상 input was 1
을 출력할 것
또는 좀 더 현실적으로:
// 이런, 실수로 이 임포트를 주석 처리하거나 삭제했음
// use crate::{SOME_GL_CONSTANT, OTHER_THING}
// 이런!
match value {
SOME_GL_CONSTANT => ..,
OTHER_THING => ..,
_ => ..,
}
이것은 사람들을 혼란스럽게 함. 특히 그들이 열거형으로 멋진 것들을 시도할 때 더욱.
enum MyEnum {
A, B, C
}
// 보통은 이렇게 작성함
match value {
MyEnum::A => ..,
MyEnum::B => ..,
MyEnum::C => ..,
}
// 하지만 이렇게 작성할 수도 있음
use MyEnum::*;
match value {
A => {},
B => {},
C => {}
}
// 그리고 나서, 만약 MyEnum을 변경한다면...
enum MyEnum { A, B, D, E };
use MyEnum::*;
// 이것은 여전히 컴파일됨!
match value {
A => {},
B => {},
C => {},
}
// `C`는 이제 "모든 것을 잡아내는" 패턴이 됨. 왜냐하면 `C`와 같은 것이 범위 내에 없기 때문.
// 여러분은 let C = value를 하고 있는 것이고, 이는 항상 매치됨!!!
Clippy는 이렇게 하지 말라고 경고하는 많은 규칙을 가지고 있음. 왜냐하면 이것이 사람들을 항상 혼란스럽게 하기 때문.
그러나 이것은 더욱 혼란스럽게 만들 수 있음:
// x를 5에 irrefutably 바인딩...
let x = 5;
// ...잠깐만요...
const x: i32 = 4;
이 코드는 컴파일되지 않음. 왜냐하면 const x
는 패턴이고, 상수는 호이스팅되며, 이제 이 코드는 다음과 같이 평가되기 때문:
let 4 = 5;
// error[E0005]: refutable pattern in local binding
// --> src/main.rs:3:5
// |
// 3 | let x = 5;
// | ^
// | |
// | 패턴 `i32::MIN..=3_i32`와 `5_i32..=i32::MAX`가 커버되지 않음
// | 누락된 패턴은 `x`가 새로운 변수가 아닌 상수 패턴으로 해석되기 때문에 커버되지 않음
// | 도움말: 대신 변수를 도입하세요: `x_var`
// |
// = 참고: `let` 바인딩은 "irrefutable pattern"을 필요로 함. 예를 들어 `struct`나 하나의 variant만 가진 `enum`처럼
"expr이 4와 같다"는 반박할 수 없는 매치가 아니며, 그렇지 않은 경우를 처리하지 않음
주위 모두를 짜증나게 만들기
// `maybe`가 Option<&str>이라고 가정. 어떤 텍스트일 수도 있고, None일 수도 있음.
let maybe_username: Option<&str> = ..;
// 이것은 한 줄 매치에서 Rust의 일반적인 패턴. 이것이 Some(..)과 매치한다면 우리는 그 문자열로 무언가를 할 수 있음.
if let Some(username) = maybe_username {
// 그래서 이 코드는 username이 존재하면 실행됨...
return username.to_uppercase();
}
// 그런데 말이죠... 이제 그 코드는 'username'이 Some("hey")과 매치할 때만 실행됨
const username: &str = "hey";
상수 호이스팅과 상수가 패턴이라는 사실의 조합은 여러분이 수수께끼 같은 Rust 코드를 작성할 수 있게 해줌
이것은 실제 문제는 아님
- 현실적으로, 이것이 혼란스러울 수 있는 유일한 이유는 여러분이
let UPPERCASE
와const lowercase
를 작성할 수 있다는 것 - 만약 대문자로 시작하는 변수를 만드는 것이 lint 오류였다면, 혼란은 일어나지 않을 것
- 열거형 variant나 상수와 매치하려고 할 때 실수로 무언가를 바인딩할 수는 없을 것이기에
- 하지만 분명히 하자면, 이것은 단지 언어의 재미있는 특이점일 뿐
macro_rules! f {
($cond: expr) => {
if let Some(x) = $cond {
println!("i am some == {x}!");
} else {
println!("i am none");
}
}
}
fn main() {
f!(Some(100));
{
f!(Some(100));
return;
const x: i32 = 5;
}
}