Ruby Quick Start (2) -- Objects in Ruby

《Ruby语言入门教程v1.0》笔记(2)


第四章 Ruby面向对象初探

1. 封装

让我们来定义一个类, 类名是 Person类名首字母要大写; 属性有姓名@name、年龄@age、国籍@motherland,实例变量用@开头;
方法有一个,叫 talk, 方法名和参数名应该用一个小写字母开头或者用一个下划线开头。

class Person
  def initialize(name, age = 18)
    @name = name
    @age = age
    @motherland = "China"
  end  # 初始化方法结束
  def talk
    puts "my name is " + @name + ", age is " + @age.to_s
    if @motherland == "China"
      puts "I am a Chinese."
    else
      puts "I am a foreigner."
    end
  end  # talk方法结束
  attr_writer :motherland  # @motherland的setter
end

p1 = Person.new("Yilin", 23)
p1.talk

p2 = Person.new("Ben")
p2.motherland = "ABC"
p2.talk

initialize 是初始化方法,相当于构造函数。
参数 age 有一个缺省值 18,可以在任何方法内使用缺省参数,而不仅仅是 initialize。 如果有缺省参数, 参数表必须以有缺省值的参数结尾。

注意我们定义了@motherland成员变量的setter:

attr_writer :motherland

相当于

def motherland=(value)
  return @motherland = value
end 

类似的可以定义getter:

attr_reader :motherland

相当于

def motherland
  return @motherland
end 


2. 继承

class Student < Person
  def talk
    puts "I am a student. my name is "+@name+", age is "+@age.to_s
  end  # talk 方法结束
end  # Student 类结束

p3 = Student.new("Guy", 23); p3.talk
p4 = Student.new("Ben"); p4.talk

<表示 Student 类是 Person 类的子类。Person 类的一切,Student 类都能继承。
但是 Student 类重写了 talk 方法,所以我们看到了不同的运行结果。
子类继承父类的时候, 除了重写方法也可以添加一些新的方法;
或是增强父类的方法(用关键字 super 指明)。

现在说一说 new 方法。
Person 类没有定义 new 方法,为什么生成 Person 类的具体实例要用 newRuby 语言已经定义了一个类 Object,如果你在定义新类的时候,没有指明新类的父类,那么Ruby解释器认为,新类的父类是 Object 类。
Object 含有 new 方法、initialize 方法等等,只要你不重写这些方法,你就自然在使用类 Object 的方法。

你写一个类的时候,是在创造一类事物的蓝图;当你 new 的时候,一个实例就按照蓝图生成了。

蓝图早已设计好了,new 的时候就是出生的时刻,那么,何时消亡呢?
这里没有 C++ 的析构函数,也没有 Java 的 finalize() 方法,Ruby 语言内建了一个比 Java更灵巧的垃圾收集器, 当某个实例不再与其它代码交互了, 垃圾收集器就回收它占用的系统资源,这个实例自然也就不存在了。
垃圾收集器是一段代码,它作它的工作,自动地、不知疲倦地随着系统一同运作,并无自己的喜恶。



3. 多态

class Worker < Person
  def talk
    puts "I am a worker. my name is "+@name+", age is "+@age.to_s
  end  # talk 方法结束
end  # Worker 类结束

p5 = Worker.new("InsaneGuy", 23); p5.talk
p6 = Worker.new("Ben"); p6.talk

Ruby 语言,只有重写(override) ,没有其它语言具有的严格意义上的重载(overload)。

Ruby 语言有自己的单例方法,还有模块插入(Mix-in),后面会深入探讨 Ruby 语言的面向对象特征。




第五章 Why Ruby?

当你决定学习或使用一门语言的时候,首先要回答的问题就是:Why XXX? 而不是一开始就去比较语言的优劣。

语言有优劣吗?有!肯定有!但是没有绝对的优劣,在不同的情景下选择合适的语言才是程序员应该关注的。

XXX可以是C++,Java,Python,当然现在要讨论的是Ruby。

Java是强静态语言,与软件的快速开发无缘。

C++不仅兼容C,而且囊括了模板、范型等特性,包罗万象。无论是系统调用、网络开发、数据库操作都能显试身手,可是程序员很难掌握这些,即使想熟练应用其中某一方面也不容易。

Ruby 灵巧,快速,但其实并不简单

Ruby中实现一个小功能,可以有 3 种甚至 4 种完全不同的思路与方法,因为Ruby在语法层次实现了冗余,但是这样一来:

  1. 程序员深入掌握 Ruby 变得不很容易;
  2. 程序员们相互读懂代码也很难;
  3. 软件生产是一种大规模群体合作的行为。许多软件公司有自己的编码规范,促使员工编码风格统一,以便于:
    • 程序解耦重构
    • 代码复用
    • 人员流动后项目如期推进。

Java 撇下 C++,成为软件工业的支柱语言,正是得力于此。

Ruby 灵巧,快速,千变万化,没有统一风格,难于解耦,在目前,自然不适合工业生产

Ruby 的语法中有许多容易产生歧义的地方,假如没有较深的功力、良好的编码风格(比如空格的使用) ,很容易犯错。软件生产总是偏向于成熟方案、成熟工具的。

Ruby语言具有动态特征,代码行为随时可以改变(比如可以重定义实例的方法)。

编程语言的发展可以分两大类,一类函数式语言,一类命令式语言

命令式语言将操作数演化成现在我们熟悉的变量,将操作码演化成方法(或叫函数),对变量执行各种操作。

面向对象编程又将基本变量和方法封装在一起, 成为一个更复杂的变量——对象。

但是, 在一个类中仍然区分基本变量和方法。

函数式语言则不同,一开始的函数式语言不区分变量和方法,一切都是表(list),表就是能够不断分解成单个元素的数学符号。

表可以是变量,可以是方法。后来的有些函数式语言,吸取了命令式语言的语法,也区分变量和方法。

变量有哪些特征呢?

  1. 变量有名字;
  2. 变量代表的那个事物应该有一个可以用数学度量的值;长度,面积,速度大小,磁场强度等
  3. 为了区别事物,我们将事物分成几个基本类型。所以,代表不同类型的事物,变量也就有了不同的类型。
  4. 事物总是有产生到消亡的一个过程,因此,代表事物的变量,也就有了生命期。在程序中,我们把变量的生命期,称之为变量的作用域。

作为一个使用者,1. 2. 是必须的。至于类型、生命期,与我何干?某个变量,我使用一下就丢弃了。

由编译内核(或解释内核)在运行时刻来判断变量类型的语言,叫动态类型语言。

在运行中,变量能够随时代表不同的事物,而不管事物是什么类型,这种语言,叫弱类型语言。这里的“弱”,是指弱化了类型的概念,不理会类型的差异。

Ruby 语言还是有基本类型。至于变量作用域,纯粹的函数式语言中是没有这个概念的。Ruby 中是有变量作用域概念的,还记得变量名前缀字符($表示全局变量,@表示实例变量,@@表示类变量)吗?实际应用中,有时会比较复杂,使用闭包时就知道了。

Ruby 是动态类型语言,不用给任何变量指定数据类型,解释器会在你第一次赋值给变量时,在内部将数据类型记录下来。

Ruby 语言中,一个变量被赋予了某个数据类型的值,在程序中你可以随时再赋予这个变量其它数据类型的值。

a = 5
print "a = ", a, " ", a.class, "\n"
a = "hh"
print "a = ", a, " ", a.class, "\n"

Ruby 的动态类型特点是一把双刃剑,熟手游刃有余,生手常常伤着自己。

在没有了编译器查错的日子里,又没有完全驾驭 Ruby 之前,如何避免常常出错呢?

有一个下口之处,就是死盯住变量的命名。用一些有意义的名字,不必太长, 但是应该少用单字符, 除非是循环指针变量。

你也许认为我自己能看懂就行了,这是十分有害的想法。在一个项目组中,程序员是要彼此相互沟通合作的。当坏习惯养成后,要改是很难的

Ruby是动态语言,你可以改变Ruby程序的结构,功能。

在Ruby程序运行中,方法、属性可以被加入或去除,新的类或对象可以被建立,新的模块可以出现:

class Person
  def talk
    puts "Today is Saturday. "
  end
end
p1 = Person.new
p1.talk  # Today is Saturday

class Person
  def talk
    puts "Today is #@date. "
  end
  attr_accessor :date
end
p1.date = "Sunday"    
p1.talk  # Today is Sunday.

除了修改方法,添加方法,你还可以除去方法:

class Person
  def talk
    puts "Today is Saturday. "
  end
end
p1 = Person.new
p1.talk  # Today is Saturday.
class Person
  undef :talk  # undef关键字将talk方法定义为不可使用
end
# p1.talk talk方法已经不存在

Ruby语言灵活,因为Ruby是动态语言;Ruby语言强大,因为Ruby是动态语言;Ruby语言初学者容易犯错误,也因为Ruby是动态语言。



Ruby的一些编码建议

一. 命名

常量全用大写的字母,用下划线分割单词。例如:MAX, ARRAY_LENGTH

类名和模块名用大写字母开头的单词组合而成。例如:MyClass, Person

方法名全用小写的字母,用下划线分割单词。例如:talk, is_prime?

在Ruby里,有时将!?附于某些方法名后面。

惊叹号!暗示这个方法具有破坏性, 有可能会改变传入的参数。

问号?表示这个方法是一个布尔方法,只会返回 truefalse

变量和参数用小写字母开头的单词组合而成。例如:name, currentValue

类名、模块名、变量名、参数名最好使用“名词”或者“形容词+名词”。

方法名最好使用“动词”或者“动词+名词”。例如:aStudent.talk



二. 空格和圆括号

关键字之后要留空格。

逗号,、分号;之后要留空格。

赋值操作符、 比较操作符、 算术操作符、 逻辑操作符, 如=+=>=<=+*%&&||等二元操作符的前后应当加空格。

一元操作符如!~等之后不加空格。

[].::这类操作符前后不加空格。

函数名之后不要留空格,紧跟左圆括号(,以与关键字区别。

左圆括号(向后紧跟,右圆括号)向前紧跟,紧跟处不留空格。

Ruby中圆括号常常被省略:

def talk name
  "Hi! " + name
end
puts talk "Yilin"     
puts talk("Yilin")     
puts (talk "Yilin") 
puts (talk("Yilin"))

优先规则会自动确定哪个参数被哪个方法使用。但是,生活并不总是美好的,事情经常变得复杂。
所以建议除了极简单的情况,还是使用圆括号为好。

圆括号还可以把几个语句约束成一个语句集合:

a = 3
b = 1; a += b if 3 > 5
print "a = ", a, "\n" # a = 3
print "b = ", b, "\n" # b = 1
c = 3
(d = 1; c += d) if 3 > 5
print "c = ", c, "\n" # c = 3
print "d = ", d, "\n" # d = nil
# 条件为假,语句集合里的变量d 没有被赋值


三. 使用 return

你在定义方法的时候,在最后一行可以显式地 return 某个值或几个值,但却不是必须的。

Ruby 方法的最后一行语句如果是表达式,表达式的值会被自动返回;最后一行语句如果不是表达式,就什么也不返回。

return 并不仅仅用在方法的最后一行。

使用 break 你能够跳出本层循环,如果要从多重循环体中跳出,可以使用 return ,结束这个方法; return 还能够从方法的某个执行点立即退出,而不理会方法的其余代码。



四. 注释

养成写注释的习惯吧!你见过没有路标的高速公路吗?

注释表明了一段代码块的功能、意图或是代码块的解释,应该简洁明了,错误的注释不如没有注释。

一般地,注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不要放在代码的下方。




第六章 深入Ruby面向对象

1. 重载与重写

在 Java 中,重载(overload)和重写(override)是用来表现多态性的两种重要方式。

override 也有译作“覆盖”、“覆写”。Java 中称作“覆写”比较恰当。

重载方法是指一个类中,方法名相同、参数列表不同的几个方法,调用时根据不同的参数调用不同的方法。方法重载与返回类型无关。

覆写方法是指子类有一个方法,方法名、参数列表、返回类型与父类的某个方法完全一致。 调用时会调用子类的方法, 而屏蔽掉父类的同名方法。

需要注意的是,子类覆写的方法,其可访问性一定要强于或等同于,父类被覆写的同名方法。

覆写发生在子类和父类之间, 当然也可以是子类和父类的父类之间。

重载不仅仅是发生在子类和父类之间,大多数时候,发生在同一个类中。

“ Ruby 语言, 只有重写, 没有其它语言具有的严格意义上的重载。 ”

由于 Ruby 同时支持缺省参数和可变参数,所以 Ruby 不支持重载。

如果在同一个类中写两个同名方法,总是写在后面的方法被执行。

在 Ruby 中,我们说覆写是指重写,我们说重载也是指重写

Ruby 是动态语言,可以随时改变类的属性、方法,所以覆写和重载的重要性就降低了。仔细体会一下,一者是增大可选择性,一者是随时修改。



2. 增强父类方法

如果我们只是想增强父类的方法,而不是完全地替代它,就可以用关键字 super 指明。

class Person
  def talk(name)
    print "my name is #{name}."
  end
end

class Student < Person
  def talk(name)
    super
    print "and I'm a student.\n"
  end
end

aPerson = Person.new
aPerson.talk("Yilin") # my name is Yilin.
print "\n\n"
aStudent = Student.new
aStudent.talk("Yilin") # my name is Yilin.and I'm a student.


3. 实例变量、类变量、类方法

如果一个变量,只能被某个实例对象使用,这样的变量称之为实例变量;如果一个变量,能被某个类的所有实例对象共享,这样的变量称之为类变量。

Ruby的常量可以定义在类和模块中,不能定义在方法中。

如果在外部访问类或模块中的常量,要使用 域作用符::

全局变量用$开头。

实例变量,变量名用@开头;类变量,变量名用@@开头。

Ruby 中所说的局部变量,可以是存在于类中、方法中、模块中、一个循环中、一个过程对象中。局部变量名用小写字母开头

如果我们希望一类事物共享某个变量,类变量能够很好地实现这个需求。

class StudentClass
  @@count = 0
  def initialize( name )
    @name = name
    @@count += 1
  end
  def talk
    puts "I am #@name, This class have #@@count students."
  end
end

p1 = StudentClass.new("Student 1 ")
p2 = StudentClass.new("Student 2 ")
p3 = StudentClass.new("Student 3 ")
p4 = StudentClass.new("Student 4 ")
p3.talk  # I am Student 3 , This class have 4 students.
p4.talk  # I am Student 4 , This class have 4 students.

与全局变量和实例变量不同,类变量在使用前必须要初始化;全局变量和实例变量如果没有初始化,其值为 nil

类方法——不依赖于任何特定实例对象的方法。类方法与实例方法的定义方式不同, 定义类方法要在方法名前加上类名和一个点号.

类方法的主要作用是为了访问类变量

class StudentClass
  @@count = 0
  def initialize
    @@count += 1
  end
  def StudentClass.student_count
    puts "This class have #@@count students."
  end
end
p1 = StudentClass.new
p2 = StudentClass.new
StudentClass.student_count  # This class have 2 students.
p3 = StudentClass.new
p4 = StudentClass.new
StudentClass.student_count  # This class have 4 students.


4. 单例方法

同一份设计蓝图(类),不同的实例对象,可以表现出不同的行为特征,这种(不牵涉继承的) 多态性在 Java 这样的静态语言里, 通过方法重载得到具体实现。

前面我们分析知道了 Ruby 中的重载是指重写, Ruby 如何来反映不同实例对象的不同行为特征呢?

在 Ruby 里,可以给具体的实例对象添加实例方法,这个方法只属于这个实例对象,我们把这样的方法称之为单例方法。

class Person
  def talk
    puts "Hi! "
  end
end
p1 = Person.new
p2 = Person.new
def p2.talk  # 定义单例方法 p2.talk
  puts "Here is p2. "
end
def p2.laugh  # 定义单例方法 p2. laugh
  puts "ha,ha,ha... "
end
p1.talk  # Hello!
p2.talk  # Here is p2.
p2.laugh  # ha,ha,ha...

实例方法,属于类的每个实例对象。单例方法只出现在单个实例对象中。

用单例方法可以极大地丰富多态性在 Ruby 中的表现力。



5. Ruby中的访问控制

在 Ruby 里,要读取,或是改变对象的属性,唯一的途径是调用对象的方法。

控制了对方法的访问,也就控制了对对象属性的访问。

访问控制 意义
public 可以被任何实例对象调用,不存在访问控制;
protected 可以被定义它的类和其子类访问,可以在类中或子类中指定给实例对象;
private 可以被定义它的类和其子类访问,不能被实例对象调用。

方法默认都是公有的( initialize 方法除外,它永远是私有的)。

class Person
  def talk
    puts " public :talk, 将调用 speak"
    speak
  end
  def speak
    puts "protected :speak,将调用 laugh"
    laugh
  end
  def laugh
    puts " private:laugh"
  end
  protected :speak
  private :laugh
end
p1 = Person.new
p1.talk
# p1.speak 实例对象不能访问 protected 方法
# p1.laugh 实例对象不能访问 private 方法
  • public 方法,可以被定义它的类和其子类访问,可以被类和子类的实例对象调用;
  • protected 方法, 可以被定义它的类和其子类访问, 不能被类和子类的实例对象直接调用,但是可以在类和子类中指定给实例对象;
  • private 方法,可以被定义它的类和其子类访问,私有方法不能指定对象。(即不能用a.method这种方式访问)

可以根据自己的需要,在程序的不同位置,改变某个方法的访问控制级别,让程序更加富于变化。

class Person
  private  # 后面的方法设定为 private
  def talk
    puts " already talk "
  end
end

p1 = Person.new
# p1.talk private 方法不能访问
class Person
  public :talk  # 动态声明talk为public
end
p1.talk     # already talk