prelude 的补充

摘要

  • TryIntoTryFromFromIterator trait 现在是 prelude 的一部分了。
  • 这可能会使得对 trait 方法的调用变得含糊不清,从而导致某些代码编译失败。

详情

标准库的 prelude 是一个模块,它包含了在每个模块中自动导入的所有内容。它包含了一些常用项,例如 OptionVecdropClone

Rust 编译器优先考虑手动导入的项,而不是来自 prelude 的项,以确保向 prelude 添加内容不会破坏任何现有代码。例如,如果你有一个名为 example 的 crate 或模块,其中包含一个 pub struct Option;,那么 use example::*; 会使得 Option 明确地指向来自 example 的那个,而不是标准库中的那个。

然而,将 trait 添加到 prelude 会以一种微妙的方式破坏现有代码。例如,如果 stdTryInto 也被导入,来自 MyTryInto trait 的 x.try_into() 调用可能会编译失败,因为对 try_into 的调用现在变得含糊不清,可能来自任何一个 trait。这就是我们尚未将 TryInto 添加到 prelude 的原因,因为有很多代码会因此而损坏。

作为解决方案,Rust 2021 将使用一个新的 prelude。它与当前的版本相同,只是新增了三个 trait

跟踪的 Issue 可以在这里找到

迁移

作为 2021 版本的一部分,新增了一个迁移 lint,rust_2021_prelude_collisions,以帮助将 Rust 2018 代码库自动迁移到 Rust 2021。

为了将你的代码迁移到兼容 Rust 2021 版本,运行

cargo fix --edition

该 lint 会检测函数或方法的调用,这些函数或方法的名称与新增 prelude trait 中定义的方法名称相同的情况。在某些情况下,它可能会以各种方式重写你的调用,以确保你继续调用之前使用的同一函数。

如果你想手动迁移代码或更好地理解 cargo fix 在做什么,我们在下面概述了需要迁移的情况以及不需要迁移的反例。

需要迁移的情况

冲突的 trait 方法

当两个在作用域内的 trait 具有相同的方法名称时,使用哪个 trait 方法会变得含糊不清。例如

trait MyTrait<A> {
  // This name is the same as the `from_iter` method on the `FromIterator` trait from `std`.  
  fn from_iter(x: Option<A>);
}

impl<T> MyTrait<()> for Vec<T> {
  fn from_iter(_: Option<()>) {}
}

fn main() {
  // Vec<T> implements both `std::iter::FromIterator` and `MyTrait` 
  // If both traits are in scope (as would be the case in Rust 2021),
  // then it becomes ambiguous which `from_iter` method to call
  <Vec<i32>>::from_iter(None);
}

我们可以通过使用完全限定语法来修复这个问题

fn main() {
  // Now it is clear which trait method we're referring to
  <Vec<i32> as MyTrait<()>>::from_iter(None);
}

dyn Trait 对象上的固有方法

一些用户在 dyn Trait 值上调用方法,该方法的名称与新的 prelude trait 名称重叠

#![allow(unused)]
fn main() {
mod submodule {
  pub trait MyTrait {
    // This has the same name as `TryInto::try_into`
    fn try_into(&self) -> Result<u32, ()>;
  }
}

// `MyTrait` isn't in scope here and can only be referred to through the path `submodule::MyTrait`
fn bar(f: Box<dyn submodule::MyTrait>) {
  // If `std::convert::TryInto` is in scope (as would be the case in Rust 2021),
  // then it becomes ambiguous which `try_into` method to call
  f.try_into();
}
}

与静态分发方法不同,在 trait 对象上调用 trait 方法不需要该 trait 处于作用域内。上述代码在作用域内没有具有冲突方法名称的 trait 时可以工作。当 TryInto trait 处于作用域内时(在 Rust 2021 中就是这种情况),这会导致含糊不清。调用应该是 MyTrait::try_into 还是 std::convert::TryInto::try_into

在这些情况下,我们可以通过添加额外的解引用或以其他方式澄清方法接收者的类型来修复。这确保了选择 dyn Trait 方法,而不是 prelude trait 中的方法。例如,将上面的 f.try_into() 变成 (&*f).try_into() 确保我们正在对 dyn MyTrait 调用 try_into,这只能引用 MyTrait::try_into 方法。

不需要迁移的情况

固有方法

许多类型定义了自己的固有方法,其名称与 trait 方法相同。例如,下面的结构体 MyStruct 实现了 from_iter,它与标准库中 trait FromIterator 中的方法名称相同

#![allow(unused)]
fn main() {
use std::iter::IntoIterator;

struct MyStruct {
  data: Vec<u32>
}

impl MyStruct {
  // This has the same name as `std::iter::FromIterator::from_iter`
  fn from_iter(iter: impl IntoIterator<Item = u32>) -> Self {
    Self {
      data: iter.into_iter().collect()
    }
  }
}

impl std::iter::FromIterator<u32> for MyStruct {
    fn from_iter<I: IntoIterator<Item = u32>>(iter: I) -> Self {
      Self {
        data: iter.into_iter().collect()
      }
    }
}
}

固有方法始终优先于 trait 方法,因此无需进行任何迁移。

实现参考

lint 在确定将 2021 版本引入代码库是否会导致名称解析冲突(从而在更改版本后破坏代码)时需要考虑几个因素。这些因素包括

  • 调用是完全限定调用还是使用点调用方法语法
    • 这会影响由于方法调用语法上的自动引用和自动解引用而导致名称如何解析。手动解引用/引用可以在点调用方法语法的情况下指定优先级,而完全限定调用需要在方法路径中指定类型和 trait 名称(例如 <Type as Trait>::method)。
  • 这是固有方法还是trait 方法
    • 接受 self 的固有方法将优先于 TryInto::try_into,因为固有方法优先于 trait 方法,但接受 &self&mut self 的固有方法由于需要自动引用而不会优先(而 TryInto::try_into 不需要,因为它接受 self)。
  • 该方法的来源是否来自 core/std?(因为 trait 不会与自身冲突)
  • 给定的类型是否实现了可能与其冲突的 trait?
  • 方法是否通过动态分发调用?(即 self 类型是否为 dyn Trait
    • 如果是,trait 导入不会影响解析,并且不需要发生迁移 lint。