Rust에서 명시적인 수명이 필요한 이유는 무엇입니까?


199

Rust 책 의 수명 장 을 읽고 있었고 명명 된 / 명시 적 수명에 대해이 예제를 보았습니다.

struct Foo<'a> {
    x: &'a i32,
}

fn main() {
    let x;                    // -+ x goes into scope
                              //  |
    {                         //  |
        let y = &5;           // ---+ y goes into scope
        let f = Foo { x: y }; // ---+ f goes into scope
        x = &f.x;             //  | | error here
    }                         // ---+ f and y go out of scope
                              //  |
    println!("{}", x);        //  |
}                             // -+ x goes out of scope

그것은 컴파일러에 의해 억제되는 오류가 있음을 나에게 매우 분명 처리 할 때 use-after-free 에 할당 된 참조가 x: 내부 범위를 완료, 후 f때문에 &f.x무효되고, 할당 된 안된다 x.

내 문제는 명시 적 수명 사용 하지 않고 문제를 쉽게 분석 할 수 있다는 것 입니다. 'ax = &f.x;

어떤 경우에 사후 사용 (또는 다른 클래스) 오류를 방지하기 위해 명시적인 수명이 실제로 필요한가?



2
이 질문의 미래 독자들을 위해, 그것은 책의 첫 번째 판에 링크되어 있으며 이제 두 번째 판이 있습니다 :)
carols10cents

답변:


205

다른 답변은 모두 명백한 점 ( 명시 적 수명이 필요한 fjh의 구체적인 예 )이 있지만 한 가지 중요한 사항이 누락되었습니다 . 컴파일러가 잘못 설명 했을 때 명시 적 수명이 필요한 이유는 무엇입니까?

이것은 실제로 "컴파일러가이를 유추 할 수있을 때 명시 적 유형이 필요한 이유"와 같은 질문입니다. 가상의 예 :

fn foo() -> _ {  
    ""
}

물론 컴파일러는를 반환한다는 것을 알 수 있습니다 &'static str. 그래서 프로그래머는 왜 그것을 입력해야합니까?

주된 이유는 컴파일러가 코드의 기능을 볼 수 있지만 의도가 무엇인지 알 수 없기 때문입니다.

함수는 코드 변경으로 인한 영향을 차단하는 자연스러운 경계입니다. 코드에서 수명을 완전히 검사 할 수있게하려면 순진한 모양의 변화가 수명에 영향을 미쳐 멀리있는 함수에서 오류가 발생할 수 있습니다. 이것은 가상의 예가 아닙니다. 내가 이해 한 것처럼 Haskell은 최상위 함수에 형식 유추에 의존 할 때이 문제가 있습니다. 녹은 새싹에서 그 특정 문제를 해결했다.

컴파일러에는 효율성 이점도 있습니다. 유형과 수명을 확인하려면 함수 서명 만 구문 분석하면됩니다. 더 중요한 것은 프로그래머에게 효율성 이점이 있다는 것입니다. 명시적인 수명이 없다면이 기능은 무엇을 하는가?

fn foo(a: &u8, b: &u8) -> &u8

소스를 검사하지 않고는 말할 수 없으며, 이는 수많은 코딩 모범 사례에 위배됩니다.

더 넓은 범위에 대한 참조의 불법 할당을 유추함으로써

범위 기본적으로 수명입니다. 좀 더 명확하게, 수명 'a은 호출 사이트를 기반으로 컴파일 타임에 특정 범위로 특수화 할 수 있는 일반 수명 매개 변수 입니다.

[...] 오류를 방지하기 위해 명시적인 수명이 실제로 필요한가?

전혀. 수명 은 오류를 방지하는 데 필요하지만, 생생한 프로그래머가 가지고있는 것을 보호하려면 명시적인 수명이 필요합니다.


18
@jco f x = x + 1다른 모듈에서 사용하고있는 타입 서명이없는 최상위 함수가 있다고 상상해보십시오 . 나중에 정의를로 변경하면 f x = sqrt $ x + 1해당 유형이에서 (으) Num a => a -> a로 변경되어 인수 와 Floating a => a -> a같이 호출 된 모든 호출 사이트에서 유형 오류가 발생합니다 . 형식 서명이 있으면 오류가 로컬로 발생합니다. fInt
fjh

11
"스코프는 본질적으로 수명입니다. 좀 더 명확하게 말하자면, 수명은 호출시 특정 범위로 특화 될 수있는 일반적인 수명 매개 변수입니다." 와우 책에 명시 적으로 포함되어 있으면 좋겠습니다.
corazza

2
@fjh 감사합니다. 내가 그 말을 이해했는지 확인하기 위해-요점은 유형을 명시하기 전에 명시 적으로 언급 sqrt $하면 변경 후에 로컬 오류 만 발생했을 것이고 다른 곳에서는 많은 오류가 발생하지 않았을 것입니다 (이 경우 훨씬 낫습니다) 실제 유형을 변경하고 싶지 않습니까?)
corazza

5
@jco 정확합니다. 유형을 지정하지 않으면 실수로 함수의 인터페이스를 변경할 수 있습니다. 이것이 하스켈의 모든 최상위 항목에 주석을 달 것을 강력히 권장하는 이유 중 하나입니다.
fjh

5
또한 함수가 두 개의 참조를 수신하고 참조를 리턴하면 때로는 첫 번째 참조를 리턴하고 때로는 두 번째 참조를 리턴 할 수도 있습니다. 이 경우 반환 된 참조의 수명을 유추 할 수 없습니다. 명시 적 수명은 그러한 상황을 피하거나 명확하게하는 데 도움이됩니다.
MichaelMoser

93

다음 예제를 보자.

fn foo<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
    x
}

fn main() {
    let x = 12;
    let z: &u32 = {
        let y = 42;
        foo(&x, &y)
    };
}

여기에서 명시적인 수명이 중요합니다. 결과 foo는 첫 번째 인수 ( 'a) 와 수명이 같으 므로 두 번째 인수보다 수명 이 길기 때문에 컴파일됩니다 . 이는의 서명에서 수명 이름으로 표시됩니다 foo. foo컴파일러 호출에서 인수를 전환하면 y오래 살지 못한다고 불평 할 것입니다 .

error[E0597]: `y` does not live long enough
  --> src/main.rs:10:5
   |
9  |         foo(&y, &x)
   |              - borrow occurs here
10 |     };
   |     ^ `y` dropped here while still borrowed
11 | }
   | - borrowed value needs to live until here

16

다음 구조의 수명 주석 :

struct Foo<'a> {
    x: &'a i32,
}

Foo인스턴스가 포함하는 참조보다 인스턴스가 수명을 초과하지 않도록 지정합니다 ( xfield).

당신이 녹 책에서 온 예는이 때문에 설명하지 않습니다 fy 변수가 동시에 범위 밖으로 이동합니다.

더 좋은 예는 다음과 같습니다.

fn main() {
    let f : Foo;
    {
        let n = 5;  // variable that is invalid outside this block
        let y = &n;
        f = Foo { x: y };
    };
    println!("{}", f.x);
}

이제는로 f지적한 변수보다 실제로 수명을 연장합니다 f.x.


9

구조 정의를 제외하고 해당 코드에는 명시적인 수명이 없습니다. 컴파일러는 수명을 완벽하게 추론 할 수 있습니다.main() 있습니다.

그러나 유형 정의에서 명시 적 수명은 피할 수 없습니다. 예를 들어, 여기에 모호성이 있습니다.

struct RefPair(&u32, &u32);

수명이 달라야합니까, 아니면 같아야합니까? 그것은 사용의 관점에서 문제가 않습니다, struct RefPair<'a, 'b>(&'a u32, &'b u32)매우 다르다struct RefPair<'a>(&'a u32, &'a u32) .

이제는 제공 한 것과 같은 간단한 경우 컴파일러 이론적 으로 다른 곳에서와 마찬가지로 수명을 없앨 있지만 이러한 경우는 매우 제한적이며 컴파일러에서 추가 복잡성이 가치가 없으며이 명확성 향상은 매우 의심스러운.


2
왜 그들이 매우 다른지 설명 할 수 있습니까?
AB

@AB 두 번째는 두 참조 모두 동일한 수명을 공유해야합니다. 즉, refpair.1은 refpair.2보다 오래 살 수 없으며 그 반대도 마찬가지입니다. 따라서 두 심판이 동일한 소유자를 가진 것을 가리켜 야합니다. 그러나 첫 번째는 RefPair가 두 부품보다 오래 지속 되기만하면됩니다.
llogiq

2
모두 수명이 통합되어 있기 때문에 @AB, 그것은 컴파일 - 지역 평생 그 작은 때문에 'static, 'static지역의 수명을 사용 가능한 따라서 귀하의 예제에서, 모든 곳에서 사용할 수있는 p수명 매개 변수의 로컬 수명으로 추정됩니다 y.
Vladimir Matveev

5
@AB 는 두 입력 수명, 즉이 경우 수명이 교차 RefPair<'a>(&'a u32, &'a u32)하는 것을 의미합니다 . 'ay
fjh

1
@llogiq "RefPair가 두 부품보다 오래 지속되어야합니까?" 나는 정반대 였지만 & u32는 RefPair가 없으면 여전히 의미가 있지만 RefPair는 심판이 죽은 상태로 이상합니다.
qed

6

이 책의 사례는 설계 상 매우 간단합니다. 평생의 주제는 복잡한 것으로 간주됩니다.

컴파일러는 여러 인수가있는 함수에서 수명을 쉽게 추론 할 수 없습니다.

또한 내 자신의 선택적 상자에는 실제로 서명 OptionBool이있는 as_slice방법 이있는 유형 이 있습니다.

fn as_slice(&self) -> &'static [bool] { ... }

컴파일러가 그것을 알아낼 수있는 방법은 전혀 없습니다.


2 인수 함수의 리턴 유형의 수명을 유추하는 IINM은 정지 시간 (IOW)과 동일하며 유한 시간 내에 결정할 수 없습니다.
dstromberg


4

함수가 두 개의 참조를 인수로 받고 참조를 리턴하면 함수의 구현은 때때로 첫 번째 참조를 리턴하고 때로는 두 번째 참조를 리턴 할 수도 있습니다. 주어진 호출에 대해 어떤 참조가 반환되는지 예측할 수 없습니다. 이 경우 각 인수 참조가 다른 수명을 가진 다른 변수 바인딩을 참조 할 수 있으므로 반환 된 참조의 수명을 유추하는 것은 불가능합니다. 명시 적 수명은 그러한 상황을 피하거나 명확하게하는 데 도움이됩니다.

마찬가지로, 구조에 두 개의 참조 (두 개의 멤버 필드로)가 있으면 구조의 멤버 함수가 때때로 첫 번째 참조를 리턴하고 때로는 두 번째 참조를 리턴 할 수도 있습니다. 또 다시 명백한 수명은 이러한 모호성을 방지합니다.

몇 가지 간단한 상황에서,이 평생 생략 컴파일러가 수명을 추론 할 수있다.


1

예제가 작동하지 않는 이유는 Rust에 로컬 수명과 형식 유추 만 있기 때문입니다. 당신이 제안하는 것은 세계적인 추론을 요구합니다. 수명을 생략 할 수없는 참조가있을 때마다 주석을 달아야합니다.


1

Rust의 새로운 이민자로서, 나의 이해는 명백한 수명이 두 가지 목적에 기여한다는 것입니다.

  1. 함수에 명시적인 수명 주석을 넣으면 해당 함수 안에 나타날 수있는 코드 유형이 제한됩니다. 컴파일러는 명시 적 수명을 통해 프로그램이 의도 한대로 작동하는지 확인할 수 있습니다.

  2. (컴파일러)가 코드 조각이 유효한지 확인하기를 원한다면, (컴파일러)는 호출 된 모든 함수를 반복해서 살펴볼 필요는 없습니다. 해당 코드에서 직접 호출하는 함수의 주석을 살펴 보는 것으로 충분합니다. 이렇게하면 프로그램 (귀하의 컴파일러)이 훨씬 쉽게 추론하고 컴파일 시간을 관리 할 수 ​​있습니다.

포인트 1에서 파이썬으로 작성된 다음 프로그램을 고려하십시오.

import pandas as pd
import numpy as np

def second_row(ar):
    return ar[0]

def work(second):
    df = pd.DataFrame(data=second)
    df.loc[0, 0] = 1

def main():
    # .. load data ..
    ar = np.array([[0, 0], [0, 0]])

    # .. do some work on second row ..
    second = second_row(ar)
    work(second)

    # .. much later ..
    print(repr(ar))

if __name__=="__main__":
    main()

인쇄됩니다

array([[1, 0],
       [0, 0]])

이런 유형의 행동은 항상 나를 놀라게합니다. 어떤 일이 일어나고 것은 즉 df와 공유 메모리 ar, 그렇게 할 때의 내용의 일부 df변경에work 있으면 그 변경도 감염 ar됩니다. 그러나 경우에 따라 메모리 효율성상의 이유로 (사본 없음) 이것이 정확히 원하는 것일 수 있습니다. 이 코드의 실제 문제는 함수 second_row가 두 번째 행 대신 첫 번째 행을 반환한다는 것입니다. 행운을 빌어 디버깅.

대신 Rust로 작성된 비슷한 프로그램을 고려하십시오.

#[derive(Debug)]
struct Array<'a, 'b>(&'a mut [i32], &'b mut [i32]);

impl<'a, 'b> Array<'a, 'b> {
    fn second_row(&mut self) -> &mut &'b mut [i32] {
        &mut self.0
    }
}

fn work(second: &mut [i32]) {
    second[0] = 1;
}

fn main() {
    // .. load data ..
    let ar1 = &mut [0, 0][..];
    let ar2 = &mut [0, 0][..];
    let mut ar = Array(ar1, ar2);

    // .. do some work on second row ..
    {
        let second = ar.second_row();
        work(second);
    }

    // .. much later ..
    println!("{:?}", ar);
}

이것을 컴파일하면

error[E0308]: mismatched types
 --> src/main.rs:6:13
  |
6 |             &mut self.0
  |             ^^^^^^^^^^^ lifetime mismatch
  |
  = note: expected type `&mut &'b mut [i32]`
             found type `&mut &'a mut [i32]`
note: the lifetime 'b as defined on the impl at 4:5...
 --> src/main.rs:4:5
  |
4 |     impl<'a, 'b> Array<'a, 'b> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...does not necessarily outlive the lifetime 'a as defined on the impl at 4:5
 --> src/main.rs:4:5
  |
4 |     impl<'a, 'b> Array<'a, 'b> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^

실제로 두 가지 오류가 발생하며 역할 'a'b교환 이있는 오류도 있습니다. 의 주석을 보면 second_row출력이이어야한다는 것을 알 수 있습니다 &mut &'b mut [i32]. 즉, 출력은 수명이있는 참조 'b(두 번째 행의 수명)에 대한 참조 여야합니다 Array. 그러나 첫 번째 행 (lifetime 'a) 을 반환하기 때문에 컴파일러는 수명 불일치에 대해 불평합니다. 올바른 장소에서. 적시에. 디버깅은 산들 바람입니다.


0

주어진 심판에 대한 계약으로 평생 주석을 소스 범위에서 유효한 동안에 만 수신 범위에서 유효하다고 생각합니다. 동일한 수명 유형에서 더 많은 참조를 선언하면 범위가 병합되므로 모든 소스 참조가이 계약을 충족해야합니다. 이러한 주석을 통해 컴파일러는 계약 이행을 확인할 수 있습니다.

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