Chaofan's

Bonvenon al la malpura mondo.

标签: Ruby

  • 理解Big-endian和Little-endian

    Big-endianLittle-endian,你也许听说过这两个概念,可能也大概知道是什么意思,但未必清楚更深入的细节。这不奇怪,因为不直接和内存打交道的程序员不会对Endian有多少实践层面的理解,而即使写过相关的程序,因为抽象泄露的魔咒,也说不清底层原理。本文试图解释big-endian和little-endian的由来,在执行层面的原理,以及主流高级语言中如何处理Endian相关问题。

    在继续之前,先解释前文提及的概念「抽象泄露」。计算机科学本身可以理解为有关抽象的学问,C语言尽管今天看来相当底层,但也是建立在CPU指令上的抽象,人们用C写的程序,实际上是被编译器转写到机器指令执行。这也意味着,机器指令层面的一些概念,并不能在C语言的模型里体现,即C语言程序的某些问题无法用C语言本身的模型解释,必须依赖更底层的原理。此种情况下,C语言的抽象显得不完备了,这就是抽象泄露。

    典型的抽象泄露例子就是a=a+1语句中,如果变量a不是一个原子(atomic)变量,那么多线程环境中这个语句可能会造成数据不一致。这个问题很难用C语言本身的逻辑理解,我们只知道机器会给a的值加上1(C语言甚至不考虑有符号整数的溢出情况!),但具体CPU是如何计算的,我们不知道。只有当我们知道这个表达式在汇编层面是通过读取内存、执行加法,再写回内存之后能理解数据竞争的起因。这也是我认为编程初学者可以从汇编入门的原因——尽管汇编也有抽象泄露(如预测执行),但总归是比C的模型完备多了,对人也不难理解,C系语言的执行模型实在有点两头不占(另一头是函数式模型)的意思。

    回到正题。来解释big-endian和little-endian的区别。

    什么是Big-endian和Little-endian

    考虑如下一段C程序。

    #include <stdio.h>
    #include <stdint.h>
    uint8_t u8s[4] = { 0x12, 0x34, 0x56, 0x78 };
    uint32_t conv(void) { return *((uint32_t*)u8s); }
    int main(void) { printf("%x\n", conv()); }

    程序把一个4字节数组当作4字节整数读取,输出的内容会是多少呢?按照通常思路,uint32_t占内存中4字节的空间,4个uint8_t也是占4字节空间,那么这个指针强转的结果自然也是16进制的12345678。但实际上,在你的电脑上通常结果是78563412。是的,不是12345678,不是87654321,也不是1e6a2c48。而且这不是什么随机行为,而是基于某种逻辑的固定倒转。

    简单来说,对该程序输出78563412的CPU,就是Little-endian的;而输出12345678,则是Big-endian的。

    这两块内存不都是4字节吗,为什么会有这个区别?一个不精确但通俗的解释是:CPU对聚合类型(数组、结构体)和基本类型的变量有不同的处理方式。

    u8s[0]u8s[1]u8s[2]u8s[3]
    0x120x340x560x78

    以上是u8s这个数组的内存布局,从低地址到高地址排列。对于Big-endian的CPU,从内存读取一个int类型也按照这个顺序。但对Little-endian来说,就要反过来了。

    int[3]int[2]int[1]int[0]
    0x120x340x560x78

    顺序是倒过来的。把以上程序的4字节调整为8字节或2字节,也会得到类似的效果。但注意,这只针对各种内置类型有效,struct {int l; int r;}int[2]有一样的顺序。

    Endian名称的由来

    中文技术资料基本把Endian统一译作「字节序」。在英文语境里,Endian倒是个有典故的词,它来自《格列佛游记》第一卷第四章:

    我下面就要告诉你的是,这两大强国过去三十六个月以来一直在苦战。战争开始是由于以下的原因:我们大家都认为,吃鸡蛋前,原始的方法是打破鸡蛋较大的一端。可是当今皇帝的祖父小时候吃鸡蛋,一次按古法打鸡蛋时碰巧将一个手指弄破了,因此他的父亲,当时的皇帝,就下了一道敕令,命全体臣民吃鸡蛋时打破鸡蛋较小的一端,违者重罚。人民对此法极为反感。历史告诉我们,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。这些内乱常常是由不来夫斯库国的君王们煽动起来的。骚乱平息后,流亡的人总是逃到那个帝国去寻求避难。据估计,先后几次有一万一千人情愿受死也不肯去打破鸡蛋较小的一端。

    所以你吃鸡蛋时先打哪边?

    代码中的字节序

    C/C++

    由于C和C++支持对变量取地址和任意指针类型的互转,我们很容易在C或C++中遇到字节序问题。结合上面使用C语言的例子,要在C语言里检测当前CPU是little-endian还是big-endian也可以用类似的做法:

    bool is_little_endian(void) {
      uint32_t test = 0x12345678U;
      return ((uint8_t*)&test)[0] == 0x78;
    }

    在Little-endian平台上,编译器会聪明地把这个函数优化成return 1。但这对我们来说并不够,因为我们可能需要在不同字节序的机器上编译不同的代码,运行时判断显然不够。好在,主流编译器都有宏用来判断:

    #ifdef __GNUC__
    #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    #define LITTLE_ENDIAN 1
    #else
    #define LITTLE_ENDIAN 0
    #elif _MSC_VER || __INTEL_COMPILER
    #define LITTLE_ENDIAN 1
    #end

    Clang会伪装自己是GCC,二者都支持通过__BYTE_ORDER__宏判断字节序。实际上,除了__ORDER_LITTLE_ENDIAN____ORDER_BIG_ENDIAN__外,实际上GCC中还有一种可能取值是__ORDER_PDP_ENDIAN__。PDP是Unix诞生时的CPU架构,其对16位整数采用Little-endian,而对32位整数使用一种奇怪的字节序,即把0x12345678表示为0x34, 0x12, 0x78, 0x56。技术上,现在已不需要考虑这种字节序。

    对于Intel编译器(ICC)和Visual Studio (MSVC),它们只存在于little-endian平台上,所以发现编译器是它们就可以放心地确认字节序。早期版本的Windows NT支持过PowerPC和MIPS这类以big-endian为主的架构,但即使是这些版本的Windows,也是运行little-endian模式的。

    如果要在所有平台上以big-endian字节序读取整数,请使用位运算:

    uint32_t read32be(uint8_t input[]) {
      return ((uint32_t)input[0] << 24) |
             ((uint32_t)input[1] << 16) |
             ((uint32_t)input[2] << 8) |
             (uint32_t)input[3];
    }

    以Little-endian方式读取,请将数组下标倒过来。

    Rust

    Rust中,可使用target_endian指令在编译期判断字节序:

    #[cfg(target_endian = "little")]
    // Code for little endian
    
    #[cfg(target_endian = "big")]
    // Code for big endian

    要以某种字节序读取数据,可使用byteorder crate。

    Java

    尽管主流CPU都是little-endian,但Java大概因为靠近网络字节序(见下文)的原因,统一使用big-endian。通过java.nio.ByteOrder类的nativeOrder方法,Java程序可以得知当前系统硬件上使用哪种字节序:BIG_ENDIAN或者LITTLE_ENDIAN

    DataInputStream采用Big-endian,如果要以Little-endian方式读取数据,可采用同前面C语言相似的位拼接做法。

    JavaScript

    JavaScript这样的脚本语言通常不太会考虑字节序问题,因为我们无法对一个对象取地址,或者查看它的内存布局。但现代JavaScript依然提供了和字节流打交道的API,这里涉及到三个概念:TypedArrayArrayBufferDataView

    简单来说,TypedArray表示某种固定类型作为元素的数组(可以是8种整数和2种浮点其中之一),ArrayBuffer表示无类型的一串字节,DataView是在前者之上读写的适配器:

    let array = new Uint32Array(1)
    array[0] = 0x12345678
    let view = new DataView(array.buffer)
    view.getUint8(0) // => 120 (0x78)
    view.getUint8(3) // => 18  (0x12)
    view.setUint32(0, 0x12345678, true)  // set data as little-endian
    view.setUint32(0, 0x12345678, false) // set data as big-endian
    view.getUint8(0) // => 18  (0x12)

    在JavaScript中,对TypedArray元素的赋值,字节序由CPU的字节序决定,所以在little-endian机器上自然得到little-endian布局的ArrayBuffer。但DataView的set方法有第三个参数表示是否使用little-endian顺序写入,要注意的是,这个参数的默认值是false,也就是big-endian,这意味着在大多数平台上setUint32及其他类似函数的行为,同一般赋值是相反的。

    Ruby

    Ruby也同理,大部分情况下用不着和二进制表示直接打交道。但Ruby中数组类型有将对象转换为二进制字节的pack方法,字符串类型有把字节串解析回对象的unpack方法。这两个方法利用字符串参数指定要读取的类型,除了类型和长度外,不同记号还代表着不同字节序:

    [100].pack 'L' # => 32-bit int in native endian
    [100].pack 'V' # => 32-bit int in little-endian
    [100].pack 'N' # => 32-bit int in big-endian
    
    # double (1.1) in native endian
    "\x9A\x99\x99\x99\x99\x99\xF1?".unpack 'D'
    # double (1.1) in little endian
    "\x9A\x99\x99\x99\x99\x99\xF1?".unpack 'E'
    # double (1.1) in big endian
    "\x9A\x99\x99\x99\x99\x99\xF1?".reverse.unpack 'G'

    自然可以用同样的方法测试当前系统字节序:

    def is_little_endian?
      [1].pack('L') == [1].pack('V')
    end

    不同硬件架构的字节序

    x86

    主流的32位和64位x86都是little-endian字节序。64位x86指令集有时又被称作amd64,因为它最早是AMD发明的。曾经的Intel提出过支持两种字节序切换的IA-64架构作为x86的64位继承者,后来因为不向前兼容而陨落在历史之中。

    x86一直是little-endian,这意味着从最早最简单的ADD指令,到今天复杂的AVX512或者AMX,只要指令的某个操作数是内存地址,那指令就一定会按照低位在前高位在后的逻辑读取数据。

    ARM

    ARM在指令集层面对没有对字节序作强制要求,也就是说,一块ARM CPU可能是little-endian,可能是big-endian,也可能二者皆支持。可以从目标Triple区分:以aarch64_bearm64_bearmeb开头的,即为big-endian,其他不带be或者eb缩写的,则是little-endian。

    为了和x86兼容,主流ARM基本都是little-endian模式。

    RISC-V

    RISC-V是little-endian,尽管有说法曰作者更喜欢big-endian,但因为主流的ARM和x86都是little-endian,因此也选择了little-endian。

    POWER

    POWER诞生在90年代,那时候RISC工作站还不是个历史概念,各大厂商都在制造自家的RISC CPU和UNIX分支,x86还是穷小子,所以大家依然倾向于big-endian。不过为了便于移植x86 PC程序,Windows NT支持的PowerPC 610还的确支持过little-endian模式,然而PowerPC和Windows的合作并没有持续几代,这个开关也被移除,直到POWER 7。

    但POWER 7的little-endian支持并不完整,例如硬件虚拟化就没法支持little-endian模式。从POWER 8开始,POWER CPU开始完整支持little-endian字节序。也就是说,POWER 8及以上的CPU支持两种字节序,由一个特权寄存器控制。甚至理论上,POWER CPU可以在运行时切换字节序。(但显然没人愿意这么做)

    在POWER支持little-endian前,Linux已经支持32位和64位的big-endian POWER CPU。而在little-endian加入以后,因为两种字节序的程序几乎没法相互兼容,所以它们在Triple上被看作两种不同的架构:ppc64指big-endian,而little-endian常用ppc64le或ppc64el表示。

    长远来看,同样是出于兼容目的,Linux将逐渐去除对64位POWER big-endian模式的支持,little-endian才是未来。不过这个故事仅限于Linux和BSD等开源社区,传统的AIX依然只会支持big-endian模式,且在可见的未来还没有改变的迹象。

    一些应用场景的字节序

    Unicode

    UTF-16和UTF-32这两种格式都会采用多个字节表示单个码点(通俗点说就是字符,尽管并不准确),这就引出little-endian和big-endian两种格式。在多数编辑器或处理字符编码的程序里,UTF-16 LE和UTF-16 BE被看作两种编码。但根据Unicode标准,使用UTF-16和UTF-32的文件都需要在开头用2字节标记文件是big-endian还是little-endian:0xFE 0xFF表示big-endian,0xFF 0xFE表示little-endian。

    对于一个内容为「Test」的文件,以下是三种编码的字节内容:

    feff 0054 0065 0073 0074 (UTF-16 BE)
    fffe 5400 6500 7300 7400 (UTF-16 LE)
    6554 7473 (UTF-8)

    开头的这两个字节被称作BOM(Byte-order mark),显然UTF-8因为基本单位是字节,所以并不需要BOM。但一些Windows程序依然会给UTF-8文件加上0xEF 0xBB 0xBF作为BOM以标记这个文件使用UTF-8。

    网络字节序

    根据RFC 1700,socket通信中的数据都使用big-endian格式传输,因此使用socket的C API时,需要用htonl/htons/ntohl/ntohs系列函数将数据在big-endian和原生字节序之间作转换,比如要绑定的端口号。一些上层库可能已经封装了这个行为,但这一点仍需牢记在心。

    WebAssembly

    WebAssembly采用little-endian,但这指的是内存读写指令采用的字节序,WebAssembly字节码中的整数都用LEB128格式表示。

    字节序不是什么

    • 数组在内存中永远是从前向后排的,和字节序无关
    • C/C++中的结构体也永远是从前向后排的,其他高级语言可能会重排对象的内存布局,但这同样和字节序无关
    • 整数或浮点数在CPU寄存器中使用何种字节顺序或位顺序,和字节序无关,实际上这对程序员也不可见,概念上我们都完全可以认为寄存器都是从前向后,类似big-endian般排列
    • 字节序只和数据从内存被加载到寄存器这一过程有关,且单位是字节而不是位

    意义何在

    字节序是一个习惯远比实际好处重要的领域。如果一定要为两种字节序列出一些好处的话:

    • Big-endian更加直观,读取一个4字节整数和4字节数组采用同样的顺序,由于网络通信使用big-endian,因此big-endian的程序在处理网络数据时还能省去字节反转的开销
    • Little-endian环境下,长度不同的数据能够保证低位重合,也就是说32位的0x1234用16位指令去读取依然是0x1234,这节省了类型强制转换时的开销

    但站在今天的角度,这些优缺点都无伤大雅。最重要的原因,不过是x86活下来成了主流,因此little-endian碰巧也成了主流,一如Windows和C语言。计算机领域各个层次都充满了抽象,如果有什么东西能穿越潮起潮落流传至今,原因几乎就是兼容性而已。

  • 用代码生成的思路做Sqlite的C++ ORM

    有一个未发布的小项目,涉及到本地数据库读写和加密,本来在Apple平台上用Swift实现,为了移植到其他平台的考虑,正在换成C++实现非GUI部分。在此前的Swift版本,我把Sqlite的C API封装为一个「穷人版」的ORM类。但这个实现使用起来遇到过度设计的问题,想调整一下做些非常规查询很麻烦。

    因为要换成C++,考虑到C++模板的功能比Swift强大,也许可以做得更多。一开始的想法非常宏大(个人项目的常见毛病),想的是用C++模板表达和SQL关系代数同等的约束(举个例子,比如用了HAVING子句,SELECT的范围就会受限制)。但这个坑太大,经过缩减变为:给定一组要查询的字段名,和一组字段类型与字段名关联的表定义,查询时通过字段名即可自动推导出结果类型。但这个坑依然很大,用到了不少可变参数模板,并不容易调试,实际使用起来的效果也不尽如人意。

    整理一下思路,有了新想法:前面的设计,是在试图用静态类型约束描述一组动态概念(SQL)。比如我在SQL里查询3个字段,而在获取结果时试图读4个字段,正常封装的做法是抛异常或者返回错误,我想要在编译时就发现这是个错误。但既然要做的查询就那些,为什么不可以把动态的部分和静态部分隔离开,类似Rust里的unsafe?这样我只要保证动态部分逻辑上不会出错就行了。如何保证呢?用脚本生成访问的代码就好了。

    所以我拿起最熟悉的Ruby,整出了如下的DSL:

    model = ModelGenerator.new
    
    model.migrate do |step|
      step.define_table 'users', if_not_exists: true do |table|
        table.primary_key :uuid, 'id'
        table.integer 'role', nullable: false
        table.text 'username', nullable: false
        table.text 'password_digest', nullable: false
        table.real 'updated', nullable: false
        table.real 'created', nullable: false
      end
    
      step.define_table 'posts', if_not_exists: true do |table|
        table.primary_key :uuid, 'id'
        table.text 'title', nullable: false
        table.text 'content', nullable: false
        table.integer 'type', nullable: false
        table.real 'created', nullable: false
        table.foreign_key 'post_user_id', 'users', 'id'
      end
    end
    
    model.alias_model 'user', 'users'
    model.alias_model 'post', 'posts'
    
    model.generate!

    这个DSL可以生成一个Repository类,其中包含常见的对users和posts的增删改查方法。如果想加入新的方法,DSL也有特殊方法指定要查询的字段。得益于Sqlite的数据类型只有四类(BLOB、TEXT、INTEGER和REAL),它们都可以轻松映射到C++的std::vector<std::byte>std::stringint64_tdouble类型。多个查询字段的结果则会被返回为一个std::tuple类型。

    DSL还定义了迁移的概念。后面如果对数据库表有任何修改,往下添加新的migrate块调用就可以。生成器会生成一个临时的Sqlite数据库,把定义的所有修改都在临时数据库里运行一遍,最后通过Sqlite API得到最终的表定义。通过这种方式,我们就不用模拟任何表的迁移过程了——再怎么模拟还能有真的在Sqlite里跑一遍方便吗?

    当然这个DSL目前还欠缺很多东西。因为是项目自用,有什么欠缺的后面需要了再加上。但我对这个思路很满意,整个生成器核心只有一百多行Ruby代码,剩下几百行是针对C++的ERB模板。理论上,后面还可以生成Java、C#、Swift等语言的代码。

    其实我比较认可Svelte这种工具的思路。良好的转译器能够省下很多麻烦。理想情况里,通过一个核心语言(可以是Vala这样专门设计的语言,也可以是已有语言如JavaScript的子集),经过编译能够在不同平台生成核心的业务和GUI代码,比如在Windows生成C#,Android生成Kotlin,iOS生成Swift代码等。剩下的部分只需要在每个平台补充少量支持代码。这个思路可能因为不够通用,也还是需要一些原生技能,所以还没看到有什么例子。(有Svelte Native,但和我预想的似乎不同)

  • 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的你参考: