Actually, I’mtrying to make Ruby natural, not simple. Ruby is simple in appearance, but isvery complex inside, just like our human body. - Matz, Ruby 发明人
Ruby是个美丽、灵巧而且方便又实用的程序语言,而Ruby on Rails正是 Ruby 程序语言爆发成长的催化剂。在了解Ruby on Rails的程序之前,学习Ruby程序语言是最重要的基础功课之一,我们在这一章节将快速带过一些基本的语法,网络上也有Ruby Taiwan社群所翻译的文章可以一读:
· Ruby中文官方网站:Ruby简介http://www.ruby-lang.org/zh_TW/about/
· Ruby中文官方网站:二十分钟Ruby体验http://www.ruby-lang.org/zh_TW/documentation/quickstart/
· Ruby中文官方网站:从其他程序语言到Rubyhttp://www.ruby-lang.org/zh_TW/documentation/ruby-from-other-languages/
· Ruby使用手册http://guides.ruby.tw/ruby/
免费的英文资源就更多了,请参考附录生态圈。
除了用C语言实现的官方版Ruby(又叫做CRuby或MRI, Matz’s Ruby Interpreter) http://ruby-lang.org/,也有其他不同实现的Ruby环境。这些实现都以RubySpec作为其语法的标准:
· JRuby http://jruby.org/是由Java实现的Ruby,运行在高性能、支持系统线程及有非常多函数库的Java虚拟机(JVM)上。JRuby算是目前Ruby要开发跨平台(Windows、Mac和Linux)桌面软件最好的选择。
· MacRuby http://www.macruby.org/是由Objective-C实现的Ruby,运作在Mac OS X操作系统上。强项在于开发Mac原生桌面软件,未来有机会可以运行在iOS上,甚至成为Apple官方程序语言之一。
· Rubinuis http://rubini.us/是用C++、Ruby和LLVM编译程序技术实现的Ruby VM,可以在Mac OS X、Debian/Ubuntu、FreeBSD、Windows上执行。LLVM可以说是当代最重要的编译程序架构,拥有各种编译程序优化技术。能给Ruby带来多少性能改善幅度,值得关注。
· Ruby Enterprise Edition(简称REE) http://www.rubyenterpriseedition.com/是个非常流行的CRuby 1.8分支版本,特别适合用来搭配Ruby on Rails和Phusion Passenger使用,相较于Ruby 1.8.7可以增加性能和节省内存使用量。
· IronRuby http://ironruby.net/是由.NET实现的Ruby,使用了Dynamic Language Runtime技术。
IRB是一个互动的Ruby环境,可以让我们练习和语法,做些简单的实验。请输入irb
就会进入互动模式:
$ irb
irb: Interactive Ruby
irb(main):001:0>
irb(main):001:0> 1 + 1
=> 2
在irb
之中,每行执行完Ruby都会自动帮你puts
输出结果。
不过,一旦程序稍微复杂一点,还是打开文本编辑器吧。让我们编辑一个文件hello.rb。Ruby脚本附档名的惯例是.rb,内容如下:
puts "Hello, World!!"
存盘后,输入:
$ ruby hello.rb
就会执行这个脚本了,它会在屏幕上输出Hello, World!!。
根据需不需要事先宣告变量型别,我们可以分类出静态分型(Statictyping)与动态分型(Dynamic typing)程序语言,前者例如Java、C、C++,后者例如Ruby、Perl、Python和PHP。根据会不会隐性自动转换型别,又可以分类出强分型(Strong typing)与弱分型(Weak typing),前者例如Ruby、Perl、Python、Java,后者例如PHP、C、C++是弱分型。让我们举个例吧:
/* PHP */
$i = 1;
echo "Value is " + $i
# 1
/* C */
int a = 5;
float b = a;
以上的PHP和C会隐性地自动转型,但是以下的Ruby程序会检查型别不相配而发生错误,这一点从PHP过来的朋友要特别注意。
# Ruby
i=1
puts "Value is " + i
#TypeError: can't convert Fixnum into String
# from (irb):2:in `+'
#
from (irb):2
另外,通常动态分型的程序语言多半也是直译式(interpreted)程序语言,也就是不需要事先编译,透过直译器(interpreter)执行即可,当然Ruby也不例外。相对的,编译式(compiled)语言则是事先编译成执行档才行执行。总结以上,Ruby是个动态强分型的直译式程序语言。
任何整数都是Fixnum对象:
5
-205
9999999999
0
完整的Fixnum API请参考Rubydoc文件。
中间带有点号的就是浮点数Float对象:
54.321
0.001
-12.312
0.0
浮点数四则运算范例如下:
puts 1.0 + 2.0
puts 2.0 * 3.0
puts 5.0 - 8.0
puts 9.0 / 2.0
# 3.0
# 6.0
# -3.0
# 4.5
要注意的是,整数四则运算结果,也会是整数:
puts 1 + 2
puts 2 * 3
puts 5 - 8
puts 9 / 2
# 3
# 6
# -3
# 4
以下是一个更复杂的四则运算例子:
puts 5 * (12-8) + -15
puts 98 + (59872 / (13*8)) * -52
完整的Float API请参考Rubydoc文件。
使用单引号或双引号括起来的是字符串String对象:
puts 'Hello, world!'
puts ''
puts 'Good-bye.'
字符串相加可以使用加号,要注意的是字符串不能直接跟数字相加,会发生异常错误:
puts 'I like ' + 'apple pie.'
puts 'You\'re smart!'
puts '12' + 12
#
更多字符串方法示范:
var1 = 'stop'
var2 = 'foobar'
var3 = "aAbBcC"
puts var1.reverse # 'pots'
puts var2.length # 6
puts var3.upcase
puts var3.downcase
完整的String API请参考Rubydoc文件。
你可能已经注意到,在Ruby里每样东西都是对象,包括字符串和数字。所有的方法都是对对象调用,你不会看到全局函式,例如PHP的strlen("test")
用法,在Ruby中是"test".length
。
#
輸出
"UPPER"
puts "upper".upcase
#
輸出
-5
的絕對值
puts -5.abs
#
輸出
Fixnum
類別
puts 99.class
#
輸出
"Ruby Rocks!"
五次
5.times do
puts "Ruby Rocks!"
end
局部变量使用小写开头,偏好单字之间以底线_
来分隔。范例如下:
composer = 'Mozart'
puts composer + ' was "da bomb", in his day.'
my_composer = 'Beethoven'
puts 'But I prefer ' + my_composer + ', personally.'
刚刚提到数字和字符串对象不能直接相加,你必须使用to_s
(转成字符串)、to_i
(转成整数)或to_f
(转成浮点数)来手动转型,范例如下:
var1 = 2
var2 = '5'
puts var1.to_s + var2 # 25
puts var1 + var2.to_i # 7
puts 9.to_f / 2 # 4.5
大写开头的是为常数,范例如下:
Foo = 1
Foo = 2 # (irb):3: warning: already initialized constant Foo
RUBY_PLATFORM # => "x86_64-darwin10.7.0"
ENV # => { "PATH" => "....", "LC_ALL" => "zh_TW.UTF-8" }
表示未设定值、未定义的状态:
nil # nil
nil.class # NilClass
nil.nil? # true
42.nil? # false
nil == nil # true
false == nil # false
Ruby偏好一律使用单行注释:
# this is a comment line
# this is a comment line
多行注释比较少见:
=begin
This is a comment line
This is a comment line
=end
使用中括号,索引从0
开始。注意到数组中的元素是不限同一类别,想放什么都可以:
a = [ 1, "cat", 3.14 ]
puts a[0] #
輸出
1
puts a.size #
輸出
3
a[2] = nil
puts a.inspect #
輸出
[1, "cat", nil]
a[99] # nil
inspect
方法会将对象转成适合给人看的字符串
如果读取一个没有设定的数组元素,默认值是nil
。更多数组方法范例:
colors = ["red", "blue"]
colors.push("black")
colors << "white"
puts colors.join(", ") # red, blue, black, white
colors.pop
puts colors.last #black
完整的Array API请参考Rubydoc文件。
Hash是一种键值对(Key-Value)的数据结构,使用大括号:
config = { "foo" => 123, "bar" => 456 }
puts config["foo"] #
輸出
123
config["nothing"] #
是
nil
在Ruby 1.9支持新的语法,比较简约:
config = { foo: 123, bar: 456 } #
等同於
{ :foo => 123, :bar => 456 }
如果读取一个不存在的值,例如上述范例的nothing
,默认值是nil
。
完整的Hash API请参考Ruby doc文件。
注意到Ruby 1.8的Hash不像Array,你无法预测里面的键值顺序。例如输入{ :a => 1, :b=> 2, :c => 3 }.merge( :d => 4 )
会输出成{:a=>1, :d=>4, :b=>2, :c=>3}
。这个问题在Ruby 1.9获得修正,会依照你给的顺序输出成{:a=>1, :b=>2, :c=>3,:d=>4}
。
如果你想确保无论Ruby 1.8或1.9都要用有顺序的Hash,在Rails环境中可以用ActiveSupport::OrderedHash
。
Symbol一种唯一且不会变动的识别名称:
config = { :foo => 123, :bar => 456 }
輸出
123
使用Symbol的理由是可以获得执行上的效率,相同名称的Symbol不会再重复建构对象,范例如下:
輸出
2151854740
輸出
2151830100
輸出
577768
輸出
577768
object_id
方法会回传Ruby内部的内存配置编号,你会发现相同内容的字符串,也会是不同的新对象,但是Symbol不会。这种特性让Symbol的主要用途是当做Hash的键。
让我们来看看一些流程控制:
puts 1 > 2 #
大於
puts 1 < 2 #
小於
puts 5 >= 5 #
大於等於
puts 5 <= 4 #
小於等於
puts 1 == 1 #
等於
puts 2 != 1 #
不等於
puts ( 2 > 1 ) && ( 2 > 3 ) #
和
puts ( 2 > 1 ) || ( 2 > 3 ) #
或
else if写成elsif
:
total = 26000
if total > 100000
puts "large account"
elsif total > 25000
puts "medium account"
else
puts "small account"
end
另外如果要执行的if
程序只有一行,可以将if
放到行末即可:
puts "greater than ten" if total > 10
三元运算符expression ?true_expresion : false_expression
可以让我们处理简易的ifelse条件,例如以下的程序:
x = 3
if x > 3
y = "foo"
else
y = "bar"
end
改用三元运算符之后,可以缩减程序行数:
x = 3
y = ( x > 3 )? "foo" : "bar"
case name
when "John"
puts "Howdy John!"
when "Ryan"
puts "Whatz up Ryan!"
else
puts "Hi #{name}!"
end
while用法范例:
i=0
while ( i < 10 )
i += 1
next if i % 2 == 0 #
跳過雙數
end
until用法范例:
i = 0
i += 1 until i > 10
puts i
#
輸出
11
loop用法范例:
i = 0
loop do
i += 1
break if i > 10 #
中斷迴圈
end
不过你很快就会发现写Ruby很少用到while、until、loop,我们会使用迭代器。
记住,只有false
和nil
是假,其他都为真。
puts "not execute" if nil
puts "not execute" if false
puts "execute" if true #
輸出
execute
puts "execute" if 1 #
輸出
execute
puts "execute" if "foo" #
輸出
execute
puts "execute" if Array.new #
輸出
execute
与Perl类似的语法,使用=~
:
#
抓出手機號碼
phone = "123-456-7890"
if phone =~ /(\d{3})-(\d{3})-(\d{4})/
ext = $1
city = $2
num = $3
end
使用def
开头end
结尾来定义一个方法:
def say_hello(name)
result = "Hi, " + name
return result
end
puts say_hello('ihower')
#
輸出
Hi, ihower
方法中的return
是可以省略的,Ruby就会回传最后一行运算的值。上述方法可以改写成:
def say_hello(name)
"Hi, " + name
end
调用方法时,括号也是可以省略的,例如:
say_hello 'ihower'
不过,除了一些方法惯例不加之外(例如puts
和Rails中的redirect_to
、render
方法),绝大部分的情况加上括号比较无疑义。
?
与!
的惯例方法名称可以用?
或!
结尾,前者表示会回传Boolean值,后者暗示会有某种副作用(side-effect)。范例如下:
array=[2,1,3]
array.empty? # false
array.sort # [1,2,3]
array.inspect # [2,1,3]
array.sort! # [1,2,3]
array.inspect # [1,2,3]
Ruby的类别其实也是一种常数,所以也是大写开头,使用new
方法可以建立出对象,例如之前所学的字符串、数组和哈希,也可以用以下方式建立:
color_string = String.new
color_string = "" #
等同
color_array = Array.new
color_array = [] #
等同
color_hash = Hash.new
color_hash = {} #
等同
= Time.new # 內建的時間類別
puts time
来看看如何自定类别:
class Person #
大寫開頭的常數
def initialize(name) #
建構式
@name = name #
对象变量
end
def say(word)
puts "#{word}, #{@name}" #
字串相加
end
end
p1 = Person.new("ihower")
p2 = Person.new("ihover")
p1.say("Hello") #
輸出
Hello, ihower
p2.say("Hello") #
輸出
Hello, ihover
注意到双引号里的字符串可以使用#{var}
来做字符串嵌入,相较起用加号+
相加字符串可以更有效率。
除了对象方法与对象变量,Ruby也有属于类别的方法和变量:
class Person
@@name = “ihower” #
類別变量
def self.say #
類別方法
puts @@name
end
end
Person.say #
輸出
ihower
所有的对象变量(@
开头)、类别变量(@@
开头),都是封装在类别内部的,类别外无法存取:
class Person
def initialize(name)
@name = name
end
end
p = Person.new('ihower')
出現
NoMethodError
錯誤
出現
NoMethodError
錯
誤
为了可以存取到@name
,我们必须定义方法:
class Person
def initialize(name)
@name = name
end
def name
@name
end
def name=(name)
@name = name
end
end
p = Person.new('ihower')
p.name
=> "ihower"
p.name="peny"
=> "peny"
Class
定义范围内也可以执行程序跟其他程序语言不太一样,Ruby的类别层级内也可以执行程序,例如以下:
class Demo
puts "foobar"
end
当你加载这个类别的时候,就会执行puts "foobar"
输出foobar。会放在这里的程序,主要的用途是来做Meta-programming。例如,上述定义对象变量的访问方法实在太常见了,因此Ruby提供了attr_accessor
、attr_writer
、attr_reader
类别方法可以直接定义这些方法。上述的程序可以改写成:
class Person
attr_accessor :name
end
p = Person.new('ihower')
p.name
=> "ihower"
p.name="peny"
=> "peny"
这里的attr_accessor
其实就是一个类别方法。
类别中的方法默认是public
的,宣告private
或protected
的话,该行以下的方法就会套用:
class MyClass
def public_method
end
private
def private_method_1
end
def private_method_2
end
protected
def protected_method
end
end
Ruby的private和protected定义和其他程序语言不同,都是可以在整个继承体系内调用。两着差别在于private只有不指定接受者(receiver)时才可以调用,你甚至不能打self.private_method_1
,默认一定就是self当成private方法的接受者。而protected方法除了可以被一个类别或子类别的对象调用,也可以让另一个相同类别的对象来当做接受者。
在面向对象的术语中,object.call_method
的意思是object收到执行call_method的指令,也就是object是call_method方法的接受者(receiver)。因此,你甚至可以改写成object.__send__(:call_method)
Ruby使用小于<
符号代表类别继承:
class Pet
attr_accessor :name, :age
def say(word)
puts "Say: #{word}"
end
end
class Cat < Pet
def say(word)
puts "Meow~"
super
end
end
class Dog < Pet
def say(word, person)
puts "Bark at #{person}!"
super(word)
end
end
Cat.new.say("Hi")
Dog.new.say("Hi", "ihower")
输出
Meow~
Say: Hi
Bark at ihower!
Say: Hi
这个范例中,Cat
和Dog
子类别覆写了Pet say方法,其中的super
是用来调用被覆写掉的Pet say方法。另外,没有括号的super
和有括号的super()
是有差异的,前者Ruby会自动将所有参数都代进去来调用父类别的方法,后者则是自己指定参数。此例中如果Dog say里只写super
,则会发生wrong number of arguments的错误,这是因为Ruby会传say("Hi", "ihower")
给Pet say而发生错误。
不同于while
循环用法,Ruby习惯使用迭代器(Iterator)来走访循环,例如each
是一个数组的方法,它会走访其中的元素,其中的do .... end
是each
方法的参数,称作匿名方法(code block)。范例程序如下:
languages = ['Ruby', 'Javascript', 'Perl']
languages.each do |lang|
puts "I love #{lang}!"
end
# I Love Ruby!
# I Love Javascript!
# I Love Perl!
其中两个直线|
中间的lang被称作Block variable区块变量,每次迭代都会被设定成不同元素。其他迭代器范例如:
#
反覆三次
3.times do
puts 'Good Job!'
end
# Good Job!
# Good Job!
# Good Job!
#
從一數到九
1.upto(9) { |x| puts x }
#
多一個索引區塊变量
languages = ['Ruby', 'Javascript', 'Perl']
languages.each_with_index do |lang, i|
puts "#{i}, I love #{lang}!"
end
# 0, I Love Ruby!
# 1, I Love Javascript!
# 2, I Love Perl!
(Code block)的形式除了do ... end
,也可以改用大括号。通常单行会会用大括号,多行会用do ... end
的形式。
3.times { puts "Hello" }
透过迭代器,我们就比较少用到while
、until
、for
等循环语法了。
#
迭代並造出另一個陣列
a = [ "a", "b", "c", "d" ]
b = a.map {|x| x + "!" }
puts b.inspect
#
結果是
["a!", "b!", "c!", "d!"]
#
找出符合條件的值
b = [1,2,3].find_all{ |x| x % 2 == 0 }
b.inspect
#
結果是
[2]
#
迭代並根據條件刪除
a = [51, 101, 256]
a.delete_if {|x| x >= 100 }
#
結果是
[51]
#
客製化排序
[2,1,3].sort! { |a, b| b <=> a }
#
結果是
[3, 2, 1]
#
計算總和
(5..10).inject {|sum, n| sum + n }
#
找出最長字串
find the longest word
longest = ["cat", "sheep", "bear"].inject do |memo,word|
( memo.length > word.length )? memo : word
end
<=>
是比较运算符,当两个数字相等于回传0
,第一个数字较大时回传1
,反之回传-1
除了迭代,也有Code block只会执行一次,例如用来开档。往常我们在文件处理完毕之后,会使用close
方法:
file = File.new("testfile", "r")
# ...
處理檔案
file.close
改用Code block语法之后,Ruby可以在Code block结束后自动关档:
File.open("testfile", "r") do |file|
# ...
處理檔案
end
#
檔案自動關
閉
Code block的这个特性不只让你少打close
方法,更可以避免你忘记(不然就语法错误了),也有视觉上缩排的好处。
在方法中使用yield
可以执行Code block参数:
#
定義方法
def call_block
puts "Start"
yield
yield
puts "End"
end
call_block { puts "Blocks are cool!" }
#
輸出
# "Start"
# "Blocks are cool!"
# "Blocks are cool!"
# "End"
def call_block
yield(1)
yield(2)
yield(3)
end
call_block { |i|
puts "#{i}: Blocks are cool!"
}
#
輸出
# "1: Blocks are cool!"
# "2: Blocks are cool!"
# "3: Blocks are cool!"
可以将Code block明确转成一个变量:
def call_block(&block)
block.call(1)
block.call(2)
block.call(3)
end
call_block { |i| puts "#{i}: Blocks are cool!" }
#
或是先宣告出
proc object
proc_1 = Proc.new { |i| puts "#{i}: Blocks are cool!" }
proc_2 = lambda { |i| puts "#{i}: Blocks are cool!" }
call_block(&proc_1)
call_block(&proc_2)
#
輸出
# "1: Blocks are cool!"
# "2: Blocks are cool!"
# "3: Blocks are cool!"
def my_sum(*val)
val.inject(0) { |sum, v| sum + v }
end
puts my_sum(1,2,3,4) # val
变量就是
[1,2,3,4]
#
輸出
10
其中my_sum方法中的val
是一个包含所有参数的数组。
{ }
def my_print(a, b, options)
puts a
puts b
puts options[:x]
puts options[:y]
puts options[:z]
end
my_print("A", "B", { :x => 123, :z => 456 } )
my_print("A", "B", :x => 123, :z => 456) #
結果相同
#
輸出
A
#
輸出
B
#
輸出
123
#
輸出
nil
#
輸出
456
使用rescue可以将异常救回来:
begin
puts 10 / 0
rescue => e
puts e.class
ensure
# ...
end
#
輸出
ZeroDivisionError
使用raise会丢出异常:
raise "Not works!!"
#
丟出一個
RuntimeError
#
自行自定异常对象
class MyException < RuntimeError
end
raise MyException
Module是Ruby一个非常好用的功能,它跟Class类别非常相似,你会在里面定义方法。只是你不能用new来建立它。它的第一个用途是可以当做Namespace来放一些工具方法:
module MyUtil
def self.foobar
puts "foobar"
end
end
MyUtil.foobar
#
輸出
foobar
另一个更重要的功能是Mixins,可以将一个Module混入类别之中,这样这个类别就会拥有此Module的方法。这回让我们拆成两个文件,debug.rb和foobar.rb,然后在foobar.rb中用require
来引用debug.rb:
首先是debug.rb
module Debug
def who_am_i?
puts "#{self.class.name}: #{self.inspect}"
end
end
然后是foobar.rb
require "./debug"
class Foo
這個動作叫做
Mixin
end
class Bar
include Debug
end
f = Foo.new
b = Bar.new
f.who_am_i? #
輸出
Foo: #
b.who_am_i? #
輸出
Bar: #
Ruby使用Module来解决多重继承的问题,不同类别之间但是拥有相同的方法,就可以改放在Module里面,然后include
它即可。
Metaprogramming是很进阶的技巧,这里示范define_method
方法可以动态定义方法:
class Dragon
define_method(:foo) { puts "bar" }
['a','b','c','d','e','f'].each do |x|
define_method(x) { puts x }
end
end
dragon = Dragon.new
dragon.foo #
輸出
"bar"
dragon.a #
輸出
"a"
dragon.f #
輸出
"f"
Ruby拥有许多反射方法,可以动态知道对象的信息:
#
這個对象有什麼方法
Object.methods
=> ["send", "name", "class_eval", "object_id", "new", "singleton_methods", ...]
#
這個对象有這個方法嗎?
Object.respond_to? :name
=> true
result ||= a
如果result
是nil
或false
的话,将a
指派给result
。以上这段程序等同于
result || ( result = a )
· Ruby 程序语言入门导览http://ihower.tw/rails3/files/ruby-intro.pdf
· Ruby 1.9 投影片http://www.slideshare.net/ihower/ruby-19-1311075