Ruby

Ruby是一门通用面向对象脚本语言,此外ruby还对函数式编程提供很好的支持,元编程也是这门语言的强大之处,编写 DSL(Domain Specific Language) 也是具有语法的优越性

相关工具

ruby

ruby是Ruby官方的本地解释器,采用c语言写成,与此同时也有JRuby等一些用Java,C++等甚至是Ruby Language写的,Unbuntu等类Unix环境可以通过
sudo apt-get install ruby
下载,也可以在Ruby官网进入下载页查看,那里有更好的说明(也可以通过下面的rvm下载)

rvm

rvm(Ruby Version Manager) 是ruby的包管理工具,它使得多版本Ruby解释器能够在本地环境共存,且无缝切换,可以通过在rvm官网的指导下进行下载,安装完成后,执行
rvm list known
可以选择自己查看想要的可供下载版本,比如下载jruby-9.2.1.0可以执行
rvm install jruby-9.2.1.0
下载完成后,我们可以通过
rvm list
查看已安装的版本,我们可以执行
rvm use jruby-9.2.1.0
将当前ruby版本切换为jruby-9.2.1.0(当然可以是你已有的其他版本)

RubyGems

RubyGems 是ruby的包管理工具,可以进入RubyGems官网下载安装RubyGems,也可以在其官网查询想要使用的包信息,进行下载,当然我们可以通过本地下载好的RubyGems进行查询及下载(Ruby的库也称呼为gem),比如下载 Nokogiri (Ruby解析XML,HTML的实用库),先用 gem 命令进行查询(支持正则表达式)
gem search "^nokogiri$"
找到适合的版本后运行(可以携带指定的版本号)
gem install nokogiri

Bundler

Bundler 可以认为是 RubyGems 功能的增强,Bundler可以使项目工程化,通过Gemfile来指定项目中的gem版本要求(类似于nodejs中的npm ),通过
gem install bundler
可以下载bundler,进入我们需要准备的项目目录执行
bundler init
将会在项目目录里产生Gemfile,我们可以编写Gemfile进行项目管理,具体细节可以访问Bundler官网 查看更多细节

Ruby Language

接下来将会尽可能全面的介绍Ruby Language语法及特性

常用函数

这里介绍一下ruby里不出意外可以随处调用的函数

print arg1, arg2, ...

向标准输出原封不动打印参数(参数可变长)
# Output: Marco Epsilon, hello, world
print "Marco Epsilon ", "hello, world"

puts arg1, arg2, ...

向标准输出打印参数(参数可变长),打印每个参数都会接着打印换行符
=begin
Output: 
        Marco Epsilon
        hello, world
=end
puts "Marco Epsilon", "hello, world"

p arg1, arg2, ...

直接向标准输出打印arg.inspect的结果和增加的换行符(一般用于调试)
    # Output: "hello, world"
    p "hello, world"

obj.class

从Object继承来的方法,可以查看obj所属类
# Output: String
p "hello, world".class

类型(也许说是常用类会更好)

Ruby类型大致分为一下几种

数值类型

FixNum

基本整数对应的类型,当超出固定容纳字节长度时,将会自动转化为Bignum

Bignum

Float

浮点数对应类型

字符串(String)

数组类型(Array)

范围类型(Range)

哈希表类型(Hash)

正则表达式(Regexp)

调用过程(Proc)

时间(Time)

日期(Date)

符号(Symbol)

...

注:上述类型只是对Ruby自带常用类的分类,是区别于其他如C/C++这样数值类型是基础类型的语言,上述类型在Ruby中本质上其实也只是一个类
我们可以通过对象class函数查看其所属类
=begin
Output:
Integer
Float
String
Array
Hash
=end
p 2019.class
p 2.3.class
p "hello, world".class
p [1,2,3].class
p Hash.new({"marco" => "epsilon","rgb" => "lala"}).class

变量(Variables)

Ruby变量分为四种 全局变量,实例变量, 类变量, 局部变量

全局变量

全局变量以$variableName声明,一般$variableName采用大写,全局变量在所有脚本中共用一个符号,所以如果不小心多个脚本都声明初始化同一个$variableName,则先加载的脚本就可能会被覆盖,所以遵循几乎所有编程语言的循循教诲,全局变量还是少用为好(ps:Ruby也有一些自带的全局变量,用到再说)

实例变量

属于类的实例的变量,使用@variableName声明,可以认为是同一个类各实例不共享的变量,注意必须在类的方法中声明或定义(其实和作用域及当前self有关),可以通过 attr_accessor,attr_reader,attr_writer 控制访问权

类变量

属于类的变量,使用@@variableName声明,所有类实例岂可共享,可以需要自行编写函数控制其访问,可以在类中直接声明或定义

局部变量

随处都可以声明和初始化的变量,未初始化为nil,且只能在作用域内使用,脱离作用域后自动销毁
注:实例变量其实还有类实例变量,类实例变量和类的实例变量其实是不同的,类实例变量是属于类的(类在Ruby中其实是一个对象),而类的实例变量是类的实例所持有的变量,而关于为什么不在上面直接分成两类的本质在于它们的确都是实例变量,只是子类和超类所持有的关系

Example

# 全局变量 不同脚本可共享
$HELLO = "全局变量: hello"
# 局部变量 只存在于当前顶级作用域
hello = "局部变量: hello"
class R
    # 设置类的实例变量@hello的访问权
    attr_accessor :hello
    # 声明初始化类变量@@hello
    @@hello = "类变量: hello"
    def shared_hello
        return @@hello
    end
    def shared_hello=(value)
        @@hello = value
    end
end
p $HELLO
p hello
r = R.new
r.hello = "类的实例变量: hello"
p r.hello
p r.shared_hello
r.shared_hello = "类的实例变量: bye,bye!"
p r.shared_hello
=begin
Output:
"全局变量: hello"
"局部变量: hello"
"类实例变量: hello"
"类变量: hello"
"类实例变量: bye,bye!"
=end

结构化编程

Ruby提供一些关键字使得我们能够很好的结构化编程

if condition1 then
...
[elsif] condition2 [then]
...
[else]
...
end

unless condition1 then
...
[else]
...
end

case expression
[when expression, expression, ...] [then]
...
[else]
...
end

while condition do
...
end

until condition do
...
end

for variable1 [ ,variable2, ...] in expression do end

上面的一些都是常规的条件控制语句,主要是分支检查和循环语句 if和unless,while和until可以相互转换

Example

i = 2
# if 语句
if i > 10 then
    puts "i大于10"
elsif i == 10 then
    puts "i等于10"
else
    puts "i小于10"
end
# unless 语句
unless i > 10 then
    puts "i小于等于10"
else
    puts "i大于10"
end
# case 语句
case i
when 0,1,2,3,4,5 then
    puts "#{i}"
else
    puts "#{-i}"
end
# while
while i >= 0 do
    puts i
    i -= 1
end
# until
until i >= 2 do
    puts i
    i += 1
end
# for
for i in (1..4)
    puts i
end
注:当if unless等语句在一行内写then可以省略(个人风格不省)
此外Ruby不同于其他编程语言之处提供了修饰符的概念,这种做法使得代码更加紧凑(算是一种语法糖吧),先执行修饰符的条件内容,若满足修饰符语义就执行表达式,主要有以下几种

... if codition

... unless condition

... while condition

... until condition


Example

i = 2
puts "i大于1" if i > 1
puts "i>0" unless i < 0
puts (i += 1) while i <= 4
puts (i += 2) until i >= 9
=begin
Output:
i大于1
i>0
3
4
5
7
9
=end
此外还有循环的控制语句,方便跳出循环

break

跳出循环或块调用

next

跳出此次循环,(如果在块中调用,则跳出此次块调用,若块要求返回值,可以使用next expression)

redo

不检查循环条件,重新进入循环

Example

for i in (0 ... 4) do
    if i >= 2 then
        break
    end
    puts i
end
for i in (0 ... 4) do
    if i == 2
        next
    end
    puts i
end
for i in (0 ... 4) do
    if i == 2 then
        i += 1
        redo
    end
    puts i
end
=begin
Output:
0
1
0
1
3
0
1
3
3
=end

面向对象

说到面向对象,我们先讨论下何谓面向对象(俨然是成为现代编程语言面向对象规格的面向对象) 一般编程语言的面向对象主要有以下特点:

封装性

继承性

多态性

其实这几点可以与结构化编程进行类比,结构化编程是为了防止goto语句过度使用导致程序结构混乱,而封装性的出现是为了防止数据的暴露(例如全局变量)导致数据混乱而出现的,而继承性则弥补了封装性的绝对化,给数据提供了暴露的接口,使得数据得以共享(继承性使得数据是有组织的共享,也绝非唯一的手段),多态性则弥补了数据类型(主要是静态语言)无法动态的"转化"类型(往往我们需要在静态语言尝试 Duck Typing (一种多态性的形式))

Ruby中可以通过 class className... end 来创建类,类是数据的封装,所以类是面向对象的基础,Ruby中类默认继承Object(先在这里提出一点:类在Ruby里其实只是一个全局对象),自定义的类可以通过调用从Object继承而来的new函数来构造对象, new 函数默认会调用 initialize 函数,我们也可以自行编写 initialize 函数来构造对象,此外,Ruby类中没有属性(这样对obj.name不加括号调用的是函数没有好意外的了)这个概念(简单起见下面所说的类的都是Ruby中的类),实例变量只能在实例的作用域定义,而且我们只能通过类实例方法访问,这里的实例变量看起来的确是其他编程语言的属性,不过它们还是有差异的,我们定义完实例变量后其实不能直接从外部访问的,我们只能通过自行编写getter,setter函数来获得修改实例变量,(修改是通过拟态方法 variableName=来完成的)

Example

class Dog
    def initialize(name)
        @name = name
    end
    def get_name()
        @name
    end
    def set_name(name)
        @name = name
    end
end
dog = Dog.new("Joke")
p dog.get_name
dog.set_name "Bike"
p dog.get_name
#Output:
=begin
"Joke"
"Bike"
=end
为了使方法更美观和模拟=,我们也可以这样定义:

Example

class Dog
    def initialize(name)
        @name = name
    end
    def name()
        @name
    end
    def name=(name)
        @name = name
    end
end
dog = Dog.new("Joke")
p dog.name
dog.name = "Bike"
p dog.name
# Output:
=begin
"Joke"
"Bike"
=end
这样对于需要暴露诸多实例变量的类都要定义setter,getter方法是无意义的活动,好在超类里帮我们定义了下面几种类宏(本质也是方法)供我们调用控制实例变量的可访问性:

attr_accessor Symbol/String

提供实例变量的符号名或字符串名,生成setter,getter函数,使得实例变量可读写

attr_reader Symbol/String

提供实例变量的符号或字符串名,生成getter函数,使得实例变量可读

attr_writer Symbol/String

提供实例变量的符号或字符串名,生成setter函数,使得实例变量可写
注:生成的setter,getter函数就是 obj.变量名=和obj.变量函数,是不是让人感觉像Ruby有属性概念一样?
这样就可以使得代码更简单

Example

class Dog
    attr_accessor :name
    attr_reader :sex
    attr_writer "age"
    def initialize(name, age, sex)
        @name = name
        @age = age
        @sex = sex
    end
end
dog = Dog.new("Joke", 3, "女")
dog.name = "Bike"
dog.age = 12
p "dog name: #{dog.name}, sex: #{dog.sex}"
# Output:
=begin
"dog name: Bike, sex: 女"
=end
类的可访问性
由于Ruby类没有属性的概念,实例变量也由方法控制,故Ruby中可访问性主要针对的是类中方法的可访问性,可以设置类的三种可访问性,分别由三种关键字确定:

public

public方法设置类中的方法为公有,以实例方法的形式向外公开方法

protected

protected方法设置类中的方法为保护成员,该实例方法只能被子类和父类调用

private

private方法设置类中的方法为私有成员,该实例方法只能在缺省实例对象时才能被调用(不能指定接收者,但能被子类继承访问(这也是很其他语言有所差别的一点))
注:initialize()在缺省指定可访问性时,访问性为private(其他的为public),可以调用obj.private_methods查看,当然也有obj.public_methods,obj.protected_methods,也可以显式的将 initialize 方法设为public或protected

Example

#!/usr/bin/ruby
# main.rb
class Student
    def initialize(name, age)
        @name = name
        @age = age
    end
    def learn()
        puts "public: #{@name} learn"
    end
    def run()
        puts "protected: #{@name} run"
    end
    def speak()
        puts "private: #{@name} speak"
    end
    public :learn
    protected :run
    private :speak
    public :initialize
end
# class Child < Parent表示Child继承Parent
class Marco < Student
    def initialize(age)
        # 调用父类构造函数
        super("Marco",age)
    end
    # 未指定访问性默认public
    def play()
        # 超类Student继承而来的保护方法run
        self.run()
        # run() 正确
        # 超类Student继承而来的私有方法speak
        speak()
        # self.speak() 错误
    end
end
student = Student.new("Marco", 18)
student.learn()
puts student.private_methods.grep(/^initialize$/)
marco = Marco.new(21)
marco.play
puts marco.private_methods.grep(/^initialize$/)
# Output:
=begin
public: Marco learn
protected: Marco run
private: Marco speak
initialize
=end
注:如果public,private,protected方法过多,上述指定方法写起来会累,所以Ruby让我们可以直接写public,private,protected直接开启一个可访问性作用域,直到下一个public,private,protected出现,才会被切换
类的继承性

Ruby类提供了继承,但是只有单继承,继承可以获得超类所有的方法(包括所有种类可访问性的方法),因此也就没有了C++的公有继承,保护继承,私有继承的概念,Ruby中所有声明的类默认继承于Object,而Object继承于BasicObject,BasicObject是一个空白类(仅含几个必须的方法)

class Parent
    attr_accessor :name
    def initialize(name)
        @name = name
    end
end

class Child < Parent
    def initialize(name)
        super(name)
    end
end
child = Child.new("marco")
p child.name
child.name = "epsilon"
p child.name
# Output:
=begin
"marco"
"epsilon"
=end
注:Ruby类中继承而来的都是方法,但是上例实例变量又是从何而来? Child 类貌似没有定义实例变量,这也是容易被忽视的一点,千万不要把实例变量当成其他语言的类属性来使用,要记住实例变量是在实例方法中定义的,所以只要有实例方法的存在,就可以随时随地的定义实例变量,下面的这个例子就说明了"野火烧不尽,春风吹又生"

Example
class Parent
    attr_accessor :name
    def initialize(name)
        @name = name
    end
end

class Child < Parent
    def initialize(name)
        super(name)
    end
end
child = Child.new("marco")
p child.name
child.remove_instance_variable(:@name)
p child.name
child.name = "epsilon"
p child.name
Parent.remove_method(:name=)
p child.name
child.remove_instance_variable(:@name)
p child.name
# Output:
=begin
"marco"
nil
"epsilon"
"epsilon"
nil
=end
通过上例我们看到,尽管删除了实例变量,但是只要定义的方法在,我们还是可以让它"死而复生"

多继承? Mix-in by Module

Ruby没有多继承,但是引入了Mix-in的概念(php7的trait也是),通过Mix-in的方法,可以很轻松的"继承"其他已有的方法,ruby的mix-in是通过moudle实现的,通过 module moduleName ... end 就能创建一个module作用域, Module 是Class的超类,所以Class有Module的所有方法,但是Module不能创建对象,需要使用Moudle类的方法可以通过 include moduleName 得到
Example
module Magic
    def hello()
        puts "#{@name}"
    end
end

class Try
    include Magic
    def initialize(name)
        @name = name
    end
end

try = Try.new("marco")
try.hello()
# Output: marco

错误处理

Ruby的错误处理一般采用的是抛出异常,Ruby使用 begin ... rescue ... end 来包围可能抛出异常的范围和处理范围,通常是
begin
 ...
 可能出现异常
rescue => 引用异常的对象
 异常处理
end
异常发生时被自动赋值的全局变量:

$!

最后发生的异常(异常对象)

!@

最近异常的位置信息

异常对象方法:
方法名 作用
class 返回异常的类名
message 返回异常的信息
backtrace 返回异常的位置信息($!.backtrace与$@等价)
建议自己编写异常时继承标准库的StandardError或者RuntimeError,这样用户能够更好的适应错误处理的变化,我们可以通过raise关键字抛出异常,用ensure定义无论异常是否后都必须执行的指令
Example
class Error < StandardError
end
class LinkError < Error
end
class ConnectError < Error
end

def try()
    begin
        #raise LinkError.new("link error may you should check")
        raise ConnectError.new("connect error,retry?")
        #raise Error.new("unchecked error")
    rescue LinkError => link
        puts link.message
        puts $@
    end
end
begin
    try()
rescue ConnectError => connect
    puts $!.message
rescue Error => error
    puts error.message
ensure
    puts "destroy resouce..."
end
此外,Ruby还提供了 rescue 修饰符,方便我们简化代码,语法: expression_1 rescue expression_2 ,如果 expression_1 抛出异常,就返回 expression_2
Example
n = Integer.new("abc") rescue 0
puts n
m = 2 / 0 rescue 0
puts m
# Output:
=begin
0
0
=end

lambda vs block vs Proc

block算是Ruby最常用的基础设施了,通过它很方便剥离可变的逻辑,它主要有以下几种用途:

循环逻辑

将block用作循环逻辑,算是很直观的用途,我们通过将序列遍历的元素,传给块,让块进行处理,使得我们可以定制化循环的逻辑,而不用自行遍历进行处理(显然会有大量相似难以复用的代码),或者记住大量特定操作的API(显然增加开发者脑负担),最常见的就是each方法了(自定义类或模块中只要实现each方法就可以复用Enumerable模块代码)
numbers = [1, 2, 3, 4, 5]
numbers.each do |num|
    puts num
end

控制逻辑

控制逻辑常用于通过传入的参数,不同block能够反应出相应的提示信息,比如过滤,排序等逻辑,通过用户自行控制其逻辑,使得用户能够高度可定制
numbers = [1, 2, 3, 4, 5, 6]
numbers.select! do |num|
    num % 2 == 0
end
numbers.each do |num|
    puts num
end

...

lambda和block大致相似,但是lambad对参数检查更加严格,而block传入的参数可以少于或多余所需的参数,此时参数用nil代替或忽略,但是lambda则要求传入的参数必须和其要求的参数个数严格相等,可以通过 lambda {|...args| block...} 创建lambda 也可以通过 ->(...args) {...block} 创建,lambda也一种特殊的Proc对象,所以lambda可以通过&lambda获得block,也可以通过Proc.lambda获得lambda
numbers = [7,4,2,3,6,5,1]
numbers.sort! &lambda {|i, j|
    i <=> j
}
numbers.each &-> (num) {puts num}
lambda和Proc区别和lambda和block区别类似
# lambda VS non-lambda Proc(regular Proc) VS block 参数异同点
numbers = [1, 2, 3, 4, 5, 6]
numbers.each do |i, j|
    if j == nil then
        puts i
    end
end
begin
    numbers.each &-> (i, j) {puts i if j == nil}
rescue ArgumentError => e
    puts "Step 2 use lambad transform occur ArgumentError #{e.message}"
end
procdure = Proc.new { |i,j| puts i if j == nil }
numbers.each &procdure

# lambda VS non-lambda Proc(regular Proc) VS block return语句异同点
def return_test(src, &block)
    numbers = [5,4,2,7,1,6,3]
    if block_given? then
        puts "#{src} begin call"
        numbers.sort! &block
        puts "#{src} result: #{numbers}"
    end
end
def test_block()
    return_test "block" do |i, j|
        return i <=> j
    end
end
def test_proc()
    procdure = Proc.new do |i, j|
        return i <=> j
    end
    return_test "non-lambda Proc (regular Proc)", &procdure
end
def test_lambda()
    return_test "lambda", &->(i, j) {return i <=> j}
end
test_block()
test_proc()
test_lambda()
对于Proc分为non-lambda对象(主要通过Proc.new创建),lambda对象(通过lambda关键字创建),此外一个block代码块(block不是对象),non-lambda和block行为差不多相同,lambda则与两者有所不同(转化为block性质不变),主要体现在以下两点:

lambda对参数要求更严格,形参和实参个数必须相同,而non-lambda和block则对于实参少于形参时用nil代替,多于时忽略

non-lambda和block尽量不要使用return关键字,因为non-lambda和block会在调用它的作用域退出到创建它的作用域上一层,而lambda则直接退出调用它的作用域

学习一门语言之前,请忘掉之前学的语言