首页 > 其他分享 >译文 | Rust 中无法办到的事情(以及如何替代)

译文 | Rust 中无法办到的事情(以及如何替代)

时间:2024-04-25 16:45:24浏览次数:23  
标签:https 办到 self Cat fn 译文 tell Rust

译者:RivTian

原文:Here

作为 Rust subreddit的主持人,我经常发布有关开发人员试图将其各自的语言范例转换为 Rust 的帖子,结果不一而足,取得了不同程度的成功。 在本指南中,我将描述开发人员在将其他语言范例转换为 Rust 时遇到的一些问题,并提出一些替代解决方案来帮助你客服 Rust 的局限性。

Rust 中的继承

可以说,继承是在面向对象语言中被问到最多的缺失特性。为什么 Rust 不让一个结构 (struct) 继承另一个结构呢?

你可以肯定地说,即使在 OO 世界中,继承的名声也没好到哪里去,而且实践者通常尽可能地喜欢组合(composition )。但是你也可以认为,允许类型以不同的方式执行方法可能会提高性能,因此对于那些特定的实例来说是可取的。

这是一个来自 Java 的经典示例:

interface Animal {
    void tell();
    void pet();
    void feed(Food food);
}class Cat implements Animal {
    public void tell() { System.out.println("Meow"); }
    public void pet() { System.out.println("purr"); }
    public void feed(Food food) { System.out.println("lick"); }
}// this implementation is probably too optimistic...
class Lion extends Cat {
    public void tell() { System.out.println("Roar"); }
}

对于 Rust,第一部分可以用 traits 实现:

#![allow(unused)]
fn main() {
	trait Animal {
	    fn tell(&self);
	    fn pet(&mut self);
	    fn feed(&mut self, food: Food);
	}
	
	struct Cat;impl Animal for Cat {
	    fn tell(&self) { println!("Meow"); }
	    fn pet(&mut self) { println!("purr");
	    fn feed(&mut self, food: Food) { println!("lick"); }
	}
}

但第二部分并没用这么容易:


#![allow(unused)]
fn main() {
	struct Lion;
	impl Animal for Lion {
	    fn tell(&self) { println!("Roar"); }
	    // Error: Missing methods pet and feed
	    // 错误: 缺少 `pet` 和 `feed` 方法
	}
}

显然,最简单的方法是复制这些方法。是的,重复是不好的。这样也会使得代码更加复杂。如果你需要代码复用的话, 不妨把这些方法抽出来, 在 Cat 和 Lion 中调用它们。

但是,你也许会察觉到,如何实现 OO 中的多态性部分呢?这就是复杂的地方。面向对象语言通常给你提供动态转发,而 Rust 让你在静态和动态分发中做出选择,不管选择哪一种都有失有得。

#![allow(unused)]
fn main() {
	// static dispatch
	// 静态分发
	let cat = Cat;
	cat.tell();let lion = Lion;
	lion.tell();// dynamic dispatch via enum // 通过enum 进行动态分发
	enum AnyAnimal {
	   Cat(Cat),
	   Lion(Lion),
	}
	// `impl Animal for AnyAnimal` left as an exercise for the readerlet animals = [AnyAnimal::Cat(cat), AnyAnimal::Lion(lion)];
	for animal in animals.iter() {
	   animal.tell();
	}
	// dynamic dispatch via "fat" pointer including vtable
	// 动态分发通过`胖` 指针来实现
	let animals = [&cat as &dyn Animal, &lion as &dyn Animal];
	for animal in animals.iter() {
	   animal.tell();
	}
}

译者注: 动态分发参见 #【译】探讨Rust中的动态分发(dynamic dispatch)# Exploring Dynamic Dispatch in Rust

注意,与垃圾收集语言不同的是,在 ( Rust 中) 每个变量在编译时必须有一个具体的类型。此外,对于 enum 的情况,使用进行委托 trait 的实现是冗长乏味的,但是像 ambassador[1] 这样的 crates 可以提供帮助。

将函数委托给成员的一种相当 hacky 的方法是使用 Deref trait for polymorphism,这样在 derefee 上可以直接调用Deref` 目标定义的函数。但是请注意,这通常被认为是一种反模式。

最后,可以为所有实现许多其他特性之一的类实现一个 trait,但它需要专门化,这是目前的一个 nightly 特性(尽管有一个可用的解决方案 workaround[2],如果你不想写出所需的所有样板代码,可以把他们打包在一个macro crate 中)。trait 很可能是相互继承的,尽管它们只规定行为,而不是数据。

链表或者其他基于指针的数据结构

许多从 C++ 来到 Rust 的人一开始会想实现一个 “简单的” 双向链表,但很快就会发现它远非 简单。这是因为 Rust 想要明确所有权,因此双向列表需要对指针和引用进行相当复杂的处理。

一个新手可能会尝试写下面的 struct:

#![allow(unused)]
struct MyLinkedList<T> {
    value: T
    previous_node: Option<Box<MyLinkedList<T>>>,
    next_node: Option<Box<MyLinkedList<T>>>,
}

当他们注意到这个方法失败时,他们会添加 Option 和 Box。但是一旦他们尝试实现插入,他们就会感到很惊讶:

impl<T> MyLinkedList<T> {
    fn insert(&mut self, value: T) {
        let next_node = self.next_node.take();
        self.next_node = Some(Box::new(MyLinkedList {
            value,
            previous_node: Some(Box::new(*self)), // Ouch
            next_node,
        }));
    }
}

当然,borrow checker[3] 不会允许这样做。值的所有权完全是混乱的。Box 拥有它所包含的数据,因此列表中每个节点都将由列表中的上一个和下一个节点拥有。Rust 中的每个数据只允许有一个所有者,所以这将至少需要一个 Rc 或 Arc 才能工作。但是即使这样做也会很快变得麻烦,更不用说引用计数带来的开销了。

幸运的是,你不需要自己编写双向链表,因为标准库已经包含了一个(std::collections::LinkedList)。而且,与简单的 Vecs 相比,这种方法可能并不能给你带来好的性能,因此你可能需要相应地进行测试。

如果你真的想写一个双向链表列表,你可以参考 Learning Rust With Entirely Too Many Linked Lists][4] ,它会教会你写链表,并在这个过程中学到很多关于 Unsafe Rust 的知识。

(此外:单列表完全可以用一连串的 box 来构建。实际上,Rust 编译器包含一个实现。)

同样的情况也适用于图结构,尽管你可能需要一个依赖项来处理图数据结构。Petgraph[5] 是目前最流行的,它提供了数据结构和一些图算法。

自引用类型

当面对自引用类型的概念时,很容易会问出: “谁拥有它?”同样,这也是 borrow checker 不乐意听到的关于 ownership的事情。

当你具有所有权关系并希望在一个结构中同时存储所有权对象和被所有的对象时,就会遇到这个问题。天真地尝试一下这个方法,你会有一段艰难的时期去尝试生命周期 (lifetime)。

我们只能猜测,许多 rustacean 已经转向 Unsafe Rust,这很微妙的,并且很容易出错。当然,使用普通指针而不是引用会消除生命周期烦恼,因为指针不会有生命周期(lifetime)的烦恼。但是,这需要手动承担管理生命周期的责任。

幸运的是,有一些 crate 可以采用这种解决方案并提供一个安全的接口,比如 ouroboros[6], self_cell[7] 和 one_self_cell[8] 等 crates。

全局可变状态

来自 C 或 C++ (或是来自动态语言) 的开发者,有时习惯于在他们的代码中创建和修改全局状态( global state )。例如,一位 reddit 用户说:“这是完全安全的,但 Rust 不让你这么做。”

下面是一个稍微简化的例子:

#include <iostream>
int i = 1;int main() {
    std::cout << i;
    i = 2;
    std::cout << i;
}

在 Rust 中,这大致可以理解为:

static I: u32;
fn main() {
	print!("{}", I);
	I = 2; // <- Error: Cannot mutate global state
	print!("{}", I);
}

许多 Rustacean 会告诉你,你并不需要这种全局的状态。当然,在这样一个简单的例子中,这是正确的。但是对于大量的用例,确实需要全局可变状态的时候,例如,在一些嵌入式应用程序中。

当然,有一种方法可以做到这一点,使用 Unsafe Rust。但是在这之前,根据场景的不同,你可能只想使用互斥对象(Mutex)即可。或者,如果可变只需要在初始化时使用一次,那么 OnceCell 或 lazy_static 就可以巧妙地解决这个问题。或者,如果你真的只需要整数,那么 std::sync::Atomic*类型也可以使用。

话虽如此,尤其是在每个字节数和资源通常映射到内存的嵌入式世界中, 拥有一个可变的静态变量通常是首选的解决方案。因此,如果你真的必须这么做,写起来像这样:

static mut DATA_RACE_COUNTER: u32 = 1;
fn main() {
    print!("{}", DATA_RACE_COUNTER);
    // I solemny swear that I'm up to no good, and also single threaded.
    // 我发誓即使是单线程,依然跑不了
    unsafe {
        DATA_RACE_COUNTER = 2;
    }
    print!("{}", DATA_RACE_COUNTER);
}

再次强调,除非真的需要,否则你不应该这样做。如果你想问这是不是一个好主意,答案是否定的。

直接初始化一个数组

新手可能会倾向于声明如下数组:

fn main() {
let array: [usize; 512];for i in 0..512 {
    array[i] = i;
}

这会报错,因为数组从未初始化。然后我们尝试给它赋值,但是没有告诉编译器,它甚至不会为我们在堆栈上保留一个写入的位置。Rust 是这样挑剔,它根据数组的内容来区分数组。此外,在我们读取它们之前,需要对它们进行初始化。

通过初始化 let array = [0usize; 512] ; ,我们以双重初始化为代价来解决这个问题,双重初始化可能会也可能不会得到优化——或者,根据类型的不同,甚至可能是无法实现的。参见 Unsafe Rust: How and when not to use it[9] 的解决方案。

参考资料

[1]ambassador: https://docs.rs/ambassador/0.2.1

[2]workaround: https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md

[3]borrow checker: https://blog.logrocket.com/introducing-the-rust-borrow-checker/

[4] Learn Rust With Entirely Too Many Linked Lists : https://rust-unofficial.github.io/too-many-lists/

[5]Petgraph: https://crates.io/crates/petgraph

[6]oeuroboros: https://docs.rs/ouroboros/0.9.2/ouroboros/

[7]self_cell: https://docs.rs/self_cell/0.8.0/self_cell/

[8]one_self_cell: https://docs.rs/once_self_cell/0.6.3/once_self_cell/

[9]Unsafe Rust: How and when not to use it: https://blog.logrocket.com/unsafe-rust-how-and-when-not-to-use-it/

标签:https,办到,self,Cat,fn,译文,tell,Rust
From: https://www.cnblogs.com/RioTian/p/18158028

相关文章

  • Rust简易入门(六)
    泛型泛型是一种编程语言的特性,它允许在代码中使用参数化类型,以便在不同地方使用相同的代码逻辑处理多种数据类型,而无需为每种类型编写单独的代码!泛型的应用类型泛型定义结构体、枚举泛型定义函数泛型与特质泛型结构体#[derive(Debug)]structPoint<T>{x:T,......
  • Rust简易入门(九)
    闭包的基础概念闭包是一种可以捕获其环境中变量的匿名函数闭包的语法相对简洁灵活,同时也具有强大的功能。闭包在Rust中被广泛用于函数式编程、并发编程以及简化代码等方面。定义闭包的语法类似(但更简单)在|内定义参数可选地指定参数/返回类型在{}内定义闭包体你......
  • Rust简易入门(七)
    迭代与循环循环定义:循环是一种控制流结构,它会反复执行一组语句,直到满足某个条件。控制条件:循环通常包含一个条件表达式,只有在条件为真时,循环体中的语句才会执行。退出条件:循环执行直到条件不再满足,或者通过break语句显式中断循环。使用场景:适用于需要反复执行某个操作直到满......
  • 我为什么学习Rust编程?
    2024-04-24下午,今下午感受到如果要睡觉就应该让音响在自己耳边程序轰炸.翻看gitee,又再次看到这个loop示例,这几天忙于工作,除了偶尔刷下rust视频,rust的电子书,其他的都看得比较少,主要还是感觉工作疲倦之余已经没有精力来继续回顾和延伸.其实后来感觉不是的,就像那会儿接......
  • Rust所有权__Ownership Rules
    First,let’stakealookattheownershiprules.Keeptheserulesinmindaswethroughtheexamplesthatillustratethem:     EachvalueinRusthasanowner.     Therecanonlybeoneowneratatime.     Whentheownergoesoutofsc......
  • 【rust】《Rust深度学习[6]-简单实现逻辑回归(Linfa)》
    什么是LinfaLinfa是一组Rust高级库的集合,提供了常用的数据处理方法和机器学习算法。Linfa对标Python上的 scikit-learn,专注于日常机器学习任务常用的预处理任务和经典机器学习算法,目前Linfa已经实现了scikit-learn中的全部算法。项目结构依赖[package]name="rust-ml-e......
  • 【rust】《Rust深度学习[4]-理解线性网络(Candle)》
    全连接/线性在神经网络中,全连接层,也称为线性层,是一种层,其中来自一层的所有输入都连接到下一层的每个激活单元。在大多数流行的机器学习模型中,网络的最后几层是完全连接的。实际上,这种类型的层执行基于在先前层中学习的特征输出类别预测的任务。全连接层的示例,具有四个输入节点......
  • 【rust】《Rust深度学习[5]-理解卷积神经网络(Candle)》
    卷积神经网络ConvolutionalNeuralNetwork,简称为CNN。CNN与一般的顺传播型神经网络不同,它不仅是由全结合层,还由卷积层(ConvolutionLayer)和池层(PoolingLayer)构成的神经网络。在卷积层和池化层中,如下图所示,缩小输入神经元的一部分区域,局部地与下一层进行对应。每一层都有一个称......
  • 【rust】《Rust深度学习[2]-数据分析和挖掘库(Polars)》
    什么是Polars?Polars是一个用于操作结构化数据的高性能DataFrame库,可以用来进行数据清洗和格式转换、数据分析和统计、数据可视化、数据读取和存储、数据合并和拼接等等,相当于Rust版本的Pandas库。Polars读写数据支持如下:  常见数据文件:csv、parquet(不支持xlsx、json文件) ......
  • 【rust】《Rust深度学习[3]-数据可视化(Plotters)》
    什么是Plotters?Plotters是一个用纯Rust开发的图形库,用于中渲染图形、图表和数据可视化。它支持静态图片渲染和实时渲染,并支持多种后端,包括:位图格式(png、bmp、gif等)、矢量图(svg)、窗口和HTML5Canvas。Plotters对不同后端使用统一的高级API,并允许开发者自定义坐标系。在Plotters......