本帖最后由 RyanBern 于 2015-7-31 10:26 编辑
[box=RoyalBlue]第1章节:类 [/box]
有了预备知识,我们就可以进入Ruby中运用最广泛的部分——类(class)了。我在这里要说的是,在介绍类之前,我认为大家应该对Ruby的语法有了大致了解,对一些基本的概念也都掌握了,例如控制语句,函数,所以就没有再重复这些东西。如果大家有不明白的,请关注下RMXP的F1,那里面已经说得很清楚了。
1.1 类&实变量&实例方法
1.1.1 类的概念
类其实是一个比较难说的概念,Ruby是面向对象的编程语言,所谓对象,就是计算机中的抽象数据,而且数据之间有一定的逻辑和结构关系。所谓类,不严格地来讲,就是抽象数据类型。翻开RMXP的F1,我们会发现,所有的类,竟然也都是一个对象,这就是我们所说的“万物皆对象”的观点。打比方来说,人类可以看作是一个类,那么人类的每一个个体就可以看作是一个对象,不知道这样是不是能好理解些。从感性的角度上来说,类应当是对象的一个集合。所有的人就构成了一个人类,而人是人类的一个实例(Instance)。类的上面定义了“属性(Property)”和“方法(Method)”。属性我们可以理解为描述(类生成)对象的性质,比方说人的身高,体重等等;方法可以理解为对象的某种操作,比方说吃饭,喝水,说话等等。对于类本身,还有专属于类的类变量和类方法。另外,类之间有继承关系,有多态性(Polymorphism)。如果你能理解这些,那么理解类也就不难了。
1.1.2 实变量&实例方法
每一个类的对象(实例)都有属于自己的变量和方法,所谓实例的变量,就是定义在该对象中的,只限于该对象自己使用的变量,实例方法,也就是只有属于这个类的对象才能使用的方法。我们举了例子来具体说明:
class Person #定义一个叫Person的类 def initialize @name = "Ryan" @gender = 'M' end def hello print "I am " + @name + "!" end end
class Person #定义一个叫Person的类
def initialize
@name = "Ryan"
@gender = 'M'
end
def hello
print "I am " + @name + "!"
end
end
这个名叫Person的类就定义好了。
首先说一下,Person是上面我们所说那个“类的类(Class)”中的一个实例,Class类中的实例只有一个方法叫做new,调用方法要用圆点运算符“.”表示。格式是a.method(…),
其中a是该类的一个实例,method是方法的名字(函数名字,必要的时候要带参数)。
【关于initialize方法】每个类都能定义一个名叫initialize的方法,这个方法比较特殊,是类的构造方法,或者成为“构造器”。当你使用Xxx.new生成一个类的实例时,便自动调用了这个initialize方法,'.new'后面跟的参数会原封不动地传递到initialize中去。
刚才我们说过了,Person是Class中的一个实例,那么Person.new的意思就是生成一个Person类的实例。我们接下来插入下面一个代码:
ryan = Person.new ryan.hello
ryan = Person.new
ryan.hello
这两句的意思是生成一个Person类的实例,储存在一个变量中,然后调用该类的方法。因此执行完这两句之后,屏幕上将会打印出I am Ryan!的字符串。
在上一章节我们已经说过,如果一个变量是对象的一部分数据,那么要把它定义成实变量的形式(即@开头的变量),这样定义出来的变量在整个类的内部都是有效的。试想如果把上面的语句中所有的@都去掉,那么后果将会是……
给initialize方法添加参数
我们回头再来看看这个程序片段。
class Person #定义一个叫Person的类 def initialize @name = "Ryan" @gender = 'M' end def hello print "I am " + @name + "!" end end
class Person #定义一个叫Person的类
def initialize
@name = "Ryan"
@gender = 'M'
end
def hello
print "I am " + @name + "!"
end
end
我们会发现,这个类别建立好了之后,所有的实变量的值都是固定的,这就导致我们只能建立相同内容的实例,因此我们要稍稍改装一下,让建立的实例拥有不同的初值。
class Person #定义一个叫Person的类 def initialize(name,gender) @name = name @gender = gender end def hello print "I am " + @name + "!" end end
class Person #定义一个叫Person的类
def initialize(name,gender)
@name = name
@gender = gender
end
def hello
print "I am " + @name + "!"
end
end
这里的initialize是所有类一般要定义的方法,在一些面向对象的语言中,把这个方法叫做“构造方法”或者“构造器”。实际上,Person.new这个语句已经在调用initialize,所有类的初始化函数名字必须是initialize,这也是Ruby的规定。这里的initialize带有2个参数,我们回忆一下函数参数的概念,就会发现@name和name根本不是一回事。这样定义好了之后,我们可以写下面的语句了。
ryan = Person.new("Ryan",'M') ryan.hello
ryan = Person.new("Ryan",'M')
ryan.hello
这样和刚才的效果相同,但是我们可以随便设置实例的初值。
实变量公开化
虽然我们可以用上面定义的类生成一个个实例,但是我们想一下这种情况:有一天Ryan出门了,见到了一个漂亮的女孩,因为我们定义了hello方法,所以Ryan很轻松地向她打了个招呼。女孩于是问:"Ryan"这个名字怎么拼啊?Ryan这时却抓耳挠腮,因为Ryan还没学会怎么让对方知道自己的名字拼写。再比如,Ryan觉得这个名字太简单了,想改成"RyanBern"(依然很矬),但是依然没有办法改变自己的名字。解决上面两个问题,就要用我们的实变量公开化。
class Person #定义一个叫Person的类 def initialize(name,gender) @name = name @gender = gender end def hello print "I am " + @name + "!" end #### def name return @name end def name=(name) @name = name end #### end
class Person #定义一个叫Person的类
def initialize(name,gender)
@name = name
@gender = gender
end
def hello
print "I am " + @name + "!"
end
####
def name
return @name
end
def name=(name)
@name = name
end
####
end
我们加入了两个方法,第一个函数是返回实例中@name的值,第二个函数的作用恰恰是修改实例中@name的值。
我们注意到,这两个方法的名称比较奇特,和@name这个实变量很相似。实际上,第一个方法叫做'name',第二个方法叫做'name=',并且带一个参数。你可能会问了,怎么会有名字这么奇怪的方法?这种方法在C++/Java中都是没有的(除非你想操作符重载,但是即使重载了,效果也不如Ruby里面的这个好),而Ruby之所以这样做,是出于对实变量保护隐藏的目的。我们在类对象的外面,是没有办法修改对象内部的变量的,如果真的需要修改,那么只能通过定义方法的形式。注意,第二个函数开头的两个name是不一样的,前面那个是方法名字,括号里面的是形参名字。
有了这两个函数,我们可以写(假设Ryan已经定义好了):
nam = ryan.name #将ryan的@name赋值给变量nam ryan.name += "Bern" #将ryan的@name后面加个"Bern"
nam = ryan.name #将ryan的@name赋值给变量nam
ryan.name += "Bern" #将ryan的@name后面加个"Bern"
这里注意,上面的例子中第二句同时调用了'name'方法和'name='方法,请仔细思考一下。
这种通过定义方法来公开变量,是Ruby中最常见的。实际上,定义这样的函数之后,可以把被定义的变量看作是类的一个属性,我们可以获取属性的值或者修改属性的值(但严格来讲,【属性】这个概念并不存在于Ruby当中)。一种简单的理解,就是把'name','name=','@name'联系在一起,'name'方法返回'@name'的值,'name='方法修改'@name'的值。将原点运算符'.'理解为“的”,那么a.name就可以理解为某人的名字。但是我们要知道,其实圆点运算符表示的就是类方法的调用,加上属性的概念是为了突出这种方法的特殊地位。这样,相关的实变量便可以通过外部访问。
当然,这样编写可能会浪费大量的篇幅,我们在这里有比较简单的形式来代替上面的两个方法。
attr_accessor :name attr_reader :name attr_writer :name
attr_accessor :name
attr_reader :name
attr_writer :name
其中,attr_accessor是后面两个的合并,同时具备两种功能。
而attr_reader只定义了'name'方法;attr_writer只定义了'name='方法,表示只允许读或者只允许写。
在这里我们注意一个问题,'attr_'开头的这一串文字,不是所谓关键字,也不是所谓变量声明。它们本身就是方法(隶属于Module),而它们的作用,就是生成其他的方法。而':name'就是它们的参数(这是一个Symbol对象,也可用字符串对象代替)。它们生成的方法有最简单的形式,即'name'方法是单纯返回@name的值,'name='方法是单纯修改@name的值。如果你想要让这两个方法变得复杂,那么就不要使用'attr_xxxx'。
因此,我们打开脚本编辑器,映入眼帘的Game_Temp,里面就是这些东西,仔细看看也没什么了不起的是吧?
1.1.3 父类&子类&类扩充&方法重写(Override)
在这里我们引出父类和子类的概念。假设我们定义好了一个类Person,现在我定义它的子类Student,那么我们要写:
class Student < Person,表示Student是Person类的子类。那么Student这个类所有的实例的性质,都会具有Person类的所有性质。即如果student是Student的一个实例,那么在Person类中定义的实变量和方法,student都能继承下来。这有点类似于“遗传”的概念。因此我们可以写student.name,student.hello等等这样的语句。
引入这一概念的原因,也是为了编程的方便。在计算机所有的对象中,虽然很多对象都属于不同的类,但是他们也同时具有很多的共同性质。那么我们可以先把它们的共同性质定义出来,做成一个父类,然后再分别定义它们特有的性质。在RGSS中,最明显的就是Window类,窗口类的父类是Window,然后Window_Base是它的一个字类,表示一般的窗口;Window_Selectable则是Window_Base的一个字类,表示具有光标的滚动窗口类。这两个类会衍生出一系列的子类,总体说分两种,不带光标以及滚动功能的,基本都是Window_Base的子类,具有光标以及滚动功能的,基本都是Window_Selectable的子类。有人会说,既然Window_Selectable是Window_Base的子类,那么我们岂不是可以把下面所有窗口类都归到Window_Selectable下?这是不行的,Window_Selectable比Window_Base更丰富,但是需要的内存空间也就更多,如果你要一个不带滚动功能的类,却设置到了Window_Selectable下,那么很多内存空间都没有被利用上,这显然是我们不需要的。
第二版新增
值得一提的是,类之间的结构定义体现了一个人编程素养的高低。在制作程序时,如果要新增某种功能,一定要引入新的类。但是怎么去引入?是直接引入所需类,还是从父类做起,以便有更好的适应性?这都是问题。对于两个不同的类,是做成父类——子类比较好,还是做成两个平行毫不相关的类比较好?答案并非十分明确。在这里举一个Java中的例子,我觉得很有参考价值。在Java的AWT包中,有Menu(菜单类)和MenuItem(菜单项类,即菜单拉开中的各个选项)。如果让你制作,你会把它们的关系定义为:
(1)父类——子类?
(2)子类——父类?
(3)两个平行类?
在Java中,这两个类的关系是第二种,Menu(菜单类)是MenuItem(菜单项类)的子类?啥?搞错了吧?菜单项包含在菜单的里面,怎么反而菜单项是父类?这个问题稍加思考,便可以赞叹编写Java-AWT包人员的高明之处。我们点开一个菜单,里面有各种菜单项,当然也包括一些可展开的菜单(即二级菜单),从这个角度上来讲,菜单就是一种特殊的菜单项,只不过它可以展开而已。因此MenuItem是Menu的父类是没问题的。
提到了父类——子类,那么还有一个东西就不得不提,那就是抽象类(abstract class)
尽管在Ruby中,并没有看到类似于抽象类的字眼,但是我们还是能够深刻地体会这种编程思想。面向抽象的编程思想也是编写这样程序的重要思想之一。抽象类是高度概括的一个类,它包含着它所有子类的一般行为。正因为它的高度概括性,它就必须作为一个父类出现,等待着别的类去继承它,扩充它。而自己本身,由于内容过于概括,因此不适合实例化一个对象。你问我这种现象的例子有没有?很明显,Object类就是一个抽象类。它定义了对象的一般行为,Object类作为所有类的祖宗,有着无可替代的高度概括性。定义出一个好的抽象类能够让你的代码更加简洁,易懂,而且显得有水平。
我们再看一个例子,RGSS1中的Game_Battler,它就是一个抽象类。它的两个已知子类分别是Game_Actor和Game_Enemy。啥?为啥说它是抽象类?我们看看下面的代码。
class Game_Battler #-------------------------------------------------------------------------- # ● 获取 MaxHP #-------------------------------------------------------------------------- def maxhp n = [[base_maxhp + @maxhp_plus, 1].max, 999999].min for i in @states n *= $data_states[i].maxhp_rate / 100.0 end n = [[Integer(n), 1].max, 999999].min return n end end
class Game_Battler
#--------------------------------------------------------------------------
# ● 获取 MaxHP
#--------------------------------------------------------------------------
def maxhp
n = [[base_maxhp + @maxhp_plus, 1].max, 999999].min
for i in @states
n *= $data_states[i].maxhp_rate / 100.0
end
n = [[Integer(n), 1].max, 999999].min
return n
end
end
这是定义MaxHp的一段代码,比较简洁。我们注意def下面那句n = base_maxhp + @maxhp_plus ....
问,base_maxhp为何物?
它并不是局部变量,那就是一个方法了。
在Game_Battler搜索def base_maxhp,结果啥也没找到。
也就是说,Game_Battler里面的maxhp引用了一个没有在类内部定义的一个方法?这怎么可以?
这怎么不可以??
我们再次搜索def base_maxhp,结果在Game_Actor和Game_Enemy中均找到了它的定义。原来,这个方法不在父类中定义,而是在子类中定义。父类的方法却要用到子类的方法,这不是差辈儿了么?不是这样的,由于父类的高度概括性,导致无法在父类中描述base_maxhp具体的执行过程,而它的两个子类中,描述base_maxhp的执行过程是可以知道的。因此,父类就弄出这么个方法放到这儿,表示我不在这里定义,而是在子类中进行定义。这样只是起说明作用却没有实体的方法叫做“抽象方法”,而在Ruby中,你甚至都不用声明一个抽象方法。
那允许我再问个问题,如果对Game_Battler类的一个对象调用maxhp方法,则又如何?岂不是要出现No method error?
要记住,既然是抽象类,一般就不用它去生成一个实例,不生成实例,何谈调用一说啊?RGSS1中出现过Game_Battler.new吗?显然没有。
第二版更新·完
当然,如果一个类的方法和属性定义不能满足我们的需求,而我们又不想再定义一个子类,这时候我们就要对原有的类进行扩充。当然,你可以在原来的类上面进行修改,不过,如果是系统整合的话,我们会采用以下的方式:
class Person def goodbye print "See you!" end end
class Person
def goodbye
print "See you!"
end
end
注意,Person我们已经在前面定义好了,但是你完全可以再写一遍class Person,表示对该类进行追加定义。
方法重写(Override)
如果一个方法已经在父类被定义过,在子类再次对它定义(通常利用父类已有的方法)就叫做方法的重写。例如:
class Student < Person def hello print "I am " + @name + ", and I am a student." end end
class Student < Person
def hello
print "I am " + @name + ", and I am a student."
end
end
这里我们重写了方法hello,那么在调用student.hello的时候,屏幕上显示的就是新定义叙述的内容(即后面多了and I am a student)。
不过,这顶多算是把原来的方法覆盖掉了,重写的味道还不是很浓。我们在定义子类方法的时候,通常是对父类的同名方法的扩充,这就要用到关键字super,例如:
class Student < Person attr_accessor :student_id #定义新属性,学号 def intialize(name,gender,student_id) super(name,gender) @student_id = student_id end end
class Student < Person
attr_accessor :student_id #定义新属性,学号
def intialize(name,gender,student_id)
super(name,gender)
@student_id = student_id
end
end
在这里,我们调用了Person类的initialize的方法,利用的是关键字super,当然参数什么的不能少。为什么不能写initialize(name,gender)呢?如果这样的话,系统会认为这是一种递归定义,即函数调用自身的过程(Ruby中允许递归定义函数),而并不是调用父类的同名方法,因此我们必须采用这样的形式。
注意,super关键字的使用有一定特殊性,在使用super的时候,要格外注意参数问题。在这里推荐,即使是被调用的方法没有参数,也要跟一对空括号'()'来表示没有参数,而不能什么都不带直接写super。这是因为,如果super后面什么都不跟,那么默认传进去的参数和正在定义的方法相同,这有可能引起错误。例如:
# 定义父类 A class A def initialize @a = 0 end end # 定义子类 B,这里 B 的 initialize 方法中,super的使用是错误的!! class B < A def initialize(b) super @b = b end end # 定义子类 C,这里 C 的 initialize 方法中,super的使用是正确的 class C < A def initialize(c) super() @c = c end end
# 定义父类 A
class A
def initialize
@a = 0
end
end
# 定义子类 B,这里 B 的 initialize 方法中,super的使用是错误的!!
class B < A
def initialize(b)
super
@b = b
end
end
# 定义子类 C,这里 C 的 initialize 方法中,super的使用是正确的
class C < A
def initialize(c)
super()
@c = c
end
end
如果有上面的定义,那么在使用B.new时,就会发生Argument Error。
方法的覆盖
如果一个方法在一个类中被定义,再次对其定义就叫做方法的覆盖。例如
class A def test p 1 end end a = A.new a.test # => 1 class A def test p 2 end end a = A.new a.test # => 2
class A
def test
p 1
end
end
a = A.new
a.test # => 1
class A
def test
p 2
end
end
a = A.new
a.test # => 2
在这里 A 类的方法'test'被定义了两次,那么后定义的会覆盖之前定义的。如果调用A.new.test,实际调用的方法取决于这句话的位置,如果在第一个test后第二个test前使用,那么实际调用的就是覆盖前的方法;如果在第二个test后使用,那么实际调用的就是覆盖后的方法。
在这一小部分的最后,我想买个关子,在方法的定义和重定义中,还有一个重要的“别名”机制alias,这个东西的存在,为我们写脚本带来了更大灵活性,那么关于alias,我们要放到后面的章节进行讲解,这里就先不说了。
应大家的要求,我们在这里布置一道小练习题。
练习:请定义一个表示水瓶的类,水瓶有两个属性,一是最大容积(用一个正整数表示),二是当前水瓶中盛放水的量(也用一个非负整数表示,不得大于最大容积)。在这个类上面定义三个类方法:1.将一个水瓶装满水;2.清空一个水瓶里面的水;3.将这个水瓶自身里面的水倒入另一个水瓶,注意,不是随机地倒,倒完之后,保证自身是空的或者对方是满的。
定义类class Bottle并验证你定义的方法。
第二版新增
*1.1.4 模块(module)简介
首先,什么是模块?模块是用于实现某些特定功能的代码的组合。它与类不同,和类相比较,模块内部定义了一些常量和方法,从完整性看,模块不如类完整。不过模块内部可以定义内部类,这样定义的好处是让类的作用更加清楚。
如何定义一个模块?
利用关键字module可以定义一个模块。
module Action WORD_ON = "On" WORD_OFF = "Off" def turn_on print WORD_ON end def turn_off print WORD_OFF end end
module Action
WORD_ON = "On"
WORD_OFF = "Off"
def turn_on
print WORD_ON
end
def turn_off
print WORD_OFF
end
end
这就是一个简单模块的定义。从这几句代码来看,定义了两个字符串常量和两个(普通)方法,而这两个方法,从名字来看是“打开”,“关闭”。可是这有什么用?我们知道,我们可以打开收音机,可以打开电脑,也可以打开煤气灶。那么,对于这几个类,都有相应的“打开”和“关闭”方法。一个个定义显然不妥,我们想到了定义父类。收音机和电脑可以归入到“家用电器(Appliance)”类中,但是煤气灶无论如何也不是什么家用电器。这可咋办?那就定义到更高的父类中,把家用电器和“煤气灶所属的类”归到一个类上去。那我们叫它“家庭用品(Utensils)”类。但是在这个类定义turn_on和turn_off方法,问题就更多了。家庭用品不但包括家用电器,煤气灶,还包括床,被单啥的,难道它们也可“打开关闭”?显然不行。那咋办?
我们经过观察,可以发现,turn_on和turn_off只是两个特定的功能,和类什么的关系不是很大。于是我们把它定义到模块当中,然后在类中把这个模块糅合进去(Mix-in)。这样,类中便有了模块中实现的方法。
要在类中糅合模块,要用include方法。
class Radio < Appliance include Action attr_reader :brand def initialize(brand) @brand = brand end end ra = Radio.new("ChangHong") ra.turn_on # => "On" ra.turn_off # => "Off"
class Radio < Appliance
include Action
attr_reader :brand
def initialize(brand)
@brand = brand
end
end
ra = Radio.new("ChangHong")
ra.turn_on # => "On"
ra.turn_off # => "Off"
这样Radio对象就可以使用Action模块里面的功能。
啥?不会用?那么我再说个简单的用法好了。Module可以当作命名空间(namespace)使用,主要是各种参数和常量的定义。啥?直接定义在外面?这可不好,万一别人定义的和你自己定义的重复了就麻烦了,还是放到命名空间里面好。我们看看著名的Fuki对话框脚本:
module FUKI # 头像图片保存目录的设定 HEAD_PIC_DIR = "Graphics/Heads/" # 是否显示尾部图标 TAIL_SHOW = true # Skin的设定 # 使用数据库默认窗口Skin情况下这里使用[""] FUKI_SKIN_NAME = "skin3" # 呼出对话框用Skin NAME_SKIN_NAME = "skin3" # 角色名字窗口用Skin # 字体大小 MES_FONT_SIZE = 22 # 呼出对话框 NAME_FONT_SIZE = 14 # 角色名字窗口 # 字体颜色 #(设定为 Color.new(0, 0, 0, 0) 表示使用普通文字色) FUKI_COLOR = Color.new(255, 255, 255, 255) # 呼出对话框 NAME_COLOR = Color.new(255, 255, 255, 255) # 角色名字窗口 # 窗口透明度 # 如修改窗口透明度请同时修改尾部箭头图形内部的透明度 FUKI_OPACITY = 255 # 呼出对话框 MES_OPACITY = 255 # 默认信息窗口 NAME_OPACITY = 255 # 角色名字窗口 # 角色名字窗口的相对位置 NAME_SHIFT_X = 0 # 横坐标 NAME_SHIFT_Y = 16 # 纵坐标 # 窗口表示时是否根据画面大小自动检查窗口出现的位置, # 自动改变位置( true / false ) # 设置成 true 可能出现箭头图标颠倒的问题 <- bbschat POS_FIX = false # 在画面最边缘表示时的稍微挪动 # 使用圆形Skin的角和方框的角重合的情况下为 true CORNER_SHIFT = false SHIFT_PIXEL = 4 # true 时挪动的象素 # 角色高度尺寸 CHARACTOR_HEIGHT = 48 # 呼出对话框的相对位置(纵坐标) POP_SHIFT_TOP = 0 # 表示位置为上的时候 POP_SHIFT_UNDER = 0 # 表示位置为下的时候 # 信息的表示速度(数字越小速度越快,0为瞬间表示) # 游戏中 可随时用数字代入 $mes_speed 来改变消息表示速度。 MES_SPEED = 1 end
module FUKI
# 头像图片保存目录的设定
HEAD_PIC_DIR = "Graphics/Heads/"
# 是否显示尾部图标
TAIL_SHOW = true
# Skin的设定
# 使用数据库默认窗口Skin情况下这里使用[""]
FUKI_SKIN_NAME = "skin3" # 呼出对话框用Skin
NAME_SKIN_NAME = "skin3" # 角色名字窗口用Skin
# 字体大小
MES_FONT_SIZE = 22 # 呼出对话框
NAME_FONT_SIZE = 14 # 角色名字窗口
# 字体颜色
#(设定为 Color.new(0, 0, 0, 0) 表示使用普通文字色)
FUKI_COLOR = Color.new(255, 255, 255, 255) # 呼出对话框
NAME_COLOR = Color.new(255, 255, 255, 255) # 角色名字窗口
# 窗口透明度
# 如修改窗口透明度请同时修改尾部箭头图形内部的透明度
FUKI_OPACITY = 255 # 呼出对话框
MES_OPACITY = 255 # 默认信息窗口
NAME_OPACITY = 255 # 角色名字窗口
# 角色名字窗口的相对位置
NAME_SHIFT_X = 0 # 横坐标
NAME_SHIFT_Y = 16 # 纵坐标
# 窗口表示时是否根据画面大小自动检查窗口出现的位置,
# 自动改变位置( true / false )
# 设置成 true 可能出现箭头图标颠倒的问题 <- bbschat
POS_FIX = false
# 在画面最边缘表示时的稍微挪动
# 使用圆形Skin的角和方框的角重合的情况下为 true
CORNER_SHIFT = false
SHIFT_PIXEL = 4 # true 时挪动的象素
# 角色高度尺寸
CHARACTOR_HEIGHT = 48
# 呼出对话框的相对位置(纵坐标)
POP_SHIFT_TOP = 0 # 表示位置为上的时候
POP_SHIFT_UNDER = 0 # 表示位置为下的时候
# 信息的表示速度(数字越小速度越快,0为瞬间表示)
# 游戏中 可随时用数字代入 $mes_speed 来改变消息表示速度。
MES_SPEED = 1
end
Fuki脚本把这些常量都定义到module当中了,他为啥不定义在最外面?显然是考虑冲突的问题。因此我们写脚本也要有这样的习惯,把常量都定义到模块中,不要定义在外面,更不要定义成全局变量。例如(某升级提示脚本):
开始觉得这样没什么,后来越看越觉得不好。
想要在别的类引用模块内部的常量,要用到'::'运算符。
当然如果一个模块已经被include在类内,就可以在类内部直接用了。
我又忍不住联想了,其实module这个东西吧,跟接口(Interface)比较相似,都是实现某些特殊功能的语句组合。他们还有一点不谋而合:由于Ruby中无法多重继承(实际上多重继承并不十分合理,个人认为,但是多重继承有可取之处),因此module便可以实现某种意义上的“多重继承”。这点利用接口(Interface)也可以做到。
第二版更新·完
1.2 两个重要的类
在这里我们要介绍两个我们经常用的类。
1.2.1 数组Array
数组可以看作是一些对象的有序集合,在内存中占据一块连续的区域。在C语言中,数组有固定长度,而且数组内包含的元素类型必须是相同的。但是在Ruby中,数组运用就灵活了很多。这里的数组不但没有固定的长度,而且内部的元素类型也不必相同。例如:[0,2,nil,[1,2]],这个数组包含4个元素,从左到右分别是两个整数,nil,还有另一个数组。对数组成员进行访问,直接用下标表示,假如a是一个数组,第0号单元就是a[0],也就是物理位置上的第一个。下标从0开始而不是从1开始,这个为处理问题提供了很大方便(我一开始也不懂为什么要从0开始,编了四年之后发现这是很方便的)。所以大家还是尽量熟悉它吧。下面我们说下数组常用方法,这个在F1中输入array搜索就能找到。
- 初始化
a = []或者a = Array.new
别小看这个东西,很多时候因为少了这句话,会引发NoMethodError for nil : NilClass - 将一个元素x放在数组的最后面
a.push(x)
将数组里面添加元素的常用方法,类似于进栈操作。当然push可以跟很多参数,表示把参数依次添加到数组末尾。 - 删除数组中最后一个元素,并返回它
element = a.pop
注意这个函数有两个功能,一是删除,二是取值,类似于出栈操作。 - 删除数组中值为val的所有元素
a.delete(val)
注意,删除之后,所有元素依然是相邻的,下标的位置可能改变。 - 删除数组中位置为nth的元素
a.delete_at(nth)
同上,删除之后其他的元素位置会移动。 - 判断数组中是否有元素val
include?(val)
注意,Ruby中,以'?'结尾的函数的功能约定为判断(当然只是一个约定),返回值要么是true,要么是false。我希望大家也把这个“传统”延续到自己的脚本编写中。 - 将数组排序
a.sort
a.sort!
a.sort!{|a,b|…}
注意,Ruby中,以'!'结尾的方法称为“重磅方法(Bang Method)”,告诉你这是一个比较危险的方法,很可能破坏原始数据。
其中第三个为带块的排序,即按照一定标准排序。例如:a是一个数组,里面元素是我们刚才定义好的Person的实例,现在要将所有元素按照年龄大小排序,那么就要写a.sort!{|a,b| a.age – b.age}。注意,花括号里面的a和b是形式参数,意思就是取好了数组中的两个元素后,再取他们的age属性,和外面的表示数组的a无关。 - 遍历整个数组(第二版变更)
each{|item| ...}
each_index{|i| ...}
each_with_index{|item, index| ...}
具体块中的操作自己设定。
例如:
a = (1..100).to_a s = 0 a.each{|n| s += n} p s # => 5050
a = (1..100).to_a
s = 0
a.each{|n| s += n}
p s # => 5050
each方法就是按照次序取出数组中所有的元素,然后根据元素进行某种操作。
each_index实际就是对数组的索引进行遍历,作用等同于(0...a.size).to_a.each{|i| ...}
each_with_index每次把索引和相对应的单元都取出来,以便在块内使用。
注意:熟悉RGSS1的人喜欢用for item in a~end的形式,其实for是一个语法糖,具体调用的方法还是each,这不过是照顾那些C++的人而设置的。
注意:使用each迭代器时,不要使用类似于delete这样对原始数组有破坏的方法,这是因为在迭代过程中要尽量保持原有数组的不变性(这种行为在C#里面是不能通过编译的)。
1.2.2 Hash表
Hash表又称关联数组,它相当于在集合A和集合B上建立了一个映射。也就是说,它将A中的每个元素映射到B中。A中的元素成为主键(key),B中的元素称为值(value)。每一个主键都对应唯一一个值。由于主键之间无法排序,所以Hash表也是没有顺序的。
具体的操作大家就自行F1,输入Hash查找一下吧。这里就不多加叙述了。
类的基础知识我们就说完了,肯定不能说的很详细,数组和Hash表的用法,大家如果想知道更多,请参考帮助文件,那说的应该比我要全。在下一章节我们要全局地分析一下RMXP默认的系统,了解一下整个程序究竟是怎样工作的,大家就敬请期待吧!
|