前言

花了十天看完了Rust Programming Language 这本书,觉得真是受益匪浅;还就方法重载、继承之类的OOP特性在社区论坛和大佬们讨论了一下,让我重新思考了一下一些在Java里曾经以为是理所当然的东西。现在把这些东西整理一下,当做复习了。

参考资料

“The book”- Rust Programming Language
Rust Forum帖子:
Is there a simple way to overload functions?
How to implement inheritance-like feature for Rust

目录

  1. Rust和重载Overloading
  2. Builder Pattern
  3. Rust和继承Inheritance

Rust和重载Overloading

由于习惯了C++和Java的重载,原来一直觉得重载就应该是OOP里天经地义的,直到遇到了Rust
Rust里是没有方法重载的,原因是:

  1. 实现完全体的Overloading与Rust的静态类型推断不兼容
    Rust里有非常方便的静态类型推断,可以搭配插件(比如说idea里的Rust插件)实现实时的静态类型推断并且标注上变量类型,在不失易读性的情况下省事,而不像Python那样完全需要人记住或者推断变量的类型是什么。但是因为Rust的静态类型推断以及可选的类型标注,使得完全的Overloading是不可能的。
    完全的Overloading,像Java里面的,可以根据方法的Argument的类型、数量,以及返回值的类型对同名方法进行重载,然而如果一个同名方法的返回值有很多种,那么就很难推断使用方法的返回值的时候到底是用了哪一个方法以及返回值的类型是什么,特别是当一个变量的类型需要根据下文里如何去使用它来推断类型的时候(这样有可能形成一个推断环)
    像这样:
fn show(i :u8) { println!("{}",i); }
fn show(i :i8) { println!("{}",i); }
fn main()
{let i = 8;show(i); //用哪一个function?
}
  1. 实现有限的Overloading可以但是没必要
    而我在帖子里跟别人讨论,假如我们只允许有限的重载呢?比如说我们只允许根据Argument的数量来重载,返回值的类型必须一样,像这样:
fn add(a: i8, b: i8) -> i8
fn add(a: i8, b: i8, c:18) -> i8

但是这样显而易见的,限制太多,而且作用没有完全体那么大,可以,但是没必要。

为什么需要重载?

  1. 我们需要应对不同的传递值和返回值类型
    已知Rust里有trait,类似于Java里有interface,虽然它们不完全相同。引用帖子的回复:Logically if we assume that function name is describing a behaviour, there is no reason, that the same behaviour may be performed on two completelly independent, not simillar types - which is what overloading actually is. If there is a situation, where operation may be performed on set of types, its clear, that they have something in common, so this commoness should be extracted to trait, and then generalizing function over trait is straight way to go
    他大概说的是:假定函数描述的是对物体的行为,那么重载实际上是一个行为对多种完全不相干的东西做操作;但是既然行为可以对这么多种东西做类似的操作,那么这么多种东西必然有相似之处,相似之处就应该抽象为trait,然后再用泛型加trait bound就好了(泛型+trait bound可以实现类似于Java接口对象的作用,trait bound请见The Book)
  2. 我们需要不同数量的Arguement
    现在我们已经用泛型加trait bound解决了Overloading的一大部分需求了,就是基于Argument类型、返回值类型的重载,但是我们的函数、方法需要不同数量的Arguement怎么办?首先想想我们的函数和方法为什么需要不同数量的Arguement?这个估计很多人没认真想过,我原来也是。这个是我个人的答案 Maybe functions and methods with defaults? Like the constructor of InputStreamReader in Java, we can just pass an input stream(presuming UTF8 encoding by default) to it or moreover specify the encoding of the input stream, so we need a constructor with only one parameter and another with two parameters.带默认值的函数或者方法,就像Java里的InputStreamReader的构造器,我们可以传编码或者用默认的UTF8.
    这种情况下,在Rust里确实是个问题,不过只是无关紧要的小问题,我们就直接写好没有默认值的函数/方法,然后再写一个有默认值的函数/方法把默认值和函数调用封装起来就好了。而对于Constructor构造器,更广泛地,我们可以使用Builder Pattern来把一个struct实例像搭积木一样拼起来,见下一章。

Builder Pattern

假设一个情景,我们要模拟一个报社发表文章的过程,那么这个过程是:
写草稿->修改审核->审核通过批准发布->发布
直接看下面的代码好了,注释很详细

//! # This module is for learning a pattern that's effective to model a workflow and is special because of Rust
//! ## Modelling a post publishing workflow
//! empty draft --add_content--> draft --review--> reviewed post --approve--> post/// For conveniently demonstrate the whole work flow
fn main()
{let mut post = Post::new();post.add_content("CONTENT!");let post = post.get_reviewed();let post = post.get_approved();post.get_printed();
}/// This struct is the final product of the workflow
pub struct Post
{///Notice the field is hidden/// So that it can not be directly constructedcontent:String
}impl Post
{/// **Notice that the new function is returning a Draft struct and the field of Post is hidden**/// which enforce that the beginning the workflow is from Draft/// the Draft struct has limited methods/// which **assures** the direction the the workflowpub fn new() -> Draft{Draft{content:String::new()}}
}/// This is the intermediate product of the workflow
/// Fields are also hidden to prevent direct constructions
/// which helps to assure the direction of the workflow
pub struct Draft
{content: String
}impl Draft
{/// New contents can be added to draftpub fn add_content(&mut self, text:&str){self.content.push_str(text);}/// To transform the draft to ReviewedDraft by using `self`/// which makes sense that once reviewed, no addition should be allowedpub fn get_reviewed(self) -> ReviewedDraft{ReviewedDraft{content : self.content}}
}/// This is another intermediate product of the workflow
/// Fields are also hidden to prevent direct constructions
/// which helps to assure the direction of the workflow
pub struct ReviewedDraft
{content:String
}impl ReviewedDraft
{/// To transform ReviewedDraft into the final product -- Post/// which makes sense that only approved draft can be post that get publishedpub fn get_approved(self) -> Post{Post{content:self.content}}
}impl Post {/// To print posts/// which is allowed because it's been reviewed and approvedpub fn get_printed(&self){println!("PRINT: {}",self.content);}/// Drop a post/// Taking `self` and doing nothingpub fn drop(self){}/// Return contents/// which is allowed because it's been reviewed and approvedpub fn content(&self)->&str{&self.content}
}

可以在idea里装上Rust插件,看main里的每一步每一个变量的类型是什么,这是Rust的静态类型推断带来的好处。
使用Builder Pattern在组装一个struct的每个阶段都可以限制可以用到的方法,这样可以避免误操作,而且更加容易组装复杂的对象,还不会有运行时的性能拖累,这些都是Rust的所有权系统和静态推断带来的好处。

Rust和继承Inheritance

一句话概括:在Rust里我们用Strategy Pattern去刻画type之间的关系。
如果不明白Strategy Pattern是什么或者具体怎么实现的,请继续往下看。
Rust里是没有继承的,为什么?
我们先来看看继承可以干什么:继承描述了一个"is-a"的关系,像"Dog is animal",我们就可以用class Dog extends Animal来刻画。Java里子类通过单继承可以得到父类的public的属性attribute和方法(在类以及类的继承中attribute和method是耦合的,先记住这一点),而C++中的多继承也是类似,只不过可以继承到多个父类的属性和方法。因为Java不允许多继承,所以推出了接口interface的概念来弥补单继承的一些缺点,同时避免C++钻石继承的问题。
而继承的问题是什么?其中一个是Circle / Ellipse Problem,说的就是以继承为基础的编程范式下,圆应该是椭圆的子类,但是圆不能继承椭圆的一些方法,比如说沿轴拉伸,椭圆拉伸之后还是椭圆,但是圆经过沿轴拉伸之后就不一定能称为圆了。而在C++/Java里都没能很好地解决这个问题。
而Rust的type + trait的范式却可以轻松解决Circle / Ellipse Problem,精髓在于trait以及Rust中type + trait范式表达的是has a的关系
trait可以看做弱化了的接口interface,因为trait里只能定义方法,不能定义属性,而Java的interface里可以定义静态属性以及final. 另外Rust里attribute和method是解耦的,这跟Java和C++的class不一样。
一个非常简单的例子就可以全部解释完一下问题:

  1. 为什么在Java/C++中的class里attribute和method是耦合的?
  2. 为什么Rust里type + trait表达的是"has a"的关系?为什么Rust里attribute和method可以解耦?
  3. Rust里type + trait是怎么替代Java中class + inheritance + interface的?
  4. Rust里的type + trait相比Java/C++中class + inheritance的优点缺点是什么?

这个例子如下

public class People
{ int eatenApple =0;public void eatApple(){ eatenApple++; System.out.printf("%d apple eaten",eatenApple); }
}public class Student extends People
{ //other attributes and methods specificly related to Student
}public static void main(){Student s = new Student();s.eatApple();
}

假设People类的实例都可以吃苹果,可以调用eatApple(),并且报告自己吃了多少个,那么我们要创建多一个Student类,那么直接extends就可以获得int eatenApplepublic void eatApple(),我们不用写一行重复代码就可以使Student的实例获得eatApple的能力。注意这里attribute和method是耦合的,也就是eatApple()要依赖这个eatenApple来发挥作用。这个也很合理嘛,如果把attribute比作信息,method比作对象的行为,那么确实在现实生活中我们需要一些信息才能做出对应的行为,信息attribute和行为method应该天然地耦合在一起。这就解释了上面的第一个问题:为什么在Java/C++中的class里attribute和method是耦合的?
但在Rust里我们只有type + trait,那我们能不能做到类似于Java的继承从而也使一个struct Student可以从struct People那里继承方法和属性(然后可以偷懒少写代码)呢?答案是不能。
因为Rust里attribute和method是解耦的,attributes在struct里定义,methods在impl里定义。如果我们要“继承”method我们可以定义一个trait,让“子类”实现这个trait,但是还是不能继承属性,如下

pub trait EatApple{
fn eat_apple(&mut self);
}pub struct People
{ eaten_apple: u8}
impl EatApple for People{fn eat_apple(&mut self){ self.eaten_apple += 1; println("{} apple eaten", self.eaten_apple); }
}pub struct Student
{ eaten_apple: u8,// other fields
}
impl EatApple for Student{fn eat_apple(&mut self){ self.eaten_apple += 1; println("{} apple eaten", self.eaten_apple); }
}
impl Student{
//other methods
}

但是这个算是哪门子的继承!?完全就是复制粘贴相同的代码!
那我们能不能在这里用trait的默认实现default implementation来避免重复写代码呢?答案是不能。像下面

pub trait EatApple{
fn eat_apple(&mut self){self.eaten_apple+=1; //compile errorprintln!("{} apple eaten",self.eaten_apple);}
}

在这里,定义trait的时候我们根本不知道也不能限制实现这个trait的type里面有没有这个eaten_apple的属性,所以编译不通过。
以下是一种方法

pub trait EatApplePlus{fn get_apple(&self) -> usize;fn set_apple(&mut self, num:usize);fn eat_apple(&mut self){self.set_apple(self.get_apple()+1);println!("{} apple eaten",self.get_apple());}}

这样Student只用实现get_apple()set_apple()就可以了,可以偷一点点懒

pub struct Student{apple : usize}impl EatApplePlus for Student{fn get_apple(&self)->usize{self.apple}fn set_apple(&mut self, num:usize){self.apple = num;}}

但是现在问题又来了,怎么封装?如果我们不想暴露get_apple()set_apple(),只暴露eat_apple()怎么办?这是完全合理的,因为我们不想让API用户获得和操作struct里的数据。但是如果用trait的话,这个trait是pub,那么API用户是可以直接用get_apple()set_apple()的,就像这样:

fn main(){let mut student = Student{apple:0};student.eat_apple();// methods defined in the trait but we don't want to exposestudent.set_apple(1024);student.get_apple();}

我们只想在调用eat_apple()的时候递增1,但是现在乱套了。EatApplePlus是为了解决Rust中method和attribute解耦的问题,但是却带来了封装的问题。这个问题是因为我们企图用Rust实现"is a"的逻辑带来的,而实际上Rust自己的逻辑是"has a",我们需要完全不同的思路。
那么怎么解决这个问题呢?社区大神给出了答案(我稍微修改了一下),详见How to implement inheritance-like feature for Rust的Solution:

pub struct AppleEater {apple: u32}impl AppleEater {pub fn report(&self) {println!("I ate {} apple!", self.apple);}pub fn eat_apple(&mut self) { self.apple += 1; }
}pub trait AsAppleEater {fn as_apple_eater(&self) -> &AppleEater;fn report(&self){ self.as_apple_eater().report() }}pub trait AsMutAppleEater: AsAppleEater {fn as_mut_apple_eater(&mut self) -> &mut AppleEater;fn eat_apple(&mut self){ self.as_mut_apple_eater().eat_apple();self.report();}}

我们像如上定义AppleEaterAsAppleEaterAsMutAppleEater,那么如果我们要给Teacher实现吃苹果的方法,就可以这样写:

impl AsAppleEater for Teacher {fn as_apple_eater(&self) -> &AppleEater{ &self.apple_eater }
}impl AsMutAppleEater for Teacher {fn as_mut_apple_eater(&mut self) -> &mut AppleEater{ &mut self.apple_eater }
}

自然地,API用户还需要给Teacher这个type加上一个属性apple_eater : AppleEater
总体而言,需要额外添加的就只是

// in struct Teacher{}
apple_eater : AppleEater// in impl
impl AsAppleEater for Teacher{..}
impl AsMutAppleEater for Teacher{..}

虽然不能像Java用继承一样完全一行额外的代码不用添加,但是相较EatApplePlus也是偷懒不少了。而且这样写还可以避免EatApplePlus暴露不必要代码细节的弊端。

使用AppleEaterAsAppleEaterAsMutAppleEater就是在给attribute和method解耦,把作为一个可以吃苹果的人需要的attribute放进AppleEater里,把可以做的行为放进AsAppleEaterAsMutAppleEater这两个trait里。我们可以把这样的代码解释成"每一个teacher心里都有一个AppleEater",而"作为一个AppleEater"(AsAppleEaterAsMutAppleEater)有这两个trait定义的行为。

所以Rustdtype + trait实际是描述has a的关系,也因此我们可以用类似的方法把attribute和method解耦,这就解释了第二个问题为什么Rust里type + trait表达的是"has a"的关系?为什么Rust里attribute和method可以解耦?

而至于第三个问题Rust里type + trait是怎么替代Java中class + inheritance + interface的?,我们用上面代码所述的方法就可以替代了。

至于第四个问题Rust里的type + trait相比Java/C++中class + inheritance的优点缺点是什么?我个人的看法可以总结如下:

  1. type + trait表示的has a的关系没有class + inheritance表达的is a的关系那么直观,就行People和Student的那个例子那样
  2. 基于trait实现方法可以给程序员更大灵活度,因为不像继承那样限制了数据结构;但又由于attribute和method的解耦,程序员需要写多一点点逻辑上看是冗余的代码。
  3. type + trait解决了Circle / Ellipse Problem.
    怎么解决的?这要看回上面的代码。
    为什么我们需要定义两个trait(AsAppleEater, AsMutAppleEater)并且其中一个trait(AsMutAppleEater)还是以另一个作为supertrait(AsAppleEater)的呢?(注:一个trait B如果有supertrait A,那么对于一个type必须先实现supertrait A才能实现trait B)
    这个是为了更好地划分一个type能实现的行为,假设说一个type是低权限的,它只实现了AsAppleEater,那么它就只能调用report(),这个type的实例就可以拿来传输数据,因为它们没有改变数据的方法,如果说一个type有更高的权限,它还实现了AsMutAppleEater,那么它可以改变它自身携带的数据。而我们可以用AsEllipseAsMutEllipse来解决Circle / Ellipse Problem,如下
pub struct Ellipse
{a : f32,b : f32
}
pub trait AsEllipse
{fn as_ellipse(&self) -> &Ellipse;fn calc_area(&self) -> f32{PI * self.as_ellipse.a * self.as_ellipse.b}....
}
pub trait AsMutEllipse : AsEllipse
{fn as_mut_ellipse(&mut self) -> &mut Ellipse;fn strech(&mut self, x1 : f32, x2 : f32){self.as_mut_ellipse.a *= x1;self.as_mut_ellipse.b *= x2;}...
}pub struct Circle{ circle: Ellipse,r: f32
}
impl AsEllipse for Circle{fn as_ellipse(&self) -> &Ellipse{&self.circle}
}

因为即使Circle是Ellipse,但是它的“权限”没Ellipse高,所以只能impl AsEllipse,但是不能impl AsMutEllipse,也就因此没有拉伸strech的操作,所以完美解决这个问题。

总结

看完Rust Programming Language 这本书,再经过和社区大神的讨论,我觉得“Rust是C++以来几十年工程经验的结晶”这种说法真的不是无脑吹,希望Rust的旗子可以插满全世界吧,因为相比之下C++真的太危险了orz

Rust中的面向对象编程Rusty OOP相关推荐

  1. python oop编程_Python 3中的面向对象编程(OOP)

    python oop编程 In this article you'll pick up the following basic concepts of OOP in Python: 在本文中,您将了解 ...

  2. 我的WCF之旅(7):面向服务架构(SOA)和面向对象编程(OOP)的结合——如何实现Service Contract的继承...

    当今的IT领域,SOA已经成为了一个非常时髦的词,对SOA风靡的程度已经让很多人对SOA,对面向服务产生误解.其中很大一部分人甚至认为面向服务将是面向对象的终结,现在的面向对象将会被面向服务完全代替. ...

  3. Atitit 面向对象编程(OOP)、面向组件编程(COP)、面向方面编程(AOP)和面向服务编程(SOP)的区别和联系...

    Atitit 面向对象编程(OOP).面向组件编程(COP).面向方面编程(AOP)和面向服务编程(SOP)的区别和联系 1. 面向组件编程(COP) 所以,组件比起对象来的进步就在于通用的规范的引入 ...

  4. 面向对象编程(OOP)特性 类和对象

    面向对象编程(OOP)  思维导图 一.类和对象 1.对象 随处可见的一种事物就是对象,对象是事物存在的实体.人们思考这些对象都是由何种部分组成的,通常会将对象划分为动态部分和静态部分.静态部分,顾名 ...

  5. VSCode自定义代码片段9——JS中的面向对象编程

    JavaScript的面向对象编程 {// JS'OOP// 9 如何自定义用户代码片段:VSCode =>左下角设置 =>用户代码片段 =>新建全局代码片段文件... =>自 ...

  6. 什么是面向对象编程(OOP)?

    Java 程序员第一个要了解的基础概念就是:什么是面向对象编程(OOP)? 玩过 DOTA2 (一款推塔杀人的游戏)吗?里面有个齐天大圣的角色,欧洲战队玩的很溜,国内战队却不怎么会玩,自家人不会玩自家 ...

  7. JS面向对象编程(OOP)

    什么是JS面向对象编程(OOP)? 用对象的思想去写代码,就是面向对象编程. 上面这张图就是一个对象,紫色部分就是车的属性,黄色部分就是修改车的方法: 把他们集合到一个构造函数内,就是这样的 func ...

  8. 《Kotin 极简教程》第7章 面向对象编程(OOP)(1)

    第7章 面向对象编程(OOP) 最新上架!!!< Kotlin极简教程> 陈光剑 (机械工业出版社) 可直接打开京东,淘宝,当当===> 搜索: Kotlin 极简教程 http:/ ...

  9. 《Kotlin 程序设计》第五章 Kotlin 面向对象编程(OOP)

    第五章 Kotlin 面向对象编程(OOP) 正式上架:<Kotlin极简教程>Official on shelves: Kotlin Programming minimalist tut ...

  10. 泛型编程、STL的概念、STL模板思想及其六大组件的关系,以及泛型编程(GP)、STL、面向对象编程(OOP)、C++之间的关系

    介绍STL模板的书,有两本比较经典: 一本是<Generic Programming and the STL>,中文翻译为<泛型编程与STL>,这本书由STL开发者 Matth ...

最新文章

  1. 深度学习中的优化简介
  2. 基于matlab的对流层散射信道特性仿真,对流层散射信道建模和FPGA实现
  3. 关​于​h​i​b​e​r​n​a​t​e​中​双​向​外​键​关​联​o​n​e​-​t​o​-​o​n​e​的​p​r​o​p​e​r​t​y​-​r​e​f​=​的​问​题(转)...
  4. 知识图谱前沿跟进,看这篇就够了,Philip S. Yu 团队发布权威综述,六大开放问题函待解决!...
  5. STL源码剖析读书笔记--第6章第7章--算法与仿函数
  6. MySQL 当记录不存在时insert,当记录存在时update
  7. JavaSE各阶段练习题----IO流
  8. ubuntu与win10互换硬盘
  9. java see 方法_Java 反射常用方法
  10. CoreException: Could not get the value for parameter compilerId for plugin execution default-compile
  11. 7天期限已过,谷歌披露已遭利用的 Windows 内核 0day 详情
  12. 如何使用EasyRecovery的监控硬盘功能
  13. Telerik ui kendo for jquery 2022源码版
  14. mysql卸载不_mysql卸载不干净解决方法
  15. Origin—使用基底线来拟合曲线的各个峰值
  16. 如何解决windows资源管理器已停止工作?两种方法教会你
  17. 凝视联通4G和4G+战略落地半年报,从数据亮点中找出路
  18. html5图片格式有什么,jpeg是什么?
  19. Linux 多线程 Pthread 互斥量
  20. RFID卡片的扇区与块地址的计算

热门文章

  1. Xubuntu22.04安装dock美化任务栏
  2. 你有没有一个御用冷笑话 说来听听~
  3. Java中详细使用JWT(JJWT)
  4. 上任第十年,库克功与过
  5. 南通大学杏林学院计算机专业,南通大学杏林学院代码
  6. "中国东信杯"广西大学第二届程序设计竞赛(同步赛)
  7. 二元函数最大最小值定理证明_Von Neumann最小最大值定理的归纳法证明
  8. 电脑中病毒,文件夹变成快捷方式
  9. 安庆师范大学c语言程序设计,安庆师范大学计算机与信息学院欢迎你!
  10. 竞价单页设计需要了解的知识