3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rubyで文字列と数値が足し算出来ない理由

3
Last updated at Posted at 2016-04-27

はじめに

某所で「数値と文字列の足し算でエラー起きてた」というものを見かけた。
確かに数あるLLの中には暗黙的に型変換を行ってどうにかエラーにならないようにしているものがある。

phpの場合
echo "1" + 2; # => 3
echo 1 + "2"; # => 3
perlの場合
print "1" + 2; # => 3
print 1 + "2"; # => 3
JavaScript(nodejs)の場合
console.log("1" + 2); // => "12"
console.log(1 + "2"); // => "12"

とりあえずすぐに試せるもので試してみたが、挙動は異なるにせよ動作する。
では、Rubyではなぜ動かない、動くように実装されていないのだろうか。

文字列 + 数値の場合

この場合、String#+メソッドが呼ばれている。
String#+の実装であるstring.cのrb_str_plusを読んでみよう。

VALUE
rb_str_plus(VALUE str1, VALUE str2)
{
    VALUE str3;
    rb_encoding *enc;
    char *ptr1, *ptr2, *ptr3;
    long len1, len2;

    StringValue(str2);
    enc = rb_enc_check_str(str1, str2);
    RSTRING_GETMEM(str1, ptr1, len1);
    RSTRING_GETMEM(str2, ptr2, len2);
    str3 = rb_str_new(0, len1+len2);
    ptr3 = RSTRING_PTR(str3);
    memcpy(ptr3, ptr1, len1);
    memcpy(ptr3+len1, ptr2, len2);
    TERM_FILL(&ptr3[len1+len2], rb_enc_mbminlen(enc));

    FL_SET_RAW(str3, OBJ_TAINTED_RAW(str1) | OBJ_TAINTED_RAW(str2));
    ENCODING_CODERANGE_SET(str3, rb_enc_to_index(enc),
			   ENC_CODERANGE_AND(ENC_CODERANGE(str1), ENC_CODERANGE(str2)));
    RB_GC_GUARD(str1);
    RB_GC_GUARD(str2);
    return str3;
}

初っ端でStringValue(str2)としているが、このStringValueが何者か調べてみるとるりまにページがあった

val が String でなければ to_str メソッドを使って String に変換します。

つまり、数値のほうにto_strメソッドがあれば動作するはずだ。
るりまでto_strについて調べてみたらObject#to_strが見つかった。
該当ページの説明には以下のような注意書きがある。

このメソッドを定義する条件は、

  • 文字列が使われるすべての場面で代置可能であるような、
  • 文字列そのものとみなせるようなもの

という厳しいものになっています。

各主要数値クラスはこの条件に合致していないと判断されたのだろう。
そのためto_strが実装されておらず、結果としてString#+メソッドでエラーとなる。

どうしてもString#+に数値を渡したい場合にどのようにすべきか考えてみた。
to_sならば主要数値クラスには実装されているのでこうすればとりあえずは動く。
(Numeric, Integerクラスにはto_sメソッドがないが、これらは抽象クラスなので気にしないことにする)

class Numeric
  def to_str
    self.to_s
  end
end

p "1" + 2 # => "12"
p "1" + 1.234 # => "11.234"

PHPやPerl的な挙動(selfを数値に変換して足し算する)にしたい方も多いであろうが、String#+は四則演算の足し算を行うメソッドではなく、文字列の連結メソッドである。
そのためJavaScript的な挙動にするほうがRuby的には正しいであろう。
もしどうしてもselfを数値に変換して足し算したい場合は下記のようにすればよい。

class String
  alias orig_plus +
  def + other
    if other.kind_of? Integer
      return self.to_i + other
    elsif other.kind_of? Float
      return self.to_f + other
    elsif other.kind_of? Rational
      return self.to_r + other
    elsif other.kind_of? Complex
      return self.to_c + other
    else
      orig_plus other
    end
  end
end

元々のString#+String#orig_plusに退避し、各数値クラスに合わせてselfを変換すると動作する。
なお、この場合同様にString#-String#*String#%をそれぞれの演算に合わせて実装すべきであるが、String#*の引数は整数であるため元のString#*を捨てることになるし、String#%も引数は整数の場合もあるため元のString#%も捨てることになる。

数値 + 文字列の場合

この場合、各数値クラスの+メソッドが呼ばれている。
例としてFixnum#+の実装であるnumeric.cのfix_plusを読んでみよう。

static VALUE
fix_plus(VALUE x, VALUE y)
{
    if (FIXNUM_P(y)) {
	long a, b, c;
	VALUE r;

	a = FIX2LONG(x);
	b = FIX2LONG(y);
	c = a + b;
	r = LONG2NUM(c);

	return r;
    }
    switch (TYPE(y)) {
      case T_BIGNUM:
	return rb_big_plus(y, x);
      case T_FLOAT:
	return DBL2NUM((double)FIX2LONG(x) + RFLOAT_VALUE(y));
      default:
	return rb_num_coerce_bin(x, y, '+');
    }
}

otherがFixnumでもBignumでもFloatでもない場合、rb_num_coerce_binが呼ばれている。
ここから先は実際にnumeric.cを読んでいただきたいが、最終的にはotherのcoerceメソッドをコールしている。
るりまにNumeric#coerceのページがあるのでそちらを読んでみる。

自身と other が同じクラスになるよう、自身か other を変換し [other, self] という配列にして返します。

Object#to_strほどの注意書きはないが、Numericのサブクラスは実装すべきとしている。
Stringは当然Numericのサブクラスではないためcoerceが実装されておらず、結果として+メソッドでエラーとなる。

どうしても各主要クラスの+メソッドに文字列を渡したい場合、String#coerceがあれば動作するはずだ。

class String
  def coerce other
  if other.kind_of? Integer
      return [other, self.to_i]
    elsif other.kind_of? Float
      return [other, self.to_f]
    elsif other.kind_of? Rational
      return [other, self.to_r]
    elsif other.kind_of? Complex
      return [other, self.to_c]
    else
      [other.to_f, self.to_f]
    end
  end
end

p 1 + "2" # => 3
p 1 + "a" # => 1
p 1.234 + "1.234" #=> 2.468

デフォルトではself,other共にFloatに変換するそうなので、主要数値クラスに当てはまらなければto_fするようにしてみた。

終わりに

"1" + 2は明確なポリシーを持ってエラーとしていたが、1 + "2"は理由としてはいまひとつピンと来なかった。
が、Stringは当然数値を表すクラスではないため、四則演算の対象に含まれないのは当然と言えば当然である。

代替案としてStringNumericにモンキーパッチを当てるコードを提示しているが、このパッチを当てることを推奨している訳ではない。
仮に使うとしてもModule#refineを利用するなどして限られた範囲で利用できるようにし、影響を最小限に留めるべきだろう。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?