Rust編譯器比其他語言更能捕獲隱藏的錯誤 - kerkour

banq發表於2022-05-07

讓我們看看現代編譯器和型別系統如何幫助防止許多錯誤,從而幫助提高每個人的安全性並降低軟體生產和維護的成本。

資源洩露
很容易忘記關閉檔案或連線:

resp, err := http.Get("http://kerkour.com")
if err != nil {
    // ...
}
// defer resp.Body.Close() // DON'T forget this line

另一方面,Rust 強制執行RAII(資源獲取即初始化),這使得洩漏資源幾乎是不可能的:它們在被丟棄時會自動關閉。

  let wordlist_file = File::open("wordlist.txt")?;
  // do something...

  // we don't need to close wordlist_file
  // it will be closed when the variable goes out of scope


未釋放的互斥鎖
看看這個 Go 程式碼:

type App struct {
  mutex sync.Mutex
  data  map[string]string
}

func (app *App) DoSomething(input string) {
  app.mutex.Lock()
  defer app.mutex.Unlock()
  // do something with data and input
}

到現在為止還挺好。但是當我們想要處理許多專案時,事情可能會很快變得非常糟糕

func (app *App) DoManyThings(input []string) {
  for _, item := range input {
      app.mutex.Lock()
      defer app.mutex.Unlock()
      // do something with data and item
  }
}

我們剛剛建立了一個死鎖,因為互斥鎖沒有在預期的時候釋放,而是在函式結束時釋放。

同樣,Rust 中的 RAII 有助於防止未釋放的互斥鎖:

for item in input {
  let _guard = mutex.lock().expect("locking mutex");
  // do something
  // mutex is released here as _guard is dropped
}


缺少Switch
假設我們正在跟蹤線上商店中產品的狀態:

const (
  StatusUnknown   Status = 0
  StatusDraft     Status = 1
  StatusPublished Status = 2
)

switch status {
    case StatusUnknown:
        // ...
    case StatusDraft:
        // ...
    case StatusPublished:
        // ...
}

但是,如果我們新增了StatusArchived Status = 3變數而忘記更新這條switch語句,編譯器仍然很樂意接受程式並讓我們引入一個錯誤。

在 Rust 中,非窮舉match會產生編譯時錯誤:

#[derive(Debug, Clone, Copy)]
enum Platform {
    Linux,
    MacOS,
    Windows,
    Unknown,
}

impl fmt::Display for Platform {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Platform::Linux => write!(f, "Linux"),
            Platform::Macos => write!(f, "macOS"),
            // Compile time error! We forgot Windows and Unknown
        }
    }
}


無效的指標取消引用
據我所知,不可能在安全的 Rust 中建立對無效地址的引用。

type User struct {
    // ...
    Foo *Bar // is it intended to be used a a pointer, or as an optional field?
}

甚至更好的是,因為 Rust 有Option列舉,你不必使用null指標來表示不存在的東西。

struct User {
    // ...
    foor: Option<Bar>, // it's clear that this field is optional
}


未初始化的變數
假設我們正在處理使用者帳戶:

type User struct {
  ID          uuid.UUID
  CreatedAt   time.Time
  UpdatedAt time.Time
  Email       string
}

func (app *App) CreateUser(email string) {
    // ...
    now := time.Now().UTC()

    user := User {
      ID: uuid.New(),
      CreatedAt: now,
      UpdatedAt: now,
      Email: email,
    }
    err = app.repository.CreateUser(app.db, user)
    // ...
}

很好,但是現在,我們需要將欄位新增AllowedStorage int64到User結構中。

如果我們忘記更新CreateUser函式,編譯器仍然會愉快地接受程式碼而不做任何更改並使用int64:的預設值0,這可能不是我們想要的。

而下面的 Rust 程式碼

struct User {
  id: uuid::Uuid,
  created_at: DateTime<Utc>,
  updated_at: DateTime<Utc>,
  email: String,
  allowed_storage: i64,
}

fn create_user(email: String) {
    let user = User {
      id: uuid::new(),
      created_at: now,
      updated_at: now,
      email: email,
      // we forgot to update the function to initialize allowed_storage
    };
}

產生一個編譯時錯誤,阻止我們在腳下開槍。

未處理的異常和錯誤
這聽起來可能很愚蠢,但如果你沒有異常,你就不能有未處理的異常......

panic!()Rust 中存在,但這不是處理可恢復錯誤的方式。

因此,透過強制程式設計師處理每個錯誤(或編譯器拒絕編譯程式),同時提供符合人體工程學的工具來處理錯誤(Result列舉和?運算子),Rust 編譯器有助於防止大多數(如果不是全部) ) 與錯誤處理相關的錯誤。

資料競賽
由於Sync和Send特性,Rust 的編譯器可以靜態斷言不會發生資料競爭。

它是如何工作的?您可以在Jason McCampbell的這篇精彩文章中瞭解更多資訊。

隱藏的流
在 Go 中,資料流隱藏在io.Writer介面後面。一方面,它可以簡化它們的使用。另一方面,當與我們不希望成為流的型別一起使用時,它可以保留一些驚喜,bytes.Buffer例如。

這正是一個月前發生在我身上的事情: abytes.Buffer在迴圈中被重用以呈現模板,這導致模板被附加到緩衝區而不是要清理和重用的緩衝區。

這在 Rust 中永遠不會發生,因為Streams是一種非常特殊的型別,並且永遠不會在這種情況下使用。

相關文章