一、软件工程概述

软件工程的难点:

  1. 适合的解决方式来解决用户需求
  2. 高质量代码
  3. 时间与金钱成本必须是可预测且可控的

好的软件如何度量:好的软件需要传递用户需要的功能性和表现,应该是可维护、可靠和可用的。

敏捷编程的用户故事:让用户参与到故事之中,即用户与机器的交互的场景,根据交互来进行编程。

1.1 软件工程

追求开发高质量软件

1.2 SaaS

Software as a Service
SaaS通过运行在客户端上的瘦程序(即程序本身没有多少功能,其功能调用API获取),访问在Internet上的服务形式提供的软件。

SaaS的优势:

  1. 无需安装,不需要担心硬件和OS差异
  2. 无需担心数据丢失
  3. 便于数据的分享
  4. 软件单一拷贝,单一部署硬件/软件环境

SaaS开发的框架/语言:Spring/Java,Django/Python,Rails/Ruby,Ruby是一种现代动态脚本编程语言 ,拥有面向对象、函数式、自动内存管理等

SaaS依赖的基础设施:

  1. 通讯:允许客户与服务交互
  2. 可扩展性:需求波动可以满足 + 新服务快速引入
  3. 可靠性:服务和通性 7 x 24 小时可用

1.3 面向服务的架构

面向服务的架构(Service Oriented Architecture),称SOA即所有组件都设计为服务,且服务可用组合的软件架构。每一个子系统都是独立的,就像独立的数据中心中一样。
竖井式架构(Silo Architecture):内部子系统可用直接共享数据,所有子系统都在单一的API中。

SOA主要通过重用来提高开发人员的生产力,任何SOA服务器都不能直接调用其他服务器中的数据,只能够调用其他服务器系统中提供的API来实现功能的交互。SOA提高了系统分解的能力,使得系统开发更为规范化、结构化。
相较于Silo模式,SOA模式服务之间的调用经过网络调用,其性能会受到网络波动的影响,二Silo模式数据调用直接在进程间执行,性能更好。SOA与Silo性能的差距,可以使用缓存来缩小。

1.4 云计算(Cloud Computation)

集群:由普通以太网联结的普通交换机,其具有下面特点:

  1. 具有更好的可伸缩性
  2. 使用冗余来实现可靠性

1.5 软件质量

验证(Verification):你构建的东西正确吗?是否符合规范?
校验(Validation):你构建的是对的东西吗?是客户想要的吗?

二、软件过程

2.1 P&D软件过程

计划与文档(P&D):
– 编码前,项目经理制定计划
– 撰写计划各阶段的详细文档
– 根据计划来度量进展
– 项目的变更必须反映在文档中,并可能反映在计
划中
瀑布式开发模型
螺旋模型
瀑布模型 + 原型
Rational统一软件开发过程

RUP有下面四个阶段:

  1. 初始阶段
  2. 精化阶段
  3. 构建阶段
  4. 迁移阶段
    P&D项目经理
    P&D依赖于项目经理
    P&D

三、Ruby程序设计语言

3.1 初识Ruby

Ruby的特性:

  • Ruby是一种高级程序设计语言

  • Ruby是一种面向对象的语言

  • Ruby是一种解释型语言(例如Python),通过解释器边解释边执行。

  • Ruby是一种动态类型、强类型语言。

    • 静态类型与动态类型:变量是否可以引用所有变量类型,是则是动态类型。
    • 强类型与弱类型:是否存在不同类型变量进行隐式转换,允许则是弱类型。
  • Ruby允许元编程。

安装:

1
2
3
4
5
wget https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.5.tar.gz
tar -xzvf ruby2.4.5
cd ruby2.4.5
./configure
make && make install

3.2 Ruby基础

注释

在 Ruby 中,您可以使用以下方式添加注释:

  1. 单行注释:使用 # 符号在一行的开头添加注释。该行后的任何内容都将被视为注释。
1
2
# 这是一个单行注释
puts "Hello, World!" # 这是另一个单行注释
  1. 多行注释:使用 =begin=end 来包围多行注释。
1
2
3
4
5
=begin
这是一个多行注释
这里可以写多行注释内容
=end
```

请注意,Ruby 中的注释只是在代码执行过程中被忽略的文本,用于提供代码的解释、说明和文档。它们不会被编译或执行。注释对于帮助其他开发人员理解您的代码以及对代码进行调试和维护非常有用。


定义变量

在 Ruby 中,可以使用以下方式定义变量:

  1. 局部变量(Local Variables):局部变量以小写字母或下划线开头。它们的作用域限制在当前的代码块或方法中。
1
2
3
name = "Alice"
age = 25
_count = 10
  1. 实例变量(Instance Variables):实例变量以 @ 符号开头。它们的作用域限制在当前对象的实例中。
1
2
@name = "Alice"
@age = 25
  1. 类变量(Class Variables):类变量以 @@ 符号开头。它们的作用域限制在当前类及其子类中。
1
@@count = 10
  1. 全局变量(Global Variables):全局变量以 $ 符号开头。它们的作用域在整个 Ruby 程序中都是可见的。
1
$global_variable = "Hello"
  1. 常量(Constants):常量以大写字母开头。它们的值在定义后不能被改变,常量不能在方法(函数)中定义。
1
2
PI = 3.14159
MAX_VALUE = 100
  1. 伪变量:伪变量不是真正的变量,其无法被重新赋值,但是可以获取当前某些属性。
1
2
3
4
5
6
7
1. self:表示当前对象的引用。在类定义中,self指的是类本身;在实例方法中,self指的是调用该方法的对象;在类方法中,self指的是类本身。

2. truefalsenil:表示布尔值和空值。true表示真,false表示假,nil表示空值。

3. __FILE__:表示当前文件的文件名。

4. __LINE__:表示当前代码所在的行号。

请注意,变量在使用之前需要先进行初始化,可以直接赋值或通过其他方式进行赋值。变量名应具有描述性且易于理解,以提高代码的可读性。


定义方法

在 Ruby 中,你可以使用 def 关键字来定义方法。以下是定义方法的基本语法:

1
2
3
def 方法名(参数1, 参数2, ...)
# 方法体
end

在上面的语法中,def 关键字用于声明方法的开始,后面紧跟方法名和参数列表。参数列表是可选的,你可以根据需要在括号中列出方法的参数。方法体是方法的实际代码,它包含在 defend 关键字之间。

下面是一个简单的示例,展示如何定义一个接受参数并打印输出的方法:

1
2
3
def greet(name)
puts "Hello, #{name}!"
end

你可以调用定义的方法,方法调用将执行方法体中的代码。调用方法时,你需要提供方法所需的参数。例如:

1
greet("Alice")

上述代码将调用 greet 方法,并将字符串 “Alice” 作为参数传递给它。方法将输出 “Hello, Alice!”。

Ruby方法可以返回参数值使用关键字return,后跟要返回的值。以下是一个示例:

1
2
3
4
5
6
7
def add_numbers(a, b)
sum = a + b
return sum
end

result = add_numbers(3, 4)
puts result # 输出:7

需要注意的是,Ruby方法的最后一个表达式的值将被默认作为返回值,因此在上面的例子中,可以省略return关键字,直接使用sum作为返回值:

1
2
3
4
def add_numbers(a, b)
sum = a + b
sum
end

当定义方法时,你可以为参数提供默认值,这样在调用方法时,如果没有提供相应的参数值,就会使用默认值。你可以使用以下语法来指定默认参数:

1
2
3
def 方法名(参数1 = 默认值1, 参数2 = 默认值2, ...)
# 方法体
end

默认参数的定义放在参数列表中,使用等号 = 后跟默认值。如果调用方法时没有为参数提供值,那么默认值将被使用。以下是一个使用默认参数的示例:

1
2
3
def greet(name = "Guest")
puts "Hello, #{name}!"
end

在上面的示例中,greet 方法的 name 参数有一个默认值 “Guest”。如果调用方法时没有传递参数,将使用默认值 “Guest”。例如:

1
2
greet("Alice")  # 输出 "Hello, Alice!"
greet # 输出 "Hello, Guest!"

另外,Ruby 还支持可变参数,这意味着你可以在方法定义中接受不确定数量的参数。你可以在参数名前加上 * 来指定可变参数。在方法体内,可变参数将作为数组进行处理。以下是一个使用可变参数的示例:

1
2
3
4
5
6
7
def sum(*numbers)
total = 0
numbers.each do |num|
total += num
end
total
end

在上面的示例中,sum 方法接受任意数量的参数,并将它们相加得到总和。你可以传递任意数量的参数给方法,它们将作为数组 numbers 在方法体内使用。例如:

1
2
3
4
puts sum(1, 2, 3)  # 输出 6
puts sum(4, 5) # 输出 9
puts sum(6) # 输出 6
puts sum # 输出 0

此外,Ruby 还提供了块(Block)的概念,它是一种用于传递代码的结构。你可以使用块来扩展方法的功能,使其更加灵活。在方法定义中,可以使用 yield 关键字来调用块。以下是一个使用块的示例:

1
2
3
4
5
def process
puts "Start"
yield if block_given?
puts "End"
end

在上面的示例中,process 方法在执行时,会先输出 “Start”,然后调用块(如果有提供块),最后输出 “End”。你可以在调用方法时传递一个块,块中的代码将在 yield 处执行。例如:

1
2
3
process do
puts "Processing..."
end

上述代码将输出:

1
2
3
Start
Processing...
End

循环结构

Ruby和大多数语言一样,常用的循环结构有while, do...while, untilfor循环四种。

下面,我们一一讲述这四种循环结构的使用方法。

while语句

Ruby中的循环用于执行相同的代码块若干次。当我们在开发中需要重复的做某个事情的时候,你就要想到循环了,接下来我们就来看看while循环是怎么使用的吧!

while的基本语法是:

1
2
3
while conditional [do \ :]
code
end

conditionalTrue时,执行 code

语法中 do: 可以省略不写。但若要在一行内写出 while 式,则必须以 do: 隔开条件式或程序区块。

其流程图如下:

img

下面我们给出一个实例:

1
2
3
4
5
6
i = 1
num = 5
while i <= num do
puts("我们爬了#{i}层楼" )
i = i + 1
end

以上实例输出结果为:

1
2
3
4
5
我们爬了1层楼
我们爬了2层楼
我们爬了3层楼
我们爬了4层楼
我们爬了5层楼

当条件i <= numTrue时,代码块一直被执行,直到第6次循环时,i = 6不满足条件i <= num,程序跳出循环,停止执行。

接下来我们了解do...while循环和while循环有什么不同。

do...while循环

Ruby do...while循环遍历程序。它与while循环语法非常相似,唯一的区别是do...while循环将至少执行一次。 这是因为在do...while循环中,条件写在代码的末尾。

do...while语法如下:

1
2
3
4
5
begin
code
end while conditional
# 或
code while condition

其流程图为:

do...while循环流程图

可以看出,code代码段先执行一次后,转入conditional代码判断条件是否为True。给出一段do...while循环结构的实例:

1
2
3
4
5
6
i = 0
num = 0
begin
puts("在循环语句中 i = #{i}" )
i = i + 1
end while i < num

以上实例输出结果为:

1
在循环语句中 i = 0

通过上述两个实例,你应该能够很好的理解while循环和do...while循环之间的区别了。接下来,我们了解for循环结构。

for循环

Ruby for 循环遍历特定的数字范围。 因此,如果程序具有固定次数的迭代,则使用 for 循环。 Ruby for 循环将在表达式中的每个元素执行一次。

其语法如下:

1
2
3
for variable [, variable ...] in expression [do]
code
end

for循环针对 expression 中的每个元素分别执行一次 code

下面我们提供一个使用for循环遍历数组的实例:

1
2
3
4
x = ["红", "绿", "蓝", "黄", '五颜六色']
for i in x do
puts i
end

该实例的输出结果为:

1
` `绿` `` `` `五颜六色

最后我们来了解until循环结构。

until循环

在Ruby中,until循环和while循环很相似又很不相似,他们就像一对欢喜冤家,while循环是当conditionalTrue时进入循环,而untile循环是当conditionalFalse时进入循环。

其语法为:

1
2
3
until conditional [do]
code
end

conditionalFalse时,执行 code

语法中 do 可以省略不写。但若要在一行内写出 until 式,则必须以 do 隔开条件式或程序区块。

我们也提供一个实例,以供你查看其与while循环的不同之处:

1
2
3
4
5
6
i = 1
num = 5
until i > num do
puts("我们爬了#{i}层楼" )
i = i + 1
end

该实例的输出结果为:

1
2
3
4
5
我们爬了1层楼
我们爬了2层楼
我们爬了3层楼
我们爬了4层楼
我们爬了5层楼

我们可以看出,当我们将conditional改为i > num,与i <= num刚好相反时,程序给出了同样的输出,这样也证明了while循环和until循环是一对刚好相反的循环结构。

在下一小节讲述完Ruby中的条件判断语句后,我们继续了解三种在循环中经常使用的语句,break语句,next语句和redo语句。


分支结构

if语句

if语句的语法为:

1
2
3
if (condition)
code
end

if语句测试条件condition。如果conditiontrue,则执行code

其流程图如下:

if语句流程图

我们提供一个实例来具体讲解if语句:

1
2
3
4
5
6
num = 0
condition = 1
if num == condition
puts "In Condition"
end
puts "Out Condition"

该实例的输出结果为:

1
Out Condition

从实例中我们很明显的看出if语句的条件不满足,所以直接执行after_if_code,输出一条语句。

if-else语句

非黑即白,这句话在Ruby中也适用,我们可以使用if-else语句来实现非黑即白的功能,其语法如下:

1
2
3
4
5
6
7
if condition1 [then]
code1
[elsif condition2 [then]
code2]...
[else
code3]
end

if表达式用于条件执行。值 falsenil,其他值都为。请注意,

Ruby 使用 elsif,不是使用 else ifelif

如果 condition 为真,则执行 code。如果 condition不为真,则执行 else 子句中指定的 code

通常我们省略保留字 then 。若想在一行内写出完整的 if 式,则必须then 隔开条件式和程序区块。如下所示:

1
if a == 1 then a = 0 end

说了这么多,我们来一个实例了解一下if-else语句吧!

1
2
3
4
5
6
7
8
x = 1
if x > 2
puts "x 大于 2"
elsif x <= 2 and x != 0
puts "x 是 1"
else
puts "无法得知 x 的值"
end

以上实例的输出结果为:

1
x 是 1

我们来解读一下这段代码的执行过程:

  • x = 1不满足x > 2的条件,if条件判断失败,进入else对应的code
  • 由于具有if嵌套,故继续判断elsif条件是否满足,x = 1满足x <= 2 and x != 0,进入对应的code段,输出x 是 1
  • 程序跳出if语句,程序结束

unless语句

while循环和until循环一样,在条件判断语句中,也有这样一对“死对头”,这就是if语句和unless语句。unless语句的基本语法是:

1
2
3
4
5
unless condition [then]
code1
[else
code2 ]
end

如果 condition 为假,则执行 code1。如果 condition 为真,则执行 else 子句中对应的 code2

其流程图如下:

unless语句流程图

你可以在irb中亲自尝试这一语句,但我们不推荐使用这一语句,因为使用unless语句会大大降低程序的可读性,如果不是业务需要,请尽量使用if语句替代unless语句。

case语句

最后我们还要介绍一种强大的条件判断语句,case语句。其基本语法为:

1
2
3
4
5
6
case expression
[when condition [, condition ...] [then]
code1 ]...
[else
code2 ]
end

case 语句先对一个 expression 进行匹配判断,然后根据匹配结果进行分支选择。

它使用 === 运算符比较 when 指定的 condition,若一致的话就执行 when 部分的内容。

通常我们省略保留字 then 。若想在一行内写出完整的 when 式,则必须then 隔开条件式和程序区块。如下所示:

1
when a == 1 then a = 0 end

因此一段case代码:

1
2
3
4
5
6
7
8
case expr0
when cond1, cond2
code1
when cond3, cond4
code2
else
code3
end

等价于:

1
2
3
4
5
6
7
8
_tmp = expr0
if cond1 === _tmp || cond2 === _tmp
code1
elsif cond3 === _tmp || cond4 === _tmp
code2
else
code3
end

我们通过一段判断商品价格的程序来深入了解case语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
price = 85
case price
when 0..10
puts "便宜"
when 11..50
puts "普通"
when 51..100
puts "稍贵"
when 101...1000
puts "贵"
else
puts "很贵"
end

以上实例的输出结果为:

1
稍贵

通常来说,如果我们需要使用到多个判断语句或者细分判断逻辑,我们推荐使用case语句,使用case语句有助于美化代码结构,加快代码执行。

到这里我们就已经介绍完了常见的条件判断语句了,接下来我们补充一下上一小节我们提到的三种常见的控制语句。

  • break 语句 该语句用来终止最内层的循环。通常配合if语句使用,在某一条件下跳出循环,进行外层操作。
  • next 语句 该语句用来跳到循环的下一个迭代。通常配合if语句使用,在某一条件下跳过本次迭代的剩余部分,直接执行下一迭代。
  • redo 语句 该语句用来重新开始最内部循环的该次迭代,并不予检查循环条件。相当于重新执行一遍本次迭代。

异常

在Ruby中,异常处理是一种用于处理错误和异常情况的机制。当程序发生错误或遇到无法处理的情况时,会引发异常。以下是Ruby中异常处理的基本概念和用法。

  1. 抛出异常(Raise Exception):
    在Ruby中,可以使用raise关键字抛出异常。raise语句通常包含一个异常类(可以是内置的异常类或自定义的异常类)和一个可选的错误消息。
1
raise ExceptionClass, "Error message"

例如,下面的代码抛出一个RuntimeError异常:

1
raise RuntimeError, "Something went wrong"
  1. 捕获异常(Catch Exception):
    可以使用beginrescue关键字来捕获异常。begin块中的代码被监视,如果发生异常,会跳转到rescue块,并执行相关的处理代码。

rescue 块可以捕获特定类型的异常,也可以使用Exception来捕获所有类型的异常。

例如,下面的代码捕获RuntimeError异常并进行处理:

1
2
3
4
5
begin
# 可能会引发异常的代码
rescue RuntimeError => e
puts "Error occurred: #{e.message}"
end
  1. 处理多个异常
    可以在rescue块中处理多个异常,每个异常使用不同的rescue子句。
1
2
3
4
5
6
7
begin
# 可能会引发异常的代码
rescue ExceptionClass1
# 处理异常的代码
rescue ExceptionClass2
# 处理异常的代码
end

例如,下面的代码捕获RuntimeErrorArgumentError两种异常:

1
2
3
4
5
6
7
begin
# 可能会引发异常的代码
rescue RuntimeError
# 处理 Runtime Error 的代码
rescue ArgumentError
# 处理 Argument Error 的代码
end
  1. 最终处理(Ensure):
    可以使用ensure关键字定义一个最终处理块,其中的代码无论是否发生异常都会执行。
1
2
3
4
5
6
7
begin
# 可能会引发异常的代码
rescue ExceptionClass
# 处理异常的代码
ensure
# 最终处理的代码
end

例如,下面的代码无论是否发生异常,都会执行最终处理块中的代码:

1
2
3
4
5
6
7
begin
# 可能会引发异常的代码
rescue RuntimeError => e
puts "Error occurred: #{e.message}"
ensure
puts "Final processing"
end

这些是Ruby中异常处理的基本概念和用法。通过使用raise抛出异常,beginrescueensure来捕获和处理异常,可以有效地处理错误和异常情况,提高程序的健壮性和可靠性.


迭代器

在讲述迭代器概念前,我们先讲述另一个概念,块(block)。

块(block

块就是指一块代码,在这个块中我们可以实现若干功能,当块不传入参数时,其就是执行一段代码,而当这个块传入了参数时可以将它看作一个方法调用。块是一个非常强大的模块。您可以使用块来实现回调(但它们比Java匿名内部类更简单),并实现迭代器。

块只是括号 {} 之间或doend之间的代码块。

例如:

1
2
3
4
5
{ puts "Hello" } # 块的一种方式
do
club.enroll(person) # 块的另一种方式
person.socialize
end

按照Ruby的代码规范,如果块中只包含一行代码,则我们使用大括号 {}将其包住,如果块中包含多行代码,我们则使用do\end块来实现。

一旦你创建了一个块之后,你就可以将其视为一个方法调用。你可以通过将块放在你需要调用块的代码行末尾来实现。

除此之外,通过使用yield声明,块还可以被多次调用,你可以将yield声明看作一个方法调用,调用在这个块外的一段代码。例如下面这段代码:

1
2
3
4
5
6
7
def call_block
puts "Start of method"
yield
yield
puts "End of method"
end
call_block { puts "In the block" }

执行call_block方法的输出结果为:

1
2
3
4
Start of method
In the block
In the block
End of method

每一个yield都调用了一次块外的代码块,你还可以给yield传输参数,来实现不同的功能,语法如下:

1
2
3
4
def call_block
yield("hello", 99)
end
call_block {|str, num| ... }

迭代器(Iterator

不断重复的工作我们叫做迭代,在Ruby中用来执行重复多次相同的事的就是迭代器(Iterator)!

  • 一个Ruby迭代器就是一个简单的能接收代码块的方法(比如each这个方法就是一个迭代器)。特征:如果一个方法里包含了yield调用,那这个方法肯定是迭代器
  • 迭代器方法和块之间有如下传递关系:块被当成一个特殊参数传给迭代器方法,而迭代器方法内部在使用yield调用代码块时可将参数值传入块
  • Ruby中的容器对象(如上一实训中我们介绍的ArrayRangeHash对象等)都包含了两个简单的迭代器:
    • each:最简单的迭代器,它会对容器对象的每个元素调用
    • collect:将容器对象中的元素传递给,在中处理后返回一个包含处理结果的数组(Array

each迭代器

下面我们先从数组(Array)迭代器开始,这是一段遍历数组中所有元素并将其输出的代码:

1
2
3
arr = [1, 2, 3, 4, 5]
arr.each { |num| puts num }
# num 是一个局部变量,each迭代器每一次迭代将数组中的一个元素赋值给 num

行后,输出为:

1
2
3
4
5
1
2
3
4
5

以上这段代码我们使用到了最简单的迭代器each,我们下面介绍一个与each很相似的迭代器each_with_index

1
2
3
4
languages = ['Ruby', 'Javascript', 'Java']
languages.each_with_index do |lang, index|
puts "#{index}, I love #{lang}!"
end

执行后,输出为:

1
2
3
0, I love Ruby!
1, I love Javascript!
2, I love Java!

我们可以发现新的迭代器each_with_index使用了两个参数langindex,其中,lang 就是一个局部变量,其值为数组中的一个元素,index就是元素在数组中的下标。

collect迭代器

介绍完each迭代器,我们来介绍一下collect迭代器的用法和作用。

1
2
3
a = [1,2,3,4,5]
b = a.collect{ |x| x * 2 }
puts b.join(',')

输出为:2,4,6,8,10

上述代码中,collect迭代器进行了如下操作:

  • 将数组a中的每一个元素都传递给了{ |x| x * 2 }
  • 中进行了翻倍(x * 2)操作
  • 在所有元素都处理完后,返回新数组[2, 4, 6, 8, 10]

Ruby中的迭代器千变万化,不同的数据类型有着不同的迭代器,不同数据类型的eachcollect迭代器也有着不同的特性,了解他们的最好办法就是阅读Ruby Doc官方文档,并且在实践中去试验他们有着怎样的功能。

3.3 Ruby数据类型

字符串

创建字符串

创建字符串的一种方法是在Ruby程序中使用'"来创建所谓的字符串文字。我们已经在我们的Hello World程序中体验了这种方法。以下代码显示了'"的用法。

1
2
puts 'Hello world'
puts "Hello world"

类似于Perl,Ruby中可以用'"来创建字符串,但与C和Java语言不一样,他们使用"来创建字符串,'来创建单字符。

那么Ruby中使用'"之间有什么区别呢?在上面的代码中是没有区别的。但是,请看下面的代码:

1
2
puts "Betty's pie shop"
puts 'Betty\'s pie shop'

因为Betty's中包含撇号,其符号和单引号一样,第二行需要使用反斜杠来转义撇号,使Ruby明白撇号是字符串而不是字符串结束标志。反斜杠后面的单引号被称为转义字符

接下来,我们将讲述更多'"创建字符串时的区别。

单引号'只支持以下两种转义字符:

  • \' – 一个单引号
  • \\ – 一个反斜杠

除了这两种转义字符之外,单引号之间的所有内容都是按其字面意思进行处理的。

而双引号"允许更多的转义字符。甚至"还允许你在字符串文字中嵌入变量或Ruby代码 —— 这通常称为内插:

1
2
name = 'Educoder'
puts "Hello, #{name}!"

转义字符

上面引入了转义字符这个概念,接下来,我们列出一些可以使用在"创建的字符串中的转义字符。

  • \" – 一个双引号
  • \\ – 一个反斜杠
  • \a – 蜂鸣声
  • \b – 退格
  • \r – 回车符
  • \n – 换行符
  • \s – 空格
  • \t – tab

字符串的常用方法

按照上面的方法创建的字符串都是String的一个对象,有了这个对象,我们就可以使用字符串庞大的方法集来做我们想要的操作了!

下表列出了字符串的一些常用方法,包括方法的调用方式,方法的返回值类型以及方法的描述。(假设已创建名为str的字符串)

方法调用方式 返回值 描述
str.length Integer 字符串的长度,空字符串返回 0
str.include?(other_str) TrueFalse 字符串包含,传入的参数为另一个字符串,如果该字符串中包含该子串,则返回True,否则返回False
str.insert(index, other_str) new_str 字符串插入,参数index是待插入的下标,other_str是另一个字符串,返回的是插入后新的字符串,下标从 0 开始计算
str.split(pattern=$;, [limit]) Array 字符串分割,将字符串按照pattern进行分割,默认分割符为空格,返回值是一个包含若干字符串的数组
str.gsub(pattern, replacement) new_str 字符串替换,将字符串按照pattern匹配的字符更换为replacement,返回替换后的字符串
str.replace(other_str) other_str 字符串整体替换,将字符串整体替换成新的字符串
str.delete([other_str]+) new_str 字符串删除,传入参数[other_str]+可包含多个字符,该方法匹配到str中的所有字符并删除,返回新的字符串
str.strip new_str 清除空格,清除掉str中字符串前后的所有空格,换行符,回车符。不包含字符间的空格,返回新的字符串
str.reverse reverse_str 字符串翻转,将字符串顺序翻转,返回翻转后的字符串
str.to_i Integer 字符串转换为数字, 如果字符串以数字开头,则转换为开头数字的整型值,如果字符串不以数字开头,则返回 0
str.chomp new_str 去掉字符串末尾的\n\r
str.chop new_str 去掉字符串末尾的最后一个字符,不管是\n\r还是普通字符
str.downcase new_str 将字符串转换为全小写
str.upcase new_str 将字符串转换为全大写

接下来我们通过一小段程序来大致的了解一下这些方法的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/ruby
str1 = "Edu Coder"
puts str1.downcase
puts str1.upcase
puts str1.length
puts str1.include?("Coder")
puts str1.split
str2 = " Hello, Educoder!\n"
puts str2
puts str2.strip
new_str= str2.chomp
puts new_str
puts new_str.chop
str3 = "2018, New Year!"
puts str3.to_i
puts str3.reverse
puts str3.insert(5, "Happy")
puts str3.gsub(/[A-Za-z, ]/, "!")
puts str2.replace("Goodbye, 2017")
puts str3.delete("aeiou")

输出为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
edu coder                #转换为全小写
EDU CODER #转换为全大写
9 #字符长度为9
true #包含子串"Coder"
Edu
Coder #分割成两个字符,按序输出
Hello, Educoder! #原字符串
Hello, Educoder! #去除掉前后所有空格,换行符,回车符
Hello, Educoder! #去除掉行尾换行符
Hello, Educoder #去除掉行尾一个字符
2018 #转换为字符串开头的数字
!raeY weN ,8102 #字符串翻转
2018,Happy New Year! #在第5个字符处插入 Happy
2018!!!!!!!!!!!!!!!! #将所有英文字符,空格,英文半角感叹号替换为英文半角感叹号
Goodbye, 2017 #字符串整体替换
2018,Hppy Nw Yr! #删除掉字符串中所有元音字母

Tips: chomp方法针对\r\n\r\n\n\r这四种字符有着不同的处理,在实训右侧的命令行窗口打开irb体验一下吧!

数组

Ruby数组(Array)可存储诸如StringIntegerFixnumHashSymbol 等对象,甚至可以是其他Array对象,从而构建二维甚至多维数组。Ruby数组不像其他语言中的数组那么刚性,当向数组添加元素时,Ruby数组会自动增长。

创建数组

有多种方式创建或初始化数组。一种方式是通过 new 方法:

1
arr = Array.new

由于Ruby中数组的长度会随着元素的增加而增加,所以你不需要传递一个长度参数给new方法,Ruby也能创建一个空数组,当然你也可以加上长度参数,例如:

1
arr = Array.new(5)

上面这条语句将会创建出一个长度为5,内容值全为nil的数组,[nil, nil, nil, nil, nil],记住nil也是一个值哦!

如果我们在创建数组时就想初始化数组的值,我们该怎么做呢?别担心,万能的Ruby也有办法帮你解决!

1
arr = Array.new(5, 'Educoder')

这样我们就能将数组内的值全部变为你传入的第二个参数Educoder了,就像这样["Educoder", "Educoder", "Educoder", "Educoder", "Educoder"]

相似的,还有两种方式,也是通过Array类来创建数组:

1
Array.[]( 1, 2, 3 ) #=> [1, 2, 3]Array[ 1, 2, 3 ]    #=> [1, 2, 3]

当然,我们还有更简便的方法来创建一个数组,那就是直接写出一个数组!

1
[1, 2, 3] #=> [1, 2, 3]

一般的,我们都使用最后一种方式来创建数组。

通过Range对象,我们能够初始化一定范围的数组,例如:

1
2
3
4
5
6
# 使用 Range 创建起始数字到结束数字的范围
range = (1..4)

# 将范围转换为数组
# arr = [1,2,3,4]
arr = range.to_a

访问数组元素

Ruby数组中的每个元素都与一个索引(index)相关,并可通过索引(index)进行获取。

数组的索引从 0 开始,这与C或Java中一样。一个负数的索引时相对于数组的末尾计数的,也就是说,索引为 -1 表示数组的最后一个元素,-2 表示数组中的倒数第二个元素,依此类推。

下面我们展示以下如何访问数组的元素:

1
2
3
4
5
6
7
8
arr = [1, 2, 3, 4, 5, 6]
arr[2] #=> 3
arr[100] #=> nil
arr[-3] #=> 4
arr[2, 3] #=> [3, 4]
arr[1..4] #=> [2, 3, 4, 5]
arr[1..-3] #=> [2, 3, 4]
arr.at(0) #=> 1

我们展示了4种访问数组元素的方式:

  • 直接输入索引法,arr[index],如果该索引(index)存在,则返回数组在该索引(index)处的值,若不存在,则返回nil

  • 索引区间法,arr[start, end],返回数组从索引startend之间的所有元素,如果这个索引区间内不包含任何元素,则返回nil

  • 范围(Range)法,这个方法与第二种方法相似,只是将索引区间变为了Ruby中的范围类型Range.

    范围(Range) 范围(Range)是由有顺序、有规律的元素对象组成,任何有顺序,有规律的一组对象,都可以用Range对象来定义,如数字、字母、字符串、甚至时间.

    上面的 1..3返回的就是由1, 2, 3构成的一个范围。

  • at法,arr.at(index),使用了Array的一个类方法,返回数组在传入索引(index)处的值。

说完常规的访问数组元素的方法,我们就要来说一些突出Ruby易读性的方法了,这些方法可谓见名知意。例如下面:

1
2
3
4
arr.first    #=> 1
arr.last #=> 6
arr.take(3) #=> [1, 2, 3]
arr.drop(3) #=> [4, 5, 6]

我们可以看出,first方法是访问数组第一个元素的方法,last方法是访问数组最后一个元素的方法,take(n)方法返回数组前n个元素,drop(n)方法返回数组在n索引后的所有元素。


数组的常用方法

下表列出了一些数组的常用方法,更加详细和全面的方法指南可以在http://ruby-doc.org/core-2.3.1/Array.html 中找到。(假设已创建名为arr的数组)

方法调用方式 返回值 描述
arr.empty? truefalse 判段数组是否为空
arr.push(element) new_array 在数组的最后加入元素element,返回加入元素后的新数组
arr << 6 new_array 同上
arr.insert (index, elements) new_array 在指定位置index塞进元素elements,可以塞多个元素 arr.insert (3, 'a', 'b', 'c')
arr.delete(element) element 删除数组中所有为element的元素
arr.compact new_array 删除数组中所有空元素 nil,返回删除空元素后的新数组
arr.uniq new_array 清除数组中的重复元素,返回去除重复后的新数组
arr.reverse new_array 翻转arr,返回翻转后的新数组
arr.clear [ ] 删除数组中的所有元素,返回空数组
arr.count Integer 没有参数时,返回数组的大小,带有参数时,返回数组中与参数相同元素的个数
arr.includes?(element) truefalse 判断数组arr中是否包含元素element
arr.sort new_array 将数组按照首字母进行排序,也可定制排序规则
arr.sample element 从数组中随机取样,带参数取样个数,则可取多个样本
arr.flatten new_array 将多维数组转换成一维数组
arr.join(',') String 将数组使用连接符,连接成一个字符串

接下来,我们通过一段小程序来看看这些方法是如何工作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
arr = ['2018', nil, 'Happy', 'New', 'Year']
puts arr.empty? #=> false
puts arr.include?('New') #=> true
arr.push('!') #=> 在数组末尾添加 !
arr = arr.compact #=> 去除数组中的 nil 值
puts arr.join(' ') #=> 2018 Happy New Year !
arr.insert(1, '!') #=> 在索引 1 处增加 !
puts arr.join(' ') #=> 2018 ! Happy New Year !
puts arr.count('!') #=> 2
arr.delete('!') #=> 删除掉所有 !
puts arr.count('!') #=> 0
arr.clear #=> 清空数组
puts arr.empty? #=> true
puts "----------" #输出间隔
arr2 = [1, 2, 3, 3, 4, 5, 6]
arr2 = arr2.uniq #=> 数组去重
puts arr2.join(',') #=> 1,2,3,4,5,6
arr2 = arr2.reverse #=> 数组元素翻转
puts arr2.join(',') #=> 6,5,4,3,2,1
arr2 = arr2.sort #=> 数组按照首字母排序
puts arr2.join(',') #=> 1,2,3,4,5,6
puts arr2.sample #=> 5
puts "----------" #输出间隔
arr3 = [[1, 2], [3, 4]]
puts arr3.count #=> 2
arr3 = arr3.flatten #=> 将二维数组转换为一维数组
puts arr3.count #=> 4

在上面的程序中,每行的注释标明了操作的作用或输出的内容。最后的flatten方法,我们通过处理前后的数组结构来更直观的了解他的作用。

  • 处理前: [[1, 2], [3, 4]]
  • 处理后: [1, 2, 3, 4]

可以看出,flatten将一个二维数组打开,按序变成一个一维数组。

哈希

哈希(Hash)是类似 "book" => "comment" 这样的键值对的集合。哈希的索引是通过任何对象类型的任意键来完成的,而不是一个整数索引,其他与数组相似。记住,哈希不会维持键值对的顺序,那不是他们的工作。它只会在两个对象之间配对:一个键和一个值。

如果你尝试通过一个不存在的键访问哈希,则方法会返回nil

创建哈希

与数组一样,Ruby有不同的方式来创建哈希(Hash)。您可以通过new方法创建一个空的哈希(Hash):

1
hash = Hash.new

上面的new方法还可以带上参数default_value

1
2
3
hash = Hash.new(0)
hash[0] #=> 0
hash[100] #=> 0

这样我们就能创建一个带有默认值的哈希,这时通过一个不存在的键访问哈希,就会返回0了!

最常用的创建哈希(Hash)的方式如下:

1
2
3
4
language = {}
language['Ruby'] = 'Wonderful'
language['Python'] = 'Excellent'
language #=> {"Ruby"=>"Wonderful", "Python"=>"Excellent"}

通过将一个空哈希直接赋值给变量,然后我们将哈希的键Ruby放在[]内,哈希的值'Wonderful'放在=后,我们就可以构建一个我们想要的哈希了!

同理我们也可以直接将上面的哈希赋值给一个变量:

1
2
3
language = { "Ruby" => "Wonderful",
"Python" => "Excellent" }
language = { Ruby: "Wonderful", Python: "Excellent" }

上面两种方式都可以完成这一功能,但在Ruby-2.3.1中,我们推荐使用下面的方式


访问哈希

上面我们已经构建了一个有意义的哈希language,如果我们想要查看我们对于编程语言的评价,我们该怎么做呢?就把我们的键再放在[]里,但是,这次不要再用=了,因为你没有给language分配任何新的信息,你只是在查找它。

1
language['Ruby']      #=> 'Wonderful'

当数据量大起来的时候,我们可能也记不住我们使用过什么键名了,这个时候,我们可以使用keys方法来查看当前哈希的所有键名。例如:

1
2
3
4
language = { Ruby: "Wonderful",
Python: "Excellent",
PHP: "Best" }
language.keys #=> [:Ruby, :Python, :PHP]

keys方法会将哈希的所有键名用数组的方式返回,因为哈希是键值对的集合,那我们有了keys肯定也就有valuesvalues方法返回当前哈希的所有值,在这里我们就不做演示了,你可以在右侧的命令行中自行尝试


常用的哈希方法

下表列出了一些哈希的常用方法,更加详细和全面的方法指南可以在http://ruby-doc.org/core-2.3.1/Hash.html 中找到。(假设已创建名为hsh的哈希)

方法调用方式 返回值 描述
hsh == other_hash truefalse 判断两个哈希是否相等。键相等,值相等,键值对应关系均相等才返回true
hsh.clear {} 清空当前哈希
hsh.delete(key) value 从哈希中删除匹配key的键值对,并返回对应的值value
hsh.has_key?(key) truefalse 判断哈希中是否包含键key
hsh.has_value?(value) truefalse 判断哈希中是否包含值value
hsh.to_s String 将哈希的内容转换为字符串输出
hsh.invert new_hsh 将哈希的键值对颠倒,键变值,值变键,构成新的哈希返回
hsh.key(value) key 返回给定值对应的键。如果找不到该值,则返回0
hsh.length Integer 返回哈希的大小
hsh.merge(other_hash) new_hsh hshother_hash合并成一个新的哈希。重复键的项值采用other_hash
hsh.store(key, value) value 在哈希中加入新的键值对
hsh.to_a Array 将哈希转换为数组,格式为: [[ key1, value1 ], [ key2, value2 ]]
hsh.values Array 将哈希中的全部转换为数组,格式为:[value1,value2...,valuen]

我们通过一小段程序来着重了解上面的几个方法,所有方法你都可以在右侧的命令行中打开irb试一试。

1
2
3
4
hash = { Ruby: "Wonderful",
Python: "Excellent",
PHP: "Best" }
hash.has_key?("Ruby") #=> false

我们发现,我们创建的哈希中竟然没有我们输入的键Ruby,接下来我们做更多的测试。(为了节省篇幅,本小节以下程序均使用已创建的hash变量)

1
2
3
4
5
6
7
8
9
10
hash_with_string = { "Ruby" => "Wonderful",
"Python" => "Excellent",
"PHP" => "Best" }
hash_with_string.has_key?("Ruby") #=> true
hash == hash_with_string #=> false
hash.key("Wonderful") #=> :Ruby
hash_with_string.key("Wonderful") #=> "Ruby"
# 判断两个对象是否相等,可以通过 obj1 == obj2来判断
# 若相等,则返回 true, 否则为 false
:Ruby == "Ruby" #=> false

通过上面我们知道了,两种创建Hash的方式是有区别的,那我们为什么推荐使用hash的生成方式呢?细心的读者可能已经发现,代码中:Ruby"Ruby"是不一样的对象,我们知道后者是字符串,那前者是什么呢?答案是:符号Symbol

Symbol 符号Symbol的形式是:symbol_name,它也是对象,一般作为名称标签来使用,用来表示方法等对象的名称。

符号能实现的功能大部分字符串String也能实现,但是想哈希键这样只是判断是否相等的处理,符号Symbol会比字符串String更高效。 另外符号Symbol可以与字符串String相互转化。

而高效的原因就在于,对一个Symbol的多次引用用的是同一个ObjectSymbol会节省内存,也是使用的同一个Object来进行比对,所以他的速度会更快。

3.4 Ruby 面向对象

定义对象

在Ruby中,可以通过定义类来创建对象。类是对象的蓝图,它定义了对象的属性和行为。以下是在Ruby中定义对象的步骤:

  1. 使用关键字class定义一个类,后面跟着类的名称。类名的首字母通常大写。例如:
1
2
3
class Person
# 对象定义
end
  • 在类中定义实例变量作为对象的属性。实例变量以@符号开头,可以在类的任何方法中使用。例如:
1
2
3
4
5
6
class Person
def initialize(name, age)
@name = name
@age = age
end
end
  • 在类中定义方法作为对象的行为。方法是类中的函数,可以执行特定的操作。例如:
1
2
3
4
5
6
7
8
9
10
class Person
def initialize(name, age)
@name = name
@age = age
end

def introduce
puts "My name is #{@name} and I am #{@age} years old."
end
end

上述例子中,initialize方法可以看作构造函数,构造函数使用@ 关键字修饰的变量可以看作类的属性;同时也可以显式定义类的属性,使用关键字@@修饰,例如:

1
2
3
4
5
class MyClass
@@class_var = 20
end

puts MyClass.class_variables

运行上述代码会输出@@class_var的值,它是MyClass类的一个类变量。

  1. 使用类的new方法创建对象。new方法是类的构造函数,用于实例化对象并调用initialize方法进行初始化。例如:
1
person = Person.new("John", 25)
  1. 可以通过对象调用类中定义的方法。例如:
1
person.introduce

这将输出:My name is John and I am 25 years old.


封装对象

在Ruby中,类的封装是通过访问控制符(Access Control)来实现的。Ruby提供了三种访问控制符,分别是publicprivateprotected,用于控制类中方法和属性的可见性。

  • public:公共方法可以从类的内部和外部访问。默认情况下,所有的方法都是公共的。
1
2
3
4
5
6
7
8
class MyClass
def public_method
puts "This is a public method"
end
end

obj = MyClass.new
obj.public_method # 调用公共方法
  • private:私有方法只能从类的内部调用,无法从外部直接访问。私有方法不能被类的实例直接调用,只能通过类的内部方法来间接调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass
def public_method
puts "This is a public method"
private_method # 可以在类的内部调用私有方法
end

private

def private_method
puts "This is a private method"
end
end

obj = MyClass.new
obj.public_method # 调用公共方法,会间接调用私有方法
# obj.private_method # 错误!无法直接调用私有方法
  • protected:受保护方法可以从类的内部以及该类的实例的上下文中访问。受保护方法可以被类的实例直接调用,也可以在同一个类的其他实例方法中调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass
def public_method
puts "This is a public method"
protected_method # 可以在类的内部调用受保护方法
end

protected

def protected_method
puts "This is a protected method"
end
end

obj = MyClass.new
obj.public_method # 调用公共方法,会直接调用受保护方法
# obj.protected_method # 错误!不能直接调用受保护方法

通过使用这些访问控制符,可以控制类中方法和属性的可见性,实现封装的概念。这样可以隐藏内部实现细节,防止外部直接访问和修改类的内部状态,提供了更好的封装性和安全性。

对象继承

父类也称基类,其声明方法与一般的类的声明方法一样。父类中存在着一些公共的属性和方法,子类继承于父类。

子类继承于父类,拥有父类中的属性和方法,它自己也可根据实际情况声明一些属于自己的属性和方法。

子类声明方法:

1
class ChildClass < FatherClass

符号<后跟的类名就是父类名称。在Ruby中,继承语法只支持单继承,如果要实现多重继承,必须要使用Mix-In扩展功能,这一功能,我们将在下一个实训,模块中为大家讲解。

下面,我们给出一段代码来实际观察类的继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Father
def self.says
puts "I am father"
end
def self.father_info
puts "father info"
end
end
class Son < Father
def self.says
puts "I am son"
end
def self.son_info
puts "son info"
end
end
Father.says #=> I am father
Father.father_info #=> father info
Son.says #=> I am son
Son.son_info #=> son info
Son.father_info #=> father info

在这里我们发现,子类和父类中定义了一些同名方法,而只要子类与父类定义的方法名称相同,子类的方法就会覆盖掉父类的方法,调用时也就会调用子类的方法,而忽略掉父类的方法。

除此之外,子类也可以直接调用父类的构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Father
def initialize(name)
@name = name
end
def says
puts "I am father"
end
end
class Son < Father
def says
puts "I am son. name: #{@name}"
end
end
son = Son.new("Jack")
son.says #=> I am son. name: Jack

上述代码中,initialize方法就相当于类的构造函数,也叫做初始化函数,该方法在类实例化自动调用。子类Son未定义initialize方法,故其直接调用父类的initialize方法。

同时,我们发现了一个新的语法,son = Son.new("Jack"),这一句代码实现了类实例化,创建了一个类的实例,对象。我们将在下一小节中详细的论述对象这一概念

那如果,子类自己也定义了initialize方法,会怎么样呢?我们来试试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Father
def initialize(name)
@name = name
end
def says
puts "I am father"
end
end
class Son < Father
def initialize(name)
@name = "son_#{name}"
end
def says
puts "I am son. name: #{@name}"
end
end
son = Son.new("Jack")
son.says #=> I am son. name: son_Jack

结果就是,子类的initialize方法覆盖了父类的initialize方法,覆盖不仅仅适用与初始化方法,也适用于类方法和实例方法,只要子类与父类定义的方法名称相同,子类的方法就会覆盖掉父类的方法。

3.5 Ruby文件操作

输入与输出

进阶gets方法

先简单的回顾一下我们常用的 gets 语句,gets 语句可用于获取来自名为 STDIN标准屏幕的用户输入。例如:

1
val = gets

gets 方法从键盘输入的字符串,会带有最后输入的 \n 字符,这和 C/C++ 是一样的。如果想去掉 \n,在 Ruby 中可以在gets 的结果上级联调用 chomp 方法,即 gets.chomp

chomp 是一个字符串方法,它只从您的键盘中检索字符串。从字符串的末尾删除回车字符(即它将删除\n\r\r\n)。

gets 方法除了无参数调用方式外,还有传参调用,其调用语法如下:

1
gets(sep, limit)

说明:

  • sep 是分隔符,键盘输入字符中若包含该分隔符,则将输入字符以分隔符为界分为多段,依次输入
  • limit 是输入上限字符数,若键盘输入字符数超过该上限数,则分为多段,每段包含上限数个字符。

除了 gets 方法可以获取键盘输入之外,Ruby 还提供 ARGV 从命令行接收参数,例如文件 test.rb 中有如下代码:

1
2
3
4
5
# test.rb
def hello(name)
puts "Hello, #{name}"
end
hello(ARGV[0])

当在命令行输入 ruby test.rb Educoder 时,系统将会在屏幕打印 Hello, EducoderARGV[0] 从命令行中获取命令的参数,并传送给 Ruby 文件,并可以按照命令参数的下标 0 来获取到第一个参数。

输出

除了我们之前常用的 putsp 方法,我们还有很多标准化输出方法,接下来我们一一介绍这些方法。在开始之前,我们先介绍 Ruby 标准输出流。

STDOUT 是 IO 对象的实例,它是程序的实际标准输出流。除非额外设置,STDOUT 总是指向屏幕显示的。可以直接使用 STDOUT 输出,例如:

1
$stdout << "Hello " << "world!\n"

其输出为: Hello world!

printf方法

printf 方法按照格式序列将字符串输出,常见的格式序列如下:

格式域 作用
b 作为二进制输出
c 作为字符表示输出(ASCII码表)
d 作为整数输出
f 作为浮点数输出
o 作为八进制数输出
s 作为字符串输出
u 作为无符号小数输出
x 作为十六进制数输出,a-f为小写字母
X 作为十六进制数输出,A-F为大写字母
\n 换行输出

下面我们来看看它们的作用,代码中 #=> 符号后的字符为该行代码输出值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 添加符号在数字前
printf("%d", 123) #=> "123"
printf("%+d", 123) #=> "+123"
# `#' 号在输出八进制数时使其前面带上八进制数标志 `0`
# `+' 号改变负数的输出格式
printf("%o", 123) #=> "173"
printf("%#o", 123) #=> "0173"
printf("%+o", -123) #=> "-173"
printf("%o", -123) #=> "..7605"
# `#' 号在输出 非0 十六进制数时使其前面带上十六进制数标志 `0x`
# `+' 号改变负数的输出格式
printf("%x", 123) #=> "7b"
printf("%#x", 123) #=> "0x7b"
printf("%#x", 0) #=> "0"
# `#' 号使输出 非0 二进制数带上前缀 `0b'
# `+' 号改变负数的输出格式
printf("%b", 123) #=> "1111011"
printf("%#b", 123) #=> "0b1111011"
# `#' 号使输出浮点数带上小数点
printf("%.0f", 1234) #=> "1234"
printf("%#.0f", 1234) #=> "1234."

putc方法

puts 方法不同,puts 方法输出整个字符串到屏幕上,而 putc 方法输出一个字符到屏幕上。

该方法的语法为: putc(obj)

  • obj 是个数字时,将其转换为字符输出(ASCII码表)
  • obj 是个字符串时,选择其的第一个字符进行输出。
  • 该方法用于单个字符的输出,不用于字符串输出,因为他会截断字符串,造成输出的不准确。

通过一个实例来加强对其的理解:

1
putc 'Hello'

其输出为: H

文件操作

文件对象化

Ruby 中一切皆是对象,要对文件做操作,我们首先也需要创建出一个文件对象:

1
File.new(filename, mode="r" [, opt])

说明:

  • filename 为需要操作的文件名(可带路径)
  • mode 为打开方式,将在下表中罗列出所有支持的打开方式
  • opt 为其它可选参数,用来做文件操作的控制。
mode_type 作用描述
"r" 只读,起始位置为文件开头(默认方式)
"r+" 可读可写,起始位置为文件开头
"w" 只写,复写已有文件/创建新的文件
"w+" 可读可写,复写已有文件/创建新的文件
"a" 只写,在文件的末尾写/创建新的文件
"a+" 可读可写,在文件的末尾读写/创建新的文件
"b" 仅用于二进制文件
"t" 仅用于文本文件

opt 可选参数包括:

  • :mode,与上述打开方式相同
  • :encoding,通过 "extern:intern" 指定文件的内部编码和外部编码模式
  • :autoclose,当值为 false 时,则在此 IO 实例完成后,文件将保持打开状态

文件的打开

当我们需要持续的使用文件对象时,我们采用上面的方式来创建文件对象,之后在对它读取/进行其它操作,假如我们只需要读取这个文件,我们可以通过 File.open 方法结合前面块的知识,将打开的文件对象作为参数传递给代码块,当这个块终止时,文件也将自动关闭。语法如下:

1
File.open("filename", "mode") do |file|  # ... process the fileend

说明:

  • filenamemode 均和上面创建文件对象时的参数一致
  • file 局部变量则是打开的文件对象传递给代码块的形式参数

Notice: 当没有相关的块时,File.openFile.new 功能一致

文件的读取和写入

用于简单 I/O 的方法也可用于所有的文件对象。所以,gets 方法从标准输入读取一行,file.gets 从文件对象 file 中读取一行。

除此之外,File类也有自己的方法用来读取/写入文件数据。

文件的读取

read方法可以从文件对象中读出指定个数的字符,语法如下:

1
file.read([length])

说明:

  • length 为正整数时,其从文件对象 file 中以二进制模式读出 length 个字节
  • length 值为 nil省略该参数时,其读取文件对象直至遇到 EOF
  • length = 0 时,其返回空字符串("")。

EOF

全称是 End-of-File, 其存在于文件末尾处,读取到 EOF符意味着该文件已读取完毕。

我们通过一个实例来了解 read 方法是如何工作的:

假设testfile 文件中存放的数据如下:

1
This is line oneThis is line twothis is line three

以下是执行代码:

1
f = File.new("testfile")puts f.read(16)# 读取整个文件File.open("testfile") do |f|  data = f.read  p dataend

输出结果为:

1
This is line one` `This is line one\nThis is line two\nThis is line three\n

从结果上我们可以很直观的观察到使用 File.newFile.open 方法打开的文件对象在读取时的反应是一致的,无差别的。

除了 read 方法外,还有类似的 readlinereadlines 方法:

  • readline 方法与 gets 方法一致,读取出文件的一行,并置文件指针于下一行开头处,直到取到读取完毕时。
  • readlines 方法则是将整个文件读取出来,并按照行号依次存放在一个数组中返回。

在上段代码和文件的基础上:

1
# testfile 文件保持与上述一致f = File.new("testfile")puts f.readlinelines = f.readlinesp linesputs lines[0]

输出结果为:

1
This is line one` `["This is line two\n", "This is line three\n", "This is new line"]` `This is line two

文件的写入

read 方法相对应,File类也有 write 方法来进行文件写入。语法如下:

1
file.write(string)

该方法按照文件对象 file 打开方式的不同,按规则写入新字符串 string,并保存。我们也通过一个实例来加强对方法的理解:

1
# testfile 文件保持与上述一致f = File.new("testfile", "a+")f.write("This is new line!")

此时文件 testfile 的内容为:

1
This is line oneThis is line twoThis is line threeThis is new line!

我们可以很直观的看出新增了最后一行,与我们写入的行一致!

文件的关闭

在进行外文件的读取/写入后,我们不能一直保持文件的打开状态,因为文件打开后会存在放内存中,而内存的资源是紧张而宝贵的,所以在我们使用完文件对象后,我们应该及时关闭文件,以高效的运用内存资源。Ruby 中文件关闭的语法如下:

1
file.close

close 方法将会:

  • 将所有挂起的写入操作传送到操作系统
  • 关闭掉文件对象 file 相关的文件
  • 不允许任何数据操作,关闭 I/O 流
  • 将文件对象 file 状态置为已关闭。

目录

类似于文件对象化,目录也可以对象化,目录对象化只需要一个参数,目录名称 dir_name,语法如下:Dir.new(dir_name),这将创建出一个目录对象。

浏览目录

浏览目录中基本操作包括:

  • 使用 Dir.chdir 方法切换目录。下面的实例将当前目录切换为 /usr/bin:

    Dir.chdir("/usr/bin")

  • 查看当前目录,我们可以通过 Dir.pwd 查看当前目录:puts Dir.pwd,输出为当前目录,例如: /usr/bin

  • 使用 Dir.entries 获取指定目录内的文件和目录列表:

    假设当前目录下包含的文件和目录有 file.rbtestfile,那么这个程序:

    puts Dir.entries("./").join(" ")

    的返回值将会是: ".. file.rb . testfile"

    返回值说明:

    • . 表示当前目录
    • .. 表示上一级目录

    我们注意到 Dir.entries 返回一个数组,包含指定目录内的所有项。我们使用数组方法 join 来将其连接起来,连接符为" "

  • Dir.foreach 提供了相同的获取指定目录内的文件和目录列表功能:

    1
    Dir.foreach("/usr/bin") do |entry|puts entryend

Dir.foreach 方法将目录 /usr/bin 内的所有项依次传递给局部变量 entry,块内语句 puts entry 将其输出。

  • 获取目录列表的一个更简洁的方式是通过使用 Dir 的类数组的方法: Dir["/usr/bin/*"]

    该方法将扫描所有位于目录 /usr/bin 下的文件和目录。

创建目录

有些程序需要将文件存放在一个新的目录中,这时我们可以通过 Dir.mkdir 方法创建目录,例如:Dir.mkdir("new_dir") 将会在系统当前目录下创建名为 new_dir 的目录。

如果我们需要给目录增添权限,我们也可以通过传递第二个参数,例如 Dir.mkdir( "mynewdir", 755 ) 来给新创建的目录增添权限掩码为 755 的权限。

Tips:掩码 755 设置所有者(owner)、所属组(group)、每个人(world [anyone])的权限为 rwxr-xr-x,其中

  • 读取: r = read = 4
  • 写入: w = write = 2
  • 执行: x = execute = 1

临时文件目录

除了正式目录外,系统中还有临时目录,专门用于存储临时文件。临时文件是那些在程序执行过程中被创建,但不会永久性存储的信息。

Dir.tmpdir 提供了当前系统上临时目录的路径,但是该方法默认情况下是不可用的。要调用 Dir.tmpdir 方法,必须要引入 'tmpdir' 文件。

您可以把 Dir.tmpdirFile.join 一起使用,来创建一个临时文件:

1
2
3
4
5
6
require 'tmpdir'
tempfilename = File.join(Dir.tmpdir, "test_temp_file")
#=> tempfilename 的值为 "/tmp/test_temp_file"
tempfile = File.new(tempfilename, "w")
tempfile.puts("This is a temporary file")
tempfile.close

上述代码中的 File.join 方法是 File 类提供用来拼接文件名,该方法将参数传入的字符串通过连接符 / 来连接成一个大的字符串。

删除目录

Dir.delete 方法可用于删除目录。除此之外,Dir.unlinkDir.rmdir 也有同样的功能。这三个方法删除掉指定目录,当目录非空时,该方法将会报错。以下是这三个方法创建新目录 testdir 的示例:

1
Dir.delete("testdir")Dir.unlink("testdir")Dir.rmdir("testdir")

四、Sass架构

MVC框架

MVC介绍

Model-View-Controller(MVC)框架是常见的一种应用服务器框架,服务器中通过控制器(Controller)将数据(Model)与UI表示(View)分离:

image-20231020102203226

一个实体(Entity)由:一个模型 + 一个控制器 + 一组视图组成:

image-20231020102310031

“所有MVC应用都包括一个‘客户端’ 部分(如Web浏览器) 和一个“云”部分(如云上的Rails应用程序)” 这种说法是错的,因为MVC应用不包括客户端部分。

MVC中的VIEW,并不是代表HTML页面,而是一个视图程序,HTML页面是由这个视图程序加工而来的。

MVC替代

image-20231020102753733

模型,数据库,活动记录

内存中的对象vs.存储中的对象

image-20231020104958594

将内存汇总的对象序列化后存储到固态介质中,生成存储中的对象;存储中的对象通过反序列化生成内存中的对象。对象的基本操作: CRUD (Create, Read,Update, Delete)。

活动记录(ActiveRecord):每个模型都知道如何使用通用机制进行CRUD操作

控制器,路由

REST

REST思路:URI命名资源 , 而不是页面 或者动作,使用 HTTP请求 + URI 刻画某个资源,并对该资源实施动作,其响应(repsonse)包括了发现其他RESTful资源的超链接

(在SOA意义上)带有类似特征的一组操作组成的服务称为RESTful服务


路由(Route)

  • 在MVC中,用户可以进行的每个交互都是由控制器中的动作来处理的 – Ruby语言中的方法(method)处理这种交互

  • 一个路由将 <HTTP method, URI> 映射到控制器的动作

image-20231020110215076

Rails路由子系统如下:

image-20231020110256244
  • 通过给定的控制器动作生成<method,URI>,分发<method,URI>到正确的控制器动作,而URI和提交查询的参数则被解析为一个Hash作为动作的输入:

image-20231020110803301

下面通过GET /movies/3/edit HTTP/1.0来进行举例:

  • 匹配路由:GET /movies/:id/edit {:action=>"edit", :controller=>"movies"}
  • 解析通配符的参数:params[:id]=“3”
  • 消息分发:对movies_controller.rb中的edit方法进行调用

下面是RESTful资源的CRUD方式(以movies举例):

image-20231020111523383

模板视图和Haml

视图由标记和在运行时发生的选择性插入组成,插入填写内容通常是:变量的值,或对一小段代码运行后的结果。以前,这就是应用程序(例如PHP)

但现在不要在视图中添加功能代码!

image-20231020112505066

五、Rails框架

Rails作为MVC框架

image-20231021135020902

Rails应用程序执行过程:

  1. **路由(**在routing.rb中)将传入的URL映射到控制器动作并提取任何可选参数,路由的“通配符”参数(例如:id),加上URL中“?”之后的任何内容被放入params[]哈希表中,可在控制器动作中访问
  2. 控制器动作设置实例变量,视图可见,views/目录下的子目录和文件名匹配控制器动作名称
  3. 控制器动作最终导致一个视图被呈现,即view被返回

image-20231021135525696

Rails哲学

  • 约定优先于配置原则:如果命名遵循某些约定,则不需要配置文件。这点与JAVA不太一样,Java中需要使用配置文件或是注释来绑定路由,而在Rails中不需要,例如:image-20231021140410050

  • Don’t Repeat Yourself (DRY):提取公共功能的开发原则

创建

必须调用ActiveRecord模型实例上的savesave!方法将实际更改保存到数据库,如果操作失败“!”版本方法将抛出异常,create方法是 new 方法与 save方法的组合,其具有原子性。

创建后,对象获得一个主键(每个ActiveRecord模型表中的id列)

六、行为驱动设计(BDD)和用户故事介绍

6.1 敏捷生命周期

6.2 行为驱动的设计

6.3 生产力和工具

6.4 用户故事

创建用户故事

用户故事的评判从五个方面来进行:

  • Specific
  • Measurable
  • Achievable
  • Relevant
  • Timeboxed