前情提要:
第12天,往细节探索去!昨天我们讲到broc是有名字的内存块物件,可储存变数;lambda是一种method方法,严格检查参数数目。今天想要更深地讨论变数:)
Ruby经典面试题目#12
Ruby的类别变数与类别实体变数,与实体变数有何不同?What is difference between class variable,class instance variable and instance variable?
我们曾在第四天时讨论过类别方法和实体方法(leafor)。
还记得我下的这个结论:
如果要将实体方法,运用在某个客制化的实体,就使用instance method;
如果某个方法并不会和某个特定的实体变数绑在一起,就使用class method。
实体变数instance variable
实体变数是一个比较好理解的概念,来举个例子吧:
我想把每天跑步的习惯RunDaily写成class,为了持续维持好习惯,方法有两个:早上跑morning_run或者晚上跑evening_run,如果想早上跑(morning run),实体变数@mr会带入参数5km,晚上(evening)跑的话,@ er带入10km。
今天是第12天了~
我们创造出day12的如下:
class RunDaily
def morning_run(km)
@mr = km
end
def evening_run(km)
@er = km
end
end
day12 = RunDaily.new
p day12.morning_run(5)
p day12
p day12.evening_run(10)
p day12
我们可以看到实体变数(instance variable)以@开头,不需要先在class开头宣告,原因是:
Ruby的实体变数不是public,仅作用于于self指示的物件。除非明确提供其他方法,
否则无法从物件以外变更或查看。原文
5
#<RunDaily:0x000055e64755a770 @mr=5>
10
#<RunDaily:0x000055e64755a770 @mr=5,@er=10>
从输出结果看到day12这个物件的方法是Rundaily,动态地加入了两个实体变数mr和er。
实体变数的属性(attribute)
物件的实体变数,就是物件的属性(attribute),就算是同一个class的不同物件,其属性也不同。
(还记得我们在Ruby铁人赛第一天提到,Ruby世界观里,万物皆物件吗?)
我很喜欢一种说法:每一天都是全新的一天,昨天、今天,明天都是不同的物件,独立的个体:)
让我们来创造新物件,解释不同物件的属性。
假设我创造了明天这个新物件(第13天)Day13遇到休假日,所以早上一口气跑了21km:
day13 = RunDaily.new
p day13.morning_run(21)
p day13
结果显示此物件存在于不同的內存位置,而且变数也不同:
21
#<RunDaily:0x0000561a9376e1d0 @mr=21>
属性存取器(attribute accessors)
就像我们有时候会想知道每个特定的日子分别跑了几公里,或者是重新提取每天铁人赛的文章内容到底是什么。
这时候能够读取实体变数的属性是非常重要的,让我们可以更方便的读取这些不同的物件(因为,凡走过必留下痕迹!就像翻开自己写过的日记或铁人赛一样。)
案例一:Yesterday
现在来IronmanDairy类别里写一个属性存取器(attribute accessors)的公开方法,让我们可以设定(set_dairy)、取得(get_dairy)昨天Day12的铁人赛文章标题:
class IronmanDairy
def set_dairy(title)#write dairy
@title = title
end
def get_dairy #read dairy
@title
end
end
day11 = IronmanDairy.new
p day11
day11.set_dairy(“Explain the difference between block,proc,lamdba.”)
day11.get_dairy #取出昨天文章的标题
p day11
日记day11物件被我们读取出来了:显示出內存位置,及@title实体变数:
#<IronmanDairy:0x000055d4f44e2748>
#<IronmanDairy:0x000055d4f44e2748 @title=“Explain the difference between block,proc,lamdba.”>
案例二:Today
set_dairy和get_dairy方法虽然让我们易于了解属性的写入与读取方式,但把细节拆解开来的代码却显得过于冗长。
为了产生同时具有读Read+写Write功能的实体变数(在这里是@title),每次都要写出这一对set_dairy和get_dairy方法,不是很累吗?
(就像写日记一样,你不会把写日记解释为:这是一组打开日记本,写日记,然后再阖上日记本的动作。)
你就只是想。要。写。日。记而已!
有没有精简的方法呢?
(你猜对了!只要仔细找一找手册,Ruby里通常都有方法!)
为了秉持着每一个今天都比昨天更好的精神,我们提出改良版本:
假设我们要写第12天新文章day12,可以利用写入title=method,及取得titlemethod,查看文章标题,取代原本的set_dairy和get_dairy:
class IronmanDairy
def title=(title)#write dairy
@title = title
end
def title #read dairy
@title
end
end
day12 = IronmanDairy.new
day12.title =“class variable,class instance variable and instance variable”
p day12
p day12.title
结果印出:
#<IronmanDairy:0x00005648020c0828>
#<IronmanDairy:0x00005648020c0828 @title=“class variable,class instance variable and instance variable”>
注意,这里的title=也是一个实体方法,我们来用.instance_methods确认一下:
p IronmanDairy.instance_methods(false)#=> [:title=,:title]
案例三:Tomorrow
有没有发现上面的代码中,大量出现这个@title实体变数呢?如果想要更进一步简化,可以用attr_accessor方式改写。
假设我们要创一个明天Day13铁人赛文章物件,直接把实体的属性存取器attr_accessor:title,指定给symbol:title,加在类别的开头即可:
class IronmanDairy
attr_accessor:title
end
day13 = IronmanDairy.new
p day13 #<IronmanDairy:0x00005579aee8bc00>
day13.title =“Still thinking…”
p day13 #<IronmanDairy:0x00005579aee8bc00 @title=“Still thinking…”>
p day13.title #“Still thinking…”
p IronmanDairy.instance_methods(false)#[:title=,:title]
从以上的[Yesterday,Today,Tomorrow]三个举例,代表持续改良提取物件属性的写法,是不是能够对于实体变数有全方位的了解了呀?
类别变数class variable
类别变数在类别开头,用@@定义,它是个危险的东西,因为所有的子类别中对类别变数的改动,都会影响其他类别的变数。我们用「鸡兔同笼」的例子,来算算不同的动物各有几只脚:
class Animal
@@legs = nil #先预设动物的脚为空值nil
def self.legs
@@legs
end
end
p Animal.legs # => nil
class Chicken < Animal #`鸡`类别继承`动物`类别
@@legs = 2
end
p Chicken.legs # => 2
p Animal.legs # => 2动物变2只脚了!
class Rabbit < Animal
@@legs = 4
end
p Rabbit.legs # => 4
p Animal.legs # => 4,动物又变4只脚了!
class Snake < Animal #笼子里加入一只蛇
@@legs = 0 #蛇没有脚!
end
p Animal.legs # => 0
p Snake.legs # => 0
p Rabbit.legs # => 0糟糕,为什么这次兔子没有脚!~~被蛇吃掉了~~
为了解决此问题,我们来研究Ruby的类别实体变数,看看是否有更好的做法。
类别实体变数class instance variable
我们在Day1中开宗明义地解释面向对象语言的精髓:物件可以具有类别和实体变数。既然类别也是一种物件,那「类别物件」当然可以有「类别的实体变数」。我们继续「蛇兔同笼」的例子,举例出三种变数大乱斗:
class Animal #案例1: animal类别- class variable
@@legs = nil #设定类别变数为nil
def self.legs
@@legs
end
end
p Animal.class_variables #印出类别变数:@@legs
p Animal.legs #类别变数:目前为空值nil
p Animal.instance_variables # => []尚未设定实体变数,所以没东西
class Animal #案例2: animal类别- instance variable
attr_accessor:legs #设定实体变数
@legs = 0
end
p Animal.instance_variables # =>现在能印出实体变数:@legs
p Animal.legs #仍然是类别变数的空值nil(dcjwsc.com)
class Animal #案例3: animal类别- class instance variable
class << self;attr_accessor:legs end
#self在类别class里,代表目前所在之处的animal类别(而不是案例1和案例2的同名类别唷)
@legs = 1 #设定「类别实体变数」,先预设为1
end
p Animal.legs # => 1 #不是nil,不是0,而是1(类别实体变数!)
class Rabbit < Animal
@legs = 4
end
p Rabbit.legs # =>兔子4只脚
p Animal.legs # =>回到类别实体变数预设值1
class Snake < Animal
@legs = 0
end
p Snake.legs # =>蛇0只脚
p Rabbit.legs # =>兔子还是4只脚!太好了~没有被吃掉
p Animal.legs # =>回到类别实体变数预设值1
以上的举例实实在在地证明我在这本书Effective Ruby中文版-写出良好Ruby程序的48个具体做法Page 56查到的观点:宁愿用类别实体变数,也不要用类别变数。类别实体变数除了会打破类别及其子类别的共享关系(如同我们举的例子中,动物的脚数目随意被改变),也提供更多的封装,让类别定义层级、或从类别方法里,唯一可存取的是类别实体变数。
最后用比一比的表格来总结:)
类别变数class variable类别实体变数class instance variable实体变数instance variable
@@类别变数@类别实体变数@实体变数
在类别开头设定可用attr_accessor的方式改写可用attr_accessor的方式改写
可用在类别方法或实体方法用在类别方法,不可用在实体方法用在实体方法