第二章 对象

对象

点睛

本章开始我们终于要开始探索ruby的源码了。首先按照预告我们先从对象的构造开始。

接下来我们来考虑一下对象作为对象而成立的必要条件吧。 之前已经说明过几次对象到底是什么,其实作为对象不可缺少的条件有三个,分别是

  • 可以区别自己和外界。(拥有识别标志)
  • 能够对外界的行动作出反应。(方法)
  • 拥有内部状态。(实例变量)

本章会对这三个特征顺序确认。

主要注目的文件是ruby.h,其他诸如object.c, class.c, variable.c也会稍微关注。

value和对象的结构体

ruby中对象的实体是由结构体来表现的。各种处理经常要和指针打交道。 结构体根据不同的类会使用不同的类型,但是指针无论是在哪个结构体中 都是VALUE类型。

VALUE的定义如下。

  71  typedef unsigned long VALUE;
(ruby.h)

VALUE实际上被用来强制转换成各种对象结构体的指针。所以当指针的长度和unsigned long不一致的时候ruby就无法正常工作。严格意义上来说,如果存在比sizeof(unsigned long)更长的指针类型ruby会无法正常工作。当然最近的系统基本上都符合上述要求,过去不符合这种要求的机器还是很多的。

至于结构体,也是有很多种类。主要是按照对象的类来进行区别。

struct RObject 凡是不符合以下条件的

struct RClass Class对象

struct RFloat 小数

struct RString 字符串

struct RArray 数组

struct RRegexp 正则表达式

struct RHash 哈希表

struct RFile IO、File、Socket之类

struct RData 上述以外所有用C语言定义的类

struct RStruct Ruby的结构体Struct类

struct RBignum 大整数

我们来看几个对象结构体的定义的例子。

      /* 用作一般对象的结构体 */
 295  struct RObject {
 296      struct RBasic basic;
 297      struct st_table *iv_tbl;
 298  };

      /* 字符串的结构体(String实例) */
 314  struct RString {
 315      struct RBasic basic;
 316      long len;
 317      char *ptr;
 318      union {
 319          long capa;
 320          VALUE shared;
 321      } aux;
 322  };

      /* 数组(Arrayの实例)的结构体 */
 324  struct RArray {
 325      struct RBasic basic;
 326      long len;
 327      union {
 328          long capa;
 329          VALUE shared;
 330      } aux;
 331      VALUE *ptr;
 332  };

(ruby.h)

我们先把逐个详细的说明放到后面,首先从整体层面上来说。

首先VALUE被定义成unsigned long类型,要作为指针使用就必须进行强制转换。

于是各个对象的构造函数就配有Rxxxx()这种形式的宏。比如struct RString的宏就是RSTING(),struct RArray的宏是RARRAY()。这些宏的用法如下。

VALUE str = ....;
VALUE arr = ....;
RSTRING(str)->len;   /* ((struct RString*)str)->len */
RARRAY(arr)->len;    /* ((struct RArray*)arr)->len */

接下来我们看到所有的对象结构体的开头都会有一个struct RBasic类型的成员basic存在。结果就是无论VALUE是指向何种对象的结构体的指针,只要被强制转换成struct RBasic*,就可以访问basic的内容。

既然这么费劲心思作出这种设计,struct RBasic一定存放着Ruby对象的重要信息。我们来看struct RBasic的定义。

 290  struct RBasic {
 291      unsigned long flags;
 292      VALUE klass;
 293  };

(ruby.h)

flags是拥有多种目的的标志,最重要的用途就是保存结构体的类型(struct RObject之类)。表示类型的标志用T_xxxx来定义。可以从VALUE通过宏TYPE()访问。比如下面的例子

VALUE str;
str = rb_str_new();    /* 生成ruby的字符串(对应结构体为RSting) */
TYPE(str)              /* 返回值为T_STRING */

这些标志都是T_xxxx的形式,struct RString 的话就是T_STRING,struct RArray的话就是T_ARRAY,对应方式十分规则。

struct RBasic的另一个成员klass保存的是对象所属的类。klass的类型是VALUE,足以说明其保存的是Ruby的对象(其实是对象的指针)。也就是说这个就是class类的对象了。

对象和类的关系在本章“方法”这一小节中会详细说明。

顺带一提成员名用klass代替class是为了防止用C++的编译器编译时和保留词class发生冲突。

关于结构体的类型

我们说过struct Basic的成员flags保存了结构体的类型。可是为什么必须保存结构体的类型呢?这是因为所有类型的结构体都是通过VALUE来处理的。当指向结构体的指针被转换成VALUE之后变量中已经不存在类型的信息,编译器也是不会特殊照顾的。于是我们只有自己管理好各自的类型了。这个也是针对所有结构体类型统一管理的一大缺陷。

那么,既然使用的结构体是由类决定的,那么为什么还要把结构体和类分开保存呢?直接从类中访问结构体的类型不就好了吗?不这样做有两个理由。

第一,抱歉我们要推翻刚才说过的话。实际上存在着不具有struct RBasic的结构体(即不存在klass成员)。比如说将在第二部分登场的struct RNode。但是这种特殊的结构体开头还是会有一个flags类型的成员。所以所有结构体只需要拥有flags就可以进行统一管理。

第二,其实类和结构体不是一一对应的。比如说用户用Ruby语言定义的类的实例全部是使用struct RObject来保存。如果要从类中访问结构体的类型就必须记录下所有类和结构体的对应关系。那还不如直接将类的信息放入结构体中来的快捷便利。

basic.flags的用途

谈到basic.flags的用途,刚才一直说是保存结构体类型,这种说法有点恶心我们还是用图来进行一下说明。此图仅仅是为了在之后遇到疑惑的时候可以方便参考,现在还不必全部理解。

仅仅是看图的话我们会发现32比特中还有21比特的空余。其实那些部分被定义为FL_USER0~FL_USER8这一系列的标志,根据结构体不同使用目的也不相同。上图为了做示范把FL_USER0也放了进去。

VALUE的填充对象

我们说过VALUE本质上是unsigned long。因为VALUE仅仅是指针,貌似 void*也能胜任,实际上不这样做是有理由的。因为VALUE也有不是指针的时候。非指针的VALUE存在以下六种情况。

  • 数值较小的整数
  • 符号(symbol)
  • true
  • false
  • nil
  • Qundef

我们按顺序来说。

数值较小的整数

Ruby中一切都是对象所以整数也是对象。但是整数的实例是在是太多了,如果每个整数都用一个结构体来表示的话那运行速度就太慢了。假如要递加从0到50000的整数,仅仅如此就要生成50000个对象的话会让人一瞬间陷入犹豫。

那么我们来实际看一下C中奖int类型变换成Fixnum的宏INT2FIX吧,我们来确认一下Fixnum的确是被填埋到了VALUE里面。

 123  #define INT2FIX(i) ((VALUE)(((long)(i))<<1 | FIXNUM_FLAG))
 122  #define FIXNUM_FLAG 0x01

(ruby.h)

左移一位之后和1相或。

110100001000        变换前   
1101000010001       变换后

就是如此,保证了保存Fixnum的VALUE总是奇数。另一方面,Ruby对象结构体内存空间的申请使用的是malloc()。一般总是会被分配到4的倍数的地址。所以地址的值和保存Fixnum的VALUE值的范围是不会重叠的。

另外,将int和long变换成VALUE还有其他一些宏,比如INT2NUM(),lONG2NUM()。它们都是以“○○2○○”的形式,NUM的话可以同时处理Fixnum和Bignum。比如INT2NUM()会把超过Fixnum范围的数转换成Bignum。NUM2INT()会把Fixnum和Bignum都转换成int类型。如果超出int的范围会发生例外,没有必要在这里进行越界检测。

符号(symbol)

符号是什么?

这个问题比较麻烦,我们先来说为什么需要符号。首先我们知道ruby内部存在着ID类型的变量。

  72  typedef unsigned long ID;
(ruby.h)

这个ID是和任意的字符串一一对应的整数。虽然话这么说,但是也不可能和这个世界上所有的字符串都一一对应吧?这种对应关系仅仅是存在于"ruby的进程之中"。关于ID的访问方法我们在下一章"名称和命名表"中讲述。

语言处理的程序需要处理大量的名字。变量名,常量名,类名,文件名等等。这些大量的名字如果用char*来保存处理实在是太不容易了。如果你硬要问是哪里不容易,我可以告诉你那就是除了内存管理还是内存管理。另外名字的比较也经常会发生,如果每次都比较字符串是否相符的话实在是效率太低。于是我们不直接处理字符串,而是将其对应到别的东西上面来处理。而那“别的东西”就是整数了。因为整数的处理是最简单的。

将ID带入到ruby的世界中的正是符号。ruby1.4之前直接将ID的值转换成Fixnum作为符号使用。现在也可以通过Symbol#to_i来访问它的值。然而在实际运用中逐渐发现把符号当作Fixnum处理实在不太妥当,于是在ruby1.6之后就有了独立的符号类Symbol了。

符号对象由于经常作为哈希表的键使用所以数量非常多。于是Symbol就和Fixnum一样被填埋进了VALUE里面。我们来看一下将ID转换成Symbol的宏ID2SYM()。

 158  #define SYMBOL_FLAG 0x0e
 160  #define ID2SYM(x) ((VALUE)(((long)(x))<<8|SYMBOL_FLAG))

(ruby.h)

左移8bit就相当于乘上256,也就是4的倍数。然后和0x0e相或(这个时候和加法没区别),这样的话最终结果也不会是4的倍数。当然也不会是奇数,也就是说不会和其他的VALUE类型相重叠。真是巧妙的方法。

最后我们来看一下ID2SYM()的逆变化SYM2ID()。

 161  #define SYM2ID(x) RSHIFT((long)x,8)
(ruby.h)

RSHIFT是右移。右移根据平台不同会出现整数的符号剩余,不剩余的区别,所以保险起见我们用宏来代替。

true false nil

这三个是Ruby里面特殊的对象。分别是代表逻辑真,逻辑假,和没有对象的对象。这三者的c语言中的值定义如下。

 164  #define Qfalse 0        /* Rubyのfalse */
 165  #define Qtrue  2        /* Rubyのtrue */
 166  #define Qnil   4        /* Rubyのnil */

(ruby.h)

这次竟然是偶数了。但是要注意0和2作为指针使用是不可能的。所以不必担心和其他的VALUE值重复。因为虚拟内存空间第一块地址通常不会被分配。同时也是为了当想要访问NULL指针的时候程序能够迅速得出错。

另外Qfalse因为值为0所以才c语言层次也是被当作逻辑假来使用的。实际上在ruby的返回逻辑真假值的函数中,返回值会被转换成VALUE或者init然后返回Qtrue/Qfalse。这种做法很常见。

至于Qnil,有专门针对VALUE判断其是否为Qnil的宏。NIL_P()

 170  #define NIL_P(v) ((VALUE)(v) == Qnil)

(ruby.h)

~p这种命名方式术语lisp风格。其表示进行的处理是返回真值的行为。也就是说NIL_P其意思"为参数是否为nil?" p来自于predicate。(断言/谓语)。这种命名规则在ruby中被广泛使用。

另外ruby中除了false和nil是假其他都是真。但是c中的话nil(Qnil)也是真。于是用c语言判定Ruby表达式真假的宏RTEST()应该如下。

169  #define RTEST(v) (((VALUE)(v) & ~Qnil) != 0)

(ruby.h)

Qnil只有第三位的比特是1,~P取反之后只有第三位是0。与其bit 和之后结果是真的只有Qfalse和Qnil。

加上!=0是为了确保结果是0或者1。因为glib这个库要求真值只能是0或者1。([ruby-dev:11049])

说来Qnil的Q是什么玩意儿?是R的话还可以理解,为什么是Q呢?向人询问的结果是因为emacs中是这样的。比预料中有趣呢。

Qundef

 167  #define Qundef 6                /* undefined value for placeholder */

(ruby.h)

这个值在解释器内部作为“未定义值”来使用。在Ruby中是不会出现的。

方法

Ruby对象最重要的性质要数拥有自我身份,能够调用方法,以及按照实例存有数据这三个方面了。这个小节我们来讲第二点,对象和方法结合的方式。

struct RClass

在ruby中类也是作为对象存在的。那当然类的对象的实体也需要一个结构体。这个结构体就是struct RClass了。这个结构体类型的flag为T_CLASS。

另外类和模块基本属于同一概念所以没有必要区分各自的实体。于是模块的结构体也是用struct RClass来表现的。模块的结构体flag被设置成T_MODULE来进行区别。

 300  struct RClass {
 301      struct RBasic basic;
 302      struct st_table *iv_tbl;
 303      struct st_table *m_tbl;
 304      VALUE super;
 305  };

(ruby.h)

首先注意到m_tbl(Method Table)这个成员。struct st_table是ruby中到处可见的哈希表。详细会在下一章“名称与命名表”中说明,总之就当作他是记录一对一关系的东西就好了。m_tbl就是用来记录这个类所有的方法的名称(ID)和方法实体直接对应关系的。关于method实体的构造方法我们会在第二部,第三部进行解说。

接下来第四个成员super正如字面意思,保存着父类的信息。因为是VALUE所以指向的是父类的类的对象(指针)。Ruby中不存在父类的类只有Ojbect。(译者注:在ruby1.6之前是如此)

实际上Object里面的所有方法都定义在Kernel这个模块中。Object只是将其引入。这个之前我们也已经讲过了。模块的功能几乎和多重继承相当,一见似乎只是用super的话无法表现一些复杂的关系,ruby中正是用了巧妙的方法使其看起来只是单一继承。这个操作我们会在第四章“类和模块”中说明。

另外受这个变化的影响Object的结构体的super的内容是Kernel实体的struct Rclass, 后者的super被定义成NULL。换句话说,super如果是NULL的话RClass就是Kernel的实体。

方法的搜索

既然类呈现这样的构造方式那么该如何调用方法也可以很容易想象了。首先探索对象类的m_tbl,如果没有找到就继续顺着super在父类中的m_tbl中寻找,依次回溯。也就是说如果知道Object也没有找到该方法,那么该方法就是还未定义。

按照以上的顺序来探索m_tbl的方法请见search_method()。

 256  static NODE*
 257  search_method(klass, id, origin)
 258      VALUE klass, *origin;
 259      ID id;
 260  {
 261      NODE *body;
 262
 263      if (!klass) return 0;
 264      while (!st_lookup(RCLASS(klass)->m_tbl, id, &body)) {
 265          klass = RCLASS(klass)->super;
 266          if (!klass) return 0;
 267      }
 268
 269      if (origin) *origin = klass;
 270      return body;
 271  }

(eval.c)

这个函数在类对象klass中寻找名称为id的方法。

RCLASS(value)的内容为((struct RClass*)(value))的宏。

st_lookup()是用来在st_table中检索和键对应的值的函数。如果找到值就返回真,并将找到的值写入第三个地址参数(body)。这边这些函数在卷尾的函数参照中会有记载,如有需要可以随时参考。

话说来每次都进行探索的话实在是太慢了。实际上被调用的参数会被保存到缓存中,第二次就不必每次都用super方法来回溯寻找了。包括缓存的搜索我们会在第十五章“方法”中进行介绍。

实例变量

这章节我们介绍成为对象必须条件的第三点,实例变量的实装。

rb_ivar_set()

实例变量是一种以对象为单位存放其特有的数据的方式。既然是对象特有那么好像将数据保存到对象内部(对象的结构体)会比较好?那么实际上是个什么情况呢?我们来看一下将实例变量带入对象中的函数rb_ivar_set()一探究竟。

      /* 向obj对象的成员变量id中代入val */
 984  VALUE
 985  rb_ivar_set(obj, id, val)
 986      VALUE obj;
 987      ID id;
 988      VALUE val;
 989  {
 990      if (!OBJ_TAINTED(obj) && rb_safe_level() >= 4)
 991          rb_raise(rb_eSecurityError,
                       "Insecure: can't modify instance variable");
 992      if (OBJ_FROZEN(obj)) rb_error_frozen("object");
 993      switch (TYPE(obj)) {
 994        case T_OBJECT:
 995        case T_CLASS:
 996        case T_MODULE:
 997          if (!ROBJECT(obj)->iv_tbl)
                  ROBJECT(obj)->iv_tbl = st_init_numtable();
 998          st_insert(ROBJECT(obj)->iv_tbl, id, val);
 999          break;
1000        default:
1001          generic_ivar_set(obj, id, val);
1002          break;
1003      }
1004      return val;
1005  }

(variable.c)

rb_raise()和rb_error_frozen()都是错误检测。这之后我们会反复强调,错误检测虽然在现实中是需要的,但是不是处理的本质。所以第一次读代码的时候可以将错误处理完全忽略。

去掉了错误处理之后就只剩下了swtich语句。类似

switch (TYPE(obj)) {
  case T_aaaa:
  case T_bbbb:
     :
}

的形式是ruby特有的书写习惯。TYPE()返回对象构造体的类型(T_OBJECT和T_STRING之类)的宏。类型flag因为是整数所以完全可以使用switch分支。Fixnum和Symbol虽然不存在构造体,但是会在TYPE()中j进行特殊处理进而返回T_FIXNUM和T_SYMBOL,所以大可不必担心。

接下来我们回到rb_ivar_set()。好像就只有T_OBJECT T_CLASS T_MODULE这三个类型的处理是分开来单独进行的。这三个被选中是因为他们的结构体的第二个成员是iv_tbl。我们来实际确认一下。

    /* TYPE(val) == T_OBJECT */
 295  struct RObject {
 296      struct RBasic basic;
 297      struct st_table *iv_tbl;
 298  };

      /* TYPE(val) == T_CLASS or T_MODULE */
 300  struct RClass {
 301      struct RBasic basic;
 302      struct st_table *iv_tbl;
 303      struct st_table *m_tbl;
 304      VALUE super;
 305  };

(ruby.h)

iv_tbl对应的是Instance Variable Table,也就是实例变量表。里面记录的是实例变量名和对应的值。

我们再来贴一下rb_ivar_set()中,当结构体拥有iv_tbl成员时的处理代码。

if (!ROBJECT(obj)->iv_tbl)
    ROBJECT(obj)->iv_tbl = st_init_numtable();
st_insert(ROBJECT(obj)->iv_tbl, id, val);
break;

ROBJECT()是用来将VALUE强制转换成struct RObject*的宏。obj指向的也有可能是struct Rclass,但是仅仅是访问第二成员变量的话是不会发生什么问题的。

st_init_numtable()是新生成st_table的函数。st_insert()是在st_table中生成关联的函数。

综上所述这段代码所做的事情,就是当iv_table()不存在的时候新建一个,然后将对应的"变量名=>对象"记录到其中。

注意一点,struct Rclass自身是类对象的结构体,所以其变量表也是类对象自己的东西。用ruby程序来说,就如下面这种情况。

class C
  @ivar = "content"
end

generic_ivar_set()

向T_OBJECT T_MODULE T_CLASS之外的结构体代入变量会是怎么样呢?

#没有iv_tbl的结构体
1000  default:
1001    generic_ivar_set(obj, id, val);
1002    break;

(variable.c)

这个时候处理会交给generic_ivar_set()。看具体的函数之前先把大框架说明一下吧。

T_OBJECT T_MODULE T_CLASS以外的构造体是没有iv_tbl成员的(之后会说明为什么没有的理由)。但是就算是没有该成员也可以通过别的手段将实例和struct st_table对应起来。ruby将这种对应关系保存在全局的st_table,即generic_iv_table中来解决此问题。

我们来看一下实际的代码。

 801  static st_table *generic_iv_tbl;

 830  static void
 831  generic_ivar_set(obj, id, val)
 832      VALUE obj;
 833      ID id;
 834      VALUE val;
 835  {
 836      st_table *tbl;
 837
          /* 总之可以先无视 */
 838      if (rb_special_const_p(obj)) {
 839          special_generic_ivar = 1;
 840      }
          /* 不存在generic_iv_tbl则新建 */
 841      if (!generic_iv_tbl) {
 842          generic_iv_tbl = st_init_numtable();
 843      }
 844
          /* 核心处理 */
 845      if (!st_lookup(generic_iv_tbl, obj, &tbl)) {
 846          FL_SET(obj, FL_EXIVAR);
 847          tbl = st_init_numtable();
 848          st_add_direct(generic_iv_tbl, obj, tbl);
 849          st_add_direct(tbl, id, val);
 850          return;
 851      }
 852      st_insert(tbl, id, val);
 853  }

(variable.c)

rb_special_const_p()当参数不是指针的时候返回真。这个if语句里面的内容如果不理解垃圾回收机制的话是无法说明的,所以我们在这里还是先省略。 请读者在阅读第五章的垃圾回收机制之后再自行理解。

st_init_numtable()刚才也出现了,是用来新建一个哈希表。

st_lookup()用来查找和key对应的值。这个时候会查找和obj所关联的实例变量表。如果找到对应的值函数整体就返回真,在第三个地址参数(&tbl)中记录找到的对应值。也就是说!st_lookup(...)将的是当没有找到对应记录之后发生的事情。

st_insert()刚才也说过了。用来将新的对应记录保存到表中。

st_add_direct()和st_insert()几乎相同,区别在于后者会在追加保存记录之前先检查一下键是否已经存在。也就是说如果使用st_add_direct()来添加新记录的话,如果已经有相同的键存在于表中,会出现一个键对应两个记录的情况。

所以能直接使用st_add_direct()的情况,基本上是刚刚确认过键不存在,或者是刚刚建立新的表这两种情况。这段代码是符合这些情况的。

FL_SET(obj, FL_EXIVAR)是用来设置obj的basic.flags为FL_EXIVAR的宏。basic.flags的所有flag都是FL_xxx的形式,可以通过FL_SET来设置flag。相反用来取消对应flag的宏叫做FL_UNSET()。另外,通常认为FL_EXIVAR的EXIVAR是external instance variable的简称。

插入这个flag是为了提高访问实例变量的速度。如过发现没有这只FL_EXIVAR这个falg,则不用通过探索generic_iv_tbl也可以知道实例变量不存在。相比之下当然是bit的校验比起探索struct st_table来的速度更快。

结构体的间隙

现在我们了解了实例变量的保存方式,那么为什么存在没有iv_table的结构体呢?比如struct RString和struct RArray就没有iv_tbl,这个是为什么?那么干脆直接把实例变量当作RBasic的成员算了。

就结论来说,可以这样做但是不应该如此。实际上这个和ruby的对象管理机制紧密相关。

ruby中字符串的数据(char[])所占用的内存使用malloc来访问。但是对象的结构体是个例外。ruby会统一管理分配,从而访问内存。这个时候如果结构体的种类(大小)参差不齐的话管理起来就十分麻烦。所以将所有的结构体用共用体Rvalue来申明并统一分配管理。共用体的大小会和成员中最大的保持一致,所以如果单独有一个结构体的体积非常大就会造成很大的浪费,于是我们还是希望素有的结构体的大小尽可能保持接近。

最常用的结构体要数struct RString(字符串)了吧。接下来是RArray(数组),RHash(哈希),RObject(所有用户定义的类) 。那么问题就来了。struct RObject的内容只有sruct RBASIC和一个指针。而struct RString RHash RArray却已经使用了struct Basic和三个指针的空间。也就是说struct Robject越多,就会造成越多的两个指针空间的浪费。更有甚者,RString如果使用了四个指针,RObject实际上就只占用了共用体的一半不到的空间,实在是太浪费了。

另一方面配置iv_tbl的好处主要是访问速度的提升和内存空间的节省,并且我们也不知道这个功能会不会被频繁使用。实际上在ruby1.2之前根本就没有导入过generic_iv_tbl,所以像String和Array中也不能使用实例变量,就算如此也没发生什么大问题。如果仅仅因为这点好处就浪费大量的空间实在是太蠢了。

所以从以上可以做出结论,因为iv_tbl而增加构造体的体积实在是无奈之举。

 960  VALUE
 961  rb_ivar_get(obj, id)
 962      VALUE obj;
 963      ID id;
 964  {
 965      VALUE val;
 966
 967      switch (TYPE(obj)) {
      /*(A)*/
 968        case T_OBJECT:
 969        case T_CLASS:
 970        case T_MODULE:
 971          if (ROBJECT(obj)->iv_tbl &&
                  st_lookup(ROBJECT(obj)->iv_tbl, id, &val))
 972              return val;
 973          break;
      /*(B)*/
 974        default:
 975          if (FL_TEST(obj, FL_EXIVAR) || rb_special_const_p(obj))
 976              return generic_ivar_get(obj, id);
 977          break;
 978      }
      /*(C)*/
 979      rb_warning("instance variable %s not initialized", rb_id2name(id));
 980
 981      return Qnil;
 982  }

(variable.c)

结构基本相同。

(A)struct RObject如果是Rclass,则检索iv_tbl。刚才说过了,有可能iv_tbl是NULL,所以不先检查一下的话会出错。接着st_lookup会在找到相应记录的时候返回真。这个if语句整体来说就是"如果已经代入过此实例变量则返回其值"。

(C)如果没有找到对应的值……也就是说如果想要访问的是还没有被代入的实例变量,那么跳过if和switch,直接运行下面的语句。这个时候会发生rb_warning(),并返回nil。这是因为ruby的实例变量不需要代入也可以被访问。

(B)另外,当结构体既不是struct RObject也不是RClass的时候,首先从generic_iv_tbl中寻找对象的实例变量表。generic_ivar_get()实现的功能不用我说也能想到。另外需要注意的是if语句。

刚才说过将进行过generic_ivar_set()处理的对象插入FL_EXIVAR。在这里这个flag使得运行高速化的特征就显现出来了。

rb_special_const_p()是什么玩意儿?这个函数当obj不存在结构体的时候为真。因为不存在构造体所以也不需要basic.flags(因为根本无从插入flag)。所以FL_xxx()遇到这种对象总是会返回假。于是这里对待rb_special_const_p()为真的对象必须格外小心翼翼。

对象的结构体

这小节中我们简要介绍对象结构体最重要的具体内容的的处理方法。

struct RString

 314  struct RString {
 315      struct RBasic basic;
 316      long len;
 317      char *ptr;
 318      union {
 319          long capa;
 320          VALUE shared;
 321      } aux;
 322  };

(ruby.h)

ptr是指向字符串的指针,len是字符串的长度,非常直观。

Ruby的字符串与其说是字符串不如说是字符序列。因为可以包含NUL在内的任何字符。所以在ruby中就算在终端设置NUL也没有多大意义,不过因为C的函数中要求NUL,所以为了便利还是将NUL设置成字符串的终端。但是NUL是不包含在len之中的。

另外解释器和扩展库中处理字符串的时候可以通过RSRTING(str)->ptr``RSTRING(str)来访问ptr和len。但是要注意以下几点。

  • 检查str是否确实指向struct RString。
  • 可以访问成员但是不能改变其内容
  • 不可以将RSTRING(str)->ptr保存到诸如临时变量之中供之后使用。

这是为什么?其中一个原因是软件工程学上的原则。不可以随便修改别人的数据。既然有接口函数就乖乖使用接口函数。不过还有其他不允许擅自访问和保存指针的理由,这和第四个成员变量aux有关。但是要详细说明aux的使用方法又必须得详细说明ruby字符串的一些特征。

ruby的字符串自身是可以改变的(mutable),所谓的变化是指

s = "str"        # 生成字符串代入s
s.concat("ing")  # 向字符串s中追加"ing"。
p(s)             # 输出"string"

这样s指向的对象的内容就变成了"string"。java和Python的字符串是没有这种特性的。硬要说的话,这个特性和Java的StringBuffer很接近。

接下来我们来看看他们之间到底有什么关系。首先既然可以发生改变自然字符串的长度len也会改变。既然长度发生改变那么这个时候内存的分配就会发生增减。当然也可以使用realloc(),但是malloc和realloc这些操作都太重了,仅仅是为了变更字符串就realloc()一次的话实在是负担太大了。

于是ptr所指向的内存空间通常会比len稍微长一点。这样的话追加的字符串正好能放入多余的内存中就可以不必调用realloc()了,这样的话速度就上去了。结构体中的aux.capa保存的就是这个多余的长度。

那么另一个aux.shared是什么玩意儿呢。这个也是为了提高从字符串序列中生成对象的速度而采用的机制。请看下面的ruby程序。

while true do  # 永远地重复
  a = "str"        # 将内容是"str"的字符串放入a
  a.concat("ing")  # 向a中追加"ing"。
  p(a)             # 输出string
end

不管是循环多少次总会在第四行的p出输出"string"。放在一般情况下那就得每次通过"str"这个式子新建一个char[]类型的字符串对象。 但是对于字符串通常情况下都不会做任何改变,这个时候就会造成多次复制char[]的资源浪费。于是我们就期望能够共用一个char[]。

作为共用所存在的就是aux.shared这个东西了。使用表达式生成的字符串对象都会共用同一个char[]。只有当真正发生变化时候才会专门去申请内存分配。使用共用的char[]的结构体中的basic.flags标志中会被设立ELTS_SHARED这个标志。aux.shared会保存原来的对象。ELTS是elements的简称。

我们回到RSTRING(str)->ptr的话题中。之所以可以访问但不能改变指针对象是因为这会使得len和capa的值和真实情况不符。另外如果要改变用序列表达式所新建的字符串对象的内容,则需要把对象中的aux.shared成员移除。

最后我们来列举几个使用RString的例子。str可以看成是指向RString的value。

RSTRING(str)->len;               /* 长度 */
RSTRING(str)->ptr[0];            /* 第一个字符 */
str = rb_str_new("content", 7);  /* 生成内容是"content"的字符串。
                                    第二个参数是其长度 */
str = rb_str_new2("content");    /* 生成内容是"content"的字符串。
                                    长度会使用strlen()来计算 */
rb_str_cat2(str, "end");         /* 在Ruby字符串中后接C的字符串 */

struct RArray

struct RArray是存放Ruby的数组实例的结构体。

 324  struct RArray {
 325      struct RBasic basic;
 326      long len;
 327      union {
 328          long capa;
 329          VALUE shared;
 330      } aux;
 331      VALUE *ptr;
 332  };

(ruby.h)

除了ptr之外几乎和struct RString一样。ptr指向的是数组的内容,len是其长度。aux的用法和struct RString中介绍的相同。aux.capa是ptr所指向的内存的真正的长度,aux.shared则是当数组为共用的时候指向共用数组的指针。

访问成员的方法也和RString类似。 通过RARRAY(arr)->ptr和RARRAY(arr)->len可以访问成员但是不能改变成员的内容。我们来看一下简单的例子。

/* 用c语言操作数组 */
VALUE ary;
ary = rb_ary_new();             /* 生成空的数组 */
rb_ary_push(ary, INT2FIX(9));   /* 将Ruby的9加入数组 */
RARRAY(ary)->ptr[0];            /* 访问编号是0的元素 */
rb_p(RARRAY(ary)->ptr[0]);      /* 输出ary[0](输出9) */

# 用ruby进行操作
ary = []      # 生成空的数组
ary.push(9)   # 将Ruby的9加入数组
ary[0]        # 访问编号是0的元素
p(ary[0])     # 输出ary[0](输出9)

struct RRegexp

RRepexp是存放正则表达式的结构体。

 334  struct RRegexp {
 335      struct RBasic basic;
 336      struct re_pattern_buffer *ptr;
 337      long len;
 338      char *str;
 339  };

(ruby.h)

ptr是已经编译好的正则表达式。str是编译前的正则表达式(正则表达式的源码),len是其长度。

处理Rexgexp对象的代码本书中将不会出现所以这里就省略了。就算要在扩展库中使用,只要不涉及很特殊的用法,参考一些接口函数就应该足够了吧。

struct RHash

struct RHash是哈希表Hash实例所在的结构体。

 341  struct RHash {
 342      struct RBasic basic;
 343      struct st_table *tbl;
 344      int iter_lev;
 345      VALUE ifnone;
 346  };

(ruby.h)

其实这个是struct st_table的wrapper。关于st_table我们会在下一章"名称和命名表"中详细解说。

ifnone存放的是搜索失败时候使用的键,默认是nil。iter_lev是为了哈希表的re-entrance(多进程安全)存在的。

struct RFile

struct RFile是服务嵌入类Io和其后继子类实例的构造体。

 348  struct RFile {
 349      struct RBasic basic;
 350      struct OpenFile *fptr;
 351  };

(ruby.h)
 19  typedef struct OpenFile {
  20      FILE *f;                    /* stdio ptr for read/write */
  21      FILE *f2;                   /* additional ptr for rw pipes */
  22      int mode;                   /* mode flags */
  23      int pid;                    /* child's pid (for pipes) */
  24      int lineno;                 /* number of lines read */
  25      char *path;                 /* pathname for file */
  26      void (*finalize) _((struct OpenFile*)); /* finalize proc */
  27  } OpenFile;

(rubyio.h)

成员几乎都保存在了struct OpenFile中。IO对象的实例并不多所以可以这样存放。各个成员的用途都有些。基本上是C语言stdio的warpper。

struct RData

struct RData和目前为止介绍的东西目的都不一样。这个主要是为了存放扩展类的结构体。

编写扩展库的类的实体当然也需要一个结构体来存放。但是结构体的类型是生成的类所决定的,所以无法事先知道类的大小和结构。于是ruby提供了一个"管理用户自定义的结构体指针的结构体",这个东西就是struct RData了。

 353  struct RData {
 354      struct RBasic basic;
 355      void (*dmark) _((void*));
 356      void (*dfree) _((void*));
 357      void *data;
 358  };

(ruby.h)

data是指向用户定义的构造体的指针。dfree是释放自定义构造体的函数,dmark是mark and sweep中进行mark的函数(涉及垃圾回收)

关于struct RDdata的说明现在还不是时候,总之先看图吧。详细的内容我们会在第五章的"垃圾回收"中介绍。

(第二章完)