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

image-20220607105302547

我们关注的Ruby的特性,最重要的是“纯面向对象语言”,基于类Class-based,动态类型。

image-20220607105837127

我们不关注的部分:

image-20220607110031886

特性对比:

image-20220607110137224

image-20220607110304554

例子:

class Hello
    def my_first_method
        puts "Hello, World!"
    end
end
x = Hello.new
x.my_first_method

在CMD中运行Ruby程序使用ruby命令,通过irb命令使用Ruby的REPL:

image-20220607110708796

Classes and Objects

基于类的OOP规则

image-20220607161842813

image-20220607162121309

例子:

image-20220607162258442

image-20220607162433838

创建和使用对象,这部分语法都很熟悉的。

image-20220607162656198

变量以任意字母作为变量名的首字母,需要注意变量是可修改的mutable(修改内容还是修改引用?)

image-20220607162748331

关于self,跟python的self一样。语法糖也类似,在相同对象中调用方法可以省略self

image-20220607163331891

Ruby中self的常见用法,直接使用方法返回self本身,例如:

image-20220607163643848

image-20220607163631036

最后,在Ruby中,每条语句换行很重要,如果多条语句放在同一行,那就需要使用分号“ ; ”分割。另外,跟python不一样,Ruby中缩进(indentation)对语义(semantics)不重要,只是良好的代码风格而已。

Object State

对象拥有自己的状态。状态组成实例变量(fields),只能被对象方法访问,state变量以@开头。如果使用了某个没有的state变量,会生成nil对象

image-20220607164510038

对象变量赋值创建了一个别名(引用),这时他们具有同样的state。

但如果new一个新的对象的引用,创建的对象就有不同的state

image-20220607165252447

例子:

image-20220607170919573

image-20220607170952331

image-20220607171140572

例子2:initialize特殊方法,用于创建对象实例时初始化,相当于构造函数。

image-20220607171241186

Ruby中因为不要求所有变量都预先声明并且初始化,相同类的不同对象实例可以有不同的实例变量。

image-20220607172125116

通过@@关键词声明类变量,也就是类的静态变量

image-20220607172310283

类常量和类方法,就是类中的静态常量和静态方法

类常量用在类中以大写字母开头,不能被修改。在外部需要使用类名修饰访问,例如C::Foo

类方法定义需要使用self.method_name来定义(说实话有点奇怪),它属于类本身,所以需要使用类名来调用

image-20220607172424494

Visibility

image-20220609164553258

Ruby中,对象的状态state总是private的,同一个类的不同对象也无法访问。通常定义getter和setter来让对象state可见(跟JAVA的POJO类似)

image-20220609164634690

Ruby的语法糖。用state变量的名称来直接定义一个getter,用变量名加=来定义一个setter(并且以等号结尾地方法在调用时可以在等号前面加空格)。这样相当于将state封装成了一个普通的成员变量。

同时,getter/setter还有简记的定义方式(类似java的注解)

image-20220609165144560

为何需要private的对象state:

image-20220609165613740

Ruby的三种visibility,public是默认值:

public在任何类中都能调用

protected在相同类或者子类中能够调用

private只有在相同实例中能调用

image-20220610123445498

三种visibility访问权限的使用方法类似C++

image-20220610123625970

Ruby中比较特别的一个细节,对于private的成员或方法,必须通过简化的方式,即m或m(args)来调用(唯一方式),不能通过self来访问。

image-20220610124209960

A Longer Example

本节以有理数类为例,综合应用前几节的内容:

image-20220610161950604

该例子中展现了很多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能够在运行时操作对象,添加改变替换方法,某些时候会有用,但会破坏抽象和封装。在大型语言中是有争议的。

image-20220613151725084

image-20220614121901761

甚至能够向内部类添加新方法

image-20220614121956960

由于所有的东西都是对象,即使是顶层函数,也是Object的对象,因此直接用def定义顶层函数,相当于添加新Object的方法。然后由于所有对象都直接或间接继承自Object,因此顶层函数可以作为每个对象的方法使用。例如:

image-20220614122759257

直接定义在Object中的方法与顶层函数相同,重复名称的方法会覆盖:

image-20220614122930274

但是,对Ruby预定义的运算符覆盖时一定要当心,例如:

class Fixnum
    def + x
        13
    end
end

这段代码覆盖了原有+运算符的定义,此时Ruby中一切的Fixnum都会受到影响,因此irb(REPL)会崩溃,因为irb本身也是Ruby编写的,会被这个覆盖影响。

(可见,Ruby给运行时程序的权力过大还是挺危险的)

image-20220614123516632

Duck Typing

面向对象中经典的鸭子类型:

在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的,实际上是一种不严格的多态实现。

image-20220614163229800

例子:

image-20220614165652765

得益于动态类型语言的鸭子类型某些时候可能是有用的,但也在一定程度上属于poor style(如果使用者调用了duck typing,就难以更改函数的内部实现了,比如double那个例子,如果有字符串参数调用过这个方法,就不能再改成x * 2的实现方式了)。因此,这种不严格的多态方式也可能带来问题。

image-20220614165940065

Arrays

Array类提供了Ruby的数组类型

image-20220614195914906

Ruby数组具有与python的list类似的语法,但注意Ruby数组在超出引用时会返回nil而不是抛出异常。

image-20220615135729852

同样,由于是动态类型语言,array允许不同数据类型;并且允许使用重载的运算符+作为作为concat方法。

image-20220615135816575

image-20220615140146768

Array的初始化方法(具体的语法需要去翻阅文档)

image-20220615140338253

然后是push和pop,从右侧入栈出栈方法

image-20220615140456938

利用shift和unshift,从左侧入栈出栈方法

image-20220615140828227

Array对象变量仍然是一个引用,因此

image-20220616102220863

值得注意的是,多维数组是引用的引用(类似C++指针的指针),因此a+[]只能改变b的外层引用,但内层地址指向的一维数组没变。

image-20220616102621609

对数组的切片操作,例如f[2,4]表示从idx为2的元素开始,取4个元素。同时,切片的替换并不需要对应数量的数组(这一点很灵活)。

image-20220616104607061

Array可以调用一些迭代方法,例如each

image-20220616104915900

Blocks

代码块,又一个与闭包相关的重要概念。代码块基本上就是一种闭包。

image-20220616105227691

image-20220616105549578

一些奇怪的事情(block的语法)。{}可以用do end来代替。

image-20220616105609854

标准库函数通过代码块定义了大量有用的高等函数,代码块充当了其他函数中一等函数闭包的作用。

image-20220616105937251

代码块在Ruby中非常强大,甚至在初始化Array时都能使用

image-20220616110640707

对于any?和all?方法,如果没有参数,就默认去判断每个元素是否为真(注意Ruby中,只有false和nil是假)

image-20220616110937069

常用高等方法:

方法名 常见作用
map / collect 与Racket的map类似,对每个元素使用f方法
inject 与Racket的reduce类似,对每个函数用f方法进行累计
select 类似filter

image-20220616113531809

代码块嵌套的使用,其中(0..i)是序列(range),与python的range类似

image-20220616115506833

Using Blocks

定义使用Block的函数时,通过yield关键词代替Block(类似匿名函数)。

Snipaste_2022-06-16_16-12-53

例子,这个例子用到了递归,并且递归传递了Block

image-20220616162245314

Procs

Block实际上是第二级,能通过yield来调用,但不能在一个对象中被传递、返回和存储。它不是真正的闭包,但可以转换成真正的闭包。(Block不是对象!)

闭包是Proc类的实例,Proc才是真正的一等表达式。

Obeject的lambda表达式可以通过一个Block来生成Proc实例闭包。

image-20220616170142003

例子:

例如Block难以实现类似Partial Application的应用,但如果使用Proc就能做到

# 由于Block不能传递,不能用Block作为存储对象
c = a.map {|x| {|y| x >= y }}
# 但Proc实例可以传递
c = a.map {|x| lambda {|y| x >= y} }

并且,Proc实例通过call方法调用其中的代码块。

image-20220616171344832

这让我们更加理解“First Class”

image-20220616172015457

Hashes and Ranges

(1)Hash

相当于Racket的python的字典或动态的ML的Record

语法类似python的字典

image-20220616172314534

能够根据键在hash表中添加值

image-20220616172810548

也能用键值对初始化hash表,使用=>连接键值。

由于和Array是不同的数据结构,hash的each方法需要两个参数(key和value)

image-20220616172859145

(2)Range

类似于用来动态存储一系列连续数的数组,但不是数组而是单独的数据结构,类似python的range,可以用于迭代。同时,Range相比之下更高效。

image-20220616173518300

两个优秀的风格:

  • 尽可能使用Range

  • 非数字索引时使用Hash

Hash和Range拥有一些与Array相同的方法(迭代器方法),这也是对duck typing有利。

image-20220616173857724

Subclassing

回到面向对象的部分,介绍Ruby的继承

image-20220622152540770

子类定义:

子类会继承超类所有的方法,而不会有继承权限的问题(例如C++和Java)。子类也不会被类型检测,能访问所有方法和成员。

image-20220622152617905

image-20220622220641263

注意反射机制:

image-20220622220924615

另外,与所有OOP语言类似,子类都(is)超类,所以子类实例(子类)是属于的超类

image-20220622221002459

但需要区分的是,子类实例本身并不是超类的实例,类有继承关系,但子类对象实例不是超类对象实例。

image-20220622221355682

注意,is_a? 或者 instance_of?之类的方法并不通常是OOP style,因为我们假设了使用的对象是 某个假定的类或类对象,它拒绝了duck typing(我们不能使用一个具有类似x,y,color等 成员和方法的其他类对象)。

image-20220622221419406

Why Use Subclassing?

image-20220623212323926

image-20220623212422873

image-20220623212632495

image-20220623212750729

Overriding and Dynamic Dispatch

例子

image-20220623225532227

image-20220623225549016

对象与闭包在很多特性上类似,但最大的不同是覆写可以让一个方法定义在超类中但在子类中调用。

image-20220623225717580

image-20220623230417661

image-20220623230527983

image-20220623230711107

image-20220623231010363

Method-Lookup Rules, Precisely

动态分派(dynamic dispatch),比较熟知的名字是后期绑定或者虚函数,用于在运行期选择调用方法的实现的流程。被认为是面向对象语言(Object-Oriented programming:OOP)的基本特性。

定义方法查找语法是需要的。

image-20220624205053885

回顾各种语言中的变量查找过程。

image-20220624205348869

Ruby中使用self来查找各种类或实例的成员或方法。

image-20220624205539680

Ruby的方法查找:

image-20220624205801610

image-20220624210107516

动态分派的过程比闭包更复杂,必须将self特殊对待。

image-20220624210157295

在Java等语言中,方法查找规则是类似的,但是更复杂(因为方法在类中能具有相同名字(重载Overload)),只有在方法名与参数数量、顺序、类型等都完全相同时,才会发生子类方法的覆写Override。这与Java等语言的静态类型有关,能够通过参数的静态类型来判断选择最符合条件的方法。这种信赖根本上基于type-checking的规则

但在Ruby中,名字相同的方法就总是会被覆写。因为不存在type checking。

这一点对于Java或者C++的使用者来说是比较容易理解的。

总的来书就是,Java和Ruby等都有动态分派(dynamic dispatch)的特性(区别于静态分派),但只有具有静态类型(static type or type chcking) 和 静态重载(static overloading)的语言才能实现上述复杂的覆写机制(即方法名与参数数量、顺序、类型等都完全相同时,才会发生子类方法的覆写)。因此Ruby的覆写机制没有那么复杂,名字相同的方法就总是会被覆写。

image-20220624210540077

Dynamic Dispatch Versus Closures

动态分派与闭包的比较:

ML的闭包示例:闭包不会被后续的shadowing影响,这在某些时候是有用的但某些时候是糟糕的(这就是闭包的代价)

image-20220624220933885

image-20220624222102557

Ruby的动态分派示例:覆写将会改变超类已定义的方法,它的代价恰好与闭包相反。可能最终会覆写其他方法所依赖的方法,甚至不知道它是重要的。例如下面的B类中的odd没有任何问题甚至更快,但是C类由于错误的覆写导致问题产生(这恰好是闭包能防止的事情)。

image-20220624221629652

image-20220624221641732

image-20220624221703213

image-20220624222044973

面向对象的代价:

某些方法的可覆写性会带来方法行为改变的问题(甚至不被覆写时也可能),这让我们难以推断“正在关注的代码”是哪一段。因此我们需要去避免一些覆写,通过类似private继承或者final 方法(类似C++或Java中的做法)的方式。

但面向对象的优势是,它使得子类更容易影响超类方法的行为(在不复制代码的情况下)。

image-20220624222141100

Optional: Dynamic Dispatch Manually in Racket

如何在Racket这样的语言中,手动实现类似动态分派的特性?

这一节的做法有点类似于之前实现解释器的方式。

尽管Racket已经有内置的类和对象,但这一节为了示范一种语言的某种语法(特性)可以在(通过)另一种语言的习惯用法(来构建)。并且为了更好的理解动态分派。

image-20220624223141713

借助struct。这里的示例中,self只是lambda匿名函数的一个用来记录对象本身的额外参数(这个额外参数的做法有点类似python,用来保证对象方法中的对象self调用),但在这里,self不是一个特殊关键词(不会被特殊对待,它可以不叫self,可以叫this、it等等)

image-20220624223643984

image-20220624223842409

本节的代码:其中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)

image-20220624225236774

但某些语言仍然难以实现类似特性,例如ML,他的type system不能实现类似的子类型判断。

但注意,老师强调了一下,难以实现并不是不能实现,我们仍然可以用之前学到的方法,在ML中用datatype构造一个足够大的“Obeject”,然后让所有的“类”都是这个datatype的值,这样,这些所谓“类”之间的“继承关系”就可以通过ML的类型系统来判断了(还是借助pattern match之类的特性)。

这其实也算是一种代价交换,ML虽然难以实现动态分派的特性,但却很容易实现闭包,相反Java是面向对象的,很容易动态分派,但闭包的实现就更难一些(例如PartA中week4时我们在Java中实现闭包的方式)。

image-20220624230456469



这篇关于Programming Languages PartC Week1学习笔记——Ruby与面向对象编程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程