해당 게시판에 올라오는 포스트는 The Rust Programming Language 의 한글 텍스트 버전을 보며 작성된 학습 게시판입니다.
전체 목차
[코딩/Rust] - [Rust] 1장 안정성 높은 언어
[코딩/Rust] - [Rust] 2장 일반적인 프로그래밍 개념
[코딩/Rust] - [Rust] 3장 러스트만의 특이함
[코딩/Rust] - [Rust] 4장 열거형(Enum), 모듈화(Module)
[코딩/Rust] - [Rust] 5장 컬렉션(Collection) - 벡터(Vector), 문자열(String), 해시맵(HashMap)
글 목차
1. 소유권(Ownership)
소유권은 러스트에서 가장 독특한 기능이며 언어 전반에 깊은 영향을 끼친다.
러스트가 가비지 컬렉터(GC) 없이 메모리 안정성을 보장하도록 해준다.
규칙(Rule)
규칙은 다음과 같다. 명심하자.
- 러스트에서 각각의 값은 소유자가 정해져 있다.
- 한 값의 소유자는 동시에 여럿 존재할 수 없다.
- 소유자가 스코프 밖으로 벗어날 때 값을 버려진다.
변수의 스코프(Scope of Variable)
- s 가 스코프 내에 나타나면 유효함.
- 유효기간은 스코프 밖으로 벗어나기 전까지.
//파일명:src/main.rs
fn main() {
// s는 아직 선언되지 않아서 유효하지 않다.
{
let s = "오마갓 나는 곧 사라져!"; // 여기서 부터 s가 유효.
// s 로 어떤 작업을 진행.
println!("s 값이 소리친다. : {}", s);
} // 해당 스코프 종료, s는 더 이상 유효하지 않다.
// s 사용 불가
} // 함수의 끝도 스코프가 종료된다고 생각하면 된다.
중괄호가 나타나는 모든 곳이 스코프라고 생각하면 편하다.
문자열 타입(String Type)
여태 보아온 문자열들은 코드 내에서 하드코딩하는 방식의 문자열 리터럴 이다.
문자열 리터럴은 불변성을 지니기에 변경할 수 없다는 점과, 프로그램에 필요한 모든 문자열을 우리가 프로그래밍하는 시점에 알 수 없다는 점 때문에 편리하기는 하지만 만능은 아니다.
String 타입은 힙에 할당된 데이터를 다루기 때문에, 컴파일 타임에 크기를 알 수 없는 텍스트도 저장 가능 하다.
//파일명:src/main.rs
fn main() {
let mut s = String::from("안녕");
s.push_str(", 세상아!"); // push_str()이 문자열에 리터럴을 추가합니다
println!("{}", s); // 이 줄이 `안녕, 세상아!`를 출력합니다
}
메모리와 할당(Memory and Assign)
String 타입은 힙에 메모리를 할당하는 방식을 사용하기 때문에 텍스트 내용 및 크기를 변경할 수 있다. 하지만 이는 다음을 의미하기도 한다.
- 실행 중 메모리 할당자로부터 메모리를 요청해야 한다.
- String 사용을 마쳐쓸 때 메모리를 해제할 방법이 필요하다.
러스트에서는 이 문제를 변수가 자신이 소속된 스코프를 벗어나는 순간 자동으로 메모리를 해제하는 방식으로 해결했다.
복사
원시 타입의 경우 스택에만 저장되는 데이터 이며, 크기가 정해진 단순한 값이므로 스택에 할당되며 복사된다.
//파일명:src/main.rs
fn main() {
let x = 5; // 5를 x 에 바인딩
let y = x; // x값의 복사본 생성, y 에 바인딩
// 두 값은 스택에 푸시 된다.
}
이동
하지만 힙에 저장되는 객체의 경우 복사되지 않고, 데이터가 이동된다.
//파일명:src/main.rs
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 에 있는 데이터를 s2 로 이동, s1 은 무효화
// 앞으로 "hello" 은 s2 의 것이다!
println!("{}, 어랏 오류 발생이다!", s1); // s1 은 무효화 되서 사용할 수 없다.
} // s2 가 스코프를 벗어남으로서 s2 해제
딱 하단의 사진을 보면 이해하기 쉽다. 검정색은 무효화되서 아예 없어졌다고 생각하면 된다.
클론
String의 힙 데이터까지 깊이 복사하고 싶을 땐 clone 이라는 공용 메서드를 사용할 수 있다.
//파일명:src/main.rs
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 힙 데이터 복사 임으로 성능에 영향이 있을 수 있다
println!("s1 = {}, s2 = {}", s1, s2);
} // s1, s2 가 스코프를 벗어남으로서 s1, s2 해제
소유권과 함수(Ownership and Function)
매개변수도 원시 타입이 아닌 이상 모든 객체는 이동된다.
//파일명:src/main.rs
fn main() {
let s = String::from("hello"); // s가 스코프 안으로 들어옵니다
takes_ownership(s); // s의 값이 함수로 이동됩니다...
// ... 따라서 여기서는 더 이상 유효하지 않습니다
let x = 5; // x가 스코프 안으로 들어옵니다
makes_copy(x); // x가 함수로 이동될 것입니다만,
// i32는 Copy이므로 앞으로 계속 x를
// 사용해도 좋습니다
} // 여기서 x가 스코프 밖으로 벗어나고 s도 그렇게 됩니다. 그러나 s의 값이 이동되었으므로
// 별다른 일이 발생하지 않습니다.
fn takes_ownership(some_string: String) { // some_string이 스코프 안으로 들어옵니다
println!("{}", some_string);
} // 여기서 some_string이 스코프 밖으로 벗어나고 `drop`이 호출됩니다.
// 메모리가 해제됩니다.
fn makes_copy(some_integer: i32) { // some_integer가 스코프 안으로 들어옵니다
println!("{}", some_integer);
} // 여기서 some_integer가 스코프 밖으로 벗어납니다. 별다른 일이 발생하지 않습니다.
반환 값과 스코프(Returns and Scope)
스코프내에 있는 변수를 스코프 밖으로 이동시킨다면 스코프 밖으로 벗어나더라도, 소유권 이전이 되었으므로 변수가 제거되지 않는다.
//파일명:src/main.rs
fn main() {
let s1 = gives_ownership(); // gives_ownership이 자신의 반환 값을 s1로
// 이동시킵니다
let s2 = String::from("hello"); // s2가 스코프 안으로 들어옵니다
let s3 = takes_and_gives_back(s2); // s2는 takes_and_gives_back로 이동되는데,
// 이 함수 또한 자신의 반환 값을 s3로
// 이동시킵니다
} // 여기서 s3가 스코프 밖으로 벗어나면서 버려집니다. s2는 이동되어서 아무 일도
// 일어나지 않습니다. s1은 스코프 밖으로 벗어나고 버려집니다.
fn gives_ownership() -> String { // gives_ownership은 자신의 반환 값을
// 자신의 호출자 함수로 이동시킬
// 것입니다
let some_string = String::from("yours"); // some_string이 스코프 안으로 들어옵니다
some_string // some_string이 반환되고
// 호출자 함수 쪽으로
// 이동합니다
}
// 이 함수는 String을 취하고 같은 것을 반환합니다
fn takes_and_gives_back(a_string: String) -> String { // a_string이 스코프 안으로
// 들어옵니다
a_string // a_string이 반환되고 호출자 함수 쪽으로 이동합니다
}
함수로 이동된 값을 다시 사용하고 싶으면 이동된 값을 반환하면 된다.
//파일명:src/main.rs
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len()은 String의 길이를 반환합니다
(s, length) // 이동된 s 를 다시 스코프 밖으로 이동 시킨다.
}
2. 참조와 대여(References ad Borrowing)
참조자, 별명, call of reference. 그거 맞다.
//파일명:src/main.rs
fn main() {
let s1 = String::from("안녕!");
let len = calculate_length(&s1); // 참조라서 이동하지 않는다!
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize { // s는 String의 참조자입니다
s.len()
} // 여기서 s가 스코프 밖으로 벗어납니다. 하지만 참조하는 것을 소유하고 있진 않으므로,
// 버려지지는 않습니다.
빌린 값 수정(Modify a borrowed value)
불변이기 때문에 당연히 불가능하다. 하단의 코드를 실행시켜 보자.
//파일명:src/main.rs
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
가변 참조자(Mutable References)
물론 가변 설정을 하면 수정 가능하다. 하단의 코드는 에러가 발생하지 않는 다.
//파일명:src/main.rs
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
가변 참조자의 큰 제약사항이 있다. 두 개의 가변 참조자 생성이 불가능하고, 불변 참조자가 있는 동안에는 가변 참조자를 만드는 것도 불가능하다. 이는 데이터 경합(data race)을 방지할 수 있다.
데이터 경합이란 다음 세 가지 상황이 겹칠 때 일어나는 특정한 경합 조건(specific race condition) 이다.
- 둘 이상의 포인터가 동시에 같은 데이터에 접근
- 포인터 중 하나 이상이 데이터에 쓰기 작업을 시행
- 데이터 접근 동기화 메커니즘이 없음
그래서 스코프 범위를 확인하면서 불변 참조자와 가변 참조자를 사용해야한다.
제약 때문에 짜증날 수 있지만, 이는 버그를 런타임이 아닌 컴파일 타임에서 빠르게 찾아내어 어느 부분이 문제인지 정확히 집어주는 기능이니. 그냥 익숙해지도록 하자.
댕글링 참조(Dangling References)
댕글링 포인터(dangling pointer)란, 어떤 메모리를 가리키는 포인터가 남아있는 상황에서 메모리를 해제해 버림으로써, 해제된 메모리를 참조하게 된 포인터를 말한다.
//파일명:src/main.rs
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle은 String의 참조자를 반환합니다
let s = String::from("hello"); // s는 새로운 String입니다
&s // String s의 참조자를 반환합니다
} // 여기서 s는 스코프 밖으로 벗어나고 버려집니다. 해당 메모리는 해제됩니다.
// s 메모리는 해제 되었는 데, 함수는 s의 참조(포인터)를 반환했다!!
해당 코드를 실행하려 한다면 다음과 같은 오류를 내뱉는다.
이 함수는 빌린 값을 반환하고 있으나, 빌린 실제 값이 존재하지 않습니다.
3. 슬라이스(Slice Type)
슬라이스(Slice)는 컬렉션(collection)을 통째로 참조하는 것이 아닌, 연속된 일련의 요소를 참조한다.
참조자의 일종으로 소유권을 갖지 않는다.
문자열 슬라이스(String Slices)
문자열 슬라이스(string slices)는 String 의 일부를 가리키는 참조자를 말한다.
//파일명:src/main.rs
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // 인덱스 0이상 5미만
let world = &s[6..11]; // 인덱스 6이상 11미만
}
//파일명:src/main.rs
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' { // 공백을 찾아서, 공백 이전 까지 슬라이스!
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("안녕 세상아!");
let word = first_word(&s);
s.clear(); // 에러!
println!("the first word is: {}", word);
}
word 라는 불변 참조자가 생겨난 이후로는, 가변 참조자와 불변 참조자가 같은 시점에 존재하므로 컴파일 에러가 발생한다.
배열 슬라이스(Array Slices)
범용적으로 슬라이스는 연속적인 데이터에 모두 사용 가능하다. 즉, 문자열 또한 배열임으로 하단의 코드와 같이 작성할 수 있다.
//파일명:src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
}
4. 구조체(Struct)
구조체는 여러 값을 묶고 이름을 지어서 의미 있는 묶음을 정의하는 데 사용한다.
정의와 인스턴스화(Defining and Instantiating)
튜플과 유사하나 접근 순서에 의존할 필요가 없어서 유연하게 사용할 수 있다.
//파일명:src/main.rs
struct Position {
x: isize; // x field
y: isize; // y field
}
fn main(){
let pos = Position {
x: 0,
y: 0,
};
println!("User Position : ({}, {})", pos.x, pos.y);
}
당연하게도 함수의 매개변수 혹은 반환값으로도 사용할 수 있다.
//파일명:src/main.rs
fn spawn_user(x:isize, y:isize) -> Position {
Position {
x: x,
y: y,
}
}
필드 초기화 축약법 사용(Using the Field Init Shorthand)
//파일명:src/main.rs
fn spawn_user(x:isize, y:isize) -> Position {
Position {
x, // 변수명과 구조체 필드명이 같을 때!
y, // 필드 초기화 축약법(field init shorthand)을 사용!
}
}
구조체 업데이트 문법(Struct Update Syntax)
다른 인스턴스에서 값을 유지한 채로 몇 개의 값만 바꿔서 새로운 인스턴스를 생성하게 되는 경우가 간혹 있다.
//파일명:src/main.rs
struct Position {
x: isize; // x field
y: isize; // y field
}
fn main(){
let pos = Position {
x: 0,
y: 0,
};
println!("User Position : ({}, {})", pos.x, pos.y);
let pos2 = Position {
x: pos.x,
y: 0,
};
}
그럴 땐 다음과 같이 수정할 수 있다.
//파일명:src/main.rs
// ...생략...
fn main(){
// ...생략...
let pos2 = Position {
y: 0,
..pos
};
}
튜플 구조체(Tuple Struct)
각 필드명을 일일이 정해 일반적인 구조체 형태로 만들면 너무 장황하거나 불필요할 경우 유용하다.
//파일명:src/main.rs
struct Color(i8, i8, i8, i8);
struct Vec3(isize, isize, isize);
fn main(){
let black = Color(0, 0, 0, 0);
let velocity = Vec3(1, 0, 0);
}
이름이 없기에 개별 값에 대한 접근은 black.0, black.1 과 같이 인덱스를 통해 접근한다.
유사 유닛 구조체(Unit-Like Struct)
어떤 타입에 대해 트레이트(Trait)를 구현하고 싶지만 타입 내부에 어떤 데이터를 저장할 필요 없는 경우 유용하다.
트레이트 파생 기능 추가(Adding Functionality with Derived Traits)
//파일명:src/main.rs
#[derive(Debug)]
struct Position {
x: isize; // x field
y: isize; // y field
}
fn main(){
let pos = Position {
x: 0,
y: 0,
};
println!("User : {:?}", pos);
}
Position 인스턴스를 디버그 출력 형식으로 사용하기 위해, 속성을 추가하여 Debug 트레이트 파생(derive) 했다.
메서드 문법(Method Syntax)
c++ class 에서 변수 따로 함수 따로 구현하는 것으로 생각하면 편하다.
//파일명:src/main.rs
#[derive(Debug)]
struct Position {
x: isize; // x field
y: isize; // y field
}
impl Position {
fn area(&self) -> isize {
self.x * self.y
}
}
fn main(){
let pos = Position {
x: 10,
y: 20,
};
println!("User의 영역 : {}", pos.area());
}
만약 self 내의 x 혹은 y 값을 변경하고 싶다면 &mut self 를 사용하면 된다.
마치 Python 에서의 class 안의 함수 구현할 때 self 의 용도와 유사하다.
이제 예제를 하나 작성해보자
5. 예제
게임 플레이어에 대한 구조체를 만들 것이다. 플레이어는 다음과 같은 데이터를 가지고 있다.
- 변하지 않는 최대 HP양
- 현재 HP양
- 플레이어의 위치
플레이어 구조체를 생성할 때에는 new() 라는 함수를 만들어서 호출 하도록 한다. 함수의 매개변수에 &self 라는 인자가 없다면 해당 함수는 정적 메소드로 동작한다.
그리고 반복문을 이용해서 사용자 입력을 받아 움직일 수 있도록 한다!
//파일명:src/main.rs
use std::io;
#[derive(Debug)]
struct Position {
// 구현 하기
}
#[derive(Debug)]
struct Player {
// 구현 하기
}
impl Player{
fn new(max_hp:isize, position:Position) -> Player{
// 구현 하기
}
}
fn main(){
let mut player = Player::new(10, Position { x:0, y:0 });
println!("플레이어 : {:?}", player);
loop {
let mut string = String::new();
io::stdin().read_line(&mut string).unwrap();
if let Some(char) = string.chars().next() {
if char == 'w' {
// 구현 하기
} else if char == 'a' {
// 구현 하기
} else if char == 's' {
// 구현 하기
} else if char == 'd' {
// 구현 하기
}
}
println!("플레이어 : {:?}", player);
}
}
다음과 유사하게 구현하면 정답이다.
//파일명:src/main.rs
use std::io;
#[derive(Debug)]
struct Position {
x: isize, // x field
y: isize, // y field
}
#[derive(Debug)]
struct Player {
max_hp : isize,
current_hp : isize,
position: Position,
}
impl Player{
fn new(max_hp:isize, position:Position) -> Player{
Player {
max_hp,
current_hp : max_hp,
position,
}
}
}
fn main(){
let mut player = Player::new(10, Position { x:0, y:0 });
println!("플레이어 : {:?}", player);
loop {
let mut string = String::new();
io::stdin().read_line(&mut string).unwrap();
if let Some(char) = string.chars().next() {
if char == 'w' {
player.position.y = player.position.y + 1;
} else if char == 'a' {
player.position.x = player.position.x - 1;
} else if char == 's' {
player.position.y = player.position.y - 1;
} else if char == 'd' {
player.position.x = player.position.x + 1;
}
}
println!("플레이어 : {:?}", player);
}
}
'코딩 > Rust' 카테고리의 다른 글
[Rust] 5장 컬렉션(Collection) - 벡터(Vector), 문자열(String), 해시맵(HashMap) (0) | 2024.03.16 |
---|---|
[Rust] 4장 열거형(Enum), 모듈화(Module) (1) | 2024.03.15 |
[Rust] 2장 일반적인 프로그래밍 개념 (1) | 2024.03.06 |
[Rust] 1장 안정성 높은 언어 (2) | 2024.03.05 |