Programming Languages PartC Week1学习笔记——Ruby与面向对象编程
2022/9/15 1:17:28
本文主要是介绍Programming Languages PartC Week1学习笔记——Ruby与面向对象编程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
@
目录- Introduction to Ruby
- Classes and Objects
- Object State
- Visibility
- A Longer Example
- Everything is an Object
- Class Definitions are Dynamic
- Duck Typing
- Arrays
- Blocks
- Using Blocks
- Procs
- Hashes and Ranges
- (1)Hash
- (2)Range
- Subclassing
- Why Use Subclassing?
- Overriding and Dynamic Dispatch
- Method-Lookup Rules, Precisely
- Dynamic Dispatch Versus Closures
- Optional: Dynamic Dispatch Manually in Racket
Introduction to Ruby
终于来到了我们比较熟悉的领域,OOP
我们关注的Ruby的特性,最重要的是“纯面向对象语言”,基于类Class-based,动态类型。
我们不关注的部分:
特性对比:
例子:
class Hello def my_first_method puts "Hello, World!" end end x = Hello.new x.my_first_method
在CMD中运行Ruby程序使用ruby
命令,通过irb
命令使用Ruby的REPL:
Classes and Objects
基于类的OOP规则
例子:
创建和使用对象,这部分语法都很熟悉的。
变量以任意字母作为变量名的首字母,需要注意变量是可修改的mutable(修改内容还是修改引用?)
关于self,跟python的self一样。语法糖也类似,在相同对象中调用方法可以省略self
Ruby中self的常见用法,直接使用方法返回self本身,例如:
最后,在Ruby中,每条语句换行很重要,如果多条语句放在同一行,那就需要使用分号“ ; ”分割。另外,跟python不一样,Ruby中缩进(indentation)对语义(semantics)不重要,只是良好的代码风格而已。
Object State
对象拥有自己的状态。状态组成实例变量(fields),只能被对象方法访问,state变量以@开头。如果使用了某个没有的state变量,会生成nil对象
对象变量赋值创建了一个别名(引用),这时他们具有同样的state。
但如果new一个新的对象的引用,创建的对象就有不同的state
例子:
例子2:initialize特殊方法,用于创建对象实例时初始化,相当于构造函数。
Ruby中因为不要求所有变量都预先声明并且初始化,相同类的不同对象实例可以有不同的实例变量。
通过@@关键词声明类变量,也就是类的静态变量
类常量和类方法,就是类中的静态常量和静态方法
类常量用在类中以大写字母开头,不能被修改。在外部需要使用类名修饰访问,例如C::Foo
类方法定义需要使用self.method_name来定义(说实话有点奇怪),它属于类本身,所以需要使用类名来调用
Visibility
Ruby中,对象的状态state总是private的,同一个类的不同对象也无法访问。通常定义getter和setter来让对象state可见(跟JAVA的POJO类似)
Ruby的语法糖。用state变量的名称来直接定义一个getter,用变量名加=来定义一个setter(并且以等号结尾地方法在调用时可以在等号前面加空格)。这样相当于将state封装成了一个普通的成员变量。
同时,getter/setter还有简记的定义方式(类似java的注解)
为何需要private的对象state:
Ruby的三种visibility,public是默认值:
public在任何类中都能调用
protected在相同类或者子类中能够调用
private只有在相同实例中能调用
三种visibility访问权限的使用方法类似C++
Ruby中比较特别的一个细节,对于private的成员或方法,必须通过简化的方式,即m或m(args)来调用(唯一方式),不能通过self来访问。
A Longer Example
本节以有理数类为例,综合应用前几节的内容:
该例子中展现了很多Ruby的语法特性:
# Section 7: A Longer Example class MyRational def initialize(num,den=1) # second argument has a default if den == 0 raise "MyRational received an inappropriate argument" elsif den < 0 # notice non-english word elsif @num = - num # fields created when you assign to them @den = - den else @num = num # semicolons optional to separate expressions on different lines @den = den end reduce # i.e., self.reduce() but private so must write reduce or reduce() end def to_s ans = @num.to_s if @den != 1 # everything true except false _and_ nil objects ans += "/" ans += @den.to_s end ans end def to_s2 # using some unimportant syntax and a slightly different algorithm dens = "" dens = "/" + @den.to_s if @den != 1 @num.to_s + dens end def to_s3 # using things like Racket's quasiquote and unquote "#{@num}#{if @den==1 then "" else "/" + @den.to_s end}" end def add! r # mutate self in-place a = r.num # only works b/c of protected methods below b = r.den # only works b/c of protected methods below c = @num d = @den @num = (a * d) + (b * c) @den = b * d reduce self # convenient for stringing calls end # a functional addition, so we can write r1.+ r2 to make a new rational # and built-in syntactic sugar will work: can write r1 + r2 def + r ans = MyRational.new(@num,@den) ans.add! r ans # 因为add!返回ans运算后的self,因此这一句其实不需要 end protected # there is very common sugar for this (attr_reader) # the better way: # attr_reader :num, :den # protected :num, :den # we do not want these methods public, but we cannot make them private # because of the add! method above def num @num end def den @den end private def gcd(x,y) # recursive method calls work as expected if x == y x elsif x < y gcd(x,y-x) else gcd(y,x) end end def reduce if @num == 0 @den = 1 else d = gcd(@num.abs, @den) # notice method call on number @num = @num / d @den = @den / d end end end # can have a top-level method (just part of Object class) for testing, etc. def use_rationals r1 = MyRational.new(3,4) r2 = r1 + r1 + MyRational.new(-5,2) # (r1.+(r1)).+ (...) puts r2.to_s (r2.add! r1).add! (MyRational.new(1,-4)) puts r2.to_s puts r2.to_s2 puts r2.to_s3 end
Everything is an Object
纯粹的面向对象语言
可以在任何东西(都是对象)上调用方法,如果方法不存在则抛出“undefined method”异常,不需要判断某个东西是不是可以调用方法(都能调用,但方法不一定都存在)。
几乎所有东西都是方法调用,包括基本运算等
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ssJwFzDF-1663159644531)(E:\OneDrive_WHU\OneDrive - whu.edu.cn\Programming_Languages_PartC\week1\week1.assets\image-20220611155402688.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dM12k3JW-1663159644532)(E:\OneDrive_WHU\OneDrive - whu.edu.cn\Programming_Languages_PartC\week1\week1.assets\image-20220611160920913.png)]
关于nil:在Ruby中用来描述某些没有包含任何数据的对象。类似于ML的unit,或者java的null。重点是nil也是一个对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1xqKF3Be-1663159644532)(E:\OneDrive_WHU\OneDrive - whu.edu.cn\Programming_Languages_PartC\week1\week1.assets\image-20220611161246206.png)]
另外,nil counts as false,nil在逻辑判断时相当于false。因此Ruby中,false代表fasle,nil也可以代表false
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fBUncJ1c-1663159644533)(E:\OneDrive_WHU\OneDrive - whu.edu.cn\Programming_Languages_PartC\week1\week1.assets\image-20220611161456293.png)]
所以,Ruby中任何结果都是对象,任何操作都是对象方法的调用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JHmwlaAa-1663159644533)(E:\OneDrive_WHU\OneDrive - whu.edu.cn\Programming_Languages_PartC\week1\week1.assets\image-20220611161706403.png)]
反射,了解java应该对这个词不陌生,指程序能在运行时获取一个(类)对象,查询“对象能做的事”并随之响应的能力:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v8jld8UG-1663159644533)(E:\OneDrive_WHU\OneDrive - whu.edu.cn\Programming_Languages_PartC\week1\week1.assets\image-20220611161902045.png)]
ruby中的用法之一,查询可调用的方法(甚至可以在两个对象的methods结果之间做运算,下图是在3的类方法中而不在nil类方法中的方法):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p8pZyU8g-1663159644534)(E:\OneDrive_WHU\OneDrive - whu.edu.cn\Programming_Languages_PartC\week1\week1.assets\image-20220611163037777.png)]
Class Definitions are Dynamic
Ruby能够在运行时操作对象,添加改变替换方法,某些时候会有用,但会破坏抽象和封装。在大型语言中是有争议的。
甚至能够向内部类添加新方法
由于所有的东西都是对象,即使是顶层函数,也是Object的对象,因此直接用def定义顶层函数,相当于添加新Object的方法。然后由于所有对象都直接或间接继承自Object,因此顶层函数可以作为每个对象的方法使用。例如:
直接定义在Object中的方法与顶层函数相同,重复名称的方法会覆盖:
但是,对Ruby预定义的运算符覆盖时一定要当心,例如:
class Fixnum def + x 13 end end
这段代码覆盖了原有+运算符的定义,此时Ruby中一切的Fixnum都会受到影响,因此irb(REPL)会崩溃,因为irb本身也是Ruby编写的,会被这个覆盖影响。
(可见,Ruby给运行时程序的权力过大还是挺危险的)
Duck Typing
面向对象中经典的鸭子类型:
在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的,实际上是一种不严格的多态实现。
例子:
得益于动态类型语言的鸭子类型某些时候可能是有用的,但也在一定程度上属于poor style(如果使用者调用了duck typing,就难以更改函数的内部实现了,比如double那个例子,如果有字符串参数调用过这个方法,就不能再改成x * 2的实现方式了)。因此,这种不严格的多态方式也可能带来问题。
Arrays
Array类提供了Ruby的数组类型
Ruby数组具有与python的list类似的语法,但注意Ruby数组在超出引用时会返回nil而不是抛出异常。
同样,由于是动态类型语言,array允许不同数据类型;并且允许使用重载的运算符+作为作为concat方法。
Array的初始化方法(具体的语法需要去翻阅文档)
然后是push和pop,从右侧入栈出栈方法
利用shift和unshift,从左侧入栈出栈方法
Array对象变量仍然是一个引用,因此
值得注意的是,多维数组是引用的引用(类似C++指针的指针),因此a+[]只能改变b的外层引用,但内层地址指向的一维数组没变。
对数组的切片操作,例如f[2,4]表示从idx为2的元素开始,取4个元素。同时,切片的替换并不需要对应数量的数组(这一点很灵活)。
Array可以调用一些迭代方法,例如each
Blocks
代码块,又一个与闭包相关的重要概念。代码块基本上就是一种闭包。
一些奇怪的事情(block的语法)。{}可以用do end来代替。
标准库函数通过代码块定义了大量有用的高等函数,代码块充当了其他函数中一等函数闭包的作用。
代码块在Ruby中非常强大,甚至在初始化Array时都能使用
对于any?和all?方法,如果没有参数,就默认去判断每个元素是否为真(注意Ruby中,只有false和nil是假)
常用高等方法:
方法名 | 常见作用 |
---|---|
map / collect | 与Racket的map类似,对每个元素使用f方法 |
inject | 与Racket的reduce类似,对每个函数用f方法进行累计 |
select | 类似filter |
代码块嵌套的使用,其中(0..i)是序列(range),与python的range类似
Using Blocks
定义使用Block的函数时,通过yield关键词代替Block(类似匿名函数)。
例子,这个例子用到了递归,并且递归传递了Block
Procs
Block实际上是第二级,能通过yield来调用,但不能在一个对象中被传递、返回和存储。它不是真正的闭包,但可以转换成真正的闭包。(Block不是对象!)
闭包是Proc类的实例,Proc才是真正的一等表达式。
Obeject的lambda表达式可以通过一个Block来生成Proc实例闭包。
例子:
例如Block难以实现类似Partial Application的应用,但如果使用Proc就能做到
# 由于Block不能传递,不能用Block作为存储对象 c = a.map {|x| {|y| x >= y }} # 但Proc实例可以传递 c = a.map {|x| lambda {|y| x >= y} }
并且,Proc实例通过call方法调用其中的代码块。
这让我们更加理解“First Class”
Hashes and Ranges
(1)Hash
相当于Racket的python的字典或动态的ML的Record
语法类似python的字典
能够根据键在hash表中添加值
也能用键值对初始化hash表,使用=>连接键值。
由于和Array是不同的数据结构,hash的each方法需要两个参数(key和value)
(2)Range
类似于用来动态存储一系列连续数的数组,但不是数组而是单独的数据结构,类似python的range,可以用于迭代。同时,Range相比之下更高效。
两个优秀的风格:
-
尽可能使用Range
-
非数字索引时使用Hash
Hash和Range拥有一些与Array相同的方法(迭代器方法),这也是对duck typing有利。
Subclassing
回到面向对象的部分,介绍Ruby的继承
子类定义:
子类会继承超类所有的方法,而不会有继承权限的问题(例如C++和Java)。子类也不会被类型检测,能访问所有方法和成员。
注意反射机制:
另外,与所有OOP语言类似,子类都是(is)超类,所以子类实例(子类)是属于的超类
但需要区分的是,子类实例本身并不是超类的实例,类有继承关系,但子类对象实例不是超类对象实例。
注意,is_a? 或者 instance_of?之类的方法并不通常是OOP style,因为我们假设了使用的对象是 某个假定的类或类对象,它拒绝了duck typing(我们不能使用一个具有类似x,y,color等 成员和方法的其他类对象)。
Why Use Subclassing?
Overriding and Dynamic Dispatch
例子
对象与闭包在很多特性上类似,但最大的不同是覆写可以让一个方法定义在超类中但在子类中调用。
Method-Lookup Rules, Precisely
动态分派(dynamic dispatch),比较熟知的名字是后期绑定或者虚函数,用于在运行期选择调用方法的实现的流程。被认为是面向对象语言(Object-Oriented programming
:OOP
)的基本特性。
定义方法查找语法是需要的。
回顾各种语言中的变量查找过程。
Ruby中使用self来查找各种类或实例的成员或方法。
Ruby的方法查找:
动态分派的过程比闭包更复杂,必须将self特殊对待。
在Java等语言中,方法查找规则是类似的,但是更复杂(因为方法在类中能具有相同名字(重载Overload)),只有在方法名与参数数量、顺序、类型等都完全相同时,才会发生子类方法的覆写Override。这与Java等语言的静态类型有关,能够通过参数的静态类型来判断选择最符合条件的方法。这种信赖根本上基于type-checking的规则。
但在Ruby中,名字相同的方法就总是会被覆写。因为不存在type checking。
这一点对于Java或者C++的使用者来说是比较容易理解的。
总的来书就是,Java和Ruby等都有动态分派(dynamic dispatch)的特性(区别于静态分派),但只有具有静态类型(static type or type chcking) 和 静态重载(static overloading)的语言才能实现上述复杂的覆写机制(即方法名与参数数量、顺序、类型等都完全相同时,才会发生子类方法的覆写)。因此Ruby的覆写机制没有那么复杂,名字相同的方法就总是会被覆写。
Dynamic Dispatch Versus Closures
动态分派与闭包的比较:
ML的闭包示例:闭包不会被后续的shadowing影响,这在某些时候是有用的但某些时候是糟糕的(这就是闭包的代价)
Ruby的动态分派示例:覆写将会改变超类已定义的方法,它的代价恰好与闭包相反。可能最终会覆写其他方法所依赖的方法,甚至不知道它是重要的。例如下面的B类中的odd没有任何问题甚至更快,但是C类由于错误的覆写导致问题产生(这恰好是闭包能防止的事情)。
面向对象的代价:
某些方法的可覆写性会带来方法行为改变的问题(甚至不被覆写时也可能),这让我们难以推断“正在关注的代码”是哪一段。因此我们需要去避免一些覆写,通过类似private继承或者final 方法(类似C++或Java中的做法)的方式。
但面向对象的优势是,它使得子类更容易影响超类方法的行为(在不复制代码的情况下)。
Optional: Dynamic Dispatch Manually in Racket
如何在Racket这样的语言中,手动实现类似动态分派的特性?
这一节的做法有点类似于之前实现解释器的方式。
尽管Racket已经有内置的类和对象,但这一节为了示范一种语言的某种语法(特性)可以在(通过)另一种语言的习惯用法(来构建)。并且为了更好的理解动态分派。
借助struct。这里的示例中,self只是lambda匿名函数的一个用来记录对象本身的额外参数(这个额外参数的做法有点类似python,用来保证对象方法中的对象self调用),但在这里,self不是一个特殊关键词(不会被特殊对待,它可以不叫self,可以叫this、it等等)
本节的代码:其中make-polar-point子类的实现方式是重点。
; Section 7: Optional: Dynamic Dispatch Manually in Racket #lang racket ;; We can "use" dynamic dispatch in a language without it manually ;; Our "objects" will have: ;; * an immutable list of mutable "fields" (symbols and contents) ;; * an immutable list of immutable "methods" (symbols and functions taking self) (struct obj (fields methods)) ; like assoc but for an immutable list of mutable pairs (define (assoc-m v xs) (cond [(null? xs) #f] [(equal? v (mcar (car xs))) (car xs)] [#t (assoc-m v (cdr xs))])) (define (get obj fld) (let ([pr (assoc-m fld (obj-fields obj))]) (if pr (mcdr pr) (error "field not found")))) (define (set obj fld v) (let ([pr (assoc-m fld (obj-fields obj))]) (if pr (set-mcdr! pr v) (error "field not found")))) (define (send obj msg . args) ; convenience: multi-argument functions (2+ arguments) (let ([pr (assoc msg (obj-methods obj))]) (if pr ((cdr pr) obj args) ; do the call (error "method not found" msg)))) (define (make-point _x _y) (obj (list (mcons 'x _x) (mcons 'y _y)) (list (cons 'get-x (lambda (self args) (get self 'x))) (cons 'get-y (lambda (self args) (get self 'y))) (cons 'set-x (lambda (self args) (set self 'x (car args)))) (cons 'set-y (lambda (self args) (set self 'y (car args)))) (cons 'distToOrigin (lambda (self args) (let ([a (send self 'get-x)] [b (send self 'get-y)]) (sqrt (+ (* a a) (* b b))))))))) (define (make-color-point _x _y _c) (let ([pt (make-point _x _y)]) (obj (cons (mcons 'color _c) (obj-fields pt)) (append (list (cons 'get-color (lambda (self args) (get self 'color))) (cons 'set-color (lambda (self args) (set self 'color (car args))))) (obj-methods pt))))) (define (make-polar-point _r _th) (let ([pt (make-point #f #f)]) (obj (append (list (mcons 'r _r) (mcons 'theta _th)) (obj-fields pt)) ; Java-style field extension (append ; overriding by being earlier in the list (see send function) (list (cons 'set-r-theta (lambda (self args) (begin (set self 'r (car args)) (set self 'theta (cadr args))))) (cons 'get-x (lambda (self args) (let ([r (get self 'r)] [theta (get self 'theta)]) (* r (cos theta))))) (cons 'get-y (lambda (self args) (let ([r (get self 'r)] [theta (get self 'theta)]) (* r (sin theta))))) (cons 'set-x (lambda (self args) (let* ([a (car args)] [b (send self 'get-y)] [theta (atan b a)] [r (sqrt (+ (* a a) (* b b)))]) (send self 'set-r-theta r theta)))) (cons 'set-y (lambda (self args) (let* ([b (car args)] [a (send self 'get-x)] [theta (atan b a)] [r (sqrt (+ (* a a) (* b b)))]) (send self 'set-r-theta r theta))))) (obj-methods pt))))) (define p1 (make-point -4 0)) p1 (send p1 'get-x) (send p1 'get-y) (send p1 'distToOrigin) (send p1 'set-y 3) (send p1 'distToOrigin) (define p2 (make-color-point -4 0 "red")) p2 (send p2 'get-x) (send p2 'get-y) (send p2 'distToOrigin) (send p2 'set-y 3) (send p2 'distToOrigin) (define p3 (make-polar-point 4 3.1415926535)) p3 (send p3 'get-x) (send p3 'get-y) (send p3 'distToOrigin) (send p3 'set-y 3) (send p3 'distToOrigin)
但某些语言仍然难以实现类似特性,例如ML,他的type system不能实现类似的子类型判断。
但注意,老师强调了一下,难以实现并不是不能实现,我们仍然可以用之前学到的方法,在ML中用datatype构造一个足够大的“Obeject”,然后让所有的“类”都是这个datatype的值,这样,这些所谓“类”之间的“继承关系”就可以通过ML的类型系统来判断了(还是借助pattern match之类的特性)。
这其实也算是一种代价交换,ML虽然难以实现动态分派的特性,但却很容易实现闭包,相反Java是面向对象的,很容易动态分派,但闭包的实现就更难一些(例如PartA中week4时我们在Java中实现闭包的方式)。
这篇关于Programming Languages PartC Week1学习笔记——Ruby与面向对象编程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-22怎么实现ansible playbook 备份代码中命名包含时间戳功能?-icode9专业技术文章分享
- 2024-11-22ansible 的archive 参数是什么意思?-icode9专业技术文章分享
- 2024-11-22ansible 中怎么只用archive 排除某个目录?-icode9专业技术文章分享
- 2024-11-22exclude_path参数是什么作用?-icode9专业技术文章分享
- 2024-11-22微信开放平台第三方平台什么时候调用数据预拉取和数据周期性更新接口?-icode9专业技术文章分享
- 2024-11-22uniapp 实现聊天消息会话的列表功能怎么实现?-icode9专业技术文章分享
- 2024-11-22在Mac系统上将图片中的文字提取出来有哪些方法?-icode9专业技术文章分享
- 2024-11-22excel 表格中怎么固定一行显示不滚动?-icode9专业技术文章分享
- 2024-11-22怎么将 -rwxr-xr-x 修改为 drwxr-xr-x?-icode9专业技术文章分享
- 2024-11-22在Excel中怎么将小数向上取整到最接近的整数?-icode9专业技术文章分享