• Home
  • About
    • Road to Coding photo

      Road to Coding

      只要那一抹笑容尚存, 我便心无旁骛

    • Learn More
    • Email
    • Github
  • Posts
    • All Posts
    • All Tags

Ruby学习笔记(三)

06 Feb 2018

类是Ruby作为面向对象语言的特征之一,模块又是Ruby的特色花样之一,就来聊聊这两方面内容吧

一 类 (class)

类是面向对象中,定义对象的数据与行为的结构

类就像是对象的雏形或设计图,决定了对象的行为

<1> 常见类方法

下面介绍几个常用的方法:

  1. 类方法: new方法

    既然对象是类的实例,那么,类便提供了,创建对象的方法,即为new方法

    同时,数值类,字符串类,数组可以使用字面量来进行初始化

     a = Array.new
     b = [1,2,"asa"]
     c = 123
     d = hash.new
     p d #=> {}
     p a #=> []
    
  2. 实例方法: class方法

    既然每个对象都是类的实例,那么如何判断改对象的类,使用class方法,可以判断出对象的类

     array = [ ]
     hash = { }
     p hash.class #=> "Hash"
     p array.class #=> "Array"
    
  3. 实例方法: instance_of?

    用于判断某个对象是否属于某方法,和以前的约定一样返回值为真假的以”?”结尾

     array = []
     p array.instance_of?(Array)   #=> true
     p array.instance_of?(String)  #=> false
    

<2> 继承

继承是类中很重要一个概念

继承 : 通过扩展已定义的类来创建新的类

那么,继承有什么作用呢?

  1. 再不影响原有类功能的前提下,追加新的功能

  2. 可以重定义父类中的功能,使名称相同的方法产生不同的结果

  3. 在已有的功能上,追加新的处理,扩展已有功能

新创建的类叫子类(subclass),被继承的类叫父类(superclass)

注意: Ruby不支持父类的多重继承,即一个类只能继承一次,只能有一个父类,Ruby保证了单一继承关系

那么,有时候就是需要多个类的功能该怎么办,后面自有办法,使用模块(module)

Basicobject类shiRuby中最最基础的类,仅仅包含了作为一个类最低限度的方法

所以,Ruby中一般默认的继承类是Object类,我们可以看看这两个类中方法的比较

p BasicObject.instance_methods
#=> [:!, :==, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__]

p Object.instance_methods
#=> [:remove_instance_variable, :instance_of?, :kind_of?, :is_a?, :tap, :public_send, :instance_variable_defined?, :singleton_method, :instance_variable_set, :method, :public_method, :extend, :define_singleton_method, :to_enum, :enum_for, :<=>, :===, :=~, :!~, :eql?, :respond_to?, :freeze, :inspect, :object_id, :send, :display, :to_s, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :frozen?, :methods, :singleton_methods, :protected_methods, :private_methods, :public_methods, :instance_variable_get, :instance_variables, :!, :==, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__]

而我们一般创建类时,默认的继承类是Object类,所以,这个类就不能再继承其他类了,

违背单一继承的规则,会抛出异常

下面是一张Ruby中常见类的继承关系图

现在,再介绍一种方法,用来辨别继承关系

4.is_a?方法

is_a? 方法用来辨别接收者是否是参数的子类

p Array.is_a?(Object)  #=> true
p Array.is_a?(String)  #=> false
p Array.is_a?(Array)   #=> false
a = []
p a.is_a?(Array)       #=> true

<3> 类的创建

上面说了那么多,类的创建是怎么做的呢?

类定义的语法

class class_name
  class_definition
end

其中需要介绍的一个是,initialize方法

类创建一个对象的new方法,就是以initialize方法进行定义的

虽然调用的是new方法,但是再类的定义中就是用的initialize方法

class HelloWorld
  def initialize(name = "Ruby")
    @name = name   
  end

  def hello
    puts "Hello I'm #{@name}"
  end
end

m = HelloWorld.new("SinCrow")
n = HelloWorld.new
m.hello   #=> Hello I'm SinCrow
n.hello   #=> Hello I'm Ruby

上面的例子中,new方法便是由initialize方法定义的,传参也是同一道理

3.1 实例变量与实例方法

之前总是说实例变量,实例方法.到底是怎么使用的呢?

1.实例变量,就是在类的定义中,@var形式的变量,可以在类的实例方法中通用

算是类中实例方法间的全局变量,只要在同一个实例中,就可以超越方法的局限,任意引用,修改实例变量

2.实例方法,顾名思义,实例才能调用的方法,即对象的方法

备注: 不同的实例的实例变量值是可以不同的,而且这些实例变量都是存在的,只要实例存在,就不会消失

反观,局部变量的作用域,生命周期都是在方法中,只能在方法内部使用,方法调用结束,局部变量释放

3.2 存取器

那么,现在就有一个问题,关于在实例方法中的实例变量,既然是在类中定义的,随着实例存在

如果,需要使用实例变量怎么办呢?

很容易就可以构建下面的两个实例方法

class HelloWorld
  def initialize(myname = "Ruby")
    @name = myname
  end

  def name
    @name
  end

  def name=(modify_name)
    @name = modify_name
  end
end

其中需要注意的是,name=方法,一定是紧紧相连的,不然就不能定义为方法名

但是,很没意思啊,整天都要些存取器的内容,每个实例变量都要写,所以Ruby提供了定义方法

存取器 作用
attr_reader 读方法(name方法)
attr_writer 写方法(name=方法)
attr_accessor 皆有
attr_reader :a
attr_writer :b
attr_accessor :c, :d

这样定义就可以了,用法相当于直接调用name方法与name=方法

3.3 self变量

介绍一个十分特殊的变量,self变量

以前就提到过,Ruby是完全的面向对象,

实际上,在顶部,是main对象,平时调用的函数式方法也是有接收者的

只不过是不在意接收者的信息,实际上函数式方法的接收者,就是self

我不清楚Java的this指针与self有何区别

但是,Ruby是解释性语言,所以,他的self严格的与上下文息息相关

self变量,其实就是指当前的层次

来看看,下面的例子

p self    #=> main

class SelfTest
  p self    #=> SelfTest
  
  def method_test
    p self   #=> #<SelfTest:0x0000000001c48500>
  end
end

m = SelfTest.new
m.method_test

对于最外层顶部来讲,就是main,

对于类来讲,就是类名

对于实例方法来讲,就是本实例,而本实例是存在于内存上的,所以就是地址了

那么,self变量有什么用呢?

可以进行变量自身的调用,而且,可以隐式调用,Ruby缺省条件下,接收者就是self

看下面这个例子:

class Hello
  attr_accessor :name
  def initialize(myname)
    @name = myname
  end
  
  def hello
    puts "Hello ,My name is #{name}"
  end
end

实例方法中的hello,获取到,name变量的值,但是,name并不是传进来的参数

而且,这个方法是没有Bug的!

其实这个name是 #{self.name},换成{@name}也是可以的

那么,平时调用函数式方法,是不是也可以加上self呢?

答案是,不行!,函数式方法,多为private(私有)方法,必须是隐式调用

注意: 调用写方法的时候,不能隐式调用,必须显式调用,不然会有很诡异的事情发生呦,来看下面的例子

name = "hello"   #=> name视为变量
self.name = "hello"  #=> name= 视为写方法

这样懂了吧? (滑稽)

3.4 类方法

我们在这里就不多纠缠了,类方法就是给类对象使用的方法

对,实际上我们区分方法按照接收者去区分

但是,实际上,Ruby所有的方法都应该叫做实例方法,每一个对象都有类

类方法调用者(接收者)是类,各个类,都是Class类的实例对象

下面介绍4种Ruby类方法的定义形式

  1. 单例方法形式

     在class HelloWorld已存在条件下
    
     class << Hello
       def hello
         puts "I'm Class method"
       end
     end
    
  2. class上下文中,引用self形式

     class Hello
       class << self
         def hello
           puts "Class method"
         end
       end
     end
    
  3. 显式标明类名,进行实例方法形式的定义

     class Hello
       def Hello.name(myname)
         puts "my name is #{myname}"
       end
     end
    
  4. 等效于3,类名可以用self变量取代,但是,self切记不能省略,不然会视为实例方法

     class Hello
       def self.name
         puts "My name is Ruby"
       end
     end
    

虽然提到了四种定义的形式,准确的来讲应该是三种,其中第四种用的最多,单例方法的形式,重复量太大

而将,类方法以实例方法的形式定义,很方便

后面还会有通过模块的方法进行类方法定义,也是很方便的方法

备注: class « 类名 ~ end 的写法为单例类定义,其中的方法是单例方法,后面会详解

3.5 常量

类中肯定也是可以定义常量的,一般常量为大写,通过”::”进行访问,类似于类方法的访问

class Hello
VERSION = "v1.0"
end

p Hello::VERSION #=> "v1.0"

3.6 类变量

类变量,类似于实例变量,是在整个类的实例方法间都可以任意引用,修改的变量

但是,区别于实例变量,类变量是子类继承后仍然能够存在的变量,存储期更长

实例变量,是随着实例对象存在而存在.那么,类变量就是随着类对象存在而存在

@@class_var是存在于类中的,所以几个实例对象,是共享这个类变量的

,但是,类变量定义存取器,不能使用attr系列方法,attr系列只能给实例变量使用

可以看看下面的例子:

class Hello
  @@hello_count = 0
  attr_accessor :name
  def initialize(myname = "Ruby")
    @name = myname
  end

  def hello
    puts "my name is #{name}"
    @@hello_count += 1
  end

  def self.count
    @@hello_count
  end
end

a = Hello.new
b = Hello.new("NIOH")
c = Hello.new("Uncharted")
p Hello::count   #=> 0
a.hello
p Hello::count   #=> 1
b.hello
p Hello::count   #=> 2
c.hello
p Hello::count   #=> 3

3.7 修饰方法的关键字

曾经看过C,C种存在着,static关键字,可以用来修饰函数,使函数只能在本文件中调用,常说的:

具有内部链接的静态类型函数,链接,存储期,作用域是C中进行控制的几个关键点

随着代码组织的不同,相应的关键字发生变化(hepangda说的,不是我说的)

在,Ruby中有这样三个修饰方法的关键字:public, private, protected

之前被国内的概念误导了很长时间,

他们死要纠缠访问与调用这两个概念, , ,

其实,这三个关键字分得很清楚,可以用下面几句话概括:

1. public以外的两类方法,都不能被显式的实例对象调用

2. public方法为默认属性,可以任意访问调用,无论是类还是实例对象,或是同父类的实例对象

3. protected方法相当于类之内的public方法,可以被随意调用,即所谓的被同父类的实例对象访问

接上一条,也就是指可以在其他的方法中,调用protected方法,其他方法的实例对象是self

4. private方法就更好说了,只能进行隐式的调用,即自身调用(self)

即private是这个类进行私有实现所使用的,只能隐式的被self调用,不能显示地指定接收者

要写,只能 private_method_test,不能obj.private_method_test

有一句话,挺恰当的,private私有方法是内部实现所使用的,对其他的类(非同父类),实例都是不可见的

在Ruby社区提问,得到了这样比较好的答案

访问和调用扯半天,真的是拖裤子放P

<4> 扩展类

4.1 给原有类添加方法

很简单,就是定义和类名相同的类,如果是已存在的类,则会追加方法

class String
  def count_word
    array = split(/\s+/)    #=> array = self.spilt(/\s+/)
    return array.size
  end
end

str = "hello world"
p str.count_word #=> 2

4.2 继承

前面说了很多次继承了,下面是继承的语法

class SubClass < SuperClass
  class_definition
end

还是要提醒一点,Ruby缺省继承为Object类

4.3 alias与undef

这两个关键字在其他语言中可能也用过

alias就是Linux中命令别名的命令,这里同义

alias  new_name    old_name
alias :new_symbol :old_symbol

undef在C中应该也是使用过的,用来取消命令别名

undef 方法名
undef :方法名

用来删除方法定义的,例如:我们可以在子类中删除不需要的父类方法定义,并且重新定义

聊一聊”单例类”

之前,进行类方法的定义时说过,使用单例类的形式进行类方法的定义

什么叫单例类?

单例类,指的就是,并非对类定义进行修改,只是针对这一个实例进行方法等的修改,只对这一个实例有效

所以叫单例类,单例类中的方法叫单例方法

str1 = "hello"
str2 = "world"
class << str1
  def hello
    puts "I'm hello"
  end
end

str1.hello #=> "I'm hello"
str2.hello #=> undefined method `hello' for "world":String (NoMethodError)

上面的例子即可说明hello方法对于同类实例str1与str2而言,只对str1有效,所以叫做单例方法

那么,我们定义类方法,与单例(实例)又有什么关系呢?

这里就可以清晰地说明: Ruby中一切皆对象,类都是Class类的实例,所以使用单例方法,就是添加类方法

前面说过,Ruby部支持父类多重继承(没有Java diao,对吧?(滑稽))

但是,Ruby却很好的处理了一定情况下的需求,靠得就是模块(module)

下面,我们来仔细看看模块(module)

二 模块 (module)

<1> 模块的作用与意义

模块为什么存在,从下面两点来说明:

1.1 提供命名空间

之前其实一直没有注意过,但是,一旦工程量比较大的时候,命名空间就十分重要了.

系统的讲,命名空间就是指对方法,常量,类的名称进行区分和管理的单位

使用模块(module),天生进行命名空间的区分,互相不污染

举个简单的例子:

很火爆的网络Web服务器Nginx

extern ngx_module_t ngx_kqueue_module;
extern ngx_module_t ngx_eventport_module;
extern ngx_module_t ngx_devpoll_module;
extern ngx_module_t ngx_epoll_module;
extern ngx_module_t ngx_select_module;


static char *ngx_event_init_conf(ngx_cycle_t *cycle, void *conf);
static ngx_int_t ngx_event_module_init(ngx_cycle_t *cycle);
static ngx_int_t ngx_event_process_init(ngx_cycle_t *cycle);
static char *ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);

static char *ngx_event_connections(ngx_conf_t *cf, ngx_command_t *cmd,
    void *conf);
static char *ngx_event_use(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static char *ngx_event_debug_connection(ngx_conf_t *cf, ngx_command_t *cmd,
    void *conf);

static void *ngx_event_core_create_conf(ngx_cycle_t *cycle);
static char *ngx_event_core_init_conf(ngx_cycle_t *cycle, void *conf);

使用ngx作为统一的命名空间,当然这里只是举例子,实际上Ruby中使用模块可以避免命名空间冲突的问题

A模块中的foo方法,与B模块中的foo方法完全是两个方法

我们使用类似于调用类中的方法的放法来调用模块方法,模块名.方法名,也叫模块函数

FileTest.exist?("/usr?bin/ruby")
FileTest.size("/usr?bin/ruby")

如果,在当前命名空间中不与模块函数冲突,那么,可以使用include来省略模块名

include Math
sin(30)
sqrt(100)

注意,一定是不发生命名空间冲突的时候,可以使用include进行命名空间的合并,反之,抛出异常

def sin
  5
end

include Math
p sin(5)   
#> in `sin': wrong number of arguments (given 1, expected 0) (ArgumentError)from test.rb:6:in `<main>'

这个错误,会在继承机制中进行解答,见下

1.2 利用Mix-in进行扩展

前面提到过,Ruby只支持单一父类继承,那么问题来了,如果碰到需要继承多个类的功能时怎么办?

答案是,使用模块来进行类功能的扩展

可以将模块作为通用功能的实现,来完成多个功能类似的类的扩展

Mix-in指的就是,将通用功能设计在模块中实现,之后include,则module的变量,方法都可以在类中使用

那么,套用前人总结的经验,一般Mix-in解决这两种问题:

1. 虽然两个类拥有相似的功能,但是不惜望将他们作为相同的类来考虑时

2. Ruby不支持父类的多重继承,因此无法对已继承的类添加共通的功能的时候

Mix-in Example
module MyModule
  module_difinition
end

class MyClass1
  include MyModule
end

class MyClass2
  include MyModule
end

<2> 创建模块

上面说了这么多,下面说说模块的用法吧

首先,使用module关键字

至于语法,与class是一致的

module module_name
  module_difination
end

下面是一个模块的使用示范

module Hello
  Version = "v1.0"
  def hello
    puts "Hello"
  end

  module_function :hello
end

p Hello::Version    #=> "v1.0
Hello.hello         #=> "Hello"

include Hello
p Version           #=> "v1.0"
hello               #=> "Hello"

现在来说说上面中的一些之前没有见到过的东西

2.1 常量

常量的用法同类中的常量(并非类变量)

模块中,不能出现类变量,实例变量(如有不懂,看前面)

2.2 方法

模块中的方法,定义是同类方法的

但是,需要注意的是:

如果要想使模块函数对外可见,使其可以使用”模块名.方法名”的形式调用

这里指的是,使用模块进行模块函数的调用,任何包含此模块的上下文都可以直接调用模块函数

那么,一定要使用module_function方法,进行模块函数的声明,使其对外可见,参数是模块函数的符号名

另外,

模块方法中的self,是当前模块对象

module HelloName
  p self              #=> "HelloName"
  def hello
    p self
  end

  module_function :hello
end

HelloName.hello       #=> "HelloName"

区别于类的不同之处:类中的实例方法,self只当前的对象(实例,有实体)

而模块函数中的self指模块对象,因为模块不具有实例

但是,模块中的self一旦Mix-in之后,便表示类对象,所以,Mix-in一般不建议模块使用self

<3> Mix-in

聊聊Mix-in及其相关

首当其冲的是,判断一个类是否Mix-in了某模块,使用include? 方法

class ClassName
end
ClassName.include?(kernel)   #=> "true"

现在,就来说说一个很关键的,类的继承机制

在类中include的模块会做为虚拟的父类进行方法的继承,以及方法的查找

来看一个例子:

module TestModule
end

class TestClass
  include TestModule
end

p TestClass.ancestors   #=> "[TestClass, TestModule, Object, Kernel, BasicObject]"
p TestClass.superclass  #=> "Object"

如上所示,TestModule模块作为虚拟的”父类”进入了TestClass的继承顺序列表中

但是,TestClass的直接父类还是Object类

就像这样,我们可以使用模块功能来实现构建多个功能类似的扩展类的需求

<4> 来聊聊模块方法查找规则

  1. 原类中定义了同名方法时,优先使用类中的方法

     module M
       def hello
         puts "M#hello"      
       end
     end
    
     class C
       include M
       def hello
         puts "C#hello"
       end
     end
    
     C.new.hello #=> "C#合理咯"
    

    原因就是: p C.ancestors #=> [C, M, Object, Kernel, BasicObject]

    类的继承顺序上,C类在M模块之前

  2. 在同一个类中,优先使用最后一个包含的模块

     module M1
       def hello
         puts "M1#hello"      
       end
     end
    
     module M2
       def hello
         puts "M2#hello"      
       end
     end
    
     class C
       include M1
       include M2
     end
    
     C.new.hello  #=> "M2#hello"
    

    同理,看继承顺序: [C, M2, M1, Object, Kernel, BasicObject]

  3. 嵌套模块和类时,继承也是线性的

     module M1
     end
    
     module M2
     end
    
     module M3
       include M2
     end
    
     class C
       include M1
       include M3
     end
    
     p C.ancestors #=> "[C, M3, M2, M1, Object, Kernel, BasicObject]"
    
  4. 多次使用include嵌套同一模块,忽略已经包含的模块

     module M1
     end
    
     module M2
     end
    
     class C
       include M1
       include M2
       include M1
     end
    
     p C.ancestors  #=> "[C, M2, M1, Object, Kernel, BasicObject]"
    

之前提到单例方法时,都是使用 “ class « 实例 ~ end “ 的形式定义单例方法

对于类(Class类的实例)来讲,就是进行类方法的定义

出了使用这种形式以外,我们还可以灵活使用模块,来实现单例方法

同之前的例子,看一下这个例子

module Hello
  def hello
    puts "hello"
  end
 #module_function :hello
end

str1 = "hello"
str2 = "world"
str1.extend(Hello)
p str1.hello    #=> "hello"
p str2.hello    #=> "nil"in `<main>': undefined method `hello' for 										  "world":String (NoMethodError)

将单例方法,定义成为模块函数,之后对单例使用Object#extend方法,进行单例方法的扩展即可

注意: 其中的module_funcation是不能有的,他会将模块函数定义为某唉进行调用的外部函数

如果将类作为实例,那自然就是类方法了,

现在,我们可以系统的来说:

使用模块,一方面可以利用Mix-in特性来,扩展类.另一方面,可以使用Object#extend方法来扩展单例

即,使用include可以帮助我们突破继承的限制.extend可以跨过类,使用模块来扩展对象

<5> 类与Mix-in

之前说过很多次类方法

我们可以这样理解类方法:

1. Class类的实例方法 (任何类都是Class类的实例)

2. 类对象的单例方法(只对此类适用,对其他类不适用,这些类都作为对象来考虑)

所以,在扩展类的方面,我们这样综合类与模块

module Hello
  def hello
    puts "hello,method"
  end
end

module Bye
  def bye
    puts "Bye,method"
  end
end

class Test
  include Hello         #=> include将Hello模块扩展为类的实例方法
  extend Bye            #=> extend将Bye扩展为类的类方法
end

m = Test.new
p m.hello
p Test.bye

又扯到之前提到过的: 其实Ruby中一切皆对象,所谓的类方法其实也是实例方法

面向对象程序设计

这部分,我也不时啃的十分清楚,说说下面几个问题:

  1. 什么是对象

  2. 封装与多态

  3. 鸭子类型


来说点其他的吧,寒假学习,确实容易放松,这篇博客拖了很长时间

一方面是理解起面向对象,走了一点弯路,另一方面还是太皮了

不过现在搞得差不多了,Ruby整个语法框架差不多了

后面也就是运算符,异常处理,块的内容,理解消化挺快的

接下来,就是一系列Ruby默认类的内容了,很方便,有各种黑魔法(可能就拖起来看了,唉~)

之前逛得太多了,下来改回归Kernel的任务了

驱动,CS:APP还是要好好对待,估计到开学勉强差不多

对了,还有算法题,那就好好写写BFS与DFS吧,写的深入一点

February 15, 2018 3:41 AM



Ruby Share Tweet +1