Day11. 活用 Ruby Class

Class 是Ruby很重要的观念,要学习 Ruby 的一定要学会class & 物件。我们会在Day11-16 详细讲解何谓 class

以下为Day11会提及的内容

  • Constructor
  • Constructor with argument
  • getter & setter & attr_accessor
  • instance variable & instance method

Introduction

除了无法单独存在的Block以外,所有的东西都是物件。

Ruby and Rails一段时间後发现,在Rails的专案中,我们只能在定义models, controllersclass里面定义行为,不过大家有没有想过,到底是哪个执行绪跑了这些class? Ruby on Rails替我们做了很多事情,只要我们遵循惯例走就好。

写一段时间Rails的朋友们,可能已经习以为常,但仔细想在专案里面,下列的写法有很多看点

  • 为什麽 Controller不用写建构子?
  • <Rails 为继承,那被继承的 ApplicationController, ApplicationRecord 又是什麽?
  • Controller before_action 是如何被实作?
# Controller: controllers/admin/order_controller.rb
module Admin
  class OrdersController < ApplicationController
  end
end

# Model: models/order.rb
class Order < ApplicationRecord
end

这系列的文章可能没有办法带到太多Rails的观念,不过汉汉老师会不断的在文章里面提到自己使用Rails的一些经验

constructor

建构子就是在初始化的时候给定一些变数,以及基本的设定。constructor,也有人叫做initializer,所以只要看到这两个英文单字就是在讲建构子。以Ruby程序语言来说的话,我们会在建构子初始化设定值在实体变数instance variable

以下方的车子为例,我们可以透过建构子指定车子的颜色、车子的大小。在 Ruby 中,建构子以initialize 表示。

class Car
  def initialize(color, size)
    @color, @size = color, size
  end
end

red_car = Car.new('red', 'big')    #=> #<Car:0x00007fd4ddc8ffa0 @color="red", @size="big">
blue_car = Car.new('blue', 'big')  #=> #<Car:0x00007fd4ddc8c3c8 @color="blue", @size="big">

我们可以想像到,当我们初始化一个物件red_car, blue_carCar就像是工厂,red_car, blue_cary则是被产出来的物件,而工厂就是Car类别,然而读者也要清楚知道,Car也是被创造的物件。在 Ruby 语言当中,所有物件的源头都在Basic Object

介绍一个方法superclass,透过superclass我们可以找到 Ruby 物件的源头

Car.superclass
#=> Object
Car.superclass.superclass
#=> BasicObject
Car.superclass.superclass.superclass
#=> nil

⭐️我们可以在建构子做计算

class DataSet
  def initialize(ary)
    @ary = ary 
    @size = ary.is_a?(Array) ? ary.count : 0
  end
end

data_it = DataSet.new([1, 2, 3])    #=> #<DataSet:0x00007fd4ddcfd0f0 @ary=[1, 2, 3], @size=3>

default argument

我们可以在建构子设定参数的预设值。参数的预设值除了可以使用 || 来表示以外,还可以在括号里面填入方法名後面的参数填入预设值

#=> default value with || operator
class Animal
  def initialize(*args)
    @species, @examples = args[0] || '猩猩', args[1] || '小庞'
  end
end

orangutan = Animal.new #=> #<Animal:0x00007fd4ddd2d980 @species="猩猩", @examples="小庞">
#=> default value with assigning arguments
class Animal
  def initialize(species = '猩猩', examples = '小庞')
    @species, @examples = species, examples
  end
end

orangutan = Animal.new #=> #<Animal:0x00007fd4ddd56d58 @species="猩猩", @examples="小庞">

另外我们也说明,括号在Ruby里面可有可无,举例来说,下列两种宣告新物件的方法相同,不过依照惯例,若没有必要使用初始值,我们不用写括号。

orangutan = Animal.new
orangutan = Animal.new()

⭐️ 在一般的程序语言中,预设参数的给法若在顺序上有差异,使用上也会有差别。

class Animal
  def initialize(species, examples = '小庞')
    @species, @examples = species, examples
  end
end

turtle = Animal.new('甲鱼')  #=> #<Animal:0x00007fd4ddd763b0 @species="甲鱼", @examples="小庞">

⭐️ 不过Ruby在某些状况下会自己判断预设值

class Animal
  def initialize(species = '猩猩', examples)
    @species, @examples = species, examples
  end
end

orangutan = Animal.new('阿豪') #=> #<Animal:0x00007fd4dddb6550 @species="猩猩", @examples="阿豪">

⭐️ 宣告新物件时,括号可以省略

orangutan = Animal.new('阿豪')
orangutan = Animal.new '阿豪'

⭐️ 有些特殊物件的宣告是可以不用透过new,像阵列、字串等。我们会比较偏爱literals constructor的用法胜於使用一般建构子的宣告方式

# 这样用会比较冗长,比较少人会这样用
ary = Array.new
str = String.new

# literal constructor
ary = [1, 2, 3]
str = "qwer"

⭐️ 看一下如何使用 hash 做为参数宣告

class Animal
  def initialize(species = '猩猩', examples:)
    @species, @examples = species, examples
  end
end

orangutan = Animal.new(examples: '阿豪') #=> #<Animal:0x00007fd4de0530d8 @species="猩猩", @examples="阿豪">

⭐️ Hash 搭配预设值

class Animal
  def initialize(species = '猩猩', examples: '小庞')
    @species, @examples = species, examples
  end
end

orangutan = Animal.new

⭐️ 使用 options = {} 搭配建构子宣告

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小庞'
  end
end

orangutan = Animal.new  #=> #<Animal:0x00007fd4d3d1b8c0 @species="猩猩", @examples="小庞">

⭐️ 以下写法会造成语法错误

# 语法错误  Predefined Args cannot allowed after named arg
class Animal
  def initialize(species = '猩猩', examples: '小庞', options = {})
    @species, @examples = species, examples
  end
end

⭐️ 预设值搭配Hash的使用方法要注意

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小庞'
  end
end

#=> {:a=>"a"} 被当作 @species
#=> #<Animal:0x00007fd4d794d028 @species={:a=>"a"}, @examples="小庞">
orangutan = Animal.new(a: 'a') 

# 预期内的行为
#=> #<Animal:0x00007fd4d9c8eb18 @species="猩猩", @examples="小庞">
orangutan = Animal.new('猩猩', a: 'a') 

hash预设值填在options里面,而若宣告新物件给a: 'a',原本的examples: '小庞' 将会被覆盖。

class Animal
  def initialize(species = '猩猩', options = {examples: '小庞'})
    @species, @examples = species, options[:examples]
  end
end

#=> #<Animal:0x00007fd4d9f67e70 @species="猩猩", @examples=nil>
orangutan = Animal.new('猩猩', a: 'a') 

我们也可以使用splat operator

class Animal
  def initialize(species = '猩猩', **options)
    @species, @examples = species, options[:examples] || '小庞'
  end
end

#=> #<Animal:0x00007fd4dd1195b8 @species="猩猩", @examples="小庞">
orangutan = Animal.new('猩猩', a: 'a')

block 可以当作变数使用

class Animal
  def initialize(species = '猩猩', it_day11)
    @species, @block = species, it_day11
  end
end

orangutan = Animal.new('猩猩', -> (x) {x + 1})
#=> #<Animal:0x00007fd4db77f558 @species="猩猩", @block=#<Proc:0x00007fd4db77f5a8@(irb):164 (lambda)>>

getter

Ruby的世界中,我们可以使用getter, setter 取得与改写值。当我们想要取得动物的种类找不到,是因为我们没有给方法取得值。

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小庞'
  end
end

orangutan = Animal.new
orangutan.species  # NoMethodError (undefined method `species' for #<Animal:0x00007fd4dcad19a8>)

⭐️ 我们可以透过一些方法取得值

  • << 表示法 ➡️ Day12介绍
  • instance_eval ➡️ Day13介绍
# 法1. 透过 instance_eval 取得值
orangutan.instance_eval { @species }   #=> 猩猩
orangutan.instance_eval { species }    #=> 猩猩

# 法2. 对 orangutan 加入 getter 的单体方法,使其可以取用值
class << orangutan
  def species
    @species
  end
end

# 可以使用 getter
orangutan.species #=> 猩猩

⭐️ 接着开始介绍getter,以下为 getter的使用方式

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小庞'
  end
  
  def species
    @species
  end
end

orangutan = Animal.new
orangutan.species        #=> 猩猩

⭐️Rubygetter ,有更简洁的写法

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小庞'
  end
  
  attr_reader :species
end

orangutan = Animal.new
orangutan.species         #=> 猩猩

setter

⭐️ 我们可以利用setter 来去设定实体变数

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小庞'
  end
  
  # getter
  attr_reader :species
  
  # setter
  def species=(species)
    @species = species
  end
end

orangutan = Animal.new
orangutan.species = '猩'
orangutan.species          #=> 猩

⭐️Ruby也有更简洁的用法

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小庞'
  end
  
  # getter
  attr_reader :species
  
  # setter
  attr_writer :species
end

orangutan = Animal.new
orangutan.species = '猩'
orangutan.species          #=> 猩

attr_accessor

⭐️ 对於取值、改写值的用法,Ruby 提供了 attr_accessor 作为getter, setter 使用。

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小庞'
  end
  
  # getter/setter
  attr_accessor :species
end

orangutan = Animal.new
orangutan.species = '猩'
orangutan.species          #=> 猩

⭐️ 若要对getter, setter 进行客制化功能的话,只能够自己写

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小庞'
  end
  
  attr_reader :species
  
  # setter
  def species=(species)
    @species = "❌ #{species}"
  end
end

orangutan = Animal.new
orangutan.species = '猩'
orangutan.species          #=> "❌ 猩"

instance method

下列的例子中,drive为物件的方法,称为instance method

class Car
  def initialize(color, size)
    @color, @size = color, size
  end
  
  def drive
    [color.capitalize, 'car', 'boo boo!'].join(' ')
  end
  
  private
  
  attr_reader :color
end

red_car = Car.new('red', 'big') 
blue_car = Car.new('blue', 'big')  

red_car.drive    #=> "Red car boo boo!"
blue_car.drive   #=> "Blue car boo boo!"

首先要澄清两件事情:

  • 若不写attr_reader 一样可以动作。

  • colorself.color的简写,这边我们改使用实体变数@color也可以。个人习惯在唯读的变数加入attr_reader,并且毋需在外部取得值的地方,例如上例的@color,汉汉老师就会把它写在private里面,避免外部可以取得值。

Ruby的世界中,一共有public, private, protected三种方法,目前还没有用过protected过,还没有碰过需要区分的情境。若好奇的参考 这篇 论述即可。

class Car

  # public 方法
  
  private
  
  # 私有方法
  
  protected
  
  # 保护方法
end

analyse_color可以得知,self.变数等於实体变数。此外,object_id可以用来判断物件实际的位址。

class Car
  def initialize(color, size)
    @color, @size = color, size
  end
  
  def reflect
    self
  end
  
  def analyse_color
    color.object_id == @color.object_id
  end
end

red_car.reflect             #=> #<Car:0x00007fd065a07520 @color="red", @size="big">
red_car.reflect == red_car  #=> true

red_car.analyse_color

instance variable

实体变数为长相@开头的变数,都为实体变数instance variable。上面已经提到了很多实体变数的观念,这边会再详述实体变数的概念。

@color, @size, @species, @examples

实体变数是内部存放资料的地方,我们可以利用实体变数来取值和给值,并且我们不一定要在初始化的时候宣告实体变数。下例即为令新的实体变数@age,并给他get,set两种方法(复习一下attr_accessor

class Car
  def initialize(color = 'red', size = 'big')
    @color, @size = color, size
  end
  
  attr_accessor :age
end

car = Car.new
car.age = 20
car.age      #=> 20

我们实际看 car物件的实体变数

car.instance_variables
#=> [:@color, :@size, :@age]

结尾语

这边花了很大的篇幅,介绍了class的基本用法,後面的章节会介绍类别方法。

如今已经到了第11天,看着订阅数与观看数的微幅增长,确实比起跟着股票的涨跌而心情起舞,感受还要好。看到有人愿意花时间点开我的文章,我真的很开心。

第一天列出了这一期铁人赛的大纲,在写的过程中一直改变想法,30篇的文章架构不断的再改变。一来是下班时间并没有那麽多,加上若一篇文章讯息量太大,反而会造成反效果。所以这阵子不断的调整文章,调整能够让读者看到更精辟的重点!

我也推荐同样是工程师的朋友们写文章,虽然表面上好像很花时间,比起自己在边工作的时候,只记自己看得懂的笔记。多用心记录,整理成主题式的文章,未来再反刍自己的文章也比较不会看不懂,其实算是很好的投资。

参考资料


<<:  换了一个框架

>>:  [烧烤吃到饱-3] 猪对有韩式烤肉吃到饱-台中精武店 #中秋节烤肉精选店家

CSS Box-Shadow

前言 写CSS还蛮常会用到阴影,使元素更具有立体感,今天就来认识一下阴影有什麽属性吧。 首先我们要先...

CMoney第八届菁英软件工程师战斗营_面试经历简述&第一周心得

Hi 我是Fanny 接下来的日子要来分享有关这个营队点点滴滴, (排版不好敬请见谅) 首先今天一开...

Day27 火堆实作 - 连接模组方块

在进到 " Shading " 之前,我们必须先调整 " Partic...

# Day 26 Page migration (一)

文件 原文文件:Page migration 翻译: .. _page_migration: ===...

Day 1 : 前言+本系列会使用到的东西(vscode、xampp、virtualbox、ubuntu、python安装说明)

前言: 大家好,这是我第一次参加铁人赛 主要是想记录一下自己学过的东西 并和大家分享一些我觉得很重要...