对象, 变量, 常量和类
在ruby中表现数据的基本单位称为对象。
常见对象与其所属的类
对象 | 类 |
数值 | Numeric |
字符串 | String |
散列 | Hash |
正则表达式 | Regex |
文件 | File |
符号 | Symbol |
数组 | Array |
此外还有range和execption对象等
变量分为以下几种:
- 全局变量: 以$来表示
- 实例变量: 以@开头来表示
- 类变量: 以@@开头来表示
- 局部变量: 以英文字母或者_来表示
除了以上几种变量类型 还有伪变量类型(一种在ruby里面预先定义的变量类型)
Ruby中,nil、true、false、self等都是伪变量
全局变量与局部变量示例
新建两个文件,一个为scopetest.rb, 一个为variable.rb
variable.rb内容
$x = 1
x = 1
scopetest.rb内容
$x = 200
require_relative 'sub'
x = 3
p $x
p x
结果为:1 3
常量以大写字母命名
且一旦赋值以后就不在可以被更改,如果赋值会产生异常。
ruby中的保留字
LINE | ENCODING | FILE | BEGIN |
END | alias | and | begin |
break | case | def | defined? |
do | else | elsif | end |
ensure | false | for | while |
if | in | module | next |
nil | not | or | redo |
rescue | retry | return | self |
super | then | true | undef |
unless | until | when | while |
yield |
类和模块
当想知道某个对象属于哪个类时,我们可以使用class方法
ary = []
str = "hello world"
p ary.class #=> Array
p str.class #=> String
当想知道某个对象是否属于某个类时,我们可以使用instance_of?方法
ary=[]
str="Helloworld."
p ary.instance_of?(Array) #=>true
p str.instance_of?(String) #=>true
p ary.instance_of?(String) #=>false
p str.instance_of?(Array) #=>false
继承
通过扩展已定义的类来创建新类称为继承。
继承后创建的新类称为子类(subclass),被继承的类称为父类2(superclass)。通过继承可以实现以下操作。
- 在不影响原有功能的前提下追加新功能
- 重新定义原有功能, 使名称相同的方法产生不同的效果
- 在已有功能的基础上追加处理,扩展已有功能
BasicObject类是Ruby中所有类的父类,它定义了作为Ruby对象的最基本功能。备注BasicObject类是最最基础的类,甚至连一般对象需要的功能都没有定义。因此普通对象所需要的类一般都被定义为Object类。字符串、数组等都是Object类的子类。
子类与父类的关系称为“isa关系”3。例如,String类与它的父类Object就是isa关系。
str = "hello" ;
p str.is_a?(String) #=> true
p str.is_a?(Object) #=> true
p str.is_a?(BasicObject) #=> true
由于instance_of?方法与is_a?方法都已经在Object类中定义过了,因此普通的对象都可以使用这两个方法。
示例:创建一个类
class HelloWorld # class 语句
def initialize(myname = "Ruby") # 类方法
@name = myname # 初始化实例变量
end
def hello # 实例方法
puts "hello, world. I am #{@name}"
end
end
bob = HelloWorld.new("Bob")
alice = HelloWorld.new("Alice")
ruby = HelloWorld.new("Ruby")
bob.hello
alice.hello
ruby.hello
class 类名
类的定义
end
类名的首字母必须大写。
名为initialize的方法比较特别。使用new方法生成新的对象时,initialize方法会被调用,同时new方法的参数也会被原封不动地传给initialize方法。因此初始化对象时需要的处理一般都写在这个方法中。
而只要在同一个实例中,程序就可以超越方法定义,任意引用、修改实例变量的值。另外,引用未初始化的实例变量时的返回值为nil。
存取器
在Ruby中,从对象外部不能直接访问实例变量或对实例变量赋值,需要通过方法来访问对象的内部。
对类进行扩充,增加存取方法:
class HelloWorld # class 语句
def initialize(myname = "Ruby") # 类方法
@name = myname # 初始化实例变量
end
def hello # 实例方法
puts "hello, world. I am #{@name}"
end
def name=(name) # 存
@name = name
end
def name # 取
return @name
end
end
bob = HelloWorld.new("Bob")
bob.hello
bob.name =("kobe")
p bob.name
bob.name =("kobe") #=> 实例变量@name的值此时为kobe
p bob.name
当对象越来越多时,需要定义的存取器越来越多,导致代码越来越复杂。因此ruby提供了存取器的快捷方式
定义 | 意义 |
attr_reader :name | 只读(定义name方法) |
attr_writer :name | 只写(定义 name=方法) |
attr_accessor :name | 读写(定义以上两个方法) |
在实例方法中,可以用self这个特殊的变量来引用方法的接收者
class HelloWorld # class 语句
attr_accessor :name
def initialize(myname = "Ruby") # 类方法
@name = myname # 初始化实例变量
end
def greet
puts "Hi, I am #{self.name}"
end
def greet_again
puts "Hi, I am #{name}"
end
bob = HelloWorld.new("Bob")
bob.greet #=> Hi, I am Bob
bob.greet_again #=> Hi, I am Bob
调用方法时,如果省略了接收者,Ruby就会默认把self作为该方法的接收者。因此,即使省略了self,也还是可以调用name方法
但是有一点需要注意:
在调用像name=方法这样的以=结束的方法时,即使实例方法中已经有了name="Ruby"这样的定义,但如果仅在方法内部定义名为name的局部变量,也不能以缺省接收者的方式调用name=方法。这种情况下,我们需要用self.name="Ruby"的形式来显式调用name方法。
备注 虽然self本身与局部变量形式相同,但由于它是引用对象本身时的保留字,因此即使对它进行赋值,也不会对其本身的值有任何影响。像这样,已经被系统使用且不能被我们自定义的变量名还有nil、true、false、__FILE__、__LINE__、__ENCODING__等。
如何在已经定义的类中追加新的类方法?
方法的接收者就是类本身(类对象)的方法称为类方法。
第一种:
class << 类名 ~ end 这个特殊的类定义中,以定义实例方法的形式来定义类方法。
class << HelloWorld
def hello(name)
puts "#{name} said hello"
end
end
HelloWorld.hello("Ruby")
第二种:
在class语句中使用self时,引用的对象是该类本身,因此,我们可以使用class << self ~ end 这样的形式,在class语句中定义类方法。这种方式很常见
class HelloWorld
class << self
def greet(name)
puts "#{name} said Hello"
end
end
end
HelloWorld.greet("Perl")
第三种:
我们还可以使用 def 类名.方法名 ~ end 这样的形式来定义类方法。
class HelloWorld
def self.speak(name)
puts "#{name} said hello"
end
end
HelloWorld.speak("Matz")
备注 class << 类名 ~ end 这种写法的类定义称为单例类定义,单例类定义中定义的方法称为单例方法。
常量
常量就是指拥有固定值,且值一旦被定义就无法更改的变量,通常用大写字母来命名。
对于在类中定义的常量,我们可以像下面这样使用::通过类名来实现外部访问。
类变量
以@@开头的变量称为类变量。类变量是该类所有实例的共享变量,这一点与常量类似,不同的是我们可以多次修改类变量的值。另外,与实例变量一样,从类的外部访问类变量时也需要存取器。不过,由于attr_accessor等存取器都不能使用,因此需要直接定义。
class HelloCount
@@count = 0
def self.hello
@@count += 1
puts "hello #{@@count}"
end
def self.counts
@@count
end
end
HelloCount.hello
HelloCount.hello
HelloCount.hello
p HelloCount.counts
限制方法的调用
到目前为止,我们定义的方法都能作为实例方法被任意调用,但是有时候我们可能并不希望这样。例如,只是为了汇总多个方法的共同处理而定义的方法,一般不会公开给外部使用。
Ruby提供了3种方法的访问级别,我们可以按照需要来灵活调整。
- public…以实例方法的形式向外部公开该方法
- private…在指定接收者的情况下不能调用该方法(只能使用默认接收者的形式调用)
- protected…在同一个类中时可将该方法作为实例方法调用
public 和 private 的例子
class AccTest
def pub
puts "pub is a public meth!"
end
public :pub
def priv
puts "priv is a private meth"
end
private :priv
end
acc = AccTest.new
acc.pub #=> 正常调用
acc.priv #=> 报错
希望统一定义多个方法的访问级别时,可以使用下面的语法。
class AccTest
public
def pub
puts "pub is a public meth!"
end
private
def priv
puts "priv is a private meth"
end
end
没有指定访问级别的放马默认为public,但initialize方法是列外,它通常被定义为private。
定义为protected的方法,在同一个类(及其子类)中可作为实例方法使用,而在除此以外的地方则无法使用。
示例:
class Point
attr_accessor :x, :y
protected :x=, :y=
def initialize(x = 0.0, y = 0.0)
@x, @y = x, y
end
def swap(other)
tmp_x, tmp_y = @x, @y
self.x, self.y = other.x, other.y
other.x, other.y = tmp_x, tmp_y
return self
end
end
obj = Point.new
obj2 = Point.new(30, 40)
obj.swap(obj2)
p [obj.x, obj.y]
p [obj2.x, obj2.y]
p obj2.x=20 #=> 错误❌,因为方法不是public方法,无法在类外部调用
给原有的类添加方法
Ruby允许我们在已经定义好的类中添加方法。
class String
def count_str
ary = self.split(/\s+/)
return ary.size
end
end
str = "Just another ruby newbie"
p str.count_str #=> 4
继承
class 类名 < 父类名
类定义
end
示例:重新定义[]运算符
其中super来调用父类中的[]方法
class RingArray < Array
def [](i)
idx = i % self.size
super (idx)
end
end
obj = RingArray[1, 2, 3, 4]
p obj[4]
定义类时没有指定父类的情况下,Ruby会默认该父类为Object类。
Object类提供了很多便于实际编程的方法,但在某些情况下希望使用更加轻量的类,
而这时就可以使用BasicObject类。
BasicObject 类只提供了作为Ruby对象的最低限度的方法。类对象调用 instance_methods 方法后,就会以符号的形式返回该类的实例方法列表。下面我们就用这个方法来对比一下Object类和BasicObject类的实例方法。
定义BasicObject的子类时,与Object类不同,需要明确指定BasicObject类为父类。
alias与undef
有时我们会希望给已经存在的方法设置别名,这种情况下就需要使用alias方法。alias方法的参数为方法名或者符号名。
alias 别名 原名 # 直接使用方法名
alias :别名 :原名 # 使用符号名
class C1
def hello
puts "hello"
end
end
class C2 < C1
alias old_hello hello
def hello
puts "hello again"
end
end
class C2 < C1
alias :old_hello :hello
def hello
puts "hello again"
end
end
C2.new.hello
C2.new.old_hello
undef 方法名 # 直接使用方法名
undef :方法名 # 使用符号名例如,在子类中希望删除父类定义的方法时,可以使用undef。
单例类
在下面的例子中,我们分别将"Ruby"赋值给str1对象和str2对象,然后只对str1对象添加hello方法。这样一来,两个对象分别调用hello方法时,str1对象可以正常调用,但str2对象调用时程序就会发生错误。
class << str
def hello
puts self.size
end
end
str2 = ""
str.hello #=> 5
str2.hello #=> No method error
到目前为止,我们已经多次只在特定的某个类中添加类方法。Ruby中所有的类都是Class类的对象,因此,Class类的实例方法以及类对象中所添加的单例方法都是类方法。
模块
模块是Ruby的特色功能之一。如果说类表现的是事物的实体(数据)及其行为(处理),那么模块表现的就只是事物的行为部分。模块与类有以下两点不同。
- 模块不能拥有实例
- 模块无法被继承、
Mixin就是将模块混合到类中。在定义类时使用include,模块中的方法、常量就都能被类使用。
- 虽然两个类拥有相似的功能,但是不希望把它们作为相同的种类(Class)来考虑
- Ruby不支持父类的多重继承,因此无法对已经继承的类添加共通的功能
module MyMoudle
def hello
puts "hello I am Moudle"
end
end
class MyClass1
include MyMoudle
end
class MyClass2
include MyMoudle
end
MyClass1.new.hello
MyClass2.new.hello
模块可以提供独立的命名空间
所谓命名空间(namespace),就是对方法、常量、类等名称进行区分及管理的单位。由于模块提供各自独立的命名空间,因此A模块中的foo方法与B模块中的foo方法就会被程序认为是两个不同的方法。同样,A模块中的FOO常量与B模块的FOO常量也是两个不同的常量。
例如,在FileTest模块中存在与获取文件信息相关的方法。我们使用“模块名.方法名”的形式来调用在模块中定义的方法,这样的方法称为模块函数。
p FileTest.exist?('case.rb')
p FileTest.size("case.rb")
p Math::PI
p Math.sqrt(2)
如果没有定义与模块内的方法、常量等同名的名称,那么引用时就可以省略模块名。通过include可以把模块内的方法名、常量名合并到当前的命名空间。下面是刚才提到的Math模块的例子。
我们使用module语句来创建模块。语法与创建类时几乎相同,模块名的首字母必须大写。
module 模块名
模块定义
end
module HelloModule
Version = "1.0"
def hello(name)
puts "Hello, #{name}."
end
module_function :hello
end
p HelloModule::Version
HelloModule.hello("Ruby")
HelloModule.__send__(:hello, "Perl")
备注:如果只是定义了方法,但是并没有通过 module_function symbol 方法将模块函数公开给外部使用的话,无法通过模块名.方法名的形式调用。 一旦通过module_function symbol 公开方法以后,就能以这种方法调用方法。
以“模块名.方法名”的形式调用时,如果在方法中调用self(接收者),就会获得该模块的对象。
def myself
self
end
p HelloModule.myself #=> HelloModule 模块名
Mix-in
Note: 关于Mix-in
当我们想要知道一个类的继承关系时可以使用xxx.ancestors 来获取
p Regexp.ancestors #=> [Regexp, Object, Kernel, BasicObject]
当我们想要知道一个类的父类可以用 Regexp.superclass方法来获取
假设有个类C, 类C的实例在调用方法时,Ruby会按类C、模块M、类C的父类Object这个顺序查找该方法,并执行第一个找到的方法。被包含的模块的作用就类似于虚拟的父类。
虽然Ruby采用的是不允许具有多个父类的单一继承模型,但是通过利用Mixin,就既可以保持单一继承的关系,又可以同时让多个类共享功能。
单一继承的优点就是简单,不会因为过多的继承而导致类之间的关系变得复杂。但是另一方面,有时我们又会希望更加积极地重用已有的类,或者把多个类的特性合并为更高级的类,在那样的情况下,灵活使用单一继承和Mixin,既能使类结构简单易懂,又能灵活地应对各种需求。
查找方法的规则
1.同继承关系一样,原类中已经定义了同名的方法时,优先使用该方法。
2.在同一个类中包含多个模块时,优先使用最后一个包含的模块。
3.嵌套include时,查找顺序也是线性的.
4.相同的模块被包含两次以上时,第二次以后的会被省略。
extend方法
extend方法可以使单例类包含模块,并把模块的功能扩展到对象中。
module A
def name
puts "I am A"
end
end
str = "Hello"
str.extend(A)
str.name
class V
end
V.extend(A)
V.name
extend既可以用来扩展实例方法,也可以扩展类方法。见上面的示例。
module A
def name
puts "I am A"
end
end
module B
def name
puts "I am B"
end
end
module C
include B
def name
puts "I am C"
end
end
class TestModule
include C
include A
end
p TestModule.ancestors #=> [TestModule, A, C, B, Object, Kernel, BasicObject]
class TestModule
include C, A
end
p TestModule.ancestors #=> [TestModule, C, B, A, Object, Kernel, BasicObject]
注意这里导入模块时,第一种方式是分行,第二种是用逗号隔开,先后顺序有区别,参考结果。
逗号之前的模块会被优先查找,而分行形式,最后倒入的模块会被优先查找。
如果类 Mix-in 了模块,就相当于为该类添加了实例方法,这种情况下self代表的就是被mix-in的类的对象。
即使是相同的方法,在不同的上下文调用时,其含义也会不一样,因此对于Mixin的模块,我们要注意根据实际情况判断是否使用模块函数功能。一般不建议在定义为模块函数的方法中使用self。
除了之前介绍的定义类方法的语法外,使用 extend 方法也同样能为类对象追加类方法。下面是使用 extend 方法追加类方法,并使用 include 方法追加实例方法的一个例子。
module ClassMethod
def cmethod
"class method"
end
end
module InstanceMethods
def imethod
"instance method"
end
end
class MyClass
extend ClassMethod
include InstanceMethods
end
p MyClass.cmethod
p MyClass.new.imethod
在这里,extend 与 include的区别在于 extend 用于扩展类方法,而include是扩展实例方法,这个不能搞错。
其实不管是类也好,实例也好,本质上都是对象,因为对于Class类来说,String等类,都是它的实例,因此类本质上也是对象,因此在调用方法时,只是为了区分接收者的对象类型,才有类方法和实例方法之区别。