Tại sao cuộc sống rõ ràng cần thiết trong Rust?


199

Tôi đã đọc chương trọn đời của cuốn sách Rust và tôi đã xem qua ví dụ này cho một đời có tên / rõ ràng:

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

Tôi thấy khá rõ rằng lỗi được trình biên dịch ngăn chặn là lỗi sử dụng sau tham chiếu được gán cho x: sau khi phạm vi bên trong được thực hiện, fdo đó &f.xtrở nên không hợp lệ và không nên được gán cho x.

Vấn đề của tôi là vấn đề có thể dễ dàng được phân tích mà không sử dụng thời gian tồn tại rõ ràng 'a bằng cách suy ra việc gán bất hợp pháp một tham chiếu cho phạm vi rộng hơn ( x = &f.x;).

Trong trường hợp nào là thời gian sống rõ ràng thực sự cần thiết để ngăn ngừa lỗi sử dụng sau khi sử dụng (hoặc một số lớp khác?)?


1
Điều này đã được đăng chéo lên Reddit
Người quản lý

2
Đối với những độc giả tương lai của câu hỏi này, xin lưu ý rằng nó liên kết đến phiên bản đầu tiên của cuốn sách và giờ đây là phiên bản thứ hai :)
carols10cents

Câu trả lời:


205

Tất cả các câu trả lời khác đều có điểm nổi bật ( ví dụ cụ thể của fjh là cần có thời gian sống rõ ràng ), nhưng lại thiếu một điều quan trọng: tại sao cần có thời gian sống rõ ràng khi trình biên dịch sẽ cho bạn biết bạn đã hiểu sai ?

Đây thực sự là cùng một câu hỏi như "tại sao các loại rõ ràng cần thiết khi trình biên dịch có thể suy ra chúng". Một ví dụ giả thuyết:

fn foo() -> _ {  
    ""
}

Tất nhiên, trình biên dịch có thể thấy rằng tôi đang trả về a &'static str, vậy tại sao lập trình viên phải gõ nó?

Lý do chính là trong khi trình biên dịch có thể thấy mã của bạn làm gì, nó không biết ý định của bạn là gì.

Các chức năng là một ranh giới tự nhiên để tường lửa ảnh hưởng của việc thay đổi mã. Nếu chúng ta cho phép các vòng đời được kiểm tra hoàn toàn từ mã, thì một thay đổi trông có vẻ vô tội có thể ảnh hưởng đến các vòng đời, sau đó có thể gây ra lỗi trong một chức năng ở xa. Đây không phải là một ví dụ giả thuyết. Theo tôi hiểu, Haskell có vấn đề này khi bạn dựa vào suy luận kiểu cho các hàm cấp cao nhất. Rust đã xử lý vấn đề đặc biệt đó trong nụ.

Ngoài ra còn có một lợi ích hiệu quả cho trình biên dịch - chỉ các chữ ký hàm cần được phân tích cú pháp để xác minh các loại và tuổi thọ. Quan trọng hơn, nó có một lợi ích hiệu quả cho lập trình viên. Nếu chúng ta không có tuổi thọ rõ ràng, chức năng này sẽ làm gì:

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

Không thể nói mà không kiểm tra nguồn, điều này sẽ đi ngược lại một số lượng lớn các thực tiễn tốt nhất về mã hóa.

bằng cách suy ra việc chuyển nhượng bất hợp pháp một tham chiếu đến phạm vi rộng hơn

Phạm vi cuộc sống, về cơ bản. Rõ ràng hơn một chút, thời gian tồn tại 'alà một tham số trọn đời chung có thể được chuyên môn hóa với một phạm vi cụ thể tại thời gian biên dịch, dựa trên trang web cuộc gọi.

thời gian sống rõ ràng có thực sự cần thiết để ngăn ngừa lỗi [...] không?

Không có gì. Thời gian sống là cần thiết để ngăn ngừa lỗi, nhưng cuộc sống rõ ràng là cần thiết để bảo vệ những gì lập trình viên ít tỉnh táo có.


18
@jco Hãy tưởng tượng bạn có một số chức năng cấp cao nhất f x = x + 1mà không có chữ ký loại mà bạn đang sử dụng trong một mô-đun khác. Nếu sau này bạn thay đổi định nghĩa thành f x = sqrt $ x + 1, loại của nó sẽ thay đổi từ Num a => a -> athành Floating a => a -> a, điều này sẽ gây ra lỗi loại tại tất cả các trang web cuộc gọi fđược gọi với ví dụ như một Intđối số. Có một chữ ký loại đảm bảo rằng lỗi xảy ra cục bộ.
fjh

11
"Phạm vi là thời gian sống, về cơ bản. Rõ ràng hơn một chút, trọn đời 'a là một tham số trọn đời chung có thể được chuyên môn hóa với một phạm vi cụ thể tại thời điểm cuộc gọi." Wow đó là một điểm sáng thực sự tuyệt vời. Tôi muốn nó nếu nó được bao gồm trong cuốn sách này rõ ràng.
corazza

2
@fjh Cảm ơn. Chỉ để xem nếu tôi mò mẫm nó - vấn đề là nếu loại được nêu rõ ràng trước khi thêm sqrt $, chỉ xảy ra lỗi cục bộ sau khi thay đổi và không có nhiều lỗi ở những nơi khác (sẽ tốt hơn nhiều nếu chúng tôi không 't muốn thay đổi loại thực tế)?
corazza

5
@jco Chính xác. Không chỉ định loại có nghĩa là bạn có thể vô tình thay đổi giao diện của hàm. Đó là một trong những lý do mà nó được khuyến khích mạnh mẽ để chú thích tất cả các mục cấp cao nhất trong Haskell.
fjh

5
Ngoài ra nếu một hàm nhận được hai tham chiếu và trả về một tham chiếu thì đôi khi nó có thể trả về tham chiếu đầu tiên và đôi khi là tham chiếu thứ hai. Trong trường hợp này, không thể suy ra cả đời cho tham chiếu được trả về. Cuộc sống rõ ràng giúp tránh / làm rõ một tình huống như vậy.
MichaelMoser

93

Chúng ta hãy xem ví dụ sau đây.

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)
    };
}

Ở đây, cuộc sống rõ ràng là quan trọng. Điều này biên dịch bởi vì kết quả foocó cùng thời gian tồn tại với đối số đầu tiên ( 'a), vì vậy nó có thể tồn tại lâu hơn đối số thứ hai của nó. Điều này được thể hiện bằng tên trọn đời trong chữ ký của foo. Nếu bạn chuyển các đối số trong cuộc gọi sang footrình biên dịch sẽ phàn nàn rằng ynó không đủ sống:

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

Chú thích trọn đời trong cấu trúc sau:

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

xác định rằng một Foocá thể không nên tồn tại lâu hơn tham chiếu mà nó chứa ( xtrường).

Ví dụ bạn đi qua trong cuốn sách Rust không minh họa điều này vì fybiến đi ra khỏi phạm vi cùng một lúc.

Một ví dụ tốt hơn sẽ là:

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);
}

Bây giờ, fthực sự tồn tại lâu hơn các biến được chỉ bởi f.x.


9

Lưu ý rằng không có tuổi thọ rõ ràng trong đoạn mã đó, ngoại trừ định nghĩa cấu trúc. Trình biên dịch hoàn toàn có thể suy ra tuổi thọ trong main().

Tuy nhiên, trong định nghĩa loại, tuổi thọ rõ ràng là không thể tránh khỏi. Ví dụ, có một sự mơ hồ ở đây:

struct RefPair(&u32, &u32);

Chúng nên là những kiếp sống khác nhau hay chúng nên giống nhau? Nó không quan trọng từ quan điểm sử dụng, struct RefPair<'a, 'b>(&'a u32, &'b u32)là rất khác nhau từ struct RefPair<'a>(&'a u32, &'a u32).

Bây giờ, đối với các trường hợp đơn giản, như trường hợp bạn cung cấp, trình biên dịch về mặt lý thuyết có thể kéo dài thời gian sống như ở những nơi khác, nhưng các trường hợp như vậy rất hạn chế và không có giá trị phức tạp thêm trong trình biên dịch, và sự rõ ràng này sẽ ở rất ít nghi vấn


2
Bạn có thể giải thích tại sao chúng rất khác nhau?
AB

@AB Thứ hai yêu cầu cả hai tài liệu tham khảo chia sẻ cùng một đời. Điều này có nghĩa là ref ref.1 không thể sống lâu hơn ref ref.2 và ngược lại - vì vậy cả hai ref đều cần trỏ đến một cái gì đó có cùng chủ sở hữu. Tuy nhiên, lần đầu tiên chỉ yêu cầu RefPair tồn tại cả hai phần của nó.
llogiq

2
@AB, nó biên dịch vì cả hai tuổi thọ đều thống nhất - vì tuổi thọ cục bộ nhỏ hơn 'static, 'staticcó thể được sử dụng ở mọi nơi có thể sử dụng tuổi thọ địa phương, do đó, trong ví dụ của bạn psẽ có tham số trọn đời được suy ra là tuổi thọ cục bộ y.
Vladimir Matveev

5
@AB RefPair<'a>(&'a u32, &'a u32)có nghĩa là đó 'asẽ là giao điểm của cả hai vòng đời đầu vào, tức là trong trường hợp này là thời gian tồn tại của y.
fjh

1
@llogiq "yêu cầu RefPair tồn tại cả hai phần của nó"? Tôi mặc dù nó ngược lại ... a & u32 vẫn có thể có ý nghĩa nếu không có RefPair, trong khi một RefPair với refs chết của nó sẽ là lạ.
qed

6

Các trường hợp từ cuốn sách rất đơn giản bởi thiết kế. Chủ đề của cuộc sống được coi là phức tạp.

Trình biên dịch không thể dễ dàng suy ra thời gian tồn tại trong một hàm có nhiều đối số.

Ngoài ra, thùng tùy chọn của riêng tôi có một OptionBoolloại với một as_slicephương thức có chữ ký thực sự là:

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

Hoàn toàn không có cách nào trình biên dịch có thể tìm ra cái đó.


IINM, suy ra thời gian tồn tại của kiểu trả về của hàm hai đối số sẽ tương đương với vấn đề tạm dừng - IOW, không thể quyết định trong một khoảng thời gian hữu hạn.
dstromberg

4

Tôi đã tìm thấy một lời giải thích tuyệt vời khác ở đây: http://doc.rust-lang.org/0.12.0/guide-lifetimes.html#retinating-references .

Nói chung, chỉ có thể trả về các tham chiếu nếu chúng được dẫn xuất từ ​​một tham số cho thủ tục. Trong trường hợp đó, kết quả con trỏ sẽ luôn có cùng thời gian tồn tại với một trong các tham số; tuổi thọ được đặt tên cho biết đó là tham số nào.


4

Nếu một hàm nhận hai tham chiếu làm đối số và trả về một tham chiếu, thì việc triển khai hàm đôi khi có thể trả về tham chiếu đầu tiên và đôi khi là tham chiếu thứ hai. Không thể dự đoán tham chiếu nào sẽ được trả về cho một cuộc gọi nhất định. Trong trường hợp này, không thể suy ra thời gian tồn tại của tham chiếu được trả về, vì mỗi tham chiếu đối số có thể tham chiếu đến một ràng buộc biến khác nhau với thời gian tồn tại khác nhau. Cuộc sống rõ ràng giúp tránh hoặc làm rõ một tình huống như vậy.

Tương tự, nếu một cấu trúc chứa hai tham chiếu (dưới dạng hai trường thành viên) thì chức năng thành viên của cấu trúc đôi khi có thể trả về tham chiếu đầu tiên và đôi khi là tham chiếu thứ hai. Một lần nữa cuộc sống rõ ràng ngăn chặn sự mơ hồ như vậy.

Trong một vài tình huống đơn giản, có cuộc bầu chọn trọn đời trong đó trình biên dịch có thể suy ra thời gian sống.


1

Lý do tại sao ví dụ của bạn không hoạt động đơn giản là vì Rust chỉ có thời gian tồn tại cục bộ và kiểu suy luận. Những gì bạn đang đề nghị đòi hỏi suy luận toàn cầu. Bất cứ khi nào bạn có một tài liệu tham khảo mà cuộc đời của họ không thể bị xóa bỏ, nó phải được chú thích.


1

Là một người mới đến với Rust, sự hiểu biết của tôi là thời gian sống rõ ràng phục vụ hai mục đích.

  1. Đặt một chú thích trọn đời rõ ràng vào một hàm sẽ hạn chế loại mã có thể xuất hiện bên trong hàm đó. Tuổi thọ rõ ràng cho phép trình biên dịch đảm bảo rằng chương trình của bạn đang làm những gì bạn dự định.

  2. Nếu bạn (trình biên dịch) muốn (các) trình kiểm tra xem một đoạn mã có hợp lệ không, bạn (trình biên dịch) sẽ không phải lặp đi lặp lại bên trong mỗi hàm được gọi. Nó đủ để xem các chú thích của các hàm được gọi trực tiếp bởi đoạn mã đó. Điều này làm cho chương trình của bạn dễ dàng hơn nhiều để lý do cho bạn (trình biên dịch) và làm cho thời gian biên dịch có thể quản lý được.

Ở điểm 1., hãy xem xét chương trình sau được viết bằng Python:

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()

cái nào sẽ in

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

Kiểu hành vi này luôn làm tôi ngạc nhiên. Điều đang xảy ra là dfchia sẻ bộ nhớ ar, vì vậy khi một số nội dung dfthay đổi work, sự thay đổi đó arcũng bị ảnh hưởng. Tuy nhiên, trong một số trường hợp, đây có thể là chính xác những gì bạn muốn, vì lý do hiệu quả bộ nhớ (không có bản sao). Vấn đề thực sự trong mã này là hàm second_rowđang trả về hàng đầu tiên thay vì hàng thứ hai; chúc may mắn gỡ lỗi đó.

Thay vào đó hãy xem xét một chương trình tương tự được viết bằng 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);
}

Biên dịch này, bạn nhận được

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> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^

Trong thực tế, bạn nhận được hai lỗi, cũng có một lỗi với vai trò 'a'bthay thế cho nhau. Nhìn vào chú thích của second_row, chúng ta thấy rằng đầu ra phải là &mut &'b mut [i32], nghĩa là đầu ra được coi là một tham chiếu đến một tham chiếu có thời gian tồn tại 'b(thời gian tồn tại của hàng thứ hai Array). Tuy nhiên, vì chúng tôi đang trả về hàng đầu tiên (có thời gian tồn tại 'a), trình biên dịch phàn nàn về sự không phù hợp trọn đời. Đúng nơi. Vào đúng thời điểm. Gỡ lỗi là một làn gió.


0

Tôi nghĩ rằng một chú thích trọn đời như một hợp đồng về một ref đã cho chỉ có hiệu lực trong phạm vi nhận trong khi nó vẫn còn hiệu lực trong phạm vi nguồn. Tuyên bố nhiều tài liệu tham khảo trong cùng loại kết hợp các phạm vi, có nghĩa là tất cả các giới thiệu nguồn phải đáp ứng hợp đồng này. Chú thích như vậy cho phép trình biên dịch kiểm tra việc thực hiện hợp đồng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.