• Rust中为何要抛弃继承的特性?
  • 发布于 2个月前
  • 475 热度
    0 评论
  • 遥歌
  • 2 粉丝 55 篇博客
  •   
在 Rust 中,函数和方法是编写代码的基础,它们在某些方面是类似的,但在本质上有重要的不同。在本文中,我们将探讨这些区别,并解释方法调用的本质及其与普通函数调用的联系,以及对Rust面向对象的思考。

函数 Vs 方法
首先,定义一个函数和一个方法:

函数(Function)
函数是一个独立的代码块,它可以接受参数、进行计算并返回一个值。函数不绑定到任何对象或实例。 例子:
fn add(a: i32, b: i32) -> i32 {
    a + b
}
上例中,add 是一个简单的函数,接受两个整数并返回它们的和。

方法(Method)
方法实际上是绑定到对象的函数。在 Rust 中,这通常是通过实现某个类型的 impl 块来定义方法。方法可以访问对象的数据。 例子:
struct Point {
    x: i32,
    y: i32,
}

impl Point {
    fn move_by(&mut self, dx: i32, dy: i32) {
        self.x += dx;
        self.y += dy;
    }
}
在这个例子中,move_by 是一个方法,它绑定到 Point 结构体的实例上,并能修改实例的 x 和 y 字段。

主要区别
1.函数是独立的,而方法是绑定到结构体、枚举或特质(trait)实例的。

2.方法拥有一个叫 self 的特殊参数,代表所绑定的实例。


方法调用的本质
当你调用一个方法时,你写的代码类似于 instance.method(args...),例如 point.move_by(1, 1)。然而,这种写法其实是一种语法糖。Rust 编译器在内部会将这种方法调用转换为一种更类似于函数的形式。

脱糖
让我们回顾一下 Point 的方法 move_by。当你写下如下代码时:
let mut point = Point { x: 0, y: 0 };
point.move_by(1, 1);
Rust 在编译时实际上处理的是下面这种形式:
let mut point = Point { x: 0, y: 0 };
Point::move_by(&mut point, 1, 1);
这里,Point::move_by 函数的第一个参数是 &mut self,也就是 &mut point,表示对 point 的可变引用,允许方法修改 point 的状态。

为什么要有方法?
这样的设计有几个好处:
封装:方法可以操作内部状态而无需暴露给调用者。
简洁性:方法允许我们对操作进行逻辑分组,即将与某种数据类型紧密相关的函数放在一起。

易用性:调用方法比传统函数更直观,因为它们通过点操作符直接关联到特定的对象。


OOP的思考
Rust对面向对象的支持并不是全面的,从Java相比主要有三个区别:
1.不支持继承:类型是平行兄弟关系,并不存在上下继承关系
j2.ava的Interface可以作为类型,而Trait并不是类型
3.java提供class关键字,而Rust只有impl关键字提供绑定方法的语义。
从上面的讨论得知,其实方法的就是Rust提供了对象的语义,提供了封装的解决方案。

在继承关系方面,比如你写出 object.somefun() ,站在java的角度看,这个somefun方法,可能位于对应class本身,也可能是存在其父系所有超类中,所以你可以想象认为关于函数的寻址就是一个动态的过程,在一个层级类型体系中搜索。所以每个object都会拥有其类型信息,而类型信息由有继承信息。

我们暂且不太累这种把重用代码、方法分散在层级体系中的做法,带来哪些不好的影响。

Rust中 impl{} 语法,提供了一个极其简单的语义,就是绑定一组函数(如果函数带self,那么就是方法)。绑定一组函数到指定的类型(结构体、枚举等)上,也可以是绑定到泛型类型。

当用点语法进行方法方法调用时,函数的寻址就在编译器期间就完成(dyn trait除外),而且由于没有层级结构,这种绑定是清晰明了的。

不过为了简化代码,Rust不得不引入一些隐式操作,比如强制解引用多态等等,其本质上还是编译器在间接函数寻址。比如Vec、String类型上的可以直接调用的方法,本质上是Slice类型提供的,这种代码重用方式,并没有使用类型层次结构,也能同样达到很好的效果。

总结
我使用Java开发程序超过20年,OOP的思想早就深深刻在脑海里,所以在初学Rust时,带着OOP的思维方式学习Rust,时时刻刻感受到别扭。要学好rust必须学会Rust的思考方式,还要“忘记”OOP。
用户评论