Rust编译器比其他语言更能捕获隐藏的错误 - kerkour


让我们看看现代编译器和类型系统如何帮助防止许多错误,从而帮助提高每个人的安全性并降低软件生产和维护的成本。

资源泄露
很容易忘记关闭文件或连接:

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是一种非常特殊的类型,并且永远不会在这种情况下使用。