序言的补充

概要

  • TryIntoTryFromFromIterator 特性现在是序言的一部分。
  • 这可能会使对特性方法的调用产生歧义,从而导致一些代码编译失败。

详细信息

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

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

然而,向序言添加特性可能会以一种微妙的方式破坏现有代码。例如,如果也导入了 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