前幾天的晚上朋友在 Facebook 上問了一個問題。
1raise HTTPError, 'Not Found'
和
1raise HTTPError.new('Not Found')
哪個比較快?也因為這樣,我們意外的發現 Ruby 對上面兩段程式碼的定義上其實是不太一樣的。
在 Ruby 中 raise
一般情況下有以下幾種運作方式。
預設情況
1# Case 1
2raise # => #<RuntimeError>
3
4# Case 2
5raise 'NotFound' # => #<RuntimeError: "Not Found">
一般用法
1# Case 3
2raise HTTPError, 'Not Found'
3
4# Case 4
5raise HTTPError.new('Not Found')
不過,上面這兩段程式碼的差異在哪裡呢?從 Ruby 的原始碼可以看到 .new
的行為會多做一次檢查。
1static VALUE
2make_exception(int argc, const VALUE *argv, int isstr)
3{
4 VALUE mesg, exc;
5 int n;
6
7 mesg = Qnil;
8 switch (argc) {
9 case 0:
10 break;
11 case 1:
12 exc = argv[0];
13 if (NIL_P(exc))
14 break;
15 // 檢查了是否為字串
16 if (isstr) {
17 mesg = rb_check_string_type(exc);
18 if (!NIL_P(mesg)) {
19 mesg = rb_exc_new3(rb_eRuntimeError, mesg);
20 break;
21 }
22 }
23 n = 0;
24 // 繼續跟 raise HTTPError, 'Not Fonud' 一樣的行為
25 goto exception_call;
26
27 case 2:
28 case 3:
29 exc = argv[0];
30 n = 1;
31 exception_call:
32 mesg = rb_check_funcall(exc, idException, n, argv+1);
33 if (mesg == Qundef) {
34 rb_raise(rb_eTypeError, "exception class/object expected");
35 }
36 break;
37 default:
38 rb_check_arity(argc, 0, 3);
39 break;
40 }
41 if (argc > 0) {
42 if (!rb_obj_is_kind_of(mesg, rb_eException))
43 rb_raise(rb_eTypeError, "exception object expected");
44 if (argc > 2)
45 set_backtrace(mesg, argv[2]);
46 }
47
48 return mesg;
49}
從這個角度看,我們會發現差異其實只是「多一次檢查」的程度,甚至不太影響運行的效能。不過從 Programming Ruby 這本書中的範例,卻發現了一個稍微意想不到的使用方法。
1def readData(socket)
2 data = socket.read(512)
3 if data.nil?
4 raise RetryException.new(true), "transient read error"
5 end
6 # .. normal processing
7end
8
9# ...
10
11begin
12 stuff = readData(socket)
13 # .. process stuff
14rescue RetryException => detail
15 retry if detail.okToRetry
16 raise
17end
仔細一看,明明應該是 @message
的數值,被放入了非字串的數值,而且這個錯誤還提供了 #okToRetry
這樣的方法讓我們可以獲取到這個數值。
回到剛剛 Ruby 中 make_exception
的原始碼,在 2 ~ 3 個參數的情況下,回傳的 mesg
變數是透過呼叫一個透過 idException
指標定義的方法來產生的,而傳入的參數剛好是 raise
的第二個參數。
1 case 2:
2 case 3:
3 exc = argv[0];
4 n = 1;
5 exception_call:
6 mesg = rb_check_funcall(exc, idException, n, argv+1); // 呼叫 idException 指標對應的某個方法
7 if (mesg == Qundef) {
8 rb_raise(rb_eTypeError, "exception class/object expected");
9 }
10 break;
idException
對應的其實是呼叫物件上的#exception
方法,是所有Error
類型物件必須存在的方法。
也就是說,實際上我們可以讓我們的 Error
附帶一些額外資訊,在一些情況下處理錯誤的時候可以用來輔助我們。
不過,既然第一個參數已經被我們自訂的錯誤資訊替換了,那麼 Ruby 是怎麼設定錯誤訊息的?
實際上,在 idException
對應的 #exception
方法上,會複製現有的物件,件並且把錯誤訊息放進去。
在 #exception
的原始碼是這樣實作的。
1static VALUE
2exc_exception(int argc, VALUE *argv, VALUE self)
3{
4 VALUE exc;
5
6 if (argc == 0) return self;
7 if (argc == 1 && self == argv[0]) return self;
8 exc = rb_obj_clone(self);
9 exc_initialize(argc, argv, exc);
10
11 return exc;
12}
1static VALUE
2exc_initialize(int argc, VALUE *argv, VALUE exc)
3{
4 VALUE arg;
5
6 rb_scan_args(argc, argv, "01", &arg);
7 rb_ivar_set(exc, id_mesg, arg);
8 rb_ivar_set(exc, id_bt, Qnil);
9
10 return exc;
11}
也就是說,我們實際上 raise
出來的例外,其實是被重新修改過的,不過這也讓我們在使用 Ruby 的錯誤上可以更加的彈性。
這也是為什麼像是 Rubocop 之類的軟體,會建議使用下面這種方式產生錯誤的原因。
1raise NotFoundError
2
3raise NotFoundError, 'Current page is unavailable'
關於
#exception
的部分感謝五倍的同事在討論的時候提出來,才發現還有後續的處理將#message
設定上去。