## 各自発表 > [name=makicamel] https://speakerdeck.com/koic/rubocop-under-a-microscope > [name=ken3ypa] ## 概要 - ライブラリアップデート当番をよりやっていきたい - そのためには日々ライブラリを読んでおいた方が捗りそう - 自分の技術力向上にもよさそう ## お題 - https://github.com/SamSaffron/fast_blank ## このgemについて - Active Support が提供している String#blank? をより高速に実行するため、メソッドのC拡張を提供している。これにより、従来の String#blank? よりも高速に動作するというもの ## 見どころ ### ベンチマーキング > 1.2–20x faster than Active Support とのことで、従来のString#blank? よりも高速を謳っている。String#blank? が定義されていたら undef_method し、C で blank? / blank_as? を再実装している。 - https://github.com/SamSaffron/fast_blank/blob/d1695581e397b0b00fb589edae0dec99ce846e42/lib/fast_blank.rb#L1-L4 - https://github.com/SamSaffron/fast_blank/blob/d1695581e397b0b00fb589edae0dec99ce846e42/ext/fast_blank/fast_blank.c#L91-L95 ### 全角スペースの扱い - C拡張で実装された String#blank? は、ActiveSupport実装とは違い、全角スペース(0x3000)の考慮をしていない。そのため、`" ".blank?`はgemの導入によって異なる結果を返してしまう ```with_fast_blank.rb > " ".blank? => false ``` ```without_fast_blank.rb > " ".blank? => true ``` 挙動を一致させたい場合は、blank_as? を利用すること。 blank? を blank_as? に差し替えておくことも有効そう。 参考: [fast_blankと日本語環境 - Qiita](https://qiita.com/jkr_2255/items/42afcade901de9efaf68) ## その他リンク集 - 開発の経緯とか - https://github.com/rails/rails/pull/9958 - https://github.com/rails/rails/issues/9992 - https://changelog.com/posts/fast_blank-stringblank-in-c C の実装をチラッと読む ```c #include <stdio.h> #include <ruby.h> #include <ruby/encoding.h> #include <ruby/re.h> #include <ruby/version.h> #define STR_ENC_GET(str) rb_enc_from_index(ENCODING_GET(str)) // Ruby 2.2.0 以降かどうかを判定 #ifndef RUBY_API_VERSION_CODE # define ruby_version_before_2_2() 1 #else # define ruby_version_before_2_2() (RUBY_API_VERSION_CODE < 20200) #endif // このコードの解説はこちら // 上記の関数は文字列が空白のみで構成されているかどうかを判定するものです。具体的には特定のUnicodeコードポイントを基にして空白をチェックしています。 // blank_as? は日本語など static VALUE rb_str_blank_as(VALUE str) { rb_encoding *enc; char *s, *e; enc = STR_ENC_GET(str); s = RSTRING_PTR(str); if (!s || RSTRING_LEN(str) == 0) return Qtrue; e = RSTRING_END(str); while (s < e) { int n; unsigned int cc = rb_enc_codepoint_len(s, e, &n, enc); switch (cc) { case 9: case 0xa: case 0xb: case 0xc: case 0xd: case 0x20: case 0x85: case 0xa0: case 0x1680: case 0x2000: case 0x2001: case 0x2002: case 0x2003: case 0x2004: case 0x2005: case 0x2006: case 0x2007: case 0x2008: case 0x2009: case 0x200a: case 0x2028: case 0x2029: case 0x202f: case 0x205f: case 0x3000: #if ruby_version_before_2_2() case 0x180e: #endif /* found */ break; default: return Qfalse; } s += n; } return Qtrue; } static VALUE rb_str_blank(VALUE str) { rb_encoding *enc; char *s, *e; // 指定された文字列のエンコーディング情報を取得 enc = STR_ENC_GET(str); // 文字列の先頭ポインタを取得 s = RSTRING_PTR(str); // 文字列が空かどうかを判定 if (!s || RSTRING_LEN(str) == 0) return Qtrue; // 文字列の末尾ポインタを取得 e = RSTRING_END(str); // 文字列の先頭から末尾までループ while (s < e) { int n; // rb_enc_codepoint_len: rb_enc_codepoint_len関数は、ポインタsから始まる文字列での現在の文字(コードポイント)と、その文字が何バイトで構成されているかを取得 unsigned int cc = rb_enc_codepoint_len(s, e, &n, enc); // 与えられたコードポイントccが空白文字であるかどうかを判定 // もし現在の文字が空白でなく、かつNULL文字(終端)でない場合、Qfalse(Rubyのfalseオブジェクトを表すマクロ)を返す if (!rb_isspace(cc) && cc != 0) return Qfalse; s += n; } // 文字列を走査してすべての文字が空白であることが確認された場合、Qtrueを返します。 return Qtrue; } // RubyのStringクラスに新たにblank?とblank_as?を追加 void Init_fast_blank( void ) { // rb_cString に, "blank?" というメソッドが呼ばれた場合, rb_str_blank という関数を呼ぶように設定, 引数を取らない場合は0 rb_define_method(rb_cString, "blank?", rb_str_blank, 0); rb_define_method(rb_cString, "blank_as?", rb_str_blank_as, 0); } ```