[0x02] Understanding Onwership
1. What is Ownership?
Ownership
은 Rust
프로그램의 메모리 관리 방법에 대한 규칙이다.
- 모든 프로그램들은 실행되고 있을 때, 메모리를 관리해야 한다.
Java
같은 언어들은, garbage collector
를 통해 낭비되는 메모리를 최소화하는 방법을 사용한다.
C
같은 언어들은, 메모리의 할당과 해제를 명시적으로
해주어야 한다.
Rust
는 새로운 접근법으로 메모리를 관리하는데, 바로 컴파일러가 체크하는 Ownership
의 개념이다.
컴파일러가 체크하는 만큼, 프로그램의 실행 시 퍼포먼스를 저해하는 일은 없다.
1.1 Ownership Rules
Ownership
에 관한 규칙들을 우선 살펴보자.
- 모든
값
들은 owner
라 불리는 변수
를 갖는다.
- 하나의
값
에는 하나의 owner
만 있을 수 있다.
owner
가 scope
밖으로 나가게 되면, 값
은 drop
된다.
1.2 Variable Scope
- 변수의
scope
는 다른 언어에서의 개념과 동일하다.
fn main() {
// s 가 아직 선언되지 않음, 아직 유효하지 않음
let s = "Hello"; // s 가 선언됨, 유효함
println!("{}", s); // s 를 이용해 프로그램이 동작함
} // scope가 끝났기 떄문에, s 는 더이상 유효하지 않음
1.3 The String Type
Rust
에서의 String
은 literal
한 스트링과, 자료형으로써의 스트링이 나뉜다.
- 여기서 살펴 볼
String Type
은 힙 영역에 저장되며, 가변적인 크기를 가지는 자료형이다.
1.3.1 Memory Allocation
String
이 가변적인 크기를 갖는다는 것은 아래 두 과정을 수행한다는 것이다.
- 런타임 중, 메모리 할당자에 의해 메모리가 할당(
allocating
)되어야 한다.
- 사용한 메모리를 메모리 할당자에게 반환(
returning
)하는 방법이 있어야 한다.
- 메모리를 할당(
allocate
)하는 것은 String::from
메소드를 호출할 때 일어난다.
- 메소드 내부에서 메모리 할당자에게 메모리를 요청한다.
- 메모리를 반환(
return
)하는 것은 해당 메모리에 대한 owner
가 scope
를 벗어날 경우 자동으로 일어난다.
Rust
는 scope
가 끝나는 curly bracket(}
)에 대해 drop()
함수를 자동으로 호출한다.
- 이
drop
함수가 메모리를 반환하는 것이다.
1.4 Ways Variables and Data Interact
1.4.1 [_] Move
- 여기서는
Rust
의 Ownership
과 관련하여, 메모리 단에서 어떤 일이 일어나는지 살펴본다.
- 아래의 코드를 살펴보자.
{
let s1 = String::from("hello");
let s2 = s1;
}
Rust
에서는 String
과 같은 힙 영역에 할당되는 데이터에 관해 아래 그림과 같이 이해할 수 있다.
Ownership
은 Rust
프로그램의 메모리 관리 방법에 대한 규칙이다.Java
같은 언어들은,garbage collector
를 통해 낭비되는 메모리를 최소화하는 방법을 사용한다.C
같은 언어들은, 메모리의 할당과 해제를명시적으로
해주어야 한다.Rust
는 새로운 접근법으로 메모리를 관리하는데, 바로 컴파일러가 체크하는Ownership
의 개념이다.
컴파일러가 체크하는 만큼, 프로그램의 실행 시 퍼포먼스를 저해하는 일은 없다.
Ownership
에 관한 규칙들을 우선 살펴보자.- 모든
값
들은owner
라 불리는변수
를 갖는다. - 하나의
값
에는 하나의owner
만 있을 수 있다. owner
가scope
밖으로 나가게 되면,값
은drop
된다.
scope
는 다른 언어에서의 개념과 동일하다.fn main() {
// s 가 아직 선언되지 않음, 아직 유효하지 않음
let s = "Hello"; // s 가 선언됨, 유효함
println!("{}", s); // s 를 이용해 프로그램이 동작함
} // scope가 끝났기 떄문에, s 는 더이상 유효하지 않음
Rust
에서의 String
은 literal
한 스트링과, 자료형으로써의 스트링이 나뉜다.String Type
은 힙 영역에 저장되며, 가변적인 크기를 가지는 자료형이다.String
이 가변적인 크기를 갖는다는 것은 아래 두 과정을 수행한다는 것이다.- 런타임 중, 메모리 할당자에 의해 메모리가 할당(
allocating
)되어야 한다. - 사용한 메모리를 메모리 할당자에게 반환(
returning
)하는 방법이 있어야 한다.
allocate
)하는 것은 String::from
메소드를 호출할 때 일어난다.- 메소드 내부에서 메모리 할당자에게 메모리를 요청한다.
return
)하는 것은 해당 메모리에 대한 owner
가 scope
를 벗어날 경우 자동으로 일어난다.Rust
는scope
가 끝나는 curly bracket(}
)에 대해drop()
함수를 자동으로 호출한다.- 이
drop
함수가 메모리를 반환하는 것이다.
Rust
의 Ownership
과 관련하여, 메모리 단에서 어떤 일이 일어나는지 살펴본다.{
let s1 = String::from("hello");
let s2 = s1;
}
Rust
에서는 String
과 같은 힙 영역에 할당되는 데이터에 관해 아래 그림과 같이 이해할 수 있다.
- 이 때,
let s2 = s1;
코드에 관해서는 아래와 같이Shallow Copy
가 일어난다.
- 하지만,
Rust
에서는 위 그림과 같이 두 개의 포인터가 하나의 영역을 가리키지 않는다. - 이 때,
Ownership
의 개념이 들어온다.
- 따라서, 아래 코드는 에러를 일으킨다.
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", world!", s1); // s1의 값은 s2로 `move` 했기 때문에 compile error 를 일으킨다.
}
1.4.2 [_] clone
- 만약
String
에 대하여Deep Copy
를 하고 싶다면,clone
함수를 사용하면 된다.
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}, world!", s1);
}
1.4.3 [_] copy
- 스택에만 저장되는 타입(
integer
등)에 대해서는Copy
이라는trait
이 구현되어 있다. - 따라서, 힙에 저장되는 데이터처럼
clone
함수가 따로 필요 없다.
fn main() {
let x = 5;
let y = x;
}
- 특히, 튜플의 경우 내부 원소들이 모두
Copy
가 구현된 타입이라면Copy
를 사용할 수 있다.
1.5 Ownership and Functions
1.4
에서 살펴본move
,clone
,copy
의 개념을 토대로함수
에서와 연관지어 살펴보자.
1.5.1 Parameters (arguments)
- 아래 코드와 주석을 참고하자.
fn main() {
let s = String::from("hello"); // s 가 선언됨
takes_ownership(s); // s 의 데이터는 함수 안으로 `move` 함.
// 따라서, s 는 더 이상 이 scope 내에서 유효하지 않음
let n = 5; // n 이 선언됨
makes_copy(n); // n 은 i32 이므로, `Copy`가 일어남
// 따라서, n 은 이 scope 내에서 계속 유효함
} // s, n 이 scope를 벗어남.
// 하지만, s 는 `move`로 함수 내부에 값이 들어갔으므로 `drop`되지 않음
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 이 scope 를 벗어났기 때문에, `drop`이 호출되고 메모리가 반환됨.
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integer 가 scope 를 벗어남
- 즉, 저장되는 공간이
스택
,힙
둘 중 어느 곳이냐에 따라 다르게 동작함을 확인할 수 있다.
1.5.2 Return Value
- 함수가 값을
return
하는 것도ownership
을 이동시킬 수 있다. - 아래 코드와 주석을 참고하자.
fn main() {
let s1 = gives_ownership(); // gives_ownership() 함수의 return value는
// s1 으로 `move` 한다.
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 가 takes_and_gives_back() 함수 안으로 `move` 하므로,
// s2 는 더 이상 이 scope 에서 유효하지 않다.
// 이후 다시 return 되어 s3 로 `move` 한다.
} // s2 는 유효하지 않은 변수이므로 제외하고
// s1, s3 는 scope가 끝났으므로 `drop` 함수의 호출에 의해 메모리를 반환한다.
fn gives_ownership() -> String {
let some_string = String::from("yours"); // some_string 을 선언함
some_string // some_string 이 return 되며 `move` 한다.
} // some_string 은 함수의 리턴값으로서 `move` 되었으므로,
// `drop` 함수를 호출하지 않는다.
fn takes_and_gives_back(a_string: String) -> String { // a_string 으로 소유권이 `move`되어 들어옴
a_string // a_string 이 return 되며 `move` 한다.
} // a_string 은 함수의 리턴값으로서 `move` 되었으므로,
// `drop` 함수를 호출하지 않는다.
- 함수를 호출할 때마다,
ownership
을 이동하는 것은 사실 좀 번거로운 작업이다. - 다행히
Rust
에서는 reference 라는 개념을 사용할 수 있다!
2. References and Borrowing
2.1 References
Reference
란 포인터처럼 어떤 변수가 소유
하고 있는 데이터의 주소
값이다.
- 하지만, 포인터와는 달리
Reference
는 특정 자료형의 유효한 값임을 보장한다.
- 아래 코드는
Reference
에 대한 예제 코드이다.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // `move` 가 아니라 `referencing` 임
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
- 변수
s
, s1
, 그리고 String::from("hello")
에 대한 개념은 아래 그림을 통해 이해할 수 있다.
Reference
란 포인터처럼 어떤 변수가 소유
하고 있는 데이터의 주소
값이다.Reference
는 특정 자료형의 유효한 값임을 보장한다.Reference
에 대한 예제 코드이다.fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // `move` 가 아니라 `referencing` 임
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
s
, s1
, 그리고 String::from("hello")
에 대한 개념은 아래 그림을 통해 이해할 수 있다.
C++
에서 자주 사용하던&
와 비슷한 개념으로 이해할 수 있다.- 따라서, 위의 코드에서
calculate_length()
가 끝났다고 하더라도s: &String
은drop
되지 않는다.
2.2 Mutable References
- 함수 인자로 전달한 데이터를
mutable
하게 다루고 싶을 때는mutable references
를 이용하면 된다.
fn main() {
let mut msg = String::from("This is a message");
change_message(&mut msg); // mutable reference
println!("{}", msg);
}
fn change_message(message: &mut String) { // mutable reference
message.push_str(", I added this Lol");
}
Rust
에서는mutable reference
를 통해 데이터 값을 빌려주는(borrowing
) 것은 한 번에 하나만 가능하다.- 아래 코드는 컴파일 에러를 일으킨다.
- 이런 식의 제한을 두는 이유는 통제된 상황에서 데이터를 변경하고자 함이다.
Rust
는 이를 통해, 컴파일 타임에서 발생하는Data Race
를 해소할 수 있다.Data Race
는 아래 3가지 경우에 발생한다.- 두 개 이상의 포인터가 하나의 데이터에 접근하려 할 때
- 최소한 하나 이상의 포인터가
write
하려고 할 때 - 데이터로의 접근을
동기화
시킬 메커니즘이 없을 때
- 또,
Rust
에서는mutable reference
와immutable reference
를 혼합하여 사용할 수 없다. - 아래의 경우도 컴파일 에러를 일으킨다.
fn main() {
let mut s = String::from("Lol");
let r1 = &s; // immutable, no problem
let r2 = &s; // immutable, no problem
let r3 = &mut s; // mutable, BIG PROBLEM
println!("{}, {}, and {}", r1, r2, r3);
}
- 위 컴파일 에러에 대한 해결방법이 있다!
Reference
는&
로 지정되고, 마지막으로 사용될 때 까지를 하나의 scope 로 본다.- 따라서, 아래와 같이 코드를 수정하면 컴파일 에러 없이 실행시킬 수 있다.
fn main() {
let mut s = String::from("Lol");
let r1 = &s; // immutable, no problem
let r2 = &s; // immutable, no problem
println!("{}, and {}", r1, r2); // r1, r2 의 사용이 끝났으므로 scope 가 끝난 셈이다.
let r3 = &mut s; // mutable, BIG PROBLEM
println!("{}", r3);
}
2.3 Dangling References
Dangling Pointer
와 마찬가지로, 비어있는 위치를 가리키는Reference
를 의미한다.- 보통 메모리 할당을 해제하는 타이밍을 잘못 잡았을 때 발생한다고 한다.
Rust
에서는 컴파일 단에서 이를 막아준다.
fn main() {
let s = get_msg();
println!("{}", s);
}
fn get_msg() -> &String {
let a = String::from("TEST");
&a
}
- 에러 메세지는
borrowed value
를 받아 오지만, 데이터가 없음을 말하고 있다.
3. The Slice Type
Slice
는 이름 그대로 전체 데이터가 아니라 연속된 일부 데이터를 의미한다.
Reference
의 일종이므로, Ownership
이 없다.
- 아래 코드를 보자.
fn first_word(msg: &String) -> ?
- 어떠한
String
값을 받되, Ownership
은 move
로 받지 않는다고 할 때
String
의 일부인 '첫 번째 단어의 사이즈'를 return 하고자 한다면, 아래와 같이 코딩할 수 있다.
fn main() {
let s = String::from("This is a message");
let word = first_word(&s);
println!("{}", word);
}
fn first_word(msg: &String) -> usize {
let bytes = msg.as_bytes(); // `공백`을 찾기 위해 String 을 bytes로 변환함.
for (i, &item) in bytes.iter().enumerate() { // iter() 는 bytes 에서 iterating 하기 위해 호출
// enumerate() 는 iter 의 결과를 wrap 해서 tuple로 반환
// tuple 의 첫 번째 값은 index
// tuple 의 두 번째 값은 원소의 Reference
// destructure 를 통해 i 와 &item 에 값을 받음
if item == b' ' {
return i;
}
}
msg.len()
}
- 이런 식으로 구현은 할 수 있겠지만, 여기서 받은 *첫 번쨰 단어의 사이즈인 변수
word
의 크기가 계속 유효하다는 보장은 없다.
fn main() {
let s = String::from("This is a message");
let word = first_word(&s);
s.clear();
// ...
}
- 위 코드에서
s.clear()
이후에는 더 이상 word == 4
라고 할 수 없다.
- 이는,
s
와 word
가 각각 따로 존재하는 변수이기 때문이다.
Rust
에서는 이러한 임의 collection 에 대해 참조하기 위해 Slice
라는 개념을 제공한다.
3.1 String Slice
String Slice
란 , String
일부에 대한 Reference
로 아래와 같이 이용할 수 있다.
- 구체적으로,
String Slice
의 타입은 &str
이다.
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
- 즉,
String Slice
의 format은 [starting_index .. ending_index]
이다.
- 내부적으로
Slice
는 starting_index
와 ending_index - starting_index
의 값을 이용해서 일부분을 참조한다.
- 위 코드에 대한 개념적인 그림은 아래와 같다.
Slice
는 이름 그대로 전체 데이터가 아니라 연속된 일부 데이터를 의미한다.Reference
의 일종이므로, Ownership
이 없다.fn first_word(msg: &String) -> ?
String
값을 받되, Ownership
은 move
로 받지 않는다고 할 때String
의 일부인 '첫 번째 단어의 사이즈'를 return 하고자 한다면, 아래와 같이 코딩할 수 있다.fn main() {
let s = String::from("This is a message");
let word = first_word(&s);
println!("{}", word);
}
fn first_word(msg: &String) -> usize {
let bytes = msg.as_bytes(); // `공백`을 찾기 위해 String 을 bytes로 변환함.
for (i, &item) in bytes.iter().enumerate() { // iter() 는 bytes 에서 iterating 하기 위해 호출
// enumerate() 는 iter 의 결과를 wrap 해서 tuple로 반환
// tuple 의 첫 번째 값은 index
// tuple 의 두 번째 값은 원소의 Reference
// destructure 를 통해 i 와 &item 에 값을 받음
if item == b' ' {
return i;
}
}
msg.len()
}
word
의 크기가 계속 유효하다는 보장은 없다.fn main() {
let s = String::from("This is a message");
let word = first_word(&s);
s.clear();
// ...
}
s.clear()
이후에는 더 이상 word == 4
라고 할 수 없다.s
와 word
가 각각 따로 존재하는 변수이기 때문이다.Rust
에서는 이러한 임의 collection 에 대해 참조하기 위해 Slice
라는 개념을 제공한다.String Slice
란 , String
일부에 대한 Reference
로 아래와 같이 이용할 수 있다.String Slice
의 타입은 &str
이다. let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
String Slice
의 format은 [starting_index .. ending_index]
이다.Slice
는 starting_index
와 ending_index - starting_index
의 값을 이용해서 일부분을 참조한다.
Slice
를 다음과 같이 사용하는 것도 가능하다.
let s = String::from("hello");
let slice = &s[0..3];
let slice = &s[..3]; // == [0..3]
let len = s.len();
let slice = &s[2..len];
let slice = &s[2..]; // == [2..len]
let slice = &s[..]; // entire string
한 가지 주의할 점은
UTF-8
이 4Byte를 갖는다는 것에 유의하여 slice 를 해야 한다는 것
- 이제
Slice
를 이용해서 처음에 구현하고자 한first_word()
함수를 구현하면 아래와 같다.
fn first_word(msg: &String) -> &str {
let bytes = msg.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &msg[..i];
}
}
&msg[..]
}
- 위 코드에 대해 앞에서 했던
s.clear()
같은 코드를 추가하면 컴파일 단계에서 에러가 나는 것이 자명하다.
3.2 String Literals
String Literals
는 바이너리 코드에 그대로 저장된다는 점을 상기해보자.
let s = "Hello, world!";
- 이 때, 변수
s
의 타입은&str
이다. String Literals
가immutable
임을 확인할 수 있다.
3.3 String Slices as Parameters
- 앞에서 구현한
first_word()
는 함수 인자로&String
을 받았었다. - 하지만,
&String
대신&str
을 쓰면, 두 타입을 모두 넘겨받아 쓸 수 있다. - 이는
Deref Coercions
라는 특성에 의해 가능한데, 이는 15 장에서 확인할 수 있다.
3.4 Other Slices
- 일반적인
Slice
도 가능하다. - 아래의 코드처럼
Array
에 대해서도 slice 를 사용할 수 있다.
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
- 위에서
slice
변수의 타입은&[i32]
이다.
Summary
Ownership
, Borrowing
, Slices
들은 Rust
의memory safety
를 가능하게 해준다.
Rust
는 다른 언어들처럼 메모리 사용에 대한 권한을 제공해줌과 동시에, ownership
이라는 개념으로 메모리를 자동으로 해제할 수 있도록 한다.
- 이
Ownership
이라는 개념은 Rust
전체에 걸쳐 중요한 개념이므로, 앞으로의 챕터들에서도 주의깊게 살펴보도록 한다.
Author And Source
이 문제에 관하여([0x02] Understanding Onwership), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://velog.io/@c0np4nn4/0x02-Understanding-Onwership
저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
Ownership
, Borrowing
, Slices
들은 Rust
의memory safety
를 가능하게 해준다.Rust
는 다른 언어들처럼 메모리 사용에 대한 권한을 제공해줌과 동시에, ownership
이라는 개념으로 메모리를 자동으로 해제할 수 있도록 한다.Ownership
이라는 개념은 Rust
전체에 걸쳐 중요한 개념이므로, 앞으로의 챕터들에서도 주의깊게 살펴보도록 한다.Author And Source
이 문제에 관하여([0x02] Understanding Onwership), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@c0np4nn4/0x02-Understanding-Onwership저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)