English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
컴퓨터 프로그램은 실행 중에 사용하는 메모리 자원을 관리해야 합니다.
대부분의 프로그래밍 언어는 메모리 관리 기능을 갖추고 있습니다:
C/C++ 이런 언어는 대부분 수동으로 메모리를 관리하며, 개발자는 수동으로 메모리 자원을 요청하고 해제해야 합니다. 하지만 개발 효율성을 높이기 위해 프로그램 기능 실현에 영향을 미치지 않는 한 많은 개발자는 메모리를 적절히 해제하는 습관이 없습니다. 따라서 수동 메모리 관리 방식은 자원 낭비를 자주 유발합니다.
Java 언어로编写的 프로그램은 가상 머신(JVM)에서 실행되며, JVM은 자동으로 메모리 자원을 회수할 수 있는 기능을 갖추고 있습니다. 하지만 이 방식은 종종 실행 시간 효율성을 낮추기 때문에 JVM은 자원 회수를 최소화하려고 합니다. 이렇게 하면 프로그램이 더 많은 메모리 자원을 차지하게 됩니다.
소유권은 대부분의 개발자에게 새로운 개념이며, Rust 언어가 메모리를 효율적으로 사용하기 위해 설계된 문법 메커니즘입니다. 소유권 개념은 Rust가 컴파일 단계에서 메모리 리소스의 유용성을 더 효과적으로 분석하여 메모리 관리를 실현하기 위해 탄생했습니다.
소유권은 다음 세 가지 규칙을 가집니다:
Rust의 각 값은 하나의 변수를 가지며, 이를 소유자라고 합니다.
한 번에 하나의 소유자만 있습니다.
소유자가 프로그램 실행 범위 내에 없을 때, 그 값은 제거됩니다.
이 세 가지 규칙은 소유권 개념의 기본입니다.
다음은 소유권 개념과 관련된 개념을 소개합니다.
아래의 프로그램을 통해 변수 범위 개념을 설명합니다:
{ // 변수 s는 선언 전에 유효하지 않습니다. let s = "w3codebox"; // 이곳은 변수 s의 유효 범위입니다 } // 변수 범위가 끝났습니다. 변수 s는 유효하지 않습니다.
변수 범위는 변수의 한 성질로, 변수의 가능한 영역을 나타내며, 기본적으로 변수가 선언된 곳에서 시작하여 변수가 있는 영역이 끝날 때까지 유효합니다.
변수를 정의하고 값을 할당하면, 그 값은 메모리에 존재합니다. 이는 매우 일반적입니다. 하지만 필요한 저장할 데이터의 길이가 불확실할 때(예를 들어 사용자가 입력한 문자열열), 정의할 때 데이터 길이를 명확히 할 수 없으며, 컴파일 단계에서 데이터 저장을 위해 고정 길이의 메모리 공간을 할당할 수 없습니다. (누군가는 할당 가능한 가장 큰 공간을 할당하면 문제를 해결할 수 있다고 말하지만, 이 방법은 매우 비문명적입니다). 이는 프로그램이 실행 중에 메모리를 자체적으로 요청하여 사용할 수 있는 메커니즘을 제공해야 합니다 - 스택. 이 장에서 설명하는 모든 "메모리 리소스"는 스택이 사용하는 메모리 공간을 의미합니다.
할당이면 해제도 있어야 하며, 프로그램은 항상 특정 메모리 리소스를 점용할 수 없습니다. 따라서 리소스가 낭비되지 않는지 결정하는 중요한 요소는 리소스가 시간에 따라 해제되는지 여부입니다.
우리는 문자열 예제 프로그램을 C 언어와 같은 등가로 작성합니다:
{ char *s = "w3codebox"; free(s); // s 리소스 해제 }
물론, Rust에서는 문자열 s의 리소스를 free 함수로 해제하지 않습니다. (C 언어에서는 이렇게 쓰는 것이 잘못된 방법이라는 것은 알고 있습니다, 왜냐하면 "w3codebox"는 스택에 없으며, 여기서는 그것이)에 있다고 가정합니다. Rust는 변수의 범위가 끝날 때, Rust 컴파일러가 자동으로 리소스 해제 함수 호출을 추가하기 때문에 명시적인 해제 단계가 없습니다.
이 메커니즘은 매우 간단하게 보이지만, 프로그래머가 적절한 위치에 리소스를 해제하는 함수 호출을 추가하는 데 도움을 주는 것뿐입니다. 하지만 이 간단한 메커니즘은 프로그래머가 가장 고통스러운 프로그래밍 문제 중 하나를 효과적으로 해결할 수 있습니다.
데이터와의 교환 방식은 주로 이동(Move)과 복사(Clone) 두 가지가 있습니다:
Rust에서는 여러 가지 방식으로 데이터와의 교환을 할 수 있습니다:
let x = 5; let y = x;
이 프로그램은 값을 5 변수 x에 바인딩한 후, x의 값을 복사하여 변수 y에 할당합니다. 스택에는 두 가지 값이 있습니다 5。이 상황에서의 데이터는 "기본 데이터" 타입의 데이터로, 스택에 저장되지 않습니다. 스택의 데이터 "이동" 방식은 직접 복사로, 이는 더 많은 시간이나 저장 공간을 소비하지 않습니다. "기본 데이터" 타입은 다음과 같습니다:
모든 정수 타입, 예를 들어 i32 、 u32 、 i64 와 같습니다.
부울 타입 bool, 값이 true 또는 false입니다.
모든 플로팅 포인트 타입, f32 와 f64。
문자 타입 char.
위와 같은 데이터를 포함한 튜플(Tuples)만을 포함합니다.
하지만 데이터 교환의 경우 스택에 데이터가 저장되어 있는 경우는 다른 경우입니다:
let s1 = String::from("hello"); let s2 = s1;
첫 번째 단계에서 "hello" 값을 가진 String 객체가 생성됩니다. "hello"는 길이가 불확정된 데이터로, 스택에 저장되어야 합니다.
第二步의 경우는 약간 다릅니다(이는 완전히 진짜가 아니며, 대조와 참조를 위해 사용됩니다.):
그림을 보면 알 수 있듯이: 두 개의 String 객체가 스택에 있으며, 각 String 객체는 스택의 "hello" 문자열을 가리키는 포인터를 가집니다. s2 할당할 때, 스택 데이터만 복사되며, 스택의 문자열은 여전히 원래의 문자열입니다.
그전에 우리는 언급했지만, 변수가 범위를 벗어났을 때, Rust는 자동으로 자원 해제 함수를 호출하여 해당 변수의 스택 메모리를 정리합니다. 하지만 s1 s와 함께}2 이 모두 해제되면 스택 공간의 "hello"는 두 번 해제됩니다. 이는 시스템이 허용하지 않습니다. 안전을 위해 s2 할당할 때 s1 가 무효화되었습니다. 맞습니다. s1 의 값이 s2 이후 s1 사용할 수 없게 됩니다. 아래의 프로그램은 잘못되었습니다:
let s1 = String::from("hello"); let s2 = s1; println!("{}, world!", s1); // 오류! s1 무효화되었습니다.
따라서 실제 상황은:
s1 명죄로 인해 실체가 없어졌습니다.
Rust는 프로그램의 실행 비용을 최소화하려고 합니다. 따라서 기본적으로, 길이가 긴 데이터는 스택에 저장되며, 데이터 교환은 이동 방식으로 수행됩니다. 하지만 데이터를 단순히 복사하여 다른 목적으로 사용하려면, 데이터의 두 번째 교환 방식인 - 복사를 사용할 수 있습니다.
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}, s1, s2); }
실행 결과:
s1 = hello, s2 = hello
여기서는 실제로 스택에서 "hello"를 복사了一份이기 때문에 s1 s와 함께}2 각각의 값에 바인딩되어 있으며, 해제할 때도 두 자원으로 간주됩니다.
물론, 복사는 필요할 때만 사용하면 됩니다. 결국 복사 데이터는 더 많은 시간을 소비합니다.
변수에 대한 경우, 이는 가장 복잡한 경우입니다.
한 변수를 다른 함수에 함수의 파라미터로 전달하면, 소유권을 안전하게 처리하는 방법은 무엇인가요?
이这样的情况에서 소유권 메커니즘의 작동 원리를 설명하는 이 프로그램을 보여드립니다:
fn main() { let s = String::from("hello"); // s이 유효로 선언되었습니다 takes_ownership(s); // s의 값이 함수로 전달됩니다 // 따라서 s가 이미 이동되었다고 가정할 수 있으며, 이 부분부터는 유효하지 않습니다 let x = 5; // x이 유효로 선언되었습니다 makes_copy(x); // x의 값이 함수로 전달됩니다 // 하지만 x는 기본 타입이므로 여전히 유효합니다 // 여기서는 여전히 x를 사용할 수 있지만 s를 사용할 수 없습니다 } // 함수가 끝나면, x가 유효가 해제되고, 그 다음은 s. 하지만 s는 이미 이동되었기 때문에 해제할 필요가 없습니다 fn takes_ownership(some_string: String) { // 한 개의 String 파라미터 some_string이 전달됩니다, 유효 println!("{}", some_string); } // 함수가 끝나면, 파라미터 some_string이 여기서 해제됩니다 fn makes_copy(some_integer: i32) { // 한 개의 i32 파라미터 some_integer이 전달됩니다, 유효 println!("{}", some_integer); } // 함수가 끝나면, 파라미터 some_integer는 기본 타입이므로 해제할 필요가 없습니다
변수를 함수에 파라미터로 전달하면, 그것과 이동의 효과가 같습니다.
fn main() { let s1 = gives_ownership(); // gives_ownership이 그 반환 값을 s로 이동시킵니다1 let s2 = String::from("hello"); // s2 유효로 선언되었습니다 let s3 = takes_and_gives_back(s2); // s2 파라미터로 이동됩니다, s3 반환 값의 소유권을 얻습니다 } // s3 유효가 해제되었습니다, s2 이동됩니다, s1 유효가 해제되었습니다. fn gives_ownership() -> String { let some_string = String::from("hello"); // some_string이 유효로 선언되었습니다 return some_string; // some_string이 함수로부터 반환 값으로 이동됩니다 } fn takes_and_gives_back(a_string: String) -> String { // a_string이 유효로 선언되었습니다 a_string // a_string 被当作返回值移出函数 }
被当作函数返回值的变量所有权将会被移动出函数并返回到调用函数的地方,而不会直接被无效释放。
引用(Reference)是 C++ 开发者较为熟悉的概念。
如果你熟悉指针的概念,你可以把它看作一种指针。
实质上"引用"是变量的间接访问方式。
fn main() { let s1 = String::from("hello"); let s2 = &s1; println!("s1 is {}, s2 is {}", s1, s2); }
실행 결과:
s1 is hello, s2 is hello
& 运算符可以取变量的"引用"。
当一个变量的值被引用时,变量本身不会被认定无效。因为"引用"并没有在栈中复制变量的值:
함수 파라미터 전달의 원리와 같습니다:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
실행 결과:
The length of 'hello' is 5.
참조는 값의 소유권을 얻지 않습니다.
참조는 값의 소유권을 대여할 수 있습니다.
참조 자체는 타입이며 값이 있으며, 이 값은 다른 값의 위치를 기록합니다만, 참조는 그 값의 소유권을 가지지 않습니다:
fn main() { let s1 = String::from("hello"); let s2 = &s1; let s3 = s1; println!("{}", s)2); }
이 프로그램은 잘못되었습니다: s2 대여된 s1 은 소유권을 s로 이동했습니다3그래서 s2 s를 대여할 수 없게 됩니다1 의 소유권. s를 사용하려면2 이 값을 사용하려면, 다시 대여해야 합니다:
fn main() { let s1 = String::from("hello"); let mut s2 = &s1; let s3 = s2; s2 = &s3; // 다시 s3 대여 소유권 println!("{}", s)2); }
이 프로그램은 올바르습니다.
참조는 소유권을 가지지 않기 때문에, 소유권을 대여했어도, 그는 사용권만 가집니다(이는 집을 대여하는 것과 같습니다).
대여된 권리를 사용하여 데이터를 변경하려고 시도하면 차단됩니다:
fn main() { let s1 = String::from("run"); let s2 = &s1; println!("{}", s)2); s2.push_str("oob"); // 에러, 대여의 값을 변경하는 것은 금지됩니다 println!("{}", s)2); }
이 프로그램에서 s2 s를 변경하려고 시도하면1 의 값은 변경되지 않으며, 대여의 소유권은 소유자의 값을 변경할 수 없습니다.
물론, 변할 수 있는 대여 방식도 있습니다. 예를 들어, 집을 대여할 때, 관리자가 주인이 집 구조를 수정할 수 있도록 지정하면, 주인이 대여할 때 계약서에 이러한 권리를 부여받으면, 집을 다시装潢할 수 있습니다:
fn main() { let mut s1 = String::from("run"); // s1 는 변할 수 있는입니다 let s2 = &mut s1; // s2 는 변할 수 있는 참조입니다 s2.push_str("oob"); println!("{}", s)2); }
이 프로그램은 문제가 없습니다. 우리는 변경 가능한 참조 타입에 &mut를 사용합니다.
변경 가능한 참조와 불변 참조와 비교해보면, 권한이 다를 뿐만 아니라, 변경 가능한 참조는 중복 참조를 허용하지 않지만, 불변 참조는 허용할 수 있습니다:
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2);
이 프로그램은 잘못되었습니다. s에 중복으로 변경 가능한 참조를 했습니다.
Rust가 변경 가능한 참조에 대한 이러한 설계는 주로 병렬 상태에서 데이터 접근 충돌이 발생할 가능성을 줄이기 위해 주어졌으며, 컴파일 단계에서 이러한 일이 발생하지 않도록 합니다.
데이터 접근 충돌이 발생하는 필수 조건 중 하나는 데이터가 적어도 한 사용자에 의해 쓰이고 적어도 다른 사용자에 의해 읽거나 쓰이기 때문에, 값이 변경 가능한 참조가 되면 다른 참조에 의해 다시 참조되지 않도록 허용되지 않습니다.
이는 이름을 바꾼 개념이며, 포인터 개념이 있는 프로그래밍 언어에 있으면 실제로 접근할 수 있는 데이터를 가리키지 않는 포인터(공백 포인터가 아니라, 이미 해제된 자원도 포함될 수 있습니다)를 가리킵니다. 그들은 마치 잃어버린 풍선의 줄처럼 보여서 "휘리키 참조"라고 불립니다.
"휘리키 참조"는 Rust 언어에서 허용되지 않으며, 그렇다면 컴파일러가 찾아냅니다.
아래는 풍선 참조의典적인 예입니다:
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s }
물론이지, dangle 함수의 종료와 함께, 그 지역 변수의 값 자체는 반환 값으로 사용되지 않았고, 해제되었습니다. 그러나 그 참조는 반환되었으며, 이 참조가 가리키는 값은 더 이상 존재하지 않을 수 있습니다. 따라서 그것이 등장하지 않도록 허용되지 않습니다.