이 글은 Rust Book의 The Guessing Game에 나오는 다양한 Rust 맛보기 문법을 정리하였습니다.
error 처리(.except), string literal, String, enum, type inference를 다룹니다.
Error 처리하기
.expect()
io::stdin() // 원래는 std::io로 라이브러리를 가져온다
.read_line(&mut guess)
.expect("Failed to read line");
Result에도 method가 있다 -> 그게 .expect
- read_line() -> Result{Err} -> expect -> print String
- read_line() -> Result{OK} -> expect -> OK안에 있는 값을 출력한다(usize 안에 있는 수를 반환-byte수)
pub enum Result<T, E> {
Ok(T), // 처리의 성공 + 결과(eg. 바이트수) 를 저장
Err(E), // 처리의 실패 + 이유 저장
}
String types
Rust에서는 string을 String(String)과 string literal (&str)로 구분한다.
string type String string literal (&str)
declaration | let s: String = String::from("hello"); | let s: &str = "hello"; |
memory space | heap | in program space |
mutability | mutable | immutable |
참고로, let mut 할 때의 것은, 변수 자체가 동타입에서 가변성이 있는거지, immutable, mutable자료형에는 관련이 없다! 변수의 mutability와 자료형의 mutability를 구분하자!
문자열 다루기
// string literal &str를 사용한 예시
let my_string = " Hello World!\\n" // string &str
println!("{} addr {:p}",my_string,my_string.as_ptr());
my_string = my_string.trim(); // string 앞뒤에 문자 자르기, .trim() -> &str
println!("{}, addr {:p}",my_string,my_string.as_ptr());
일단 .trim()은 &str를 반환한다. 이렇게 되면 trim이 되어서 2바이트 뒤의 주소가 출력된다.
- as_ptr(): String이나 벡터(Vec<T>) 타입에서 힙에 저장된 데이터의 시작 주소(포인터)를 가져올 때 사용하는 함수 이것을 통해서 실제로 문자열이 저장된 장소를 출력할 수 있다.
- &my로 하면 my 변수가 stack어디에 저장되어있는지를 출력한다.
- Rust는 크기가 변하지 않는 것을 stack 크기가 변하는 string, vector를 heap에 저장한다.
- 연습 코드위 코드는 :결과를 가지며 가르키던 r/o memory의 구역이 변경된 것을 확인할 수 있다. &str은 r/o memory 구역(code text)영역에 쓰인다.
이 코드는 .to_string()을 통해서 새로운 힙에 공간을 마련하여 문자열을 할당하였다.Bonjour, le monde!, addr 0x5d20d808db80 Bonjour, le monde!, addr 0x5d20d808dba0
- fn main() { // unmutable한 같은 program을 작성하자. let mut s = " Bonjour, le monde!".to_string(); // String type println!("{}, addr {:p}",s,s.as_ptr()); // s = s.trim(); // 이렇게 하면 trim은 literal을 반환하여 오류! s = s.trim().to_string(); // 이거는 mutable하기에 오류 println!("{}, addr {:p}",s,s.as_ptr()); // heap 의 다른 곳을 포인팅하기에 주소가 바뀜! }
- addr 0x621800249098 Bonjour, le monde!, addr 0x621800249099
- fn main() { // mutable한 String으로 작업을 해보자~ let mut my = " Bonjour, le monde!\\n\\n"; // &str, 근데 &str은 immutable인데, **가변성 자체는 my라는 변수가 가르키는 것에 한정됨!** println!("{} addr {:p}",my,my.as_ptr()); my = my.trim(); // trim returns &str println!("{}, addr {:p}",my,my.as_ptr()); }
let guess: u32 = guess.parse().expect("Plz type a number..");
// .parse()는 지정된 문자열로 형변환!
- .parse() 는 변수 선언할 때 지정된 문자열로 변환
let num = "42".parse::<i32>().unwrap();
- .parse::<자료형>() 형식으로도 자료형 지정 가능!
match 표현식과 enum
match의 기본 구조
Pattern matching control flow construct
match 값 {
패턴1 => 처리할 코드1,
패턴2 => 처리할 코드2,
_ => 기본 처리 코드, // 모든 패턴에 일치하지 않는 경우
}
match 앞의 것을 pattern이라고 한다. pattern ⇒ code style을 가지며, 기본 처리는 _ ⇒ code style을 가진다.
특별한 점은 type 또는 literal을 자동으로 판단해서 비교해준다.
literal matching cases
let number = 2;
match number {
1 => println!("One"),
2 => println!("Two"),
3 => println!("Three"),
_ => println!("Other"),
}
let greeting = "hello";
match greeting {
"hi" => println!("Hi there!"),
"hello" => println!("Hello!"),
_ => println!("Greetings!"),
}
이러한 경우 compiler가 전자는 i32의 literal로 취급, 후자는 &str (string literal)로 취급한다.
type matching cases
enum Direction {
North,
South,
East,
West,
}
let direction = Direction::North;
match direction {
Direction::North => println!("Heading North"),
Direction::South => println!("Heading South"),
Direction::East => println!("Heading East"),
Direction::West => println!("Heading West"),
}
enum을 선언하고, match 구문에서 자동으로 type을 비교함. 내부에 있는 것들을 arm(갈래) 라고 함 (이때 direction은 Direction의 enum type)
enum의 기본 구조
enum Direction {
North,
South,
East,
West,
}
enum <enum 정의> {
<varient1>, <varient2>, ...}
다양한 varient를 가지는 enum
enum Message {
Quit, // No data
Move { x: i32, y: i32 }, // Struct-like data
Write(String), // Tuple-like data
ChangeColor(i32, i32, i32), // Multiple values
}
fn main() {
let msg1 = Message::Quit;
let msg2 = Message::Move { x: 10, y: 20 };
let msg3 = Message::Write(String::from("Hello"));
let msg4 = Message::ChangeColor(255, 0, 0);
match msg3 {
Message::Quit => println!("Quit message"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Write message: {}", text),
Message::ChangeColor(r, g, b) => println!("Change color to RGB({}, {}, {})", r, g, b),
}
}
단순히 타입을 가지고, 데이터를 포함하지 않을 수도 있다. 또는 데이터를 가질 수 있다. 이때 parameter가 있으면 데이터 타입을 선언해줘야한다. rust는 모든 데이터 타입을 compile time에 알아야한다. (compiler가 타입 추론이 강력해서 추론해도 됨, 변수 선언할 때 숫자나 문자열 자동 탐지 등)
match와 enum
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
std::cmp::Ordering의 method인 cmp를 통해 string을 비교할 수 있다.
Type Inference
// explicit type declaration
let x: i32 = 5; // 명시적으로 i32 타입을 지정
let name: &str = "Alice"; // 명시적으로 문자열 슬라이스 타입을 지정
// type inference
let x = 5; // 컴파일러가 x의 타입을 i32로 추론
let name = "Alice"; // 컴파일러가 name의 타입을 &str로 추론
직접 지정해주면 좋지만, type inference가 되는 경우들도 존재한다. 하지만 type inference가 되지 않는 경우들도 있으니, 이를 잘 고려해야한다. ChatGPT가 아래의 경우들로 type inference가 되지 않는 경우들을 정리하였다.
1. empty한 것의 선언
let v: Vec<i32> = Vec::new(); // 빈 벡터의 경우, 요소의 타입을 명시적으로 지정해야 함
let arr:[i32,0] = []; // 빈 배열의 경우, 타입과 빈 배열의 크기를 explict 기입
let x = Default::default(); // 기본값의 경우, 기본값의 타입으로 초기화해야 (i32: 0)
let x = 10;
let y = x as f64 * 2.5;// f64인지 i32인지 명시적이지 않음. x를 float취급해서 계산하면
// float * float로 계산 가능
2. 타입의 모호성 - 반환 타입 generic
let x: u32 = "42".parse().expect("Not a number!"); // parse()의 반환 타입이 모호하기 때문에 u32로 지정
반환 타입이 명확하지 않은 경우
let mut x = None;
x = Some(5);
// 오류: cannot infer type for type parameter `T`
let mut x: Option<i32> = None; // Option<i32> 타입으로 명시
x = Some(5);
generic한 결과가 반환되는 경우
fn print_vector<T: std::fmt::Debug>(vec: Vec<T>) {
println!("{:?}", vec);
}
또는 generic함에도 trait bound가 존재하는 경우!!! T에도 사실은 일부 type 제약을 줘야 한다. 이 경우에는 vec로 표기되는 출력이 가능해야 하기에 - std::fmt::Debug 형태의 제약이 필요하다. 연산 같은 경우는 괜찮다.
3. 함수 시그니처
fn add(a: i32, b: i32) -> i32 {
a + b
}
4. 산술 결과
let x = 10;
let y = x * 2.5;
// 오류: cannot infer type for type parameter `T`
let y = x as f64 * 2.5; // 명시적으로 x를 f64 타입으로 변환하여 타입을 맞춤