같은 구조체에 값과 해당 값에 대한 참조를 저장할 수없는 이유는 무엇입니까?


222

나는 가치가 있고 그 가치와 그 가치 내부에 대한 참조를 내 유형으로 저장하고 싶다.

struct Thing {
    count: u32,
}

struct Combined<'a>(Thing, &'a u32);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing { count: 42 };

    Combined(thing, &thing.count)
}

때로는 값이 있고 해당 값과 해당 값에 대한 참조를 동일한 구조에 저장하려고합니다.

struct Combined<'a>(Thing, &'a Thing);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing::new();

    Combined(thing, &thing)
}

때로는 값을 참조하지 않고 동일한 오류가 발생합니다.

struct Combined<'a>(Parent, Child<'a>);

fn make_combined<'a>() -> Combined<'a> {
    let parent = Parent::new();
    let child = parent.child();

    Combined(parent, child)
}

이러한 각 경우에, "충분히 오래 살지 않는다"는 오류가 발생합니다. 이 오류는 무엇을 의미합니까?


1
후자의 예에 대한 정의는 다음 Parent과 같습니다 Child.
Matthieu M.

1
@MatthieuM. 나는 그것에 대해 토론했지만 두 개의 연결된 질문에 기초하여 그것에 대해 결정했습니다. 이 질문들 중 어느 것도 구조체의 정의 또는 문제 의 방법을 보지 않았 으므로 사람들 이이 질문을 자신의 상황에 더 쉽게 맞출 수 있다는 것을 모방하는 것이 가장 좋을 것이라고 생각했습니다. 참고 나는 이렇게 대답에 메서드 시그니처를 보여줍니다.
Shepmaster

답변:


245

이것의 간단한 구현을 보자 .

struct Parent {
    count: u32,
}

struct Child<'a> {
    parent: &'a Parent,
}

struct Combined<'a> {
    parent: Parent,
    child: Child<'a>,
}

impl<'a> Combined<'a> {
    fn new() -> Self {
        let parent = Parent { count: 42 };
        let child = Child { parent: &parent };

        Combined { parent, child }
    }
}

fn main() {}

오류와 함께 실패합니다.

error[E0515]: cannot return value referencing local variable `parent`
  --> src/main.rs:19:9
   |
17 |         let child = Child { parent: &parent };
   |                                     ------- `parent` is borrowed here
18 | 
19 |         Combined { parent, child }
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function

error[E0505]: cannot move out of `parent` because it is borrowed
  --> src/main.rs:19:20
   |
14 | impl<'a> Combined<'a> {
   |      -- lifetime `'a` defined here
...
17 |         let child = Child { parent: &parent };
   |                                     ------- borrow of `parent` occurs here
18 | 
19 |         Combined { parent, child }
   |         -----------^^^^^^---------
   |         |          |
   |         |          move out of `parent` occurs here
   |         returning this value requires that `parent` is borrowed for `'a`

이 오류를 완전히 이해하려면 메모리에 값이 표시되는 방식과 해당 값 을 이동할 때 발생하는 상황에 대해 생각해야 합니다. Combined::new값의 위치를 ​​보여주는 가상 메모리 주소로 주석 을 달자 .

let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42 
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000

Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?

어떻게 child됩니까? 값이 방금 이동 parent 한 경우 더 이상 유효한 값이 보장되지 않는 메모리를 나타냅니다. 다른 코드는 메모리 주소 0x1000에 값을 저장할 수 있습니다. 정수라고 가정하여 해당 메모리에 액세스하면 충돌 및 / 또는 보안 버그가 발생할 수 있으며 Rust가 방지하는 주요 오류 범주 중 하나입니다.

이것은 평생 예방할 수있는 문제입니다 . 수명은 약간의 메타 데이터로, 현재 메모리 위치 에서 값이 얼마나 오래 유효한지 알 수 있습니다 . 그것은 Rust 이민자들이 흔히 저지르는 실수이기 때문에 중요한 차이점입니다. 녹 수명은 객체가 생성 된 시점과 파괴 된 시점 사이의 기간 이 아닙니다 !

비유로서, 다음과 같이 생각하십시오 : 사람의 삶 동안, 그들은 각각 다른 주소를 가진 많은 다른 위치에있을 것입니다. Rust의 수명은 미래에 죽을 때마다가 아니라 현재 거주 하는 주소와 관련 이 있습니다 (죽음도 주소를 변경하더라도). 주소가 더 이상 유효하지 않으므로 이동할 때마다 관련이 있습니다.

또한 수명 코드를 변경 하지는 않습니다 . 코드는 수명을 제어하고 수명은 코드를 제어하지 않습니다. 한마디로 말하면 "수명은 규범이 아니라 묘사 적"입니다.

Combined::new평생을 강조하기 위해 사용할 줄 번호로 주석 을 달자 .

{                                          // 0
    let parent = Parent { count: 42 };     // 1
    let child = Child { parent: &parent }; // 2
                                           // 3
    Combined { parent, child }             // 4
}                                          // 5

콘크리트 수명 의는 parent1 내지 4이고, (I과 같이 표현됩니다있는 포함 [1,4]). 의 콘크리트 수명 childIS [2,4], 및 반환 값의 콘크리트 수명이다 [4,5]. 0에서 시작하는 구체적인 수명을 가질 수 있습니다. 즉, 기능 또는 블록 외부에 존재하는 것에 대한 매개 변수의 수명을 나타냅니다.

수명 child자체는 [2,4]이지만 수명이 0 인 값을 나타냅니다[1,4] . 이는 참조 값이 유효하기 전에 참조 값이 유효하지 않은 한 괜찮습니다. child블록에서 돌아 오려고하면 문제가 발생합니다 . 이것은 자연 길이를 넘어서 수명을 "연장"시킵니다.

이 새로운 지식은 처음 두 가지 예를 설명해야합니다. 세 번째는의 구현을 살펴보아야합니다 Parent::child. 기회는 다음과 같습니다.

impl Parent {
    fn child(&self) -> Child { /* ... */ }
}

이것은 명시적인 일반 수명 매개 변수를 쓰지 않기 위해 수명 제거를 사용합니다 . 다음과 같습니다.

impl Parent {
    fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}

두 경우 모두,이 방법은 Child의 구체적인 수명으로 매개 변수화 된 구조가 리턴 될 것이라고 말합니다 self. 다른 방법으로, Child인스턴스 는 인스턴스 Parent를 생성 한 것에 대한 참조를 포함 하므로 해당 Parent인스턴스 보다 오래 살 수 없습니다 .

또한 생성 기능에 문제가 있음을 인식 할 수 있습니다.

fn make_combined<'a>() -> Combined<'a> { /* ... */ }

다른 형식으로 작성된 것을 볼 가능성이 높지만 :

impl<'a> Combined<'a> {
    fn new() -> Combined<'a> { /* ... */ }
}

두 경우 모두 인수를 통해 제공되는 수명 매개 변수가 없습니다. 이것은 Combined매개 변수화 될 수명이 어떤 것에 의해 제한되지 않는다는 것을 의미합니다 -호출자가 원하는 것이 될 수 있습니다. 호출자가 'static수명을 지정할 수 있고 해당 조건을 충족시킬 방법이 없기 때문에 이것은 무의미 합니다.

어떻게 고치나요?

가장 쉽고 권장되는 솔루션은 이러한 항목을 동일한 구조에 함께 배치하지 않는 것입니다. 이렇게하면 구조 중첩이 코드 수명을 모방합니다. 데이터를 소유하는 유형을 구조에 함께 배치 한 다음 필요에 따라 참조를 포함하는 참조 또는 객체를 얻을 수있는 메소드를 제공하십시오.

평생 추적이 지나치게 까다로운 특별한 경우가 있습니다. 힙에 무언가가 있으면. Box<T>예를 들어을 사용할 때 발생합니다 . 이 경우 이동 된 구조에는 힙에 대한 포인터가 포함됩니다. 지정된 값은 안정적으로 유지되지만 포인터 자체의 주소는 이동합니다. 실제로는 항상 포인터를 따르기 때문에 이것은 중요하지 않습니다.

임대 상자 (더 이상 유지되거나 지원되는) 또는 owning_ref 상자는 이 사건을 표현하는 방법입니다,하지만 그들은 기본 주소가해야 이동하지 않습니다 . 이것은 돌연변이 벡터를 배제하는데, 이는 재 할당 및 힙 할당 값의 이동을 야기 할 수있다.

렌탈로 해결 된 문제의 예 :

다른 경우에, 당신은 사용하여 같은 참조 카운팅, 어떤 종류의로 이동하실 수 있습니다 Rc또는 Arc.

추가 정보

parent구조체로 이동 한 후 컴파일러가 구조체에서 새 참조를 가져 와서 parent할당 할 수없는 이유는 child무엇입니까?

이론적으로는 가능하지만 그렇게하면 많은 복잡성과 오버 헤드가 발생합니다. 객체가 움직일 때마다 컴파일러는 참조를 "수정"하기 위해 코드를 삽입해야합니다. 이것은 구조체를 복사하는 것이 더 이상 아주 저렴한 작업이 아니라는 것을 의미합니다. 그것은 가상 옵티마이 저가 얼마나 좋은지에 따라 이와 같은 코드가 비싸다는 것을 의미 할 수도 있습니다.

let a = Object::new();
let b = a;
let c = b;

프로그래머는 매번 이동할 때마다 이를 발생시키는 대신 호출 할 때만 적절한 참조를 취하는 메소드를 작성하여 언제 발생 하는지 선택할 수 있습니다.

자체를 참조하는 유형

자체를 참조하여 유형을 만들 있는 특정 사례가 있습니다 . Option그래도 두 단계로 만드는 것과 같은 것을 사용해야합니다 .

#[derive(Debug)]
struct WhatAboutThis<'a> {
    name: String,
    nickname: Option<&'a str>,
}

fn main() {
    let mut tricky = WhatAboutThis {
        name: "Annabelle".to_string(),
        nickname: None,
    };
    tricky.nickname = Some(&tricky.name[..4]);

    println!("{:?}", tricky);
}

이것은 어떤 의미에서는 효과가 있지만 생성 된 값은 매우 제한적이므로 절대 이동할 수 없습니다 . 특히 이것은 함수에서 반환하거나 값으로 전달할 수 없음을 의미합니다. 생성자 함수는 위와 같이 수명과 동일한 문제를 보여줍니다.

fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }

무엇에 대해 Pin?

PinRust 1.33에서 안정화 된 모듈 설명서에 다음 이 있습니다 .

이러한 시나리오의 주요 예는 자체 참조 구조체를 작성하는 것입니다. 포인터를 사용하여 객체를 이동하면 객체가 무효화되어 정의되지 않은 동작이 발생할 수 있기 때문입니다.

"자기 참조"가 반드시 참조를 사용 하는 것을 의미하지는 않습니다 . 실제로 자기 참조 구조체예는 구체적으로 (강조 광산)이라고 말합니다.

이 패턴은 일반적인 차용 규칙으로 설명 할 수 없으므로 일반적인 참조로 컴파일러에 알릴 수 없습니다. 대신 우리는 원시 포인터 를 사용하지만 null이 아닌 것으로 알려진 것은 포인터를 문자열을 가리키는 것으로 알고 있기 때문입니다.

이 동작에 대한 원시 포인터를 사용하는 기능은 Rust 1.0부터 존재했습니다. 실제로, 소유-참조 및 임대는 후드 아래에 원시 포인터를 사용합니다.

Pin테이블에 추가 되는 유일한 것은 주어진 값이 움직이지 않는다는 것을 나타내는 일반적인 방법입니다.

또한보십시오:


1
이와 같은 것이 ( is.gd/wl2IAt ) 관용어로 간주됩니까? 즉, 원시 데이터 대신 메소드를 통해 데이터를 노출합니다.
Peter Hall

2
@PeterHall 확실히, 그것은 Combined소유하는 것을 소유 한다는 것을 의미 Child합니다 Parent. 실제 유형에 따라 의미가 있거나 그렇지 않을 수 있습니다. 자신의 내부 데이터에 대한 참조를 반환하는 것이 일반적입니다.
Shepmaster

힙 문제에 대한 해결책은 무엇입니까?
derekdreery

@derekdreery 아마도 당신은 당신의 코멘트를 확장 할 수 있습니까? 전체 단락이 owning_ref 상자 에 대해 말하는 이유는 무엇 입니까?
Shepmaster

1
@FynnBecker 참조 와 해당 값을 저장하는 것은 여전히 ​​불가능합니다 . Pin대부분 자기 참조 포인터를 포함하는 구조체의 안전성을 아는 방법 입니다. Rust 1.0부터 동일한 목적으로 원시 포인터를 사용하는 기능이 존재합니다.
Shepmaster

4

매우 유사한 컴파일러 메시지를 발생시키는 약간 다른 문제는 명시 적 참조를 저장하는 것이 아니라 객체 수명 종속성입니다. 그 예는 ssh2 라이브러리입니다. 테스트 프로젝트보다 큰 무언가를 개발할 때, 넣어하려고 유혹 Session하고 Channel사용자의 구현 세부 사항을 숨기고, 구조체에 서로 함께 해당 세션에서 얻을 수 있습니다. 그러나 Channel정의에는 'sess형식 주석 의 수명이 있지만 Session그렇지는 않습니다.

이는 수명과 관련된 유사한 컴파일러 오류를 발생시킵니다.

매우 간단한 방법으로 해결하는 한 가지 방법 Session은 발신자 에게 외부 를 선언 한 다음 SFTP를 캡슐화하는 동안 동일한 문제에 대해 이야기하는 이 Rust User 's Forum 게시물의 답변과 유사하게 수명 내에 구조체 내 참조에 주석을 달아주는 것입니다 . 이것은 우아하게 보이지 않으며 항상 적용되는 것은 아닙니다. 이제는 원하는 엔티티가 아닌 두 개의 엔티티가 있으므로 처리해야합니다!

아웃 턴 임대 상자 또는 owning_ref 상자 다른 답변에서도이 문제에 대한 솔루션입니다. 이 정확한 목적을위한 특수 객체가있는 owning_ref를 고려해 봅시다 OwningHandle. 기본 객체의 이동을 피하기 위해을 사용하여 힙에 객체를 할당 Box하면 다음과 같은 가능한 솔루션을 얻을 수 있습니다.

use ssh2::{Channel, Error, Session};
use std::net::TcpStream;

use owning_ref::OwningHandle;

struct DeviceSSHConnection {
    tcp: TcpStream,
    channel: OwningHandle<Box<Session>, Box<Channel<'static>>>,
}

impl DeviceSSHConnection {
    fn new(targ: &str, c_user: &str, c_pass: &str) -> Self {
        use std::net::TcpStream;
        let mut session = Session::new().unwrap();
        let mut tcp = TcpStream::connect(targ).unwrap();

        session.handshake(&tcp).unwrap();
        session.set_timeout(5000);
        session.userauth_password(c_user, c_pass).unwrap();

        let mut sess = Box::new(session);
        let mut oref = OwningHandle::new_with_fn(
            sess,
            unsafe { |x| Box::new((*x).channel_session().unwrap()) },
        );

        oref.shell().unwrap();
        let ret = DeviceSSHConnection {
            tcp: tcp,
            channel: oref,
        };
        ret
    }
}

이 코드의 결과는 Session더 이상 사용할 수 없지만 사용할 코드 와 함께 저장됩니다 Channel. OwningHandle객체 는 구조체를 저장할 때 Box를 참조 하는를 참조하지 않기 때문에 Channel이름을 지정합니다. 참고 : 이것은 단지 내 이해입니다. 아주 가까운 것으로 보인다 있기 때문에 나는이 정확하지 않을 수 있습니다 의심이 논의 OwningHandleunsafety .

여기에 한 가지 흥미로운 세부 사항은이 때문이다 Session논리적으로 유사한 관계가 TcpStreamChannel에있다가 Session, 아직 소유권이 촬영되지 않고 그렇게 주위에 형의 주석이 없습니다. 대신 핸드 셰이크 방법 의 문서가 다음과 같이이를 처리하는 것은 사용자의 책임입니다 .

이 세션은 제공된 소켓의 소유권을 가지지 않으므로 통신이 올바르게 수행되도록 소켓이이 세션의 수명을 유지하도록하는 것이 좋습니다.

제공된 스트림은이 세션 동안 프로토콜을 방해 할 수 있으므로 다른 곳에서 동시에 사용하지 않는 것이 좋습니다.

따라서 TcpStream사용법은 코드의 정확성을 보장하기 위해 프로그래머에게 달려 있습니다. 와 함께, OwningHandle"위험한 마술"이 일어나는 곳에주의를 기울이십시오 unsafe {}.

이 문제에 대한 더 높은 수준의 토론은이 Rust 사용자 포럼 스레드에 있습니다. 여기에는 안전하지 않은 블록이 포함되지 않은 임대 상자를 사용하는 다른 예제와 솔루션이 포함되어 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.