终于来到了我们比较熟悉的领域,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:
基于类的OOP规则
例子:
创建和使用对象,这部分语法都很熟悉的。
变量以任意字母作为变量名的首字母,需要注意变量是可修改的mutable(修改内容还是修改引用?)
关于self,跟python的self一样。语法糖也类似,在相同对象中调用方法可以省略self
Ruby中self的常见用法,直接使用方法返回self本身,例如:
最后,在Ruby中,每条语句换行很重要,如果多条语句放在同一行,那就需要使用分号“ ; ”分割。另外,跟python不一样,Ruby中缩进(indentation)对语义(semantics)不重要,只是良好的代码风格而已。
对象拥有自己的状态。状态组成实例变量(fields),只能被对象方法访问,state变量以@开头。如果使用了某个没有的state变量,会生成nil对象
对象变量赋值创建了一个别名(引用),这时他们具有同样的state。
但如果new一个新的对象的引用,创建的对象就有不同的state
例子:
例子2:initialize特殊方法,用于创建对象实例时初始化,相当于构造函数。
Ruby中因为不要求所有变量都预先声明并且初始化,相同类的不同对象实例可以有不同的实例变量。
通过**@@关键词**声明类变量,也就是类的静态变量
类常量和类方法,就是类中的静态常量和静态方法
类常量用在类中以大写字母开头,不能被修改。在外部需要使用类名修饰访问,例如C::Foo
类方法定义需要使用self.method_name来定义(说实话有点奇怪),它属于类本身,所以需要使用类名来调用
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来访问。
本节以有理数类为例,综合应用前几节的内容:
该例子中展现了很多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
纯粹的面向对象语言
可以在任何东西(都是对象)上调用方法,如果方法不存在则抛出“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)]
Ruby能够在运行时操作对象,添加改变替换方法,某些时候会有用,但会破坏抽象和封装。在大型语言中是有争议的。
甚至能够向内部类添加新方法
由于所有的东西都是对象,即使是顶层函数,也是Object的对象,因此直接用def定义顶层函数,相当于添加新Object的方法。然后由于所有对象都直接或间接继承自Object,因此顶层函数可以作为每个对象的方法使用。例如:
直接定义在Object中的方法与顶层函数相同,重复名称的方法会覆盖:
但是,对Ruby预定义的运算符覆盖时一定要当心,例如:
class Fixnum
def + x
13
end
end
这段代码覆盖了原有+运算符的定义,此时Ruby中一切的Fixnum都会受到影响,因此irb(REPL)会崩溃,因为irb本身也是Ruby编写的,会被这个覆盖影响。
(可见,Ruby给运行时程序的权力过大还是挺危险的)
面向对象中经典的鸭子类型:
在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的,实际上是一种不严格的多态实现。
例子:
得益于动态类型语言的鸭子类型某些时候可能是有用的,但也在一定程度上属于poor style(如果使用者调用了duck typing,就难以更改函数的内部实现了,比如double那个例子,如果有字符串参数调用过这个方法,就不能再改成x * 2的实现方式了)。因此,这种不严格的多态方式也可能带来问题。
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
代码块,又一个与闭包相关的重要概念。代码块基本上就是一种闭包。
一些奇怪的事情(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类似
定义使用Block的函数时,通过yield关键词代替Block(类似匿名函数)。
例子,这个例子用到了递归,并且递归传递了Block
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”**
相当于Racket的python的字典或动态的ML的Record
语法类似python的字典
能够根据键在hash表中添加值
也能用键值对初始化hash表,使用**=>**连接键值。
由于和Array是不同的数据结构,hash的each方法需要两个参数(key和value)
类似于用来动态存储一系列连续数的数组,但不是数组而是单独的数据结构,类似python的range,可以用于迭代。同时,Range相比之下更高效。
两个优秀的风格:
尽可能使用Range
非数字索引时使用Hash
Hash和Range拥有一些与Array相同的方法(迭代器方法),这也是对duck typing有利。
回到面向对象的部分,介绍Ruby的继承
子类定义:
子类会继承超类所有的方法,而不会有继承权限的问题(例如C++和Java)。子类也不会被类型检测,能访问所有方法和成员。
注意反射机制:
另外,与所有OOP语言类似,子类都是(is)超类,所以子类实例(子类)是属于的超类
但需要区分的是,子类实例本身并不是超类的实例,类有继承关系,但子类对象实例不是超类对象实例。
注意,is_a? 或者 instance_of?之类的方法并不通常是OOP style,因为我们假设了使用的对象是 某个假定的类或类对象,它拒绝了duck typing(我们不能使用一个具有类似x,y,color等 成员和方法的其他类对象)。
例子
对象与闭包在很多特性上类似,但最大的不同是覆写可以让一个方法定义在超类中但在子类中调用。
动态分派(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的覆写机制没有那么复杂,名字相同的方法就总是会被覆写。
动态分派与闭包的比较:
ML的闭包示例:闭包不会被后续的shadowing影响,这在某些时候是有用的但某些时候是糟糕的(这就是闭包的代价)
Ruby的动态分派示例:覆写将会改变超类已定义的方法,它的代价恰好与闭包相反。可能最终会覆写其他方法所依赖的方法,甚至不知道它是重要的。例如下面的B类中的odd没有任何问题甚至更快,但是C类由于错误的覆写导致问题产生(这恰好是闭包能防止的事情)。
面向对象的代价:
某些方法的可覆写性会带来方法行为改变的问题(甚至不被覆写时也可能),这让我们难以推断“正在关注的代码”是哪一段。因此我们需要去避免一些覆写,通过类似private继承或者final 方法(类似C++或Java中的做法)的方式。
但面向对象的优势是,它使得子类更容易影响超类方法的行为(在不复制代码的情况下)。
如何在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中实现闭包的方式)。