Rust – 문법 정리
아래의 설명들은 여기 에 설명된 방식으로 개발환경이 구축된 상황을 전제로 설명합니다.
변수
fn main() {
let x: i32 = 10;
let mut y: i32 = 20;
y = y + x;
println!("x: {x}");
println!("y: {y}");
}
변수는 기본적으로 불변(immutable) 입니다.
변경 가능하게 하려면 mut
키워드를 붙여주어야 합니다.
변수 타입은 생략 가능합니다.
생략된 타입은 컴파일러가 자동으로 타입을 추론합니다.
타입 추론
fn takes_u32(x: u32) {
println!("u32: {x}");
}
fn takes_i8(y: i8) {
println!("i8: {y}");
}
fn main() {
let x = 10;
let y = 20;
takes_u32(x);
takes_i8(y);
}
변수의 타입은 생략할 수 있습니다.
컴파일러는 변수가 사용된 상태를 기반으로 변수의 타입을 자동추론해줍니다.
상수
const DIGEST_SIZE: usize = 3;
static BANNER: &str = "Welcome to Rust";
fn main() {
let _size = DIGEST_SIZE;
let _banner = BANNER;
}
const
, static
모두 상수를 표현하지만 작동이 다릅니다.
const
는 함수에서 이용될 때 함수에 할당된 영역으로 값이 복사됩니다.
static
의 경우는 프로그램 전역에 단 하나의 값만이 존재하고, 모든 함수는 이 값을 이용합니다.
메모리 사용량 면에서는 static
이 유리하지만, 하나의 함수에서 문자열의 내용을 수정할 경우 모든 함수에서 영향을 받으므로 위험성도 늘어납니다.
일반적으로 const
를 사용하는 것이 권장됩니다.
변수 숨기기(Shadowing)
fn main() {
let a = 10;
println!("before: {a}");
{
let a = "hello";
println!("inner scope: {a}");
let a = true;
println!("shadowed in inner scope: {a}");
}
println!("after: {a}");
}
범위 바깥 뿐만 아니라, 범위 내부의 변수도 숨길 수 있습니다.
동일 범위 내부에서의 Shadowing 은 새로운 변수를 생성하는 것과 같아서,
타입을 변경할 수도 있습니다.
열거형(enum)
use std::any::type_name;
use std::mem::{align_of, size_of};
#[derive(Debug)]
enum CoinFace {
Front, Back
}
fn main() {
println!("Coin face is {:?}", CoinFace::Front);
println!("Coin face is {:?}", CoinFace::Back);
println!("{}: size {} bytes, align: {} bytes",
type_name::<CoinFace>(), size_of::<CoinFace>(), align_of::<CoinFace>());
}
enum 에 값을 할당하지 않는 경우 0, 1, 2, … 의 값이 할당됩니다.
enum 의 사이즈는 필요한 최소한의 사이즈를 가집니다.
(하지만 이 부분은 설명이 복잡해지므로 직접 확인하기 바랍니다.)
흐름 제어
블록
fn main() {
let _x = {
3 + 4
};
let _y = {
let _ = 3 + 4;
};
}
모든 블록은 리턴 값과 타입을 가집니다.
리턴하는 값이 없으면 ()
이 리턴값이 됩니다.
리턴값은 세미콜론 ;
이 없습니다.
if
fn main() {
let mut x = 10;
if x % 2 == 0 {
x = x / 2;
} else {
x = 3 * x + 1;
}
}
else if
가 지원되지만 match
를 쓰는 것이 권장됩니다.
fn main() {
let mut x = 10;
x = if x % 2 == 0 {
x / 2
} else {
3 * x + 1
};
}
블록이 리턴값이 있듯이, if 역시 리턴값을 가집니다.
리턴 타입이 동일하지 않으면 컴파일러 오류가 발생합니다.
for
fn main() {
let v = vec![10, 20, 30];
for x in v {
println!("x: {x}");
}
for i in (0..10).step_by(2) {
println!("i: {i}");
}
}
다른 언어와 마찬가지로 break 와 continue를 사용할 수 있습니다.
while
fn main() {
let mut x = 10;
while x != 1 {
x = if x % 2 == 0 {
x / 2
} else {
3 * x + 1
};
}
println!("Final x: {x}");
}
break, continue
fn main() {
let v = vec![10, 20, 30];
let mut iter = v.into_iter();
'outer: while let Some(x) = iter.next() {
println!("x: {x}");
let mut i = 0;
while i < x {
println!("x: {x}, i: {i}");
i += 1;
if i == 3 {
break 'outer;
}
}
}
}
중첩 루프에서 라벨을 이용해 빠져 나올 수 있습니다.
rust 는 goto
를 지원하지 않습니다.
loop
fn main() {
let mut x = 10;
loop {
x = if x % 2 == 0 {
x / 2
} else {
3 * x + 1
};
if x == 1 {
break;
}
}
println!("Final x: {x}");
}
특수한 흐름 제어
if let
fn main() {
let arg = std::env::args().next();
if let Some(value) = arg {
println!("Program name: {value}");
} else {
println!("Missing name?");
}
}
while let
fn main() {
let v = vec![10, 20, 30];
let mut iter = v.into_iter();
while let Some(x) = iter.next() {
println!("x: {x}");
}
}
match
fn main() {
match std::env::args().next().as_deref() {
Some("cat") => println!("Will do cat things"),
Some("ls") => println!("Will ls some files"),
Some("mv") => println!("Let's move some files"),
Some("rm") => println!("Uh, dangerous!"),
None => println!("Hmm, no program name?"),
_ => println!("Unknown program name!"),
}
}
역시 블록이므로 리턴값을 가질 수 있지만 모든 리턴값의 타입은 동일해야 합니다.
패턴 매칭
fn main() {
let input = 'x';
match input {
'q' => println!("Quitting"),
'a' | 's' | 'w' | 'd' => println!("Moving around"),
'0'..='9' => println!("Number input"),
_ => println!("Something else"),
}
}
_
는 위의 매칭이 모두 실패했을 때 매칭됩니다.
열거형(enum) 매칭
enum Result {
Ok(i32),
Err(String),
}
fn divide_in_two(n: i32) -> Result {
if n % 2 == 0 {
Result::Ok(n / 2)
} else {
Result::Err(format!("cannot divide {n} into two equal parts"))
}
}
fn main() {
let n = 100;
match divide_in_two(n) {
Result::Ok(half) => println!("{n} divided in two is {half}"),
Result::Err(msg) => println!("sorry, an error happened: {msg}"),
}
}
구조체(struct) 매칭
struct Foo {
x: (u32, u32),
y: u32,
}
#[rustfmt::skip]
fn main() {
let foo = Foo { x: (1, 2), y: 3 };
match foo {
Foo { x: (1, b), y } => println!("x = 1, b = {b}, y = {y}"),
Foo { y: 2, x: i } => println!("y = 2, x = {i:?}"),
Foo { y, .. } => println!("y = {y}, other fields were ignored"),
}
}
배열(array) 매칭
#[rustfmt::skip]
fn main() {
let triple = [0, -2, 3];
println!("Tell me about {triple:?}");
match triple {
[0, y, z] => println!("First is 0, y = {y}, and z = {z}"),
[1, ..] => println!("First is 1 and the rest were ignored"),
_ => println!("All elements were ignored"),
}
}
[.., z]
같은 형식으로도 매칭할 수 있습니다.
조건문 매칭
#[rustfmt::skip]
fn main() {
let pair = (2, -2);
println!("Tell me about {pair:?}");
match pair {
(x, y) if x == y => println!("These are twins"),
(x, y) if x + y == 0 => println!("Antimatter, kaboom!"),
(x, _) if x % 2 == 1 => println!("The first one is odd"),
_ => println!("No correlation..."),
}
}
if
조건문을 이용해 매칭할 수 있습니다.
메모리 관리
소유권
struct Point(i32, i32);
fn main() {
{
let p = Point(3, 4);
println!("x: {}", p.0);
}
println!("y: {}", p.1);
}
모든 변수는 범위를 가지고, 범위를 벗어나면 변수는 사라집니다.
범위 안에서 변수는 값을 소유(Ownership) 한다고 표현니다.
소유권 이동 : 변수 할당
fn main() {
let s1: String = String::from("Hello!");
println!("s1: {s1}");
let s2: String = s1;
// println!("s1: {s1}"); // compiler error
println!("s2: {s2}");
}
값을 다른 변수에 할당하면 소유권을 상실합니다.
소유권은 단 하나의 변수만 가집니다.
소유권을 상실하면 값에 대한 모든 접근이 차단됩니다.
fn main() {
let s1: String = String::from("Hello!");
println!("s1: {s1}");
let s2: String = s1.clone();
println!("s1: {s1}"); // ok
println!("s2: {s2}");
}
복사할때에는 명시적으로 clone을 사용합니다.
fn main() {
let i1 = 10;
println!("i: {i1}");
let i2 = i1;
println!("i1: {i1}"); // ok
println!("i2: {i2}");
}
정수같은 간단한 값은 복사가 됩니다.
소유권 이동 : 함수 파라미터 할당
fn say_hello(name: String) {
println!("Hello {name}")
}
fn main() {
let name = String::from("Alice");
say_hello(name);
// say_hello(name); // compiler error
}
함수 파라미터로 전달하는 것만으로도 역시 소유권이 이동합니다.
fn say_hello(name: &String) {
println!("Hello {name}")
}
fn main() {
let name = String::from("Alice");
say_hello(&name);
say_hello(&name); // ok
}
참조로 전달하면 소유권 이동은 발생하지 않습니다.
복사(copy)와 복제(clone)
fn main() {
let x = "string";
let y = x;
println!("x: {x}");
println!("y: {y}");
}
객체가 아니라 문자열 참조가 할당되므로 소유권 이동이 없습니다.
정수와 같이 간단한 값으로 취급되기 때문입니다.
#[derive(Copy, Clone, Debug)]
struct Point(i32, i32);
fn main() {
let p1 = Point(3, 4);
let p2 = p1;
println!("p1: {p1:?}");
println!("p2: {p2:?}");
}
Copy
트레잇을 구현하여 복사를 할 수 있습니다:
명시적으로 p1.clone()
을 사용하여 데이터를 복사할 수 있습니다.
복사와 복제의 차이
복사(copy) 는 아무런 커스터마이징 없이 원본 값을 그대로 복사합니다.
복제(clone) 는 커스터마이징이 가능합니다.
String 은 값 자체가 아니라 문자열에 대한 주소값만 가지고 있기에 copy 를 구현하지 않습니다.
따라서 구조체의 맴버 중 하나가 String 을 포함하면 그 구조체도 copy 를 구현할 수 없습니다.
참조 제한
공유가능한 불변 과 유일한 가변 으로 표현합니다.
한번에 하나 이상의 &T (수정 불가한 참조) 값을 가지거나, 또는
정확히 하나의 &mut T (수정 가능한 참조) 값만을 가질 수 있습니다.
공유가능한 불변
하나의 변수에 대해 다수의 불변 변수는 가능합니다.
fn main() {
let a: i32 = 10;
let b: &i32 = &a;
let c: &i32 = &a;
println!("a: {a}");
println!("b: {b}");
println!("c: {c}");
}
유일한 가변
가변 참조는 단 하나만 가능합니다.
fn main() {
let a: &mut i32 = &mut 10;
let b: &mut i32 = a;
let c: &mut i32 = a; // error[E0499]: cannot borrow `*a` as mutable more than once at a time
*b = 20;
*c = 30;
// println!("a: {a}");
println!("b: {b}");
println!("c: {c}");
}
구조체(struct)
구조체 일반
struct Person {
name: String,
age: u8,
}
fn main() {
let mut peter = Person {
name: String::from("Peter"),
age: 27,
};
println!("{} is {} years old", peter.name, peter.age);
peter.age = 28;
println!("{} is {} years old", peter.name, peter.age);
let jackie = Person {
name: String::from("Jackie"),
..peter
};
println!("{} is {} years old", jackie.name, jackie.age);
}
..peter
문법은 한 구조체에서 다른 구조체로 대부분의 값을 복사하려고 하는 경우에 하나하나 타이핑하는 수고를 덜어줍니다.
반드시 맨 마지막에 와야 합니다.
튜플 구조체
struct Point(i32, i32);
fn main() {
let p = Point(17, 23);
println!("({}, {})", p.0, p.1);
}
필드명 없이 구조체를 생성할 수도 있습니다.
구조체 메소드
#[derive(Debug)]
struct Person {
name: String,
age: u8,
}
impl Person {
fn new(name: String, age: u8) -> Person {
Person { name, age }
}
fn say_hello(&self) {
println!("Hello, my name is {}", self.name);
}
}
fn main() {
let peter = Person::new(String::from("Peter"), 27);
println!("{peter:?}");
peter.say_hello();
}
impl
키워드를 이용해 메소드를 생성할 수 있습니다.
impl Person {
fn new1(name: String, age: u8) -> Self {
Self { name, age }
}
}
Person
은 Self
로 변경할 수 있습니다.
메소드의 첫번째 파라미터가 &self
인것을 확인하세요.
메서드 리시버(Receiver)
파라미터에 self 를 할당하는 것을 리시버라고 합니다.
fn say_hello(&self) { ... }
fn say_hello(&mut self) { ... }
fn say_hello(self) { ... }
fn say_hello(mut self) { ... }
&self
: 불변 참조로 받습니다.
&mut self
: 값을 수정할 수 있습니다.
self
: 객체의 소유권을 가져옵니다. 다시 객체에 소유권을 반환하지 않으면 객체는 객체는 drop(해제)됩니다.
mut self
: 객체의 소유권과 수정할 수 있는 권한까지 받습니다.
모듈
mod foo {
pub fn do_something() {
println!("In the foo module");
}
}
mod bar {
pub fn do_something() {
println!("In the bar module");
}
}
fn main() {
foo::do_something();
bar::do_something();
}
가시성
mod outer {
fn private() {
println!("outer::private");
}
pub fn public() {
println!("outer::public");
inner::public();
}
mod inner {
fn private() {
println!("outer::inner::private");
}
pub fn public() {
println!("outer::inner::public");
super::private();
private();
}
}
}
fn main() {
outer::public();
}
pub
를 제거하면 외부에서 접근이 불가능합니다.
pub
키워드는 모듈에도 사용할 수 있습니다.
파일 분리
파일을 분리한 경우 파일명이 모듈명이 됩니다.
파일명은 기본 라이브러리의 네임스페이스와 중복이 되지 않도록 해야 합니다.
mylib.rs
fn private() {
println!("outer::private");
}
pub fn public() {
println!("outer::public");
inner::public();
}
mod inner {
fn private() {
println!("outer::inner::private");
}
pub fn public() {
println!("outer::inner::public");
super::private();
private();
}
}
main.rs
mod mylib;
fn main() {
mylib::public();
}
제너릭(generic)
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
println!("{integer:?} and {float:?}");
}
제너릭 메서드
#[derive(Debug)]
struct Point<T>(T, T);
impl<T> Point<T> {
fn x(&self) -> &T {
&self.0 // + 10
}
// fn set_x(&mut self, x: T)
}
fn main() {
let p = Point(5, 10);
println!("p.x = {}", p.x());
}
단형화
제너릭은 실제 호출이 있는 시점에 실제로 생성됩니다.
미리 만들어 놓은 타입들을 사용하는 것이 아닙니다.
트레잇(Trait)
```
```rust
https://google.github.io/comprehensive-rust/ko/modules.html