728x90
반응형

Java 개발자를 위한 Rust, 파트 2

우리는 지금까지 어디로 갔습니까?

Java 개발자를 위한 Rust 시리즈의  번째 기사 에서 우리는 몇 가지 기본 구문, 일부 패턴 일치를 보았고 간략하게 다루었으며 가장 중요한 것은 프로그램의 데이터가 있는 위치에 대해 추론하려고 했습니다.enum

우리가 신속하게 코드 라인을 저장하려고 시도했을 때 RAII로 인해 중요한 리소스의 범위를 실수로 변경했고 코드를 컴파일하려고 할 때 빌림 검사기 에서 오류가 발생했습니다.

$ rustc greeter.rs
error[E0716]: temporary value dropped while borrowed
 --> greeter.rs:6:43
  |
6 |     let anybody_at_all = env::args().collect::<Vec<String>>().get(1);
  |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^       - temporary value is freed at the end of this statement
  |                          |
  |                          creates a temporary which is freed while still in use
7 |
8 |     let who: &str = if let Some(one) = anybody_at_all {
  |                                        -------------- borrow later used here
  |

두려운 차용 검사기 는 소유borrow later used here 한 값에 대한 참조(차용)를 효과적으로 알려줍니다. 이 경우 소유자가 해제된 동안 임시 가 사용됩니다. 이것은 관리되는 언어에서 오는 Rust를 시도하는 개발자들에게 너무 잘 알려진 오류 메시지입니다. 당신이 직접 Rust를 가지고 놀기 시작했다면 조만간 모험을 하면서 그런 메시지를 접했을 가능성이 있습니다.

Java에는 사용되지 않을 때만 메모리를 해제하는 가비지 수집기가 있을 뿐만 아니라 완전히 투명하게 힙 할당을 최적화할 수 있는 컴파일러도 있습니다. 이렇게 하면 메모리, 해제 또는 메모리 할당 위치(힙 또는 스택)에 대해 생각하지 않아도 됩니다. 반면에 Rust는 개발자에게 메모리 할당 위치와 방법에 대한 결정을 내립니다. 그것은 큰 힘을 동반하지만, 또한… 예, 책임이 있습니다. 적어도 컴파일러는 안전하지 않은 코드를 생성하도록 허용하지 않습니다. 때로는 컴파일러가 작업을 완료하도록 허용하지 않는 것처럼 느껴지기도 합니다. 이것은 일반적으로 프로그램을 통해 데이터가 "흐르는" 방식을 설명하지 않는 코딩 패턴에서 비롯됩니다. 왜냐하면 JVM은 이러한 것들에 대해 생각할 필요가 없고 누구라도 가리킬 수 있는 거대한 객체 바다를 생성하지 않도록 우리를 망쳤기 때문입니다. 모든 것.

소유권 및 이동

cargo new. _ 첫 번째 인수가 있는 경우 전달되도록 코드를 약간 변경할 것입니다. 그리 놀라운 방법은 아닙니다.

use std::env;

fn main() {
    let who = env::args().nth(1).unwrap_or(String::from("World"));
    println!("Hello, {who}!");
}

잠깐만! 이것은 우리가 이전에 본 것과는 다릅니다. 지금까지 우리가 했던 모든 작업이 한 줄짜리인 이유는 무엇입니까? 음… 예. 읽기 쉽게 만들 수 있었습니다 . 또는 적어도 이것이 읽기 쉽기를 바랍니다. 이제 반복자에서 두 번째 .nth(1)항목 을 가져오는 데 필요한 모든 것은 Args여전히 Option<String>​​; 그런 다음 프로그램에 제공된 인수  있는 경우 .unwrap밖으로 이동 합니다 . 반면에 가 있는 경우 대신 메서드에 제공된 인수에서 힙 할당 인스턴스를 만듭니다 .String OptionSome_orNoneString"World": &str

위의 코드에는 다음과 같은 새로운 구문이 있습니다 . 유형 에서 호출 String::from()되는 함수 입니다. 이는 정적 메서드 호출과 동일합니다. 여기 에서 참조하는 는 에서 찾을 수 있습니다 . A 는 Java에서 와 같이 명명된 필드에 관련 데이터를 보유합니다 . 그러나 우리는 그것에 대해 조금 후에 돌아올 것입니다.from()Stringfrom()struct Stringstructclass

그래서 밖으로 옮기는 이 사업  무엇입니까? 이전 게시물에서 보았듯이 는 어딘가에 살아야 합니다 . 그것이 Rust가 소유권이라고 부르는 것입니다. 그리고 모든 데이터의 소유자 는 한 명만 있을 수 있습니다 . 프로그램에 전달된 인수가 있는 경우 에 대한 호출 은 해당 의 소유자인 변형 에서 해당 인수를 반환합니다 . 즉, 범위를 벗어나는 즉시 콘텐츠도 -ped되고 the 가 해제됩니다. 그러나 우리는 우리의 작은 프로그램 전체에서 간접 참조 를 다루고 싶지 않습니다 . 전달 된 인수인지 또는String Option::SomeString env::args().nth(1)StringOption::SomeStringDropStringOptionString"World", 우리는 우리가 인쇄 who할 적절한 것을 가리키는 것 외에는 별로 신경 쓰지 않습니다. String이것이 .unwrap_or()우리를 위해 하는 일입니다.

두 개의 개별 비트를 분해해 보겠습니다. 먼저 Option<String>단순화를 위해 를 다룰 것이므로 먼저 코드를 리팩터링하여 다음을 얻습니다.

let anybody_at_all: Option<String> = env::args().nth(1);
let who: String = anybody_at_all.unwrap_or(String::from("World"));

이제 가 있으므로 프로그램에 항상 인수가 제공 anybody_at_all: Option<String>된다고 가정해 봅시다 . 다음과 같이 메서드 호출을 직선으로 바꿀 수 있습니다 ..unwrap_or.unwrap()

let who: String = anybody_at_all.unwrap();

이 코드는 이제 panic실제로 제공되는 인수가 없을 것입니다. 먼저 인수를 제공한 다음 생략합니다.

$ cargo run -- Jane
   Compiling rusty-java-2 v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.75s
     Running `target/debug/rusty-java-2 Jane`
Hello, Jane!

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/rusty-java-2`
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:5:30
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

좋지 않습니다. 하지만 지금은 무시하고 .unwrap_or곧 우리 버전으로 돌아갈 것입니다. 실제로 성공하는 경로에 집중하여 호출 하면 it이 가진 anybody_at_all.unwrap()소유권을 변수로 이전합니다. 제공된 인수, 즉 이 예에서 from 은 우리의 that 에서 로 이동 했습니다 . 어느 시점에서 당신은 아마 무슨 일이 일어났는지 궁금할 것이고 당신이 묻는 것이 맞을 것입니다. 시행 착오가 여기서 다시 우리의 접근 방식으로 남아 있습니다 . 값 을 다시 시도해 봅시다 .Stringwho: StringString"Jane"Option::Someanybody_at_allwhoanybody_at_all.unwrap()

let anybody_at_all: Option<String> = env::args().nth(1);
let who: String = anybody_at_all.unwrap();
let _again: String = anybody_at_all.unwrap();

무엇이 _again될까요? (밑줄로 변수 이름을 시작하면 컴파일러에 사용할 계획이 없음을 알립니다). 참고가 아닙니다. 또한 에 String포함 Option되어 있으므로 입니다. 그렇다면 원래 문자열의 복사본일까요? 이것을 실행해 봅시다:

$ cargo run -- Jane
   Compiling rusty-java-2 v0.1.0
error[E0382]: use of moved value: `anybody_at_all`
   --> src/main.rs:6:26
    |
4   |     let anybody_at_all: Option<String> = env::args().nth(1);
    |         -------------- move occurs because `anybody_at_all` has type `Option<String>`, which does not implement the `Copy` trait
5   |     let who: String = anybody_at_all.unwrap();
    |                       -------------- -------- `anybody_at_all` moved due to this method call
    |                       |
    |                       help: consider calling `.as_ref()` or `.as_mut()` to borrow the type's contents
6   |     let _again: String = anybody_at_all.unwrap();
    |                          ^^^^^^^^^^^^^^ value used here after move
    |
note: this function takes ownership of the receiver `self`, which moves `anybody_at_all`
   --> .../lib/rustlib/src/rust/library/core/src/option.rs:775:25
    |
775 |     pub const fn unwrap(self) -> T {
    |                         ^^^^

For more information about this error, try `rustc --explain E0382`.
error: could not compile `rusty-java-2` due to previous error

좋아요, 무서운 오류 메시지입니다: move occurs because …​ does not implement the Copy trait?! 글쎄, 원본의 사본은 String분명히 없습니다. 그 가설에 대해 너무 많이. 그러나 우리는 무언가 움직 인 것을 봅니다 ! anybody_at_all moved 오류 메시지가 나타납니다!  Option자체가 그랬다. 어떤 fn unwrap(self)메서드 때문에 우리는 실제로 호출했습니다. 나중에 구문으로 돌아갑니다. 그러나 무슨 일이 일어났는가는 호출 anybody_at_all이 소비되는 결과를 낳았고 , 그 동안 포함된 it의 소유권 String을 who. 그래서 우리가 그것으로 할 수 있는 것은 아무것도 없고 anybody_at_all, 그것은 "사라지고" 우리가 invoke 를 했던 5행.unwrap() 너머에서 사용될 수 없습니다 .

소유권은 매우 유사한 방식으로 동일한 범위 내에서 한 변수에서 다른 변수로 이전됩니다.

let who: String = anybody_at_all.unwrap();
let moved: String = who;
println!("Hello, {who}!");
println!("Hello, {moved}!");

이것은 우리 가 변수에서 변수로 소유권을 이전하기 때문에 컴파일 되지 않습니다 . 따라서 이후에는 사용할  없으며 더 이상 사용할 수 없습니다.Stringwhomovedwhoprintln!

전화는 어때 .unwrap_or(String::from("World"))? 우리가 변종 panic을 다루는 경우를 제외하고는 정확히 동일하게 작동합니다 . Option::None이 경우 제공된 인수를 대신 반환합니다. 이것은 제네릭 형식 Option<T>이며 unwrap_ora T를 default: T 인수로 사용합니다.  String, 이 경우 a를 전달해야 합니다. 그렇기 때문에 이전 게시물에서 수행한 작업과 &str대신 처리한 문자열과 달리 힙에서 문자열을 인스턴스화해야 합니다. 이 프로그램은 "World" 항상 힙에 문자열을 할당합니다 . 이것은 메서드에 전달되는 인수이며 따라서 평가되어야 합니다. 즉, 함수 String::from가 항상 호출됩니다. 달리는 대안이 있습니다cargo clippy실제로 사용할 것을 권장합니다. 이는 Java의 람다 식에 해당하는 Rust를 사용하여 사용할 기본값을 느리게 평가하고 인수가 이미 제공된 경우 String불필요하게 힙에 할당하는 것을 방지 합니다. "World"그래도 나중 게시물을 위해 보관하겠습니다. 그래도 자유롭게 시도해 보세요!

struct그리고 차용

이제 우리는 소유권과 이사에 대해 상당히 잘 이해했으므로 차입에 대해 좀 더 자세히 살펴볼 때입니다. 앞서 말했듯이 참조는 값을 빌린 것입니다. str로드된 이진 코드에서 a에 대한 참조 또는 String실행 파일에 전달된 인수에서 가져온에 대한 참조를 다루었기 때문에 이전 게시물에서 상당한 양을 차용했습니다 . 이러한 차용은 모두 "읽기 전용"이었습니다. 참조가 가리키는 것을 변경할 수 없다는 의미입니다. &mut그들은 변경 가능한 참조인 참조 에 반대합니다 .

rustc여기 에 우리에게 부과할 중요한 규칙이 있습니다.

주어진 시간에 하나의 가변 참조 또는 여러 불변 참조를 가질 수 있습니다.
— The Rust Book
참조 규칙

즉, "살아 있는" 불변 참조자는 0개 이상 있을 수 있지만, 가변 참조자가 하나(그리고 하나만!) 있는 경우에는 불변 참조자가 0개만 있을 수 있습니다. 이는 값이 변경되는 동안 아무도 값을 읽지 않도록 보장합니다. 이것은 동시성 코드에 유용하지만, 단일 스레드 코드도 이러한 보장으로부터 이점을 얻는다는 것을 조금은 알게 될 것입니다.

그러나 이것이 프로덕션에서 무작위로 발생하는 버그가 아니라 컴파일 타임에 코드에서 전체 범주의 버그를 제거하는 데 어떻게 도움이 되는지 살펴보기 전에 RuntimeException먼저 참조 및 차용이 struct해당 impl블록과 어떻게 작용하는지 살펴보겠습니다. 먼저, structRust에서 a를 선언하는 방법은 다음과 같습니다.

struct Greeter {
    person: String,
}

structRust 의 A class는 Java의 a와 동일합니다. 먼저 struct키워드를 사용하여 선언한 다음 원하는 이름을 지정합니다. 그것이 선언하는 필드는 우리가 지금까지 본 Rust 구문을 따릅니다: <name>: <type>. 위의 예에서 person필드를 선언하는 줄은 쉼표( ,)로 끝납니다. Greeter이것은 구조체 의 마지막(유일한) 필드이므로 꼭 필요한 것은 아닙니다 . 그러나 쉼표가 필요하기 전에 선언되는 필드입니다. 항상 필드 선언을 쉼표로 끝내는 것이 좋은 방법으로 간주되므로 새 항목을 추가해야 하는 경우 버전 제어 시스템의 diff가 더 깔끔해집니다.

현재 우리 는 Java보다 structC에 더 가깝 습니다. 이 단계에서 아마도 생성자와 몇 가지 메서드를 예상할 것입니다. 전자부터 시작하겠습니다. 이 특별한 경우에는 생성자 자체가 필요하지 않습니다. 이 모든 것을 동일한 파일에 넣기 때문에 가시성은 중요하지 않습니다.  명시적으로 . 필드도 마찬가지입니다 . 즉 , 함수 의 어느 지점에서나 다음과 같은 구문을 사용하여 인스턴스화할 수 있습니다.structclassstructpubpersonmainGreeter

let greeter = Greeter {
    person: String::from("Jane"),
};

그리고 Rust의 Java에서 알 수 있듯이 생성자 가 없습니다. struct Greeter예를 들어 일부 인수 조작을 수행 하는 함수를 정의할 수 있습니다. a에 함수를 추가하는 것은 동일한 이름 struct의 블록으로 수행됩니다 . impl다음은 a 를 생성 하고 반환 하는 Greeter::new함수 의 예입니다( 참조가 아닌 자체).Personstruct

impl Greeter {
    fn new(person: String) -> Self {
        Self {
            person,
        }
    }
}

pub fn new다시 말하지만, 이 범위 외부에 노출되어야 하는 경우 함수는 로 public으로 선언 됩니다. 함수가 a 의 소유권 을 갖는 것을 볼 수 있습니다 String. 즉, 호출자가 String소유권을 이 함수로 옮긴다는 의미입니다. →그리고 화살표 a... 로 표시된 함수가 반환됩니다 Self.

Selfimpl블록 유형에 대한 자리 표시자입니다 . 이는 리팩터링할 때나 유형이 다소 장황할 때 유용합니다. 그러나 메서드 서명을 fn new(person: String) → Greeter. 함수 본문인 중괄호 안의 블록을 따릅니다. Greeter그러나 이번에 생성할 때 필드 person가 동일한 이름의 변수를 소유하도록 명시적으로 알리지 않습니다. 이전에 새로 만든 : 과 연결했을 때와 달리 String: person: String::from("Jane"). 이름이 같기 때문에 앞에 를person: 붙일 필요가 없습니다 . 중복되는 것으로 간주되므로 필요하지 않습니다. 마지막으로 본문의 한 줄은 ;새로 생성된Greeter인스턴스를 호출자에게, 즉 소유권을 호출자에게 이동합니다. 이 새로 생성 struct된 것이 스택에 생성되었다는 점도 언급할 가치가 있습니다. person의 이름 바이트가 결국 힙에 있더라도 이것은 String.

다시 Java Greeter::new의 메소드와 동일합니다 . static그렇다면 인스턴스 메서드를 어떻게 추가하시겠습니까? 먼저 인스턴스 메서드가 무엇인지 생각해 보겠습니다. 인스턴스 메소드는 객체 자체의 인스턴스에 작용하는 메소드입니다. 런타임에 발생하는 일은 인스턴스에 대한 참조가 일반 이전 함수에 암시적으로 전달된다는 것입니다. Rust는 이것을 좀 더 명시적으로 만듭니다. 다음은 구조체 에 .hi()메서드를 추가하는 코드입니다 .Greeter

impl Greeter {
    fn new(person: String) -> Self {
        Self {
            person,
        }
    }

    fn hi(&self) {
        let who = self.person.as_str();
        println!("Hello, {who}!");
    }
}

...에 대한 참조 self? 이번에는 소문자 자기? 예. 이것이 Rust가 인스턴스 메서드를 정의하는 방법입니다. 첫 번째 인수는 &self실제로 의 약어 이므로 의 유형에 대한 참조인 바인딩 이름이 지정 self: &Self됩니다 . 첫 번째 인수는 a , a 또는 a 일 수 있습니다 . 우리는 처음에 변형을 보았습니다. 그것은 인스턴스를 소비하는 인스턴스 메서드입니다. 따라서 메서드를 호출하는 데 사용된 바인딩은 일단 호출되면 쓸모 없게 됩니다. 이 구조체와 관련된 상태를 변경하기 때문에 변경 가능한 참조가 필요하다고 컴파일러에 알립니다. 한 가지 예는 Java의 setter이거나 예를 들어 . 제공된 새 항목을 재할당하려면 selfSelfGreeter&self&mut selfselfselfOption.unwrap()&mut selfset_personstruct GreeterString우리 person필드에 대한 참조는 변경 가능해야 합니다.

fn set_person(&mut self, person: String) {
    self.person = person;
}

로 선언 하면 필드 에 새 값을 할당할 때 &self변경하려고 시도하므로 컴파일러에서 불평할 것 입니다.selfperson

$ cargo run -- Jane
   Compiling rusty-java-2 v0.1.0
error[E0594]: cannot assign to `self.person`, which is behind a `&` reference
  --> src/main.rs:32:9
   |
31 |     fn set_person(&self, person: String) {
   |                   ----- help: consider changing this to be a mutable reference: `&mut self`
32 |         self.person = person;
   |         ^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be written

For more information about this error, try `rustc --explain E0594`.
error: could not compile `rusty-java-2` due to previous error

아래는 main.rs이러한 모든 변경 사항이 포함된 현재 상태의 전체 내용입니다. 그리고 몇 가지 작은 변경 사항이 더 있습니다.

   

이제 우리는 메서드 person뿐만 아니라 참조를 반환하는 필드에 대한 getter도 가지고 있습니다 . bye()그 메서드의 본문은 이전에 본 것과 약간 다릅니다 hi(). 둘 다 사실상 동일합니다. 사람의 이름을 인쇄하기 위해 사람의 참조를 얻습니다. 23번째 줄  메서드 hi()에서 self.person필드 를 가져오고 사용할 .as_str()a를 반환하는 the를 호출하는 &str 반면, 28번째 줄println! 에서는 매크로 호출 내에서 the 에 대한 참조 , 즉 a 를 가져 &옵니다 . 두 경우 모두 구조체 밖으로 이동하지 않습니다 . 그 이동은 불법입니다. 우리는 필드만 참조합니다.self.person&Stringself.person

이제 실행하면 다음과 같은 결과가 나타납니다.

❯ cargo run -- Jane
   Compiling rusty-java-2 v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s
     Running `target/debug/rusty-java-2 Jane`
Hello, Jane!
... and goodbye, Jane!

이제 이 구조체를 사용하여 차용을 약간 재생하면 이전 의 참조 규칙을 테스트할 수 있습니다.

주어진 시간에 하나의 가변 참조 또는 여러 불변 참조를 가질 수 있습니다.
— The Rust Book
참조 규칙

예를 들어 이것이 무엇을 할 것이라고 생각합니까?

let mut greeter = Greeter::new(String::from("Jane"));

let reference = &greeter;
greeter.set_person(String::from("John"));
reference.hi();

글쎄, 그것은 우리의 간단한 규칙을 위반하기 때문에 컴파일되지 않을 것입니다:

let reference = &greeter;
                -------- immutable borrow occurs here
greeter.set_person(String::from("John"));
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
reference.hi();
-------------- immutable borrow later used here

의 메서드 선언을 보면 문제가 표시됩니다 set_person(&mut self, person: Person). 가 필요 &mut self하므로 메서드를 호출할 때 묵시적으로 greeter가변적으로 차용하려고 시도합니다. 불변 참조가 아직 살아 있기 때문에 우리는 할 수 없습니다. greeteron 을 호출하기 위해 대신 사용한다면 .hi()불변 참조는 "죽은" 상태가 되고 코드는 작동할 것입니다…

이 다른 간단한 예가 무엇을 할 것이라고 생각하십니까?

let mut greeter = Greeter::new(String::from("Jane"));

let person = greeter.person();
greeter.set_person(String::from("John"));
greeter.hi();
println!("Our previous person: {person}");

person이것은 우리가 인사 하는 사람 의 필드에 대한 참조를 차용하고 있기 때문에 조금 덜 명확할 수 있습니다 . 그러나 생각해 보면 결과가 이전과 거의 같다는 사실에 놀라지 않을 것입니다.

let person = greeter.person();
             ---------------- immutable borrow occurs here
greeter.set_person(String::from("John"));
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
greeter.hi();
println!("Our previous person: {person}");
                                ------ immutable borrow later used here

빌림 자체의 "전이성"은 .person컴파일러 greeter에 의해 추적되므로 동일한 오류가 다시 발생합니다. 내부 에 대한 참조가 아직 있는 동안에는 내부 를 새 것으로 set_person교체 할 수 없습니다 . 이를 교체하면 여전히 사용 중인 이전 값을 -ping하는 것을 의미합니다. 마지막 참조가 만료된 후에만 정리할 가비지 수집기가 없다는 것을 기억하십시오.StringgreeterDrop

이 간단한 예제를 사용하여 참조의 수명 주기 를 실험 하고 참조 규칙이 어떻게 적용되는지 확인할 수 있습니다. Java 개발자로서 이 규칙에 익숙해지고 코드에서 데이터를 처리하는 방법에 대해 더 많이 생각하는 데 시간이 걸립니다. Rust는 데이터와 참조를 저장하는 위치와 방법에 대해 조금 더 생각해야 합니다.

지금 이 규칙이 왜 있는지 궁금하거나 때때로 실제로 도움이 되기보다는 제약이 더 많다고 느꼈을 수도 있습니다. 다른 스레드에 의해 동시에 수정되는 것에 대한 참조 외에 greeter.person, 이 가드에 값을 해제하는 것보다 더 많은 가치가 추가되지 않습니다… 또는 거기에 있습니까? 다음과 같은 간단한 자바 코드를 살펴보겠습니다.

List<Integer> list = new ArrayList<Integer>();

list.add(1);
list.add(2);
list.add(3);

for (Integer i : list) {
    System.out.println(i);
    list.remove(0);
}

목록을 반복하면서 값을 인쇄하고 이동하면서 제거합니다. 이 멍청한 예제와 달리 "실제" 코드에서는 이것이 계속 발생 합니다. 한동안 Java를 사용해 왔다면 이 작업이 이미 명확할 수 있지만 실행 결과는 다음과 같습니다.

1

Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
	at Program.main(Program.java:12)

글쎄, 우리는 이것에 대해 너무 멀리 가지 않았습니다! A ConcurrentModificationException, 단일 스레드에서! 이제 동일한 코드를 Rust에서 사용해 봅시다:

let mut items = vec![1, 2, 3];

for i in &items {
    println!("{i}");
    items.remove(0);
}

여기서 무슨 일이 일어나고 있는지 먼저 설명하겠습니다. Vec<i32>편리한 vec!함수형 매크로에서 다시 생성 합니다. 벡터에 있는 요소의 유형은 i32, 부호 있는 32비트 정수 - Java의 int입니다. 이는 Rust에서 정수 리터럴이 평가하는 것이기 때문입니다.

그런 다음 vec 에 대한 참조 를 반복합니다 ?! 음 ... 아니. 거기에 있는 for 루프에는 컴파일러에 의해 추가된 구문 설탕이 있습니다. for i in items- 앰퍼샌드 없이 - 는 와 동일합니다 for i in items.into_iter(). from 의 소유권을 iterator into_iter() 로 이동 하는 곳 입니다. 결국 각 항목의 소유권은 루프로 이동됩니다. While  루프 의 각 요소에 대한 참조를 제공 하고 컬렉션을 그대로 유지하는 와 동일합니다.Vecveci in &itemsi in items.iter()vecitems

루프 블록은 이제 간단해야 합니다. 이것을 실행하면 어떻게 될까요?

for i in &items {
         ------
         |
         immutable borrow occurs here
         immutable borrow later used here
    println!("{i}");
    items.remove(0);
    ^^^^^^^^^^^^^^^ mutable borrow occurs here

음, 참조 규칙은 우리가 우리의 에 대한 변경 가능한 참조를 가져오는 것을 불가능하게 만들 것입니다. items우리는 암묵적으로 which 가 그것을 변경하기 위해 .removea &mut self를 호출하여 수행 Vec합니다. 우리가 불변 참조를 사용하여 해당 Vec.

이 코드는 어떻습니까?

let mut items = vec![1, 2, 3];

for i in &items {
    for i in &items {
        println!("{i}");
    }
}

이는 참조 규칙을 위반 하지 않습니다 . 키워드가 mut 있지만 참조가 아니라 를 소유하는 변수에 있습니다 Vec<i32>. 그런 다음 중첩 반복으로 이동하여 최대 2개의 불변 참조가 동시에 활성화되며 이는 규칙에 위배 되지 않습니다 . 이 코드는 이러한 루프 후에 itemsthen:을 변경하지 않고 variable does not need to be mutable첫 번째 i가 사용되지 않는 경우 컴파일러에서 경고를 생성하도록 if this is intentional, prefix it with an underscore합니다.

그렇다면 상황은 Drop어떻습니까?

인스턴스가 범위를 벗어나면 컴파일러는 Drop인스턴스를 -ping하는 역할을 하는 일부 코드를 삽입합니다. 즉, 더 많은 인스턴스, 파일 디스크립터, 소켓 등 보유할 수 있는 모든 리소스를 해제하고 결국에는 String 인스턴스용 힙의 메모리와 같이 사용한 메모리를 해제합니다.

Rust가 리소스 해제를 처리하는 방법을 이해하기 위해 약간의 실험을 시작하겠습니다. 로 빌드한 이 hi/bye "관용구"를 기반으로 빌드 Greeter하지만 약간 변경합니다. "안녕하세요!" 한 번은 항상 "bye"라고 표시되어 있는지 확인하십시오. 바로 시작하겠습니다.

   

좋아, 그래서 우리는 이미 무엇을 알고 있습니까? struct두 가지 유형 을 선언 합니다. Scope  TrackedScope. 둘 다 String이라는 힙 할당 필드 를 포함하는 동일한 구조를 갖습니다 name.

필드에는 하나 의 Scope기능과 하나의 메서드가 있습니다. 이 함수는 간단하지만 이전 에 약간의 유용성을 추가하고 Greetera 를 생성하기 전에 &str할당된 힙으로 변환합니다 . 방법 은 조금 더 흥미롭습니다. 참조를 받지 않고 대신 소비 합니다! 그러나 그렇게 하기 전에 범위가 이제 시작되었음을 인쇄한 다음 인스턴스에서 새로 생성된 인스턴스로 이동 한 다음 반환된 인스턴스 를 반환합니다. 따라서 이름은 보존되지만 원래 인스턴스는 삭제됩니다.Stringstruct Scopestart&selfselfTrackedScope StringScopeTrackedScopeScope

에 대해 얘기 Drop! 에 대한 impl블록 도 TrackedScope있지만 특성을 구현합니다 Drop. fn drop(&mut self)Java 인터페이스와 동등한 특성 은 범위가 종료되었음을 인쇄하여 여기에서 구현 하는 단일 메서드를 선언합니다 . 해당 drop메서드 호출은 컴파일러가 올바른 위치에 자동으로 추가하는 것입니다. 코드에서 수동으로 해당 메소드를 호출할 수는 없지만 컴파일러는 (다시!) 이를 금지합니다.

수동으로 drop무언가를 원 .drop()하고 인스턴스에서 호출하는 경우 표준 라이브러리에서 사용할 수 있는 편리한 유틸리티 함수가 있습니다 std::mem::drop. drop그 기능이 얼마나 특별한지 , 신뢰할 수 없는 실제 인스턴스에 어떤 종류의 마법을 사용하는지 궁금하실 것 입니다! 자, 한번 살펴봅시다! 그 아래에는 이 신비한 유틸리티 함수의 전체 소스 코드가 있습니다.

pub fn drop<T>(_x: T) {}

기다리다?! 뭐라고요? 그것은… 비어 있습니다!

예 그렇습니다. 함수 서명에서 마술이 다시 발생합니다. 전달된 인수의 소유권을 갖습니다! 이 메서드는 범위가 없는 일반적인 over T이므로 무엇이든 허용합니다. 그리고 그것 으로 절대 아무것도 하지 마십시오 ! 하지만 그 함수를 호출할 때 의 소유권 _x이 이 범위로 이동한 다음… 컴파일러는 범위를 벗어나는 것을 확인하고 필요한 정리 코드를 삽입합니다!

이제 아래의 몇 가지 경우를 고려하여 코드에서 -ing이 Scope어떻게 발생하는지 지금 사용하겠습니다 .drop

   

주목할 가치가 있는 것은 우리가 10행 에서 끝나는 7행 에 합성 블록을 추가한다는 것 입니다. 또한 범위 는 시작할 때 변수에 바인딩되지 않습니다. 그렇다면 출력은 어떻게 되어야 한다고 생각하십니까? 이것을 실행하고 보자:two

Scope 1 started
Scope 2 started
Scope 2 ended!
Scope 3 started
Scope 3 ended!
Scope 4 started
Scope 4 ended!
Scope 1 ended!

아마 놀랍지 않게, 시작 되기 직전 에 라인 10three 의 블록 끝에서 드롭됩니다 . 반환된 항목을 바인딩하지 않으므로 시작 직후 삭제되지만 바로 정리할 수 있습니다. while 은 해당 블록의 마지막 문과 유사하게만 작동합니다. 외부 범위에서 먼저 then 을 시작하여 결과를 로컬 변수에 바인딩하고 역순으로 -ped되는 것을 볼 수 있습니다 . first , then . 말이 되네요. 나중에 선언된 구조체 바인딩은 이전에 선언된 항목에 대한 참조를 보유할 수 있으므로 역순으로 삭제하면 제대로 정리됩니다!fourtwoTrackedScopethreeonefourdropfourone

요약하자면

그것은 다시 꽤 많은 내용이었습니다. 우리는 지금까지 소유권 의 개념을 훨씬 더 깊이 탐구했습니다. 주어진 데이터 조각의 소유자는 단 한 명뿐입니다. 그러나 해당 소유권은 이전될 수 있으며 이동 이라고도 합니다 . 그런 다음 참조 규칙에 따라 데이터에 대한 참조를 빌릴 수 있는 방법을 살펴보았습니다 . 마지막으로 Rust 컴파일러가 Drop 구조체 인스턴스를 -ping하여 리소스를 해제하는 방법을 확인했습니다.

우리는 실제로 struct우리 자신의 새로운 유형을 선언하고 함수와 메소드를 추가함으로써 이 작업의 대부분을 수행했습니다. &self이렇게 함으로써 우리는 구조체 에 선언될 수 있는 &mut self  가지 다른 유형의 메서드를 살펴보았습니다 self.

마지막으로 Rust에서 for 루프가 작동하는 방식과 Java와 다른 점을 엿볼 수 있습니다. 또한 Rust 표준 라이브러리의 코드를 사용하여 위 원칙의 일부 사용법을 설명했습니다. Java와 마찬가지로 해당 소스 코드를 읽을 수 있으며 Rust 표준 라이브러리 개발자가 언어를 활용하여 몇 가지 기본 문제를 해결하기 위해 코드를 작성하는 방법에 대해 더 잘 알기 위해 일부 시간을 할애하여 일부를 읽는 것이 좋습니다. 우리가 std::mem::drop().

cargo clippy 코드를 자주 실행 하라! 일단 컴파일되면 얻은 것을 리팩토링하는 데 시간을 보내십시오. 그리고 재실행 clippy! 자신의 용어로 언어를 탐색하는 데 시간을 보내는 것이 아마도 Rust 작성을 더 잘 다룰 수 있는 가장 좋은 방법일 것입니다! Java에서는 데이터가 상주해야 하는 위치, 즉 프로그램의 어느 부분이 어떤 데이터를 소유하고 해당 데이터를 효율적으로 공유할 수 있는지에 익숙해지는 것이 처음에는 다소 어려울 수 있습니다. 그러나 데이터가 코드를 통해 흐르는 방식에 대한 저항이 가장 적은 경로를 찾는 것이 결국 더 나은 Java 작성으로 이어질 것이라고 믿습니다.

우리가 다루어야 할 것이 훨씬 더 많습니다. 따라서 Java에서 오는 Rust에 대한 다음 모험과 함께 이번 달에 새로운 게시물을 기대하세요! 이것이 통찰력이 되었기를 바라며, 주저하지 말고 의견, 의견, 제안, 수정 사항, 불만 사항을 남겨주십시오…

https://wcgw.dev/posts/2023/rusty-java-2/#_where_did_we_get_so_far

728x90

+ Recent posts