Ruby でクラスのインスタンスの get メソッドを実装し、それでもオブジェクト メソッドにアクセスするにはどうすればよいですか?
概要
クラスのインスタンスを変数に書き込み、値の取得とメソッドへのアクセスの両方にアクセスできるようにする必要があります。これを何とか実装できないでしょうか?
例えば:
class A
def initialize(value)
@value = value
end
def preview
puts "Class preview: #{@value}"
end
def something(param)
puts "Something method: #{@value * param}"
end
end
class B
attr_reader :obj
def set_object(obj)
@obj = obj
end
end
b = B.new
b.set_object(A.new(5))
b.obj # ==> 5
10 + b.obj # ==> 15
b.obj.preview # ==> "Class preview: 5"
b.obj.something(3) # ==> "Something method: 15"
解決策
コードの根本的な問題は、数値のように動作するオブジェクトを実装しているにもかかわらず、Numeric クラスによって規定されている数値のような型に関する Ruby のプロトコルに従っていないことです。
特に、Numeric#coerce メソッドによって提供される算術強制プロトコルが欠落しています。
Ruby では、算術演算子がオペランドをどう処理すればよいかわからない場合は常に、オペランドに強制メッセージを送信し、対処方法がわかっているオペランドのペアで応答するように指示します。
たとえば、+ メッセージを 10 に送信し、引数として A のインスタンスを渡すと、メソッド Integer#+ が呼び出されます。したがって、この行では次のようになります。
10 + b.obj
ここでは、整数リテラル 10 (の評価結果) にメッセージ + を送信し、式 b.obj (つまり A のインスタンス) の評価結果を引数として渡しています。
つまり、ここで得られるものは本質的に次のとおりです。
some_integer + some_a
ここで、問題は、もちろん、Integer#+ が A のインスタンスをそれ自体に追加する方法を知らないことです。ただし、すべての算術演算は算術強制プロトコルを観察します。つまり、Integer#+ の実装は次のようになります。
class Integer
def +(other)
if other.is_a?(Integer)
# I know what to do!
# Do whatever internal magic computes the sum of two `Integer`s
else
coerced_self, coerced_other = other.coerce(self)
coerced_self + coerced_other
end
end
end
トリックがわかりますか? Integer#+ は、Integer と As を追加する方法を知りません。Integer#+ がこの時点でほぼ 30 年前に書かれているのに対し、あなたは今日 A を書いたばかりなので、これは驚くべきことではありません。ただし、Integer は標準の Ruby クラスであるため、A が整数の処理方法を知っていることが前提となっています。
したがって、ここで Integer#+ が行うことは、A の強制メソッドを呼び出して「あなたが何者なのか知りませんが、ここでは私自身を引数として渡しています。あなたが私が何であるかを知っていただければ幸いです。だから私自身とあなた自身を、その二つを足し合わせる方法を知っている何かに変えてください。」
つまり、A の強制メソッドを実装する必要があります。強制のプロトコルは次のとおりです。
そこで、強制を実装しましょう。
class A
def coerce(other) = [other, @value]
end
ここで、質問内のコードを実行すると、次の結果が得られます。
#<A:0x0000000101021cd0 @value=5>
15
Class preview: 5
Something method: 15
ご覧のとおり、式 10 + b.obj は正しく 15 と評価されました。
コードに関する次の問題は、オブジェクトによって常にオーバーライドされるべき標準メソッドの一部をオーバーライドしていないことです。 BasicObject#==、Object#eql?、Object#hash、Object#to_s などのメソッドについて話しています。
特に、人間が判読できるオブジェクトのデバッグ表現を表示するために送信されるメッセージは検査されます。 Object#inspect をオーバーライドしていないため、クラスに関する情報、実装定義の識別子、およびインスタンス変数とその値のリストを含むデフォルトの実装を取得します。
次のように動作するには、inspect をオーバーライドする必要があります。
class A
def inspect = @value.inspect
end
ここで、質問内のコードを実行すると、望ましい結果が得られます。
5
15
Class preview: 5
Something method: 15
コードには他にも改善できる点がいくつかあります。
これをすべてまとめると、次のようになります。
class A < Numeric
include Comparable
def initialize(value)
super()
@value = value
end
def to_int = @value
alias to_i to_int
def to_s = @value.to_s
alias inspect to_s
def +(other)
case other
when A
self.class.new(@value + other.value)
when Integer, Float, BigDecimal, Rational, Complex
self.class.new(@value + other)
else
raise(TypeError, "Don't know how to add #{other.inspect} of class #{other.class}") unless other.respond_to?(:coerce)
coerced_self, coerced_other = other.coerce(self)
coerced_self + coerced_other
end
end
def -(other)
case other
when A
self.class.new(@value - other.value)
when Integer, Float, BigDecimal, Rational, Complex
self.class.new(@value - other)
else
raise(TypeError, "Don't know how to add #{other.inspect} of class #{other.class}") unless other.respond_to?(:coerce)
coerced_self, coerced_other = other.coerce(self)
coerced_self - coerced_other
end
end
def *(other)
case other
when A
self.class.new(@value * other.value)
when Integer, Float, BigDecimal, Rational, Complex
self.class.new(@value * other)
else
raise(TypeError, "Don't know how to add #{other.inspect} of class #{other.class}") unless other.respond_to?(:coerce)
coerced_self, coerced_other = other.coerce(self)
coerced_self * coerced_other
end
end
def /(other)
case other
when A
self.class.new(@value / other.value)
when Integer, Float, BigDecimal, Rational, Complex
self.class.new(@value / other)
else
raise(TypeError, "Don't know how to add #{other.inspect} of class #{other.class}") unless other.respond_to?(:coerce)
coerced_self, coerced_other = other.coerce(self)
coerced_self / coerced_other
end
end
def coerce(other)
case other
when Integer, Float, BigDecimal, Rational, Complex
[self.class.new(other), self]
else
[other, to_int]
end
end
def <=>(other) = to_i <=> other.to_i
def preview
puts("Class preview: #{self}")
end
def something(param)
puts("Something method: #{self * param}")
end
protected
attr_reader(:value)
end
class B
attr_reader :obj
def initialize(obj)
@obj = obj
end
end