Ruby on Rails开发入门手记

上个月的博文里说过,由于要开发同济权益的二手书管理系统,所以用PHP写过这样的代码。第一个版本是纯PHP撸成的,没有用到任何的外部框架,路由也是纯手写.htaccess文件实现的,一共就四个文件,负责分发请求,最后交给处理数据库的文件来输出对应的JSON信息。现在看来,这大概叫强行RESTful吧,哈哈。话说回来呢,当时写的时候,甚至还有点基本的ORM思想,不过觉得太麻烦就没着手完成这个。后来才知道这个模型有个高大上的英文缩写叫做ORM。当然,这个版本的代码,模块性一点都不好。五百多行,基本没法维护。

后来寒假的时候,确实是觉得这个太不行了,需要找个框架重写,顺便学习一个。用哪个框架呢?知乎搜索了一下,就用CodeIgniter吧,相对轻量,又确实有用。这个框架是MVC架构的,但是也没有实现完整的关系对象模型,仅仅是封装了方便的数据库查询器而已。这个版本,自我感觉代码结构好多了,不过好像和前端交互的API还是没有改。

到了四月初的时候,听说软件学院要搞一个Android应用开发比赛,所以呢,后端可能还得改。这次大家坐在一起开了个会把需求相对完整地定了下来。不过,还没等得及下手,一切又变了。应用还没开始做呢,就又变卦了。这次是得改变架构,网页和后台不分离。大概就剩一个月的时间了。怎么搞呢?用Ruby on Rails吧。听说,这玩意很快啊。

要说之前,Ruby这个语言我就没有碰过。虽然它和Python我都不熟,不过其实还是用Python相对多一点。但是不行啊,硬着头皮也得上。所幸Rails的文档写得确实不错,我做这个网站的过程中很大程度参考了Ruby on Rails的中文指南。如果有朋友对Rails这个框架感兴趣,非常推荐这一系列文档。同时,也听说Ruby China在国内的技术社区里面是最有氛围的一个了。

Rails无非也就像是其他MVC的Web框架一样,在目录下通过命令生成一个新项目,然后运行服务器,打开看到一个正确运行的页面。这就标志着我们的开发工作正式开始了。当然由于Rails这个框架比较复杂,生成的代码也比较多,所以新建一个控制器还不能像CodeIgniter这种框架一样直接手动新建个controller文件就好了,需要用到generate命令。敲下命令之后,发现Rails确实爆炸,自动帮我生成了RESTful风格的路由和一堆代码。

由于朝RESTful看齐的特点,直接在Rails的路由文件routes.rb里像这样写:

Rails.application.routes.draw do

  resources :books
  resources :users
  resources :appointments

  root 'welcome#index'
end

寥寥几行代码,Rails就帮我们生成好了书、用户、预约三种资源的所有路由,已经定义了网站根目录对应的控制器方法。

说到生成,Rails还有一个更厉害的东西,叫做「脚手架」。利用它,可以一键生成控制器、视图和模型的代码。虽说这些代码直接拿来用不太现实,不过也可以作为初学者学习的重要参考。

Rails的脚手架用法如下:

rails g scaffold books title:string price:decimal

这样就快速创建了一个叫做books的资源,包含title和price两个属性。脚手架为我们快速创建了路由、控制器、视图、模型,甚至测试文件。脚手架生成的代码虽然完整,但是往往不能完全满足我们的需求,我们也未必喜欢它给我们强加上的一堆网页样式。我们可以基于脚手架的代码来修改,不过更多的时候只是用它当作例子来学习,看看标准的Rails代码是个什么样子。

Rails严格区分了资源的单复数。一个资源在作为Model层的时候是单数的,比如Book;而在View层和Controller层的时候是复数的,比如books_controller.打开Rails为我们创建的这个controller,我们可以发现七种RESTful的操作已经预先定义好了,并且自带html和json两种模式。很迷的是在许多方法里只有一行语句甚至没有语句,那么Rails是怎么渲染View的呢?

实际上,Rails会自动找到与controller里的方法同名的模版文件(Rails默认用的是erb)然后渲染。当然有些方法是不需要HTML模版的,比如POST、PUT这些HTTP请求使用的controller就不需要特别的模版,用得更多的是重定向。当然,我们可以手动选择要渲染的模板。即使用render方法。

Rails的模板是怎么写的呢?因为Ruby语言不靠缩进来标记语法层次,所以Ruby可以轻松地嵌入到HTML模板文件当中,这一点比不得不自造模板语言的Django高到不知哪里去了。这类模板也就是所谓的erb。Rails可以在设置里选用其他的模板,比如haml乃至markdown。

被渲染的erb模板能够正确地加载调用它的控制器里的所有实例变量。什么意思?比如像这样:

# controllers/books_controller.rb
class BooksController
  def index
    @time = Time.now
  end
end

# views/books/index.html.erb
<p>Current time is <= @time %>.</p>

所谓的带@符号的变量是Ruby的一种语法,称为实例变量,表示类的数据成员。如果我们去掉这里的@符号,把time定义成局部变量的话,模板渲染的时候就会报错了。

我们把Rails的视图层和控制器都极简地介绍了一下。尽管没说几句话,但是已经基本可以构建一个可以运行的网站了。这都要归功于Rails的良好封装和Ruby语言的自由度。现在我们来看看之前被忽略掉的Model层。实际上,Rails更加鼓励把更多的逻辑写到模型层里面。(Rails还需要我们为controller写什么代码吗?)对于书这种资源,我们也能在models目录下发现对应的代码文件book.rb,注意到book用的是单数。

class Book < ActiveRecord::Base
end

咦,这个Book里什么代码也没有,就一个空类嘛,有什么用?

别急,如果我们在Controller里面想要查询关于Book的信息,非常简单:

def get
  @book = Book.find_by_id params[:id]
end

有趣,这个find_by_id是哪来的?噢,忘说了,Ruby里面方法调用跟Perl一样不需要括号噢。

哈哈,这就是Ruby语言的威力,它可以把字符串和代码相互转换,并且可以在运行时动态定义新的方法,甚至覆盖旧的方法。Ruby看到我们find_by_id这个方法时,不会急着报错,而是去寻找有没有「当方法找不到时的解决方案」,而Rails在这里为我们提供了一个。它会去数据库的结构里查找id这个字段,然后返回给我们想要的结果。也就是说,如果我们的Book表里新增了一个字段叫ISBN,那么我们可以直接find_by_isbn了,什么也不用改。

这只是Rails的数据库子包ActiveRecord强大功能的冰山一角。另一个值得一提的地方是它的关联机制。比如这样:

class Article < ActiveRecord::Base
  has_many :comments
end

# 注意单复数
class Comment < ActiveRecord::Base
  belongs_to :article
end

# Rails会去寻找Comment表里article_id这个字段
some_article = Article.take
some_article.comments # 我们已经得到了此文章对应的所有评论

这里对article_id这个字段名的假定体现了Rails的一个重要原则:「约定大于配置」。相比于Java繁复的各种XML配置文件,不知道Rails的这种风格有没有让你觉得清爽呢?

拖了六个月,说了这么多,其实也就是Rails最基本的一点点。不过足以让许多人认识到Rails框架和Ruby语言的魔力了。整个Ruby社区很有趣的一点就是,从Rails的插件到各个方面的库,作者都会以这个库的使用方式「看起来像英语」为荣,比如:

every 3.hours do
  sleep 2.minutes
end

得益于独特的语言文化和语言的强大能力,这样的DSL在整个Ruby社区到处都是!

文章很短,不过希望你能从中感受到用Ruby编程的魅力。就像DHH说的,成为百万富翁以后才发现,开兰博基尼还不如写Rails来得快乐。(虽然我没开过)

以下有两个页面可供想学习Ruby on Rails的你参考:

C语言里的void类型

今天去图书馆坐了坐,看罢《Essential C++》,觉得过分基础,实在没什么意思。碰巧包里还有一本神作《C标准库》,详述了实现ANSI C标准库的所有过程。第一章讲的便是assert.h的实现。这个宏本身没有什么难度,无非是在一个函数的基础上包装一下,说不定GCC或者Clang直接把这个函数做成builtin了。不过不管是书上还是musl库的代码,都有一个让我注意到的地方:

#ifdef NDEBUG
#define    assert(x) (void)0
#else
#define assert(x) ((void)((x) || (__assert_fail(#x, __FILE__, __LINE__, __func__),0)))
#endif

这里的这个__assert_fail就如同我上文所说,是一个用于输出内容的函数,作用是输出错误信息然后调用abort函数退出。

关键在于这里有个奇怪的(void)0表达式。首先我们可以判定表达式的类型是void,对吧?不过这个void类型的表达式有什么意义呢?我们学习C语言的教材对这一点基本都语焉不详。我尝试了一下,把(void)0赋给一个int类型的变量,编译器是这样给我抱怨的:

test.c:3:9: error: initializing 'int' with an expression of incompatible type 'void'
  int i = (void)0;
      ^   ~~~~~~~

反正意思就是无法把这个类型转换成匹配的int啦。

那我们转念一想,尝试用void来定义一个变量呢?得到这样的错误提示:

test.c:4:10: error: variable has incomplete type 'void'
  void j;
       ^

等等!incomplete type?哪里见过这个?对啦!如果在一个结构体内部定义一个以这个结构体为类型的对象,编译器就会抱这个错误,提示类型还没有定义完。所以写链表或者二叉树的时候,里面存储的其实是「指向节点的指针」。

继续带着疑惑,我查询了C11的标准草案(正式版是收费的,不过两者在这些基础问题上相差无几),其中有三处提到了我想知道的「void类型」:

…The void type comprises an empty set of values; it is an incomplete object type that cannot be completed…

…An lvalue is an expression (with an object type other than void) that potentially designates an object…

The (nonexistent) value of a void expression (an expression that has type void) shall not be used in any way, and implicit or explicit conversions (except to void) shall not be applied to such an expression. If an expression of any other type is evaluated as a void expression, its value or designator is discarded. (A void expression is evaluated for its side effects.)

而《C程序设计语言》里这样描述:

void对象的(不存在的)值不能够以任何方式使用,也不能被显式或隐式转换为任一非空类型。因为空(void)表达式表示一个不存在的值,这样的表达式只可以用在不需要值的地方,例如作为一个表达式语句(参见A.9.2节)或作为逗号运算符的左操作数(参见A.7.18节)。

可以通过强制类型转换将表达式转换为void类型。例如,在表达式语句中,一个空的强制类型转换将丢掉函数调用的返回值。

这下终于明白了!那么稍微总结一下:

  1. 在C语言中,void可以作为一个合法表达式的类型,亦即它在语法结构里可以作为一个表达式
  2. void类型的表达式不能转换为其他任何类型的表达式,也就没有了赋值的可能
  3. 编译器会特殊看待void类型,将其作为一个「未被定义完整」的类型,也就没有了定义变量的可能
  4. 尽管void未被定义完整,但是如同其他结构体一样,我们是可以正常使用void的,并且直接对一个void解引用,结果是void类型的表达式
  5. void类型和其他类型不相容,但是该有的表达式副作用还是会有

可以说这里面的逻辑是非常自洽且合理的。所以不得不佩服设计C语言和C++的人,这些概念就像物理定律,看上去复杂,但是用这套逻辑推导下去很多看似不同的东西都可以得到统一解释。

现在回头看assert宏的实现代码,不难理解啦。因为__assert_fail函数的返回值类型是void,但是它需要被用在一个逻辑表达式里。于是它巧妙地结合了逗号运算符,配合短路求值的规定实现了assert需要的效果。至于把最后的表达式类型也转换为void,是为了不让它作为值被赋给变量。而在定义了NDEBUG的状态下,用(void)0占位也比什么也不写来得好,编译器会提示类型不相容,而直接替换成空的话,在代码复杂的地方错误类型也许会莫名其妙。

C语言要考试了,你应该问问自己什么?

呐,这次我又来装逼了。我保证,这是这学期最后一次。

如果从我在百度C++吧的第一次发言(2009年7月2日)到今天,日子已经过去了2380天,然而我之前还没有考过一次C语言。好慌……好了好了,打住,不然待会儿真被当成装逼了。不过我说我初学的时候用while包住过main你信?至少你没有这样干过吧。

那么,我尽力,把自己带入2380个日子前,努力以一个初学者的视角来想一下C语言到底包含着什么知识点。我承认这种方法充满了应试色彩,而且对于上机写代码这种形式的考试可能并没有十分显著的作用。不过我是真的希望每个看到这篇文章的读者,即使以后不靠C语言吃饭,写不出优秀的C代码,也至少能对C语言有一个正确的理解。那我即使是背上装逼的骂名,也心满意足了。(其实我会告诉你我是因为不想复习物理了吗)

关于新手指引之类的内容,我都写在了那个「C指引」的文档里,点击打开它的GitHub页面。外加精力有限,因此不想再重复那些内容。所以我会列一些问题,并且不会给出答案。希望能迫使你思考。这些问题可能会带有我自己的主观色彩,不喜欢就点关闭咯。

先从初级开始。

  1. C语言代码文件的后缀名是什么?这类文件可以用哪些工具打开和浏览?
  2. 什么是可执行文件?它需要另外的软件打开它吗?
  3. C语言文件从源代码到可执行文件的过程叫做什么?这个过程中发生过什么?
  4. 如果我要输出Hello, world!,我需要在程序的第一行写上什么?如果不写有什么后果?
  5. 为什么程序执行时候的窗口会一闪而过?那个黑色的窗口究竟是什么?
  6. main是什么?我把代码写在main外面会有什么后果?
  7. main前面应该是void还是int?
  8. 变量的存在有什么意义?C语言里的变量有哪些类型?
  9. 我怎么去获取来自用户在命令行的输入?接受输入的函数跟输出的函数在调用方式上有什么区别?
  10. 既然一个字符串在代码里不能跨很多行书写,那么我怎么在字符串里表示换行?制表符呢?
  11. while语句和do…while语句的区别在哪里?
  12. switch…case语句里的break有什么作用?它和if语句比有什么局限性?
  13. 对于if语句括号里的内容来说,一个等号和两个等号有什么差别?两个等号中间能有空格吗?
  14. 如果变量a是double类型,那a=5/2之后a的值应该是多少?

什么,觉得太简单了是吧?那来中级的问题:

  1. 如下的代码会按何种方式执行?会输出什么结果?
int sample = 1, ok = 0;
if (sample == 1)
    if (ok)
        puts("Ok.\n");
else
    puts("No.\n");
  1. goto语句是如何使用的?为什么我们提倡不使用goto语句?
  2. 什么时候程序需要函数声明?函数声明应该放在哪里?
  3. 到底什么是EOF?EOF可以用在我们输入的什么地方?
  4. i++和++i有什么不同?i+++++i这种表达式有意义吗?
  5. 应该如何安全地读取一个文件里的内容?写文件呢?
  6. 说出数组和指针的区别(至少三个)
  7. 字符类型有数值吗?strcmp的返回值有什么含义?
  8. static和extern关键字有什么用?

啊哈,还是不满足吗?我们来看看高级篇:

  1. 为什么在main函数里定义一个非常大的数组,程序可能会崩掉,而我放到外面就不会了?
  2. char *const和const char*的区别在哪里?对于声明const char *s=”abcd”,这个指针s到底「指向」什么地方?
  3. 如果我像这样定义:
typedef char* sptr;
const sptr b;
这里的b到底是什么类型?是const char*吗?
  1. 考虑这样一个结构体:
struct sample {
    int  a;
    char b;
};

sizeof(struct sample)的值会是多少?为什么?

我所习惯的C/C++代码格式

1.函数开始的大括号专起一行。

int sample()
{
    return 0xA0246 * 0454;
}

2.类定义和内部内联函数定义。

class foo {
public:
    foo() : data(0)
    {}
    foo(int d) : data(d)
    {}
    void print()
     {  printf("%d\n", data);  }
private:
     int data;
};

3.for、while、if等关键字后面1空格,且总是使用大括号。

while (in != 0) {
    s += in;
}

4.头文件使用的保护。

#ifndef FILE_H
#deinfe FILE_H

// ...

#endif // FILE_H

5.缩进4空格。

6.类定义和实现、变量声明和函数定义勤注释。

7.太多了,最基本的就这些。

C语言中数值和字符串的相互转换

整数->字符串可以使用stdio.h中的sprintf函数,有的人可能会说到itoa,但其实itoa不是C标准库的函数,是微软自己添加的。

sprintf的原型是:

int sprintf ( char * str, const char * format, ... );

和printf用法相同。当然也可用于其它类型如double。

例:

char str[20];
int s = 1000000;
sprintf(str, "%d", s);

字符->整数同样使用的也是stdio.h中的sscanf函数,stdlib.h中也有atoi和strtol可以进行转换。

int sscanf ( const char * str, const char * format, ... );
int atoi ( const char * str );
long int strtol ( const char * nptr, char ** endptr, int base);

sscanf和atoi的用法都很简单。值得一提的是strtol这个函数。第一个参数是源字符串,第二个参数用于接收非法字符串的首地址,第三个参数是转换后的进制。

什么叫非法字符串的首地址呢?比如nptr的值是”1234f5eg”,base是10,endptr在调用后的值就是”f5eg”。如果base是16,那么endptr的值就是”g”(f和e是16进制的合法字符,而在10进制中却不是)。可以看出非法字符的类型和base有关。由于要修改指针的值,所以需要用到二重指针。另外,开头和结尾的空格会被忽略,中间的空格会被视为非法字符。

例:

char buf[] = "12435 fawr22g"
char *stop;
printf("%d\n", (int)strtol(buf,&stop,10));
printf("%s\n",stop);

输出结果为

12435

 fawr22g

另外,给出一个atoi的实现(glibc里的atoi是直接用strtol实现的):

#include <string.h>
#include <ctype.h>

int atoi(const char *s)
{
    int sign = (s[0] == '-') ? -1 : 1;
    int i, j, res = 0;
    int b = 1;
    for (j = strlen(s) - 1; j > i; --j) {
        b *= 10;
    }
    for (i = isdigit(s[0]) ? 0 : 1; i < strlen(s); ++i) {
        res += (s[i] - '0') * b;
        b /= 10;
    }
    return res;
}