在 Ruby 教程的这一部分中,我们将讨论 Ruby 中的面向对象编程。
编程语言具有过程编程,函数式编程和面向对象的编程范例。 Ruby 是一种具有某些函数式和过程特性的面向对象的语言。
面向对象编程(OOP)是一种使用对象及其相互作用设计应用和计算机程序的编程范例。
OOP 中的基本编程概念是:
- 抽象
- 多态
- 封装
- 继承
抽象通过建模适合该问题的类来简化复杂的现实。 多态是将运算符或函数以不同方式用于不同数据输入的过程。 封装对其他对象隐藏了类的实现细节。 继承是一种使用已经定义的类形成新类的方法。
Ruby 对象
对象是 Ruby OOP 程序的基本构建块。 对象是数据和方法的组合。 在 OOP 程序中,我们创建对象。 这些对象通过方法进行通信。 每个对象都可以接收消息,发送消息和处理数据。
创建对象有两个步骤。 首先,我们定义一个类。 类是对象的模板。 它是一个蓝图,描述了类对象共享的状态和行为。 一个类可以用来创建许多对象。 在运行时从类创建的对象称为该特定类的实例。
#!/usr/bin/rubyclass Beingendb = Being.newputs b
在第一个示例中,我们创建一个简单的对象。
class Beingend
这是一个简单的类定义。 模板的主体为空。 它没有任何数据或方法。
b = Being.new
我们创建Being类的新实例。 为此,我们有new方法。 b变量存储新创建的对象。
puts b
我们将对象打印到控制台以获取该对象的一些基本描述。 实际上,当我们打印对象时,我们将其称为to_s方法。 但是我们还没有定义任何方法。 这是因为创建的每个对象都继承自基本Object。 它具有一些基本功能,这些功能在所有创建的对象之间共享。 其中之一是to_s方法。
$ ./simple.rb#<Being:0x9f3c290>
我们得到对象类名。
Ruby 构造器
构造器是一种特殊的方法。 创建对象时会自动调用它。 构造器不返回值。 构造器的目的是初始化对象的状态。 Ruby 中的构造器称为initialize。 构造器不返回任何值。
使用super方法调用父对象的构造器。 它们按继承顺序被调用。
#!/usr/bin/rubyclass Beingdef initializeputs "Being is created"endendBeing.new
我们有一个存在类。
class Beingdef initializeputs "Being is created"endend
Betweening类具有一个名为initialize的构造方法。 它将消息打印到控制台。 Ruby 方法的定义位于def和end关键字之间。
Being.new
创建Being类的实例。 创建对象时,将调用构造方法。
$ ./constructor.rbBeing is created
这是程序的输出。
对象的属性是捆绑在该对象内部的数据项。 这些项目也称为实例变量或成员字段。 实例变量是在类中定义的变量,该类中的每个对象都有一个单独的副本。
在下一个示例中,我们初始化类的数据成员。 变量的初始化是构造器的典型工作。
#!/usr/bin/rubyclass Persondef initialize name@name = nameenddef get_name@nameendendp1 = Person.new "Jane"p2 = Person.new "Beky"puts p1.get_nameputs p2.get_name
在上面的 Ruby 代码中,我们有一个带有一个成员字段的Person类。
class Persondef initialize name@name = nameend
在Person类的构造器中,我们将一个member字段设置为一个值名称。 名称参数在创建时传递给构造器。 构造器是称为initialize的方法,该方法在创建实例对象时被调用。 @name是一个实例变量。 实例变量在 Ruby 中以@字符开头。
def get_name@nameend
get_name方法返回成员字段。 在 Ruby 中,成员字段只能通过方法访问。
p1 = Person.new "Jane"p2 = Person.new "Beky"
我们创建Person类的两个对象。 字符串参数传递给每个对象构造器。 名称存储在每个对象唯一的实例变量中。
puts p1.get_nameputs p2.get_name
我们通过在每个对象上调用get_name来打印成员字段。
$ ./person.rbJaneBeky
我们看到了程序的输出。 Person类的每个实例都有其自己的名称成员字段。
我们可以创建对象而无需调用构造器。 Ruby 为此有一种特殊的allocate方法。 allocate方法为类的新对象分配空间,并且不会在新实例上调用initialize。
#!/usr/bin/rubyclass Beingdef initializeputs "Being created"endendb1 = Being.newb2 = Being.allocateputs b2
在示例中,我们创建两个对象。 第一个对象使用new方法,第二个对象使用allocate方法。
b1 = Being.new
在这里,我们使用new关键字创建对象的实例。 调用构造器方法initialize,并将消息打印到控制台。
b2 = Being.allocateputs b2
在allocate方法的情况下,不调用构造器。 我们使用puts关键字调用to_s方法以显示该对象已创建。
$ ./allocate.rbBeing created#<Being:0x8ea0044>
在这里,我们看到了程序的输出。
Ruby 构造器重载
构造器重载是在一个类中具有多种类型的构造器的能力。 这样,我们可以创建具有不同数量或不同类型参数的对象。
Ruby 没有我们从某些编程语言中知道的构造器重载。 可以使用 Ruby 中的默认参数值在某种程度上模拟此行为。
#!/usr/bin/rubyclass Persondef initialize name="unknown", age=0@name = name@age = ageenddef to_s"Name: #{@name}, Age: #{@age}"endendp1 = Person.newp2 = Person.new "unknown", 17p3 = Person.new "Becky", 19p4 = Person.new "Robert"p p1, p2, p3, p4
本示例说明了如何在具有两个成员字段的Person类上模拟构造器重载。 如果未指定name参数,则使用字符串"unknown"。 对于未指定的年龄,我们为 0。
def initialize name="unknown", age=0@name = name@age = ageend
构造器有两个参数。 它们具有默认值。 如果在创建对象时未指定自己的值,则使用默认值。 请注意,必须保留参数的顺序。 首先是名字,然后是年龄。
p1 = Person.newp2 = Person.new "unknown", 17p3 = Person.new "Becky", 19p4 = Person.new "Robert"p p1, p2, p3, p4
我们创建四个对象。 构造器采用不同数量的参数。
$ ./consover.rbName: unknown, Age: 0Name: unknown, Age: 17Name: Becky, Age: 19Name: Robert, Age: 0
这是示例的输出。
Ruby 方法
方法是在类主体内定义的函数。 它们用于通过对象的属性执行操作。 在 OOP 范式的封装概念中,方法至关重要。 例如,我们的AccessDatabase类中可能有一个connect方法。 我们无需告知该方法如何准确地连接到数据库。 我们只需要知道它用于连接数据库。 这对于划分编程中的职责至关重要,尤其是在大型应用中。
在 Ruby 中,只能通过方法访问数据。
#!/usr/bin/rubyclass Persondef initialize name@name = nameenddef get_name@nameendendper = Person.new "Jane"puts per.get_nameputs per.send :get_name
该示例显示了两种调用方法的基本方法。
puts per.get_name
常见的方法是在对象上使用点运算符,后跟方法名称。
puts per.send :get_name
替代方法是使用内置的send方法。 它以方法的符号作为参数。
方法通常对对象的数据执行某些操作。
#!/usr/bin/rubyclass Circle@@PI = 3.141592def initialize@radius = 0enddef set_radius radius@radius = radiusenddef area@radius * @radius * @@PIendendc = Circle.newc.set_radius 5puts c.area
在代码示例中,我们有一个Circle类。 我们定义了两种方法。
@@PI = 3.141592
我们在Circle类中定义了@@PI变量。 它是一个类变量。 类变量以 Ruby 中的@@信号开头。 类变量属于类,而不属于对象。 每个对象都可以访问其类变量。 我们使用@@PI计算圆的面积。
def initialize@radius = 0end
我们只有一个成员字段。 它是圆的半径。 如果要从外部修改此变量,则必须使用公共可用的set_radius方法。 数据受到保护。
def set_radius radius@radius = radiusend
这是set_radius方法。 它为@radius实例变量提供了一个新值。
def area@radius * @radius * @@PIend
area方法返回圆的面积。 这是方法的典型任务。 它可以处理数据并为我们带来一些价值。
c = Circle.newc.set_radius 5puts c.area
我们创建Circle类的实例,并通过在圆对象上调用set_radius方法来设置其半径。 我们使用点运算符来调用该方法。
$ ./circle.rb78.5398
运行示例,我们得到此输出。
Ruby 访问修饰符
访问修饰符设置方法和成员字段的可见性。 Ruby 具有三个访问修饰符:public,protected和private。 在 Ruby 中,所有数据成员都是私有的。 访问修饰符只能在方法上使用。 除非我们另有说明,否则 Ruby 方法是公共的。
可以从类的定义内部以及从类的外部访问public方法。 protected和private方法之间的区别很细微。 在类的定义之外都无法访问它们。 只能在类本身内部以及继承的或父类访问它们。
请注意,与其他面向对象的编程语言不同,继承在 Ruby 访问修饰符中不扮演的角色。 只有两件事很重要。 首先,如果我们在类定义的内部或外部调用方法。 其次,如果我们使用或不使用指向当前接收器的self关键字。
访问修饰符可防止意外修改数据。 它们使程序更强大。 某些方法的实现可能会发生变化。 这些方法是很好的私有方法。 公开给用户的接口仅应在确实必要时更改。 多年来,用户习惯于使用特定的方法,并且通常不赞成向后兼容。
#!/usr/bin/rubyclass Somedef method1puts "public method1 called"endpublicdef method2puts "public method2 called"enddef method3puts "public method3 called"method1self.method1endends = Some.news.method1s.method2s.method3
该示例说明了公共 Ruby 方法的用法。
def method1puts "public method1 called"end
即使我们未指定public访问修饰符,method1也是公共的。 这是因为如果没有另外指定,默认情况下方法是公共的。
publicdef method2puts "public method2 called"end...
public关键字后面的方法是公共的。
def method3puts "public method3 called"method1self.method1end
从公共method3内部,我们调用带有和不带有self关键字的其他公共方法。
s = Some.news.method1s.method2s.method3
公共方法是唯一可以在类定义之外调用的方法,如下所示。
$ ./public_methods.rbpublic method1 calledpublic method2 calledpublic method3 calledpublic method1 calledpublic method1 called
运行示例,我们将获得以下输出。
下一个示例着眼于私有方法。
#!/usr/bin/rubyclass Somedef initializemethod1# self.method1endprivatedef method1puts "private method1 called"endends = Some.new# s.method1
私有方法是 Ruby 中最严格的方法。 只能在类定义内调用它们,而不能使用self关键字。
def initializemethod1# self.method1end
在方法的构造器中,我们称为私有method1。 带有self的方法被注释。 接收者无法指定私有方法。
privatedef method1puts "private method1 called"end
关键字private之后的方法在 Ruby 中是私有的。
s = Some.new# s.method1
我们创建Some类的实例。 禁止在类定义之外调用该方法。 如果我们取消注释该行,则 Ruby 解释器将给出错误。
$ ./private_methods.rbprivate method called
示例代码的输出。
最后,我们将使用受保护的方法。 Ruby 中的保护方法和私有方法之间的区别非常微妙。 受保护的方法就像私有的。 只有一个小差异。 可以使用指定的self关键字来调用它们。
#!/usr/bin/rubyclass Somedef initializemethod1self.method1endprotecteddef method1puts "protected method1 called"endends = Some.new# s.method1
上面的示例显示了使用中的受保护方法。
def initializemethod1self.method1end
可以使用self关键字来调用受保护的方法。
protecteddef method1puts "protected method1 called"end
受保护的方法前面带有protected关键字。
s = Some.new# s.method1
不能在类定义之外调用受保护的方法。 取消注释该行将导致错误。
Ruby 继承
继承是一种使用已经定义的类形成新类的方法。 新形成的类称为派生的类,我们派生的类称为基类。 继承的重要好处是代码重用和降低程序的复杂性。 派生类(后代)将覆盖或扩展基类(祖先)的功能。
#!/usr/bin/rubyclass Beingdef initializeputs "Being class created"endendclass Human < Beingdef initializesuperputs "Human class created"endendBeing.newHuman.new
在此程序中,我们有两个类:基础Being类和派生的Human类。 派生类继承自基类。
class Human < Being
在 Ruby 中,我们使用<运算符创建继承关系。 Human类继承自Being类。
def initializesuperputs "Human class created"end
super方法调用父类的构造器。
Being.newHuman.new
我们实例化Being和Human类。
$ ./inheritance.rbBeing class createdBeing class createdHuman class created
首先创建Being类。 派生的Human类还调用其父级的构造器。
对象可能涉及复杂的关系。 一个对象可以有多个祖先。 Ruby 有一个方法ancestors,它提供了特定类的祖先列表。
每个 Ruby 对象自动是Object和BasicObject类以及Kernel模块的后代。 它们内置在 Ruby 语言的核心中。
#!/usr/bin/rubyclass Beingendclass Living < Beingendclass Mammal < Livingendclass Human < Mammalendp Human.ancestors
在此示例中,我们有四个类:Human是Mammal,Living和Being。
p Human.ancestors
我们打印人类类的祖先。
$ ./ancestors.rb[Human, Mammal, Living, Being, Object, Kernel, BasicObject]
人类类具有三个习俗和三个内置祖先。
接下来是一个更复杂的示例。
#!/usr/bin/rubyclass Being@@count = 0def initialize@@count += 1puts "Being class created"enddef show_count"There are #{@@count} beings"endendclass Human < Beingdef initializesuperputs "Human is created"endendclass Animal < Beingdef initializesuperputs "Animal is created"endendclass Dog < Animaldef initializesuperputs "Dog is created"endendHuman.newd = Dog.newputs d.show_count
我们有四个类。 继承层次更加复杂。 Human和Animal类继承自Being类。 Dog类直接继承自Animal类,并且进一步继承自Being类。 我们还使用一个类变量来计算创建的生物的数量。
@@count = 0
我们定义一个类变量。 类变量以@@信号开头,并且它属于该类,而不是该类的实例。 我们用它来计算创造的生物数量。
def initialize@@count += 1puts "Being class created"end
每次实例化Being类时,我们都会将@@count变量加 1。 这样,我们就可以跟踪创建的实例数。
class Animal < Being...class Dog < Animal...
Animal继承自Being,Dog继承自Animal。 另外,Dog也继承自Being。
Human.newd = Dog.newputs d.show_count
我们从Human和Dog类创建实例。 我们在Dog对象上调用show_count方法。 Dog类没有这种方法。 然后调用祖父级Being的方法。
$ ./inheritance2.rbBeing class createdHuman is createdBeing class createdAnimal is createdDog is createdThere are 2 beings
Human对象调用两个构造器。 Dog对象调用三个构造器。 有两个实例化的存在。
继承在方法和数据成员的可见性中不起作用。 与许多常见的面向对象的编程语言相比,这是一个明显的区别。
在 C# 或 Java 中,公共和受保护的数据成员和方法是继承的。 私有数据成员和方法不是。 与此相反,私有数据成员和方法也在 Ruby 中继承。 数据成员和方法的可见性不受 Ruby 中继承的影响。
#!/usr/bin/rubyclass Basedef initialize@name = "Base"endprivatedef private_methodputs "private method called"endprotecteddef protected_methodputs "protected_method called"endpublicdef get_namereturn @nameendendclass Derived < Basedef public_methodprivate_methodprotected_methodendendd = Derived.newd.public_methodputs d.get_name
在示例中,我们有两个类。 Derived类继承自Base类。 它继承了所有三种方法和一个数据字段。
def public_methodprivate_methodprotected_methodend
在Derived类的public_method中,我们调用一种私有方法和一种受保护方法。 它们在父类中定义。
d = Derived.newd.public_methodputs d.get_name
我们创建Derived类的实例。 我们称为public_method,也称为get_name,它返回私有@name变量。 请记住,所有实例变量在 Ruby 中都是私有的。 无论@name是私有的还是在父类中定义的,get_name方法都会返回该变量。
$ ./inheritance3.rbprivate method calledprotected_method calledBase
该示例的输出确认,在 Ruby 语言中,子对象从其父级继承了public,protected,private方法和private成员字段。
Ruby super方法
super方法在父类的类中调用相同名称的方法。 如果该方法没有参数,它将自动传递其所有参数。 如果我们写super(),则不会将任何参数传递给父方法。
#!/usr/bin/rubyclass Basedef show x=0, y=0p "Base class, x: #{x}, y: #{y}"endendclass Derived < Basedef show x, ysupersuper xsuper x, ysuper()endendd = Derived.newd.show 3, 3
在示例中,我们在层次结构中有两个类。 它们都有显示方法。 派生类中的show方法使用super方法调用基类中的show方法。
def show x, ysupersuper xsuper x, ysuper()end
不带任何参数的super方法使用传递给Derived类的show方法的参数调用父级的show方法:此处,x = 3和y = 3。 super()方法不将任何参数传递给父级的show方法。
$ ./super.rb"Base class, x: 3, y: 3""Base class, x: 3, y: 0""Base class, x: 3, y: 3""Base class, x: 0, y: 0"
这是示例的输出。
这是 Ruby 中 OOP 描述的第一部分。
