前奏的添加

总结

  • TryIntoTryFromFromIterator 特征现在是前奏的一部分。
  • 这可能会导致对特征方法的调用产生歧义,从而导致某些代码无法编译。

详情

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

Rust 编译器优先考虑任何手动导入的项,而不是来自前奏的项,以确保对前奏的添加不会破坏任何现有代码。例如,如果您有一个名为 example 的 crate 或模块,其中包含 pub struct Option;,则 use example::*; 将使 Option 明确地引用 example 中的 Option,而不是标准库中的 Option

但是,向前奏添加特征可能会以一种微妙的方式破坏现有代码。例如,如果也导入了 stdTryInto,则对来自 MyTryInto 特征的 x.try_into() 的调用可能会失败,因为对 try_into 的调用现在不明确,并且可能来自任一特征。这就是我们还没有将 TryInto 添加到前奏的原因,因为有很多代码会以这种方式被破坏。

作为解决方案,Rust 2021 将使用新的前奏。它与当前的前奏相同,只是增加了三个新内容

跟踪问题可以在这里找到

迁移

作为 2021 版本的一部分,已添加迁移 lint rust_2021_prelude_collisions,以帮助将 Rust 2018 代码库自动迁移到 Rust 2021。

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

cargo fix --edition

该 lint 会检测调用了与新前奏特征之一中定义的方法同名函数或方法的情况。在某些情况下,它可能会以各种方式重写您的调用,以确保您继续调用与以前相同的函数。

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

需要迁移

冲突的特征方法

当范围内的两个特征具有相同的 方法名称时,使用哪个特征方法不明确。例如

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 值上调用方法,其中方法名称与新的前奏特征重叠

#![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();
}
}

与静态调度方法不同,在特征对象上调用特征方法不需要该特征在范围内。只要范围内没有具有冲突方法名称的特征,上面的代码就可以工作。当 TryInto 特征在范围内时(在 Rust 2021 中就是这种情况),这会导致歧义。调用应该是 MyTrait::try_into 还是 std::convert::TryInto::try_into

在这些情况下,我们可以通过添加额外的解引用或以其他方式阐明方法接收者的类型来解决这个问题。这确保了选择 dyn Trait 方法,而不是来自前奏特征的方法。例如,将上面的 f.try_into() 转换为 (&*f).try_into() 确保我们在 dyn MyTrait 上调用 try_into,它只能引用 MyTrait::try_into 方法。

不需要迁移

固有方法

许多类型使用与特征方法相同的名称定义自己的固有方法。例如,下面的结构体 MyStruct 实现了 from_iter,它与标准库中的特征 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()
      }
    }
}
}

固有方法始终优先于特征方法,因此不需要任何迁移。

实现参考

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

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