Rust의 Ownership
Rust의 소유권 개념입니다.
https://doc.rust-lang.org/rust-by-example/scope/move.html?highlight=ownership#ownership-and-moves
Ownership and moves - Rust By Example
Because variables are in charge of freeing their own resources, resources can only have one owner. This prevents resources from being freed more than once. Note that not all variables own resources (e.g. references). When doing assignments (let x = y) or p
doc.rust-lang.org
Because variables are in charge of freeing their own resources, resources can only have one owner.
Rust에선 Ownership, 즉 이 포스트에선 소유권이라 칭할 개념을 위와 같이 설명합니다.
각 변수들, 식별자들은 "하나"의 owner, 즉 소유권을 지닌 객체만 존재한다고요.
Ownership?
C++, python 등의 프로그래밍 언어에선 보지 못했던 개념입니다.
저도 처음 봤고요. 신기했습니다.
목적은 아래와 같습니다.
This prevents resources from being freed more than once.
Note that not all variables own resources (e.g. references).
즉, 변수의 double free 같은 메모리 버그를 방지하기 위한
여러 Rust의 강력한 정책들 중 하나입니다.
자. 그래요. 실제 코드엔 어떻게 적용될까요?
먼저 Ownership의 개념을 설명하기 위해 예시를 제공하겠습니다.
이를 위해 rust의 오버로딩 개념을 설명하겠습니다.
C++의 연산자 오버로딩, Rust의 Trait 구현
먼저, Matrix라는 user-define datatype을 정의해 보겠습니다.
struct Matrix(f32, f32, f32, f32);
single precision의 소수형 4개를 담는 튜플입니다.
main함수에선 실제 값을 바탕으로 해당 Matrix의 값들을 출력하려 합니다.
이를 위해 formatting을 이용해야 합니다.
이를 위해, Matrix 만의 formatting을 위한 C++로 치면
출력 연산자 (<<) 오버로딩이 필요합니다.
예를 들어 C++에서 아래와 같은 Matrix를 구현했다 칩시다.
typedef struct Matrix{
float f[4];
} Matrix;
배열이라 조금 다르긴 한데, 튜플 개념이 없으니 넘어갑시다.
쨋든 이렇게 user-defined 데이터 타입은, 기존의
정수형이 표준 출력, 입력 객체와 상호작용하는 연산자와
연산을 할 수 없습니다.
int main(){
Matrix p = {1.1, 1.2, 1.3, 1.4};
cout << p << '\n';
return 0;
}
이런 식의 출력이 에러를 낼 것이다 이 말입니다.
그래서, user-define 함수가 표준 출력 객체인 cout과
상호작용하기 위해선 함수 오버로딩이 필요합니다.
#include <iostream>
using namespace std;
typedef struct Matrix{
float f[4];
} Matrix;
ostream& operator<<(ostream& os, const Matrix& m){
os << m.f[0] << ' ' << m.f[1] << ' ' << m.f[2] << ' ' << m.f[3] << '\n';
return os;
}
int main(){
Matrix p = {1.1, 1.2, 1.3, 1.4};
cout << p << '\n';
return 0;
}
Rust도 마찬가집니다.
user-define datatype의 formatting 양식을 구현해야 합니다.
간단하게 구현하는 방법은 기존 post의 debug trait를 설정하는 겁니다.
[Rust] formatting
std::fmt 모듈 내 매크로 print!() 및 println!() io:stdout, 즉 표준 출력에 입력된 string을 전달하는 매크로입니다. 둘의 차이는 마지막에 개행 문자 없이도 자동으로 개행 문자를 넣어주냐 마냐입니다.
swc0317.tistory.com
#[derive(Debug)]
struct Matrix(f32, f32, f32, f32);
이렇게 Matrix 구조체를 정의하는 구문 위에
Debug trait를 설정해 주면
별도의 format 오버로딩 구현 없이도 간단하게 구조체 내부의 값을 뜯어볼 수 있습니다.
use std::fmt;
#[derive(Debug)]
struct Matrix(f32, f32, f32, f32);
fn main() {
let matrix = Matrix(1.1, 1.2, 2.1, 2.2);
println!("{:?}", matrix);
}
문제는 이런 출력 방식이 고정되기 때문에,
원하는 출력 양식이 있다면,
Display trait을 구현해주어야 합니다.
이를 위해
use std::fmt;
struct Matrix(f32, f32, f32, f32);
impl fmt::Display for Matrix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "( {} {} )\n( {} {} )", self.0, self.1, self.2, self.3)
}
}
use std::fmt를 통해 모듈을 불러오고,
위처럼 구현한다면,
위처럼 원하는 출력 양식으로 바꿀 수 있습니다.
use std::fmt;
struct Matrix(f32, f32, f32, f32);
impl fmt::Display for Matrix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "( {} {} )\n( {} {} )", self.0, self.1, self.2, self.3)
}
}
fn main() {
let matrix = Matrix(1.1, 1.2, 2.1, 2.2);
println!("{}", matrix);
}
이제 그래서 Ownership이 뭔데?
자 먼 길 돌아왔습니다. 그래서 Ownership이 뭔데?
transpose라는 Matrix에 Transpose 한 값을 return 하는 함수를
정의해 봅시다.
fn transpose(matrix: Matrix) -> Matrix {
let temp: Matrix = Matrix(matrix.0, matrix.2, matrix.1, matrix.3);
return temp;
}
이렇게 정의할 수 있을 것이고요.
자 그러면 call 해봅시다.
엥?
borrow of moved value랍니다.
use std::fmt;
struct Matrix(f32, f32, f32, f32);
impl fmt::Display for Matrix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "( {} {} )\n( {} {} )", self.0, self.1, self.2, self.3)
}
}
fn transpose(matrix: Matrix) -> Matrix {
let temp: Matrix = Matrix(matrix.0, matrix.2, matrix.1, matrix.3);
return temp;
}
fn main() {
let matrix = Matrix(1.1, 1.2, 2.1, 2.2);
let transposed: Matrix = transpose(matrix);
println!("{}", matrix);
println!("{}", transposed);
}
Rust의 Ownership은 이런 개념입니다.
어떤 정의된 식별자가, 다른 식별자를 정의하는 데 쓰이거나,
다른 함수의 인자로 전달되면, 해당 식별자를 사용할
권한이 사라집니다.
즉 위와 같은 code에서,
let transposed: Matrix = transpose(matrix);
이 구문을 실행하는 순간, matrix 식별자에 접근할 수 없는 겁니다.
첫 번째 해결법. Clone trait
그러면 해당 식별자에 어떻게 접근할 수 있느냐?
Clone 메서드를 통해 주어진 변수를 clone 해야 합니다.
예를 들어
let mut vec1 = vec![1, 2, 3, 4];
let doubled_vec1 = double_elements!(vec1.clone());
double_elements!(vec1.clone()); // 결과: vec![2, 4, 6, 8]
for i in 0..vec1.clone().len(){
print!("{} ", vec1[i]);
}
벡터 자료구조로 정의된 식별자 "vec1"에 요소들을
또 다른 벡터를 정의하는 데 쓰고, 또
vec1의 요소들을 접근하기 위해 clone 메서드를 통해 식별자의
소유권을 넘기지 않을 수 있습니다.
user-define datatype의 clone trait
문제는 user-define datatype의 clone을 call 할 때 발생합니다.
vector, tuple, list 같은 이미 정의된
데이터형들은 이미 구현된 clone 메서드가 존재하므로
별도의 구문 없이 사용할 수 있습니다.
그런데, user-defined datatype은 그딴 거 없고요.
그래서 아까 debug trait을 간단하게 불러와
출력하던 것 있잖아요? 이 방식으로 자동으로 간단하게
trait을 불러올 수 있습니다.
마찬가지로 구조체 정의하는 구문 위에
#[derive(Clone)]을 추가해 주면 충분합니다.
만약 Clone trait과 Debug trait을 모두 쓰고 싶다면
#[derive(Clone, Debug)]입니다.
use std::fmt;
#[derive(Clone)]
struct Matrix(f32, f32, f32, f32);
impl fmt::Display for Matrix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "( {} {} )\n( {} {} )", self.0, self.1, self.2, self.3)
}
}
fn transpose(matrix: Matrix) -> Matrix {
let temp: Matrix = Matrix(matrix.0, matrix.2, matrix.1, matrix.3);
return temp;
}
fn main() {
let matrix = Matrix(1.1, 1.2, 2.1, 2.2);
let transposed: Matrix = transpose(matrix.clone());
println!("{}", matrix);
println!("{}", transposed);
}
자 Clone trait을 이용해,
소유권을 넘기지 않고 값을 전달하는 방법을 살펴보았습니다.
이 방법의 문제는, 배열 같은 큰 구조체들의 Clone을
전달하면 말 그대로 값을 Clone, 복사하여 전달하기에
시간 복잡도가 N입니다. 공간 복잡도도 그만큼 잡아먹는 거구요.
두 번째 해결법. Reference
이를 해결하기 위해 Reference, 즉 포인터만 전달합시다.
예를 들어보겠습니다.
fn print_value(x: &i32) {
println!("The value given by x is {}", x);
}
fn main() {
let a = 5;
print_value(&a);
println!("The value if a is {}", a);
}
a라는 새로운 식별자를 정의했습니다. 이를
print_value 라는 함수에 전달하면 소유권이 해당
함수로 넘어가게 되고, 이후 a를 사용하지 못하게됩니다.
그러나 a를 전달하는 게 아니라, a의 reference인 &a를
전달하게 되면 값을 전달할 수 있을 뿐만 아니라
소유권도 넘기지 않을 수 있습니다.
Mutable Reference, Immutable Reference
이러한 Reference도 무작정 할 수 있는 건 아닙니다.
Immutable한 식별자는
여러 Reference를 가질 수 있습니다.
fn main()
let x = 5;
let y = &x;
let z = &x;
println!("{}", *z);
println!("{}", *y);
println!("{}", x);
}
Mutable한 Reference의 경우
한 reference만이 식별자를 가리킬 수 있습니다.
fn main() {
let mut mut_a: i32 = 5;
let ref_a1 = &mut mut_a;
let ref_a2 = &mut mut_a;
println!("{}", *ref_a1);
println!("{}", *ref_a2);
}
'프로그래밍 언어 > [Rust]' 카테고리의 다른 글
[Rust] enum (0) | 2024.12.30 |
---|---|
[Rust] 구조체 (0) | 2024.12.30 |
[Rust] 기본 골자 (2) | 2024.12.26 |
[Rust] formatting (1) | 2024.12.26 |
[Rust] Visual Studio Code로 개발 환경 세팅 (3) | 2024.12.25 |