Week 9
类和对象 Classes & Objects
- 基于类的OOP规则(Ruby)
- 全部的值都是对对象的引用
- 对象之前通过方法(对象内部的函数)调用进行交流,也就是传递消息
- 每一个对象都有一个(私有的)状态
- 每一个对象都是某一个类的实例
- 一个对象的类决定了对象的行为
- 如何处理方法调用
- 类包含了方法的定义
- Ruby实在OOP上的一个极端,只有私有状态,且所有的内容都是对象
- 类定义和方法定义:
- 定义一个新类并定义一些方法
- 方法会返回最后一个表达式,或者显式的
return
语句 - ruby使用换行定义句法,缩进不影响
- 和Java类似,ruby中有一个
self
表示当前被调用方法的对象
class Name =
def method_name1 method_args1
expression1
end
def method_name2 method_args2
expression2
end
…
end
- 创建和使用对象
ClassName.new
创建一个对象e.m
将e
评估成一个对象,然后调用方法m
,相当于“向m
发送消息”,也可以写作e.m()
- 方法可以带参数,即
e.m(e1, ..., en)
,在一些情况下括号可以省略(不推荐)
- 变量
- 方法可以使用局部变量
- 语法:以一个字母开头
- 作用域:整个函数体
- 可以写在函数体的任何位置
- 变量可异变,即
x=e
- 变量可以是顶级的,也可以定义在REPL
- 因为任何值都是对象,因此变量的内容总是对象的引用
- 方法可以使用局部变量
- self
self
是ruby的一个特殊的关键字- 用于引用到当前被调用方法的对象
- 如果想调用当前对象的另外一个方法,使用
self.m(...)
,或者直接使用m(...)
- 可以使用
self
传递/返回/存储整个对象
- ruby中对于条件结构有一个很有意思的语法
e1 if e2
,当且仅当e2
为真时执行e1
对象状态 Object State
- 一个对象的状态始终保持,从对象被创建的整个生命周期里,其状态都可以被演进和改变
- 状态只能够从该对象的方法直接访问(可以读取,写入和扩展,并将保持到下一个方法调用)
- 状态由实例变量组成,也就是域
- 语法:
@varname
,且可以出现在类内的任何位置,或者显式定义一个initialize
方法在new
的时候初始化之 - 使用复制语句到指定变量即定义之
- 使用一个未在状态中定义的变量不会产生错误,会返回一个
nil
对象
- 语法:
- 由于状态可异变,因此需要考虑别名问题
- 当使用
new
创建一个对象时,会返回一个新对象的引用,这个对象和之前创建的所有对象都不同,有着不同的状态 - 变量赋值语句将会创建一个别名,两个变量将会拥有对同一个对象的引用
- 当使用
- 初始化:
- 每一个类中都可以定义一个方法
initialize
,这个方法十分特别- 在使用
new
创建一个对象并返回引用前被隐式调用 - 传给
new
方法的参数将会被传递到initialize
方法 - 用于创建对象实例非常好,和其他OOP语言中的构造器类似
- 和其他语言类似, 同样可以设置可选参数
- 在使用
- 通常情况下,将实例变量创建在
initialize
方法里是一个很好的代码风格- 但是在ruby里面,这只是一个方便写法
- 不同于其他OOP语言,需要指明这个类的对象里面有什么域,ruby的同一个类的不同对象可以有不同的实例变量(取决于在一个非初始化方法里是否定义了变量并且是否调用了这个方法)
- 每一个类中都可以定义一个方法
- 类变量:
- 被同一个类的所有实例对象共享的状态
- 语法:
@@varname
- 比较少见但是有时非常有用
- 类常量:
- 语法:由大写字母开始
Consname
- 不应当异变
- 类外可见,使用
Classname::Consname
访问之
- 语法:由大写字母开始
- 类方法:
- 可以类比于其他OOP语言的静态方法
- 语法:在定义一个方法时,在这个方法的方法名前加
self.
- 使用时,直接使用类名调用之
- 这个方法是类的一部分,而不是某个实例,可以视作有关于这个类的辅助函数
- ruby也有着自己的对于类的转字符串方法,名称为
to_s
- 类比于Racket中的解引用,ruby中有插值(interpolation)方法,字符串以
#
开头,后跟花括号,括号内为变量则评估为字符串,否则执行逻辑
可见性 Invisibility
-
可见性:一个程序的那一部分可以访问并使用另外一部分
-
“隐藏一些内容”对模块化和抽象是很重要的一部分内容
-
OOP语言普遍都有各种方式去隐藏一些实例变量,方法,类等等
-
ruby中,对象的状态永远是私有的(private)
- 只有对象自己的方法才能访问这个对象的状态
- 其他对象不能访问这一状态,即使他们有着相同的定义(都有这个状态)
- 对于一个状态的实例变量访问,我们只能写作
@foo
而不是e.@foo
,因为不允许任何除了self
意外的对象访问形式访问实例变量
-
为了能够让对象外部的内容访问这些对象状态,我们需要自行定义一些
getter
和setter
方法用于对状态的访问和修改- ruby中对一个实例变量的访问:
def get_foo @foo end
- ruby中对一个实例变量的修改
def set_foo x @foo = x end
-
有关外部存取对象状态的语法糖
- 对域
@foo
,可以直接用域的名字命名这些方法
def foo @foo end def foo= x @foo = x end
- 如果有一个方法是等号结尾的,我们可以在等号前加空格,如上面的函数的调用可以写为
e.foo = 42
- 还有一种简短形式用于定义存取函数
attr_reader :foo, :bar attr_accessor :foo, :bar
- 但是要注意,对象状态永远私有,这些方法只是用于控制外界访问这些方法的方式
- 对域
-
为什么这些状态必须是私有的?
- 更加的OOP形式
- 可以在不影响客户端使用的情况下修改类的实现(如这一变量如何存储,如何生成等等)
- 可以定义一些方法,看起来很像一个修改器,但实际上根本没有对应的实例变量
- 可以让客户端通过相同的方法执行在不同的类中实现这一方法的不同逻辑
-
方法可见性,三种可见性
private:
,私有,仅对象本身可用protected:
,受保护,仅当前类及其子类可用public:
,公开,所有代码可用- 方法默认是公开的,但是有很多方式改变一个方法的可见性
class Foo = # by default methods public … protected # now methods will be protected until # next visibility keyword … public … private … end
- 如果一个方法是私有的,那么只能通过
m
或者m(args)
来调用,甚至不可以使用self.m
-
和
=
一样,ruby类中可以定义运算符号为名字的方法,且语法让允许直接执行四则运算,即使用r1 + r2
代替r1.+ r2
万物皆对象 Everything is an Object
-
ruby是完全的OOP,任何值均是对一个对象的引用
-
相对更简单,更小的语义规则
-
可以在任何对象上调用任何方法,只是得到一个动态的“未定义方法”错误(实际上一个内部的“方法丢失”方法产生的错误)
-
几乎所有的事情都是方法调用
-
ruby中的
nil
表示“不存在任何有效数据”,但是其是一个对象- ruby中,有两样东西是
false
,一个是false
本身,另外一个就是nil
- ruby中,有两样东西是
-
一切内容都是某一个类的方法,任何顶级方法都实际上被加入到了
Object
类中(是所有其他类的超类) -
由于
Object
类是所有类的超类,那么- 所有定义的类都会具有Object类的方法
- 那么所有的顶级方法,都可以在任何类中被调用(因为继承了下来)
- 除非重名定义以覆盖,否则就是原有的顶级方法逻辑
-
反射(Reflection)与探索性(Exploratory)编程
- 有一些方法定义在所有类上,用于告知这个类的内容
methods
,有哪些方法class
,实例的类
- 可以在运行时询问“一个对象能做什么”并进行相应的回应(反射)
- 有一些方法定义在所有类上,用于告知这个类的内容
动态的类定义 Class Definitions are Dynamic
- ruby是一门动态语言,在程序运行时我们可以修改任何东西,包括类的定义
- ruby程序可以在运行时实现对方法的增删改
- 尽管这种做法会破坏抽象并使代码分析更加困难,但是还有一些用途:想一些并非我本人定义的类(内建类)添加一些辅助方法
- 修改语法:
class someClass
def aMethod
# ...
end
end
- 对于修改后的类,所有对应的对象就将拥有新方法,尽管这些对象在该修改动作执行之前被创建
- 如果修改语法对原有方法进行了覆盖,那么所有的方法都将拥有新的方法
- 一个麻烦:动态类定义可能产生一些奇怪的语义问题
鸭子类型 Duck Typing
- 如果走起来像鸭子,叫起来像鸭子,那么这就是一只鸭子
- 尽管可能它就不是鸭子
- 在OOP中,我们希望实现一个功能的方法,需要一个
Foo
参数,但是我们能做的是接受一个参数然后执行方法,发现这个参数“走起来像Foo,叫起来像Foo”,那么尽管它可能不是Foo,在动态语言中,这种同样可以使代码正常工作的方法仍然可行 - 接受鸭子类型,意味着我们专注于方法调用的过程和结果,而避免了一些用于测试具体对象类型的语言特性
- 好处:鸭子类型实现了更大程度上的复用,更加OOP的方法——一个对象收到了怎样的信息才是最重要的
- 坏处:没有任何东西是等价的,因为和可能所谓等价内容可能产生完全不同的效果,甚至新的错误
- 避免了类型检查而使用清晰的文档保证代码的正确使用
数组 Arrays
- ruby中对
Array
类有很多特殊的语法以及已有的方法 - 数组,即用于保持任意数量个其他对象,并使用数字做索引访问,即使用
a[i]
访问,同时使用a[i]=e
修改 - 与其他语言中的数组相比
- 更加灵活和动态,可以用于几乎任何ruby的数据类型
- 甚至可以做其他语言数组视为错误的行为
- 虽然可能会比较低效
- 数组可以容纳任意类型,对于任何正数作为索引,非已知范围的访问会返回
nil
,同时如果跨范围(现有4,在7)赋值,则会自动用nil
填补新值到旧范围之间的项目 - 由于没有类型限制,一个数组内容可以放入任何类型的对象
- 可以直接使用
+
连接两个数组 - 使用
|
合并两个数组,会解决重复元素问题(需要注意的是,这种去重逻辑是基于对象所包含的eql?
方法,注意修改该方法逻辑造成的异常) - 可以使用
Array.new(x)
创建指定尺寸的数组(初始值均为nil
),同时可以指定初始值 - 可以使用数组作为一个栈(
push
和pop
于数组尾)或者队列(push
于数组尾,shift
于数组头,unshift
可以用来在头部插入数据)来使用 - 别名的定义在这里同样适用,数组本身是引用,同时数组的各个元素也是引用,这也就意味着,当数组引用本身改变时,赋值方法可能导致其内部元素的引用可能相同
- 使用
a[x,y]
可以访问从x
开始的y
个元素的数组片段,并可以修改内容,甚至是大小(赋值为小于y
个元素,这一部分就会缩成更新后结果) - 使用
a.each
形成一个迭代器访问整个数组,并在其后用大括号定义逻辑
代码块 Blocks
- 代码块基本可以视为是闭包
- 可以传入一个匿名函数到这个方法中
- 代码块可以接受0到若干个参数
- 使用词法作用域,代码块体使用代码块定义时的环境
- 上一部分提到的
a.each
后面跟着的大括号就是一个代码块 [2, 3, 5].each {puts "hi"}
或者带参数[2, 3, 5].each {|x| puts x}
- 可以像任何信息(方法调用)传入0个或1个代码块
- 被调用者(方法)可能会忽略之
- 被调用者可能会因为未传入代码块而报错
- 被调用者可能会因为为传入代码块而执行不同的行为(块中的参数不同也可能有影响)
- 直接将代码块放在其他参数之后
- 语法:
{e}
,{|x| e}
,{|x, y| e}
- 可以将大括号替换为
do ... end
,主要用于大于一行的情况
- 语法:
- 很多标准库中的方法都是接受代码块的
- ruby有显式循环,但是基本没人用,同时会使用
each
等迭代方 - 对应方法:
any?
所有元素中有一个符合条件则为真,否则为假all?
所有元素中所有都符合条件则为真,否则为假each
,相当于map
inject
,相当于fold
,保持一个累加量用来处理所有数据select
,相当于filter
(0..i)
提供一个范围,可以使用之进行隐式循环- 被调用者不会给代码块参数一个名字,相反在函数中使用
yield
或者yield(args)
调用代码块内容 - 可以使用
block_given?
判断是否有代码块传入,或者假设一定有代码块,再或者使用常规参数内容判断
过程 Procs
-
和代码块的租用很类似,但是过程是实实在在的对象而且拥有函数闭包所有的能力
-
代码块实际上第第二等函数
- ruby中几乎所有的内容都是对象,但代码块不是
- 一个方法能对代码块唯一能做的就是使用
yield
使用之 - 不能返回,不能存到对象中,不能放到数组中
- 可以将代码块变成对象:过程,其闭包实例使用
call
进行闭包中的方法调用
-
过程是一级表达式,因为可以作为计算结果,方法的返回结果,可以存入对象中并且向其他内容一样被传递
-
ruby中有很多方式可以将代码块变成一个过程的方式(使用
lambda
方法)- 封装之(本例为根据数组元素定制一些函数闭包)
c = a.map {|x| (lambda {|y| x >= y})}
- 调用之
c[2].call 15
- 封装之(本例为根据数组元素定制一些函数闭包)
-
一级实例让闭包比代码块更加强大
-
但是相对而言,在一些使用场景中代码块更方便
哈希表和范围 Hashes and Ranges
- 哈希表更像是数组,但有不同
- 键(数组中只能是整数作为索引)可以为任何对象(字符串或者通用标识等)
- 没有数字索引的自然顺序
- 不同的语法(更像是一个动态记录)
- 数据的映射关系表示
key => value
- 范围更像是存有连续数字的数组,但是更加的高效,能够产生大型的范围
- 使用之前提到的方式定义
start..end
- 使用之前提到的方式定义
- 尽可能使用范围(更加高效)
- 当单纯使用数字索引值令代码变得难懂时,使用哈希表
- 数组,哈希表和范围有一些相同的方法,这能更好地用于鸭子类型编程
- 实际上,鸭子类型在OOP中的应用和函数式编程中的顶级函数的用法有共同之处
子类 Subclassing
-
子类型:一个类型的定义拥有一个超类(默认情况下是
Object
类)class SubClass < SuperClass
-
超类的定义会影响当前子类的定义
- 子类会继承超类的所有方法
- 子类也可以按需覆盖超类的方法定义(重名函数,不同逻辑)
- 使用
super
调用超类同名方法
-
不想其他语言的继承关系,ruby中没有实例数据域的概念,所有的实例变量会在赋值时被创建
-
ruby中的子类和类型系统没有关系,只是类定义的一种形式:这个类中的方法来自于何处
-
遵循继承的原理:子类实例同时也是超类实例(使用
is_a?
),但是使用绝对类型判定则不是(使用instance_of?
)(Java里面的instanceof
和ruby里面的is_a?
是一样的) -
不过上面两个方法的使用并不是OOP风格的用法
-
可能的几个替代子类的可选思路?
- 不创建子类,直接将新方法加入到原有类型中(动态类型特有)——这样会导致整个类定义变得非常混乱,对于可能的子类更是如此(临时加入可能产生很多未知的冲突)
- 不继承,直接赋值全部代码——二者完全分离,避免了冲突,但是也失去了服用代码的好处
- 不适用子类,而是将原超类变成一个实例变量——完全封装,但是不如代码复用方便,同时两个类也没有了“类型上”的联系,没有子类的类型联系的表达能力强
-
还是子类型更好一些(在大多数我们希望复用代码同时还能联系类型)
覆盖和动态分发 Overriding and Dynamic Dispatch
- 对象与闭包的相同之处
- 闭包只有一个函数,而对象是多个方法
- 对象会显式地表示其拥有的实例变量,闭包则是将变量绑定保存到环境中
- 继承避免了代码的不断复制
- 覆盖就相当于替换方法
- 一个很大的不同:覆盖可以让一个定义在超类的方法调用子类的方法
- 当我们尝试调用内部方法时,实际上是对
self
这个对象中的方法之调用,而这个对象时整个对象,是子类的具体实例 - 由于子类和超类的实例变量的获取逻辑可能不同,因此涉及到实例变量的集成方法在两个类中的工作方式可能不同
- 动态绑定:
- 同样可称为延迟绑定或者虚拟方法
- 当在类
C
定义的方法m1
中调用self.m2()
,可以解析到定义在其子类中的方法m2
- Ruby的方法查找规则很像Racket中的
letrec
,但对于self
就比较特别 - 对
self
的规则:self
映射到当前的对象中- 向绑定于
self
的对象查找实例变量@x
- 向绑定于
self.class
中的对象查找类变量@@x
- 对于方法?
- 对于方法的查找规则:
e0.m(e1, ..., en)
- 将表达式
e0, e1, ..., en
评估为对象obj0, obj1, ..., objn
(按照上面所述的一些规则去查找一些变量及对象等等) - 令
C
为obj0
的类 - 如果
C
中定义了m
,则使用这个方法的相关代码,否则就递归地向其超类寻找,直到C
为Object
(找不到就调用method_missing
) - 评估方法的方法体
- 参数被绑定为
obj1, obj2, ..., objn
self
被绑定为obj0
- 参数被绑定为
- 将表达式
- 为了实现动态绑定,将
self
映射为方法的接收者,并评估方法体
动态绑定与闭包 Dynamic Dispatch vs. Closures
- 在ML中闭包是关闭的,我们可以覆盖闭包中的某一个函数,但是我们调用比保重中的函数时,覆盖的内容将不会起作用
- 在ruby中,由于覆盖的影响,一些并没有在子类中被覆盖的方法,很可能会在子类中产生不同的行为(其本身调用的方法改变了)
- 问题:难以推断代码的具体含义,可以使用
private
禁止覆盖 - 优势:可以在不复制代码的情况下影响子类中方法的功能
- 问题:难以推断代码的具体含义,可以使用