Skip to content

Latest commit

 

History

History
766 lines (590 loc) · 26.1 KB

File metadata and controls

766 lines (590 loc) · 26.1 KB

Java 开发者的 Ruby 入门简明教程

Java 非常成熟,久经检验,且非常快(与那些反对java的人可能还在声称的相反)。但它也非常啰嗦。从 Java 到Ruby,可以预见的 是代码规模将大大缩小。你也可以期望使用相对少的时间快速出产原型。

相似点 Ruby 与 Java 一样的地方

  • 垃圾回收器帮你管理内存。
  • 强类型对象。
  • 有 public、 private 和 protected 方法。
  • 拥有嵌入式文档工具(Ruby 的工具叫 rdoc)。rdoc 生成的文档与 javadoc 非常相似。

相异点 Ruby 与 Java 不同的地方

一些简单总结:

  • 你不需要编译你的代码。你只需要直接运行它。
  • 定义像类这样的东西时,可以使用 end 关键字,而不使用花括号包裹代码块。
  • 使用 require 代替 import
  • 所有成员变量为私有。在外部,使用方法获取所有你需要的一切。
  • 方法调用的括号通常是可选的,经常被省略。
  • 一切皆对象,包括像 2 和 3.14159 这样的数字。
  • 没有静态类型检查。
  • 变量名只是标签。它们没有相应的类型。
  • 没有类型声明。按需分配变量名,及时可用(如: a = [1,2,3] 而不是 int[] a = {1,2,3}; )。
  • 没有显式转换。只需要调用方法。代码运行之前,单元测试应该告诉你出现异常。
  • 使用 foo = Foo.new("hi") 创建新对象,而非 Foo foo = new Foo("hi")
  • 构造器总是命名为“initialize” 而不是类名称。
  • 作为接口的替代,你将获得“混入(mixins)”。
  • 相比 XML,倾向于使用 YAML。
  • nil 替代 null
  • Ruby 对 ==equals() 的处理方式与 Java 不一样。测试相等性使用 == (Java 中是 equals() )。测试是否为同一 对象使用 equals?() (Java 中是 == )。
  • Ruby 里面调用一个函数或方法,参数两边的圆括号可以省略。如:
    • p.say_hello("Hi", "Jack") 可以写成 p.say_hello "Hi", "Jack"
    • f.close() 可以写成 f.close
  • 定义函数或方法时,参数两边的圆括号也可以省略。
  • Ruby 的类可以重新「打开」,即在原有的类定义上追加新的方法或属性、覆盖已有的方法、属性。如:
    class Person < ActiveRecord::Base   # 定义了一个 Person 类,它是 ActiveRecord::Base 的子类
      def say_hello(message, name)
        puts "#{name}, #{message}"
      end
    end
    
    
    class Person   # 重新打开 Person 类,这里添加了一个 sleep 方法,并且 Person 仍然有 say_hello 方法,并且它仍然是 ActiveRecord::Base 的子类
      has_many :books    # has_many 是从 ActiveRecord::Base 继承来的类方法(静态方法), 类方法可以直接在类体中调用(就是和定义方法的同一层级),而实例方法则不可以在类体中调用
      def sleep(time_secs)
        ...
      end
    end
        

    下面是更详细的差异的介绍:

快速简明教程

代码块以 end 关键字结束

不同于 Java,Ruby 使用 end 关键字来结束代码块,而不是使用花括号。如:

类定义

class Person
  def initialize(name)
    @name = name
  end
end

函数方法定义

def say_hello
  puts "Hello"
end

一切皆对象

Ruby 中的数字、字符串、数组等都是对象,它们都有自己的方法。如:

3.times { puts "Hello" }  # 输出三次 Hello
"Hello".length
[1, 2, 3].reverse

基本数据类型

数值

1 + 2
2 * 3
10 / 5
10 % 3

字符串

"Hello, " + "World"
"Hello" * 3
"Hello".length
"Hello".reverse
  • 单引号字符串中的特殊字符不会被转义,而双引号字符串中的特殊字符会被转义。如:
    puts 'Hello\nWorld'  # 输出 Hello\nWorld
    puts "Hello\nWorld"  # 输出 Hello
                         #      World
        
  • 双引号字符串中可以使用 #{} 来插入变量或表达式。如:
    name = "Jack"
    puts "Hello, #{name}"  # 输出 Hello, Jack
        

nil

Ruby 中的 nil 相当于 Java 中的 null

Symbol

:name

Symbol 是一种特殊的字符串(但Symbok 类的和表示字符串的 String 类没有直接关系),它的值是唯一的。Symbol 通常用来表示一个名字或标识符。

boolean

Ruby 中的 true 和 false 都是对象,它们都是 TrueClass 和 FalseClass 的实例。

在 Ruby 中,除了 false 和 nil 为假,其他值都为真。

在 Ruby 代码中,还经常看到 if obj.present? 等方法,这些方法是 Rails 提供的,它们是对 Ruby 的扩 展。其中,=obj.present?= 方法会判断 obj 是否为 nil 或空字符串或空数组、空散列

数组

[1, 2, 3]
[1, 2, 3].length
[1, 2, 3].reverse
[1, 2, 3] << 4
  • 数组中的元素可以是不同类型的对象。
  • 数组中的元素可以通过索引访问,索引从 0 开始。
  • 数组中的元素可以通过 << 方法添加到数组的末尾。
数组的常用方法
each

each 方法用于遍历数组中的元素。如:

[1, 2, 3].each { |i| puts i }
map

map 方法用于对数组中的每个元素执行块中的操作,返回一个新的数组。如:

[1, 2, 3].map { |i| i * 2 }
select

select 方法用于从数组中选择满足条件的元素,返回一个新的数组。如:

[1, 2, 3].select { |i| i > 1 }
reduce

reduce 方法用于对数组中的元素进行累加。如:

[1, 2, 3].reduce { |sum, i| sum + i }
each_with_index

each_with_index 方法用于遍历数组中的元素,同时获取元素的索引。如:

[1, 2, 3].each_with_index { |i, index| puts "#{index}: #{i}" }
each_with_object

each_with_object 方法用于遍历数组中的元素,同时传递一个对象。如:

[person1, person2].each_with_object({}) { |person, hash| hash[person.name] = person.age }
group_by

group_by 方法用于根据块中的条件对数组中的元素进行分组。如:

[person1, person2].group_by { |person| person.gender }
in_groups

in_groups 方法用于将数组分成若干组。如:

[1, 2, 3, 4, 5].in_groups(2)
in_groups_of

in_groups_of 方法用于将数组分成若干组,每组包含指定个数的元素。如:

[1, 2, 3, 4, 5].in_groups_of(2)

哈希

哈希是一种键值对的数据结构,类似于 Java 中的 Map。如:

{ "name" => "Jack", "age" => 20 }
{ :name => "Jack", :age => 20 }
{ name: "Jack", age: 20 }

上述代码中:

  1. 第一行的哈希中的键和值都是字符串。
  2. 第二行的哈希中的键是 Symbol,值是字符串。
  3. 第三行的哈希中的键是 Symbol,值是字符串,也就是说在一个哈希中, key: value 的形式等价于 :key => value 的形式。
  4. 哈希是一种键值对的集合。
  5. 哈希中的键和值可以是任意类型的对象。
  6. 哈希中的键是唯一的。
哈希的常用方法
each

each 方法用于遍历哈希中的键值对。如:

{ name: "Jack", age: 20 }.each { |key, value| puts "#{key}: #{value}" }
map

map 方法用于对哈希中的每个键值对执行块中的操作,返回一个新的数组。如:

{ name: "Jack", age: 20 }.map { |key, value| value }
select

select 方法用于从哈希中选择满足条件的键值对,返回一个新的哈希。如:

{ name: "Jack", age: 20 }.select { |key, value| value > 18 }
keys

keys 方法用于获取哈希中的所有键。如:

{ name: "Jack", age: 20 }.keys
values

values 方法用于获取哈希中的所有值。如:

{ name: "Jack", age: 20 }.values
merge

merge 方法用于合并两个哈希。如:

{ name: "Jack" }.merge({ age: 20 })
merge!

merge! 方法用于将另一个哈希合并到当前哈希中。如:

{ name: "Jack" }.merge!({ age: 20 })

类的基础知识

初始化函数

初始化函数相当于 Java 中的构造函数,它是在创建对象时自动调用的函数。在 Ruby 中,初始化函数的名字是 initialize

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end
end

属性和 getter/setter

上例中的 @name@age 是 Person 类的属性,它们是实例变量,只能在类的内部访问。如果要在类的外部访问这两个属性,需要提供 getter 和 setter 方法。如:

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end
 
  def name
    @name
  end
 
  def name=(name)
    @name = name
  end
end
 
p = Person.new("Jack", 20)
puts p.name  # 这是调用 getter 方法, 而不是直接访问实例变量
p.name = "Tom"  # 这是调用 setter 方法, 而不是直接设置实例变量

上述代码中, name 方法是 getter 方法, name 方法是 setter 方法。Ruby 提供了一种更简洁的方式来定义 getter 和 setter 方法,如:

class Person
  attr_accessor :name

  def initialize(name, age)
    @name = name
    @age = age
  end
end

如果只需要 getter 方法,可以使用 attrattr_reader 方法;如果只需要 setter 方法,可以使用 attr_writer 方法。

class Person
  attr :name
  attr_writer :name

  def initialize(name, age)
    @name = name
    @age = age
  end
end

继承

和 Java 一样,Ruby 也是只支持单继承的。Ruby 使用 < 操作符来表示继承关系。如:

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end
end

class Student < Person
  def initialize(name, age, school)
    super(name, age)
    @school = school
  end
end

静态方法

Ruby 中的静态方法使用 self 关键字来定义。如:

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end

  def self.say_hello
    puts "Hello"
  end
end

上述代码中, say_hello 是一个静态方法,可以通过 Person 类直接调用。 还有另一种常用的定义静态方法的方式,如:

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end
 
  class << self
    def say_hello
      puts "Hello"
    end
  end
end

类可以重新打开

Ruby 的类可以重新「打开」,即在原有的类定义上追加新的方法或属性、覆盖已有的方法、属性。如:

class Person < ActiveRecord::Base   # 定义了一个 Person 类,它是 ActiveRecord::Base 的子类
  def say_hello(message, name)
    puts "#{name}, #{message}"
  end
end
 
 
class Person   # 重新打开 Person 类,这里添加了一个 sleep 方法,并且 Person 仍然有 say_hello 方法,并且它仍然是 ActiveRecord::Base 的子类
  has_many :books    # has_many 是从 ActiveRecord::Base 继承来的类方法(静态方法), 类方法可以直接在类体中调用(就是和定义方法的同一层级),而实例方法则不可以在类体中调用
  def sleep(time_secs)
    ...
  end
end

混入和 Enumerable

Ruby 虽然和 Java 一样只支持单继承,但是 Ruby 提供了一种叫做「混入」的机制,可以在类中引入模块(module),从而实现多继承的效果。如:

module MyModule
  def say_hello
    puts "Hello"
  end
end
 
class Person
  def xxx
  end
end
 
class Student < Person
  include MyModule
end

现在 Student 类既继承了 Person 类的 xxx 方法,又引入了 MyModule 模块的 say_hello 方法。

因为模块(module关键字定义的部分)和类不同的是,模块不能被实例化,也就是不能创建模块的对象。所以混入模块,只会继承模块的方法,而不会继承模块的属性,从而避免了多继承的问题。

Ruby 标准库中的 Enumerable 模块就是一个很好的例子。Enumerable 模块提供了很多方法,如 each=、=map=、=select=、=reject=、=detect=、=sort 等,这些方法可以被任何实现了 each 方法的类包含进来,从而实现了类似于 Java 中的 Collection 接口的效果。如:

class MyCollection
  include Enumerable
  def each
    ...
  end
end

现在 MyCollection 类就可以像数组一样使用 map=、=select=、=reject=、=detect=、=sort 等方法了。而数组实际上也是实现了 each 方法并混入了 Enumerable 模块的类。

Kernel 模块

Ruby 中的 Kernel 模块是一个特殊的模块,它包含了很多常用的方法,这些方法可以直接在任何地方调用,而不需要通过模块名或类名来调用。如:

puts "Hello"
sleep 1

上述代码中的 putssleep 方法都是 Kernel 模块中的方法,可以直接调用。

事实上,Ruby 中的很多方法都是定义在 Kernel 模块中的,这是因为 Ruby 中的所有类都是 Object 类的子类,而 Object 类又混入了 Kernel 模块,所以所有的类都可以直接调用 Kernel 模块中的方法。

如果你希望定义一个全局方法,可以直接定义在 Kernel 模块中,这样就可以在任何地方调用了。如:

module Kernel
  def say_hello
    puts "Hello"
  end
end
 
say_hello

Block 和 Proc

Java 8 中引入了 Lambda 表达式,Ruby 中的 Block 和 Proc 与之类似,都是相当于一个函数对象。

Block

Block 并不是一个对象,而是一段代码块,是原生的语法元素、可以在方法调用时传递给方法。如:

def say_hello
  puts "Hello"
  yield if block_given?  # block_given? 用于判断在调用这个函数时,是否使用了 block, yield 会执行传递给 say_hello 方法的 Block
end
 
say_hello # 输出 "Hello"
 
say_hello do
  puts "World"
end   # 输出 "Hello" 和 "World"

也可以使用 花括号 代替 do/end:

say_hello { puts "World" }  # 输出 "Hello" 和 "World"

Proc

proc 看起来和 Block 类似,但是 proc 是一个对象,可以赋值给变量,也可以作为参数传递给方法。如:

my_proc = Proc.new { puts "Hello" }
my_proc.call  # 输出 "Hello"
 
def say_hello(block)
  block.call
end
 
say_hello(my_proc)  # 输出 "Hello"
将 Block 转换为 Proc

有时候我们需要将 Block 转换为 Proc 对象,可以使用 & 符号:

def say_hello(&blk)
  blk.call
end
 
say_hello { puts "Hello" }  # 输出 "Hello"

上面的代码中,&blk 会将传递给 say_hello 方法的 Block 转换为 Proc 对象。

其他定义 Proc 的方式
lambda

lambda 是 Proc 的一种特殊形式,可以使用 lambda 方法定义一个 Proc 对象:

my_proc = lambda { puts "Hello" }
my_proc.call  # 输出 "Hello"

lambda 定义的 Proc 和普通的 Proc 的区别在于,lambda 会检查传递给它的参数个数是否正确,而普通的 Proc 不会。如:

         my_proc = Proc.new { |a, b| puts a + b }
         my_proc.call(1)  # 输出 1
          
         my_proc = lambda { |a, b| puts a + b }
         my_proc.call(1)  # 报错
****** ->

       ->  lambda 的一种简写形式,可以用来定义一个 Proc 对象:

         my_proc = -> { puts "Hello" }
         my_proc.call  # 输出 "Hello"
&:my_method

有时候我们会看到这样的写法:

p1 = Person.new
p2 = Person.new
 
[p1, p2].each(&:say_hello)

这里的 &:say_hello 实际上是将 :say_hello Symbol对象转换为 Proc 对象,然后传递给 each 方法。这种写法等价于:

[p1, p2].each { |p| p.say_hello }

这种用法看起来很像 Java 8 中的方法引用,但实际上是 Ruby 的一个语法糖。

那么这是如何将 Symbol 对象转换为 Proc 对象呢?实际上 & 后面跟一个对象,就是调用这个对象的 to_proc 方法,将其转换为 Proc 对象。如:

&:say_hello 就是将调用 :say_hello 这个 Symbol 对象的 to_proc 方法,而 Symbol 的 to_proc 方法是这样定义的:

class Symbol
  def to_proc
    Proc.new { |obj, *args| obj.send(self, *args) }
  end
end

元编程

元编程是指在运行时动态地创建类和方法,或者修改现有类和方法的技术。Ruby 是一种动态语言,非常适合进行元编程。

可以理解为有点像 Java 中的反射机制,但是 Ruby 的元编程更加强大和灵活。

Ruby 提供了一些方法来动态地定义类和方法,如 method_missing, define_method, class_eval, instance_eval 等。

通过元编程,我们可以实现很多功能,如动态地创建类和方法,动态地修改类和方法,动态地调用方法等。

虽然你在编码时有可能不会用到元编程,但是了解元编程的原理和技术,可以帮助你更好地理解 Rails 等 Ruby 库或者框架的特性和机制。

更多关于元编程的内容,可以参考 「Ruby 元编程」 这本书。

method_missing

method_missing 是 Ruby 中一个非常重要的方法,当调用一个对象不存在的方法时,Ruby 会调用这个对象的 method_missing 方法。

通过重写 method_missing 方法,我们可以实现很多功能,如动态地创建方法,动态地调用方法等。

例如,我们可以定义一个类,当调用这个类的不存在的方法时,输出方法名:

class MyClass
  def method_missing(name, *args)
    puts "You called #{name} with #{args.inspect}"
  end
end

obj = MyClass.new
obj.hello(1, 2, 3)  # 输出 "You called hello with [1, 2, 3]"

那么 method_missing 方法有哪些实际用途呢?考虑我们定义一个 ActiveRecord 模型:

class User < ActiveRecord::Base
end

ActiveRecord 库会根据约定的命名规则,将 User 类和数据库中的 users 表对应起来,当我们调用 User 类的一些方法:

user = User.find(1)
user.username = "Tom"

那么 user.username setter 方法是如何实现的呢?如果我们是 ActiveRecord 库的作者,我们就可以使用 method_missing 方法来实现这些方法:

class ActiveRecord::Base
  def method_missing(name, *args)
    if db_columns.include_by_name?(name)  # 判断数据库中是否有这个字段
      db_columns.write_by_column_name(name, args.first)  # 写入数据库
    else
      super  # 调用父类的 method_missing 方法
    end
  end
end

define_method

define_method 方法可以动态地定义方法,它接受一个方法名和一个块,然后定义一个方法。

例如,我们可以定义一个类,动态地定义一个方法:

class MyClass
  define_method :hello do |name|
    puts "Hello, #{name}!"
  end
end

obj = MyClass.new
obj.hello("Tom")  # 输出 "Hello, Tom!"

method_missing 方法不同, define_method 方法是在类对象上调用的,而且定义好之后,这个方法就会一直存在,直到这个类被销毁。而 method_missing 方法是在实例对象上调用的, 并且每次调用不存在的方法时,都会调用 method_missing 方法。

method_missing 的那个示例中,考虑到性能问题,不希望每次调用 user.username= 的时候都调用 method_missing 方法,我们可以使用 define_method 方法来定义这个方法:

class ActiveRecord::Base
  def method_missing(name, *args)
    if db_columns.include_by_name?(name)  # 判断数据库中是否有这个字段
      self.class.send(:define_method, name) do |value|
        db_columns.write_by_column_name(name, value)  # 写入数据库
      end
      send(name, args.first)  # 调用刚刚定义的方法, 而且这样一来,下次调用这个方法就不会再调用 method_missing 方法了
    else
      super  # 调用父类的 method_missing 方法
    end
  end
end

class_eval

class_eval 方法可以动态地定义类的方法,它接受一个字符串作为参数,然后定义一个方法。

例如,我们可以定义一个类,动态地定义一个方法:

class MyClass
  class_eval %{
    def hello(name)
      puts "Hello, \#{name}!"
    end
  }
end

obj = MyClass.new
obj.hello("Tom")  # 输出 "Hello, Tom!"

instance_eval

instance_eval 方法可以动态地定义实例对象的方法,它接受一个字符串作为参数,然后定义一个方法。

例如,我们可以定义一个类,动态地定义一个方法:

class MyClass
  def initialize(name)
    @name = name
  end
end
 
obj = MyClass.new("Tom")
obj.instance_eval %{
  def hello
    puts "Hello, \#{@name}!"
  end
}
 
obj.hello  # 输出 "Hello, Tom!"
 
obj2 = MyClass.new("Jerry")
obj2.hello  # 报错, 因为 obj2 没有定义 hello 方法, 请自行搜索学习 Ruby 中的 singleton class 的概念

instance_exec

instance_exec 方法和 instance_eval 方法类似,但是它可以接受块作为参数。

例如,我们可以定义一个类,动态地定义一个方法:

class MyClass
  def initialize(name)
    @name = name
  end
end

obj = MyClass.new("Tom")
obj.instance_exec("Jerry") do |name|
  puts "Hello, #{name}!"
end  # 输出 "Hello, Jerry!", 这里定义的仍然是一个 singleton method

还可以借助 instance_exec 方法来动态定义一个普通的实例方法:

class MyClass
  def initialize(name)
    @name = name
  end
end

MyClass.instance_exec do
  define_method :hello do
    puts "Hello, #{@name}!"
  end
end

obj = MyClass.new("Tom")
obj.hello  # 输出 "Hello, Tom!"

Class.new

示例:

Employee = Class.new(Person) do
  def hello
    ...
  end
end

e = Employee.new
e.hello

Person 成为 Employee 的父类