在 Ruby 中实现 to_int 和 to_str 的后果
我 有一个类,它公开了一个字符串值并且一个 int 值(分别是命令输出和退出代码)。除了通过 to_s
和 to_i
公开它们之外,我还使用 to_str
和 to_int
,如下所示:
class Status
def to_s
@output
end
alias :to_str :to_s
def to_i
@status.exitstatus
end
alias :to_int :to_i
end
我的想法是能够在尽可能多的情况下使用这个对象。将其强制转换为字符串或整数可以提高可用性。例如,我可以将对象与字符串连接起来:(
a_string = "Output was: " + results
我想用它作为 int 强制转换的示例,但 Fixnum.+ 不喜欢它,所以它实际上不起作用:)
an_int = 1 + results
我读过的所有内容到目前为止,这可能是一件“坏”事。常见的主题是这样的:“当你的对象可以表示为 string/int 时,使用 to_s
/to_i
,但是 to_str
/to_int
仅当您的对象基本上是一个字符串/int”。
毫无疑问,我的类“从根本上”不是字符串或整数。然而 我对这条规则有一些问题:
- 它使我的班级不太灵活/可用。例如:如果没有 Status.to_str,我无法使用 String.+ 将 Status 输出与其他字符串连接起来。
- 这似乎违背了鸭子打字的精神。对象的用户(即:将其作为参数获取的方法)不应该关心该对象是什么,而应该只关心它可以做什么。 (在这种情况下,“do”意味着“可以表示为字符串/整数”。)
- “本质上是字符串/整数”的参数对我来说非常模糊。例如,您会看到
Float.to_int
被多次提及。故事是这样的,由于浮点数总是有一个整数部分,所以to_int
是一个有效的方法。然而,我认为这是虚假的:Float不是一个整数(因为它有一个非整数分量),因此尝试等同它们的“类型”没有多大意义。您可以合法地将 Float 转换为整数(通过截断),但是我可以说我也可以将我的 Status 转换为整数(通过“截断”所有非退出代码信息)。
所以,我的问题是:实施 to_str
和 to_int
是否有任何真正的(即实际的)危害?
更新:Jörg W Mittag 举了一个例子,让我想到了一些事情。重新表述一下问题:当您已经拥有 to_s
/to_i
时,是否真的需要拥有 to_str
/to_int
? (除了特定方法已经期望 to_str
而不是 to_s
)
例如,在 Jörg 的 Array.join 示例中,数组成员在转换分隔符时通过 to_s 进行转换通过 to_str。但这真的有必要吗?如果 Array.join 调用separator.to_s,那么您可以成功地将更多对象传递给它(例如:整数、符号等)并获得更大的灵活性。 Ruby 会从这种分离中受益吗?
I have a class which exposes a string value and an int value (a command output and exit code respectively). In addition to exposing them through to_s
and to_i
, I'm also using to_str
and to_int
, like so:
class Status
def to_s
@output
end
alias :to_str :to_s
def to_i
@status.exitstatus
end
alias :to_int :to_i
end
My idea behind this is to be able to use this object in as many situations as possible. having it coerceable to a string or int increases that usability. For instance, I can concatenate the object with a string:
a_string = "Output was: " + results
(I wanted to use this as an example of int coercion, but Fixnum.+ doesn't like it, so it doesn't actually work:)
an_int = 1 + results
Everything I've read so far has said that this is probably a "bad" thing to do. The common theme goes like this: "Use to_s
/to_i
when your object can be represented as a string/int, but to_str
/to_int
only if your object is fundamentally a string/int".
There's no question that my class is not "fundamentally" a string or an int. However
I have some problems with this rule:
- It makes my class less flexible/usable. For example: I couldn't use String.+ to concatenate the Status output with the other string if I didn't have Status.to_str.
- It seems to violate the spirit of duck-typing. The user of an object (ie: a method that gets it as a parameter) shouldn't care what that object is, it should only care what it can do. (In this case, "do" means "can be represented as a string/int".)
- The arguments for "is fundamentally a string/int" are pretty fuzzy to me. For example, you'll see that
Float.to_int
is mentioned a lot. The story goes that since a floating-point number always has an integer component,to_int
is a valid method. However, I think this is spurious: a Float is not an integer (as it has a non-integer component) and so trying to equate their "typeness" doesn't make much sense. You can legitimately convert a Float to an integer (through truncation), but then I can say that I can convert my Status to an integer as well (by "truncating" all of the non-exit-code information).
So, my question is: is there any real (ie: practical) harm in implementing to_str
and to_int
?
Update: Jörg W Mittag gave an example that made me think of something. To rephrase the question: is there really a need to have to_str
/to_int
when you already have to_s
/to_i
? (Besides the fact that particular methods are already expecting to_str
over to_s
)
For instance, in Jörg's Array.join example, the array members are converted via to_s while the separator is converted via to_str. But is this really necessary? If Array.join called separator.to_s instead, then you could successfully pass many more objects to it (ex: Integers, Symbols, etc) and gain that much more flexibility. Does Ruby benefit from having this separation?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
这是一个糟糕的例子,因为连接字符串是 Ruby 不惯用的做法。字符串插值是首选方式:
这正常™,因为字符串插值实际上对插值表达式的结果调用
to_s
。我认为“可以表示为字符串/整数”并不是真正的行为。 IOW:“对象可以做什么”是关于特定上下文中有趣的行为,而“可以表示为字符串/整数”并不是真正有趣的行为。
例如,如果您说“Status IS-A Integer”(这本质上就是
to_int
的含义),那么您可以用它进行算术运算。但是“将 42 添加到未找到的文件”到底是什么意思呢?成功的对数是多少?失败的平方根是多少?在上面的字符串插值示例中,有趣的行为是“可以显示”。这基本上是通过实现
#to_s
来表明的。连接两个字符串,OTOH,需要两个字符串。再说一遍,这是一个相当无力的论点,因为我实际上同意你的观点:那是错误的。
在德国法中,我们有一个难以理解且不直观的原则,但我认为它完全适用于这里。它被称为“Keine Gleichheit im Unrecht”(错误中没有平等)。这意味着宪法赋予的平等基本权利仅在法律范围内适用。换句话说:橙汁并不能使谋杀合法化。
因此,仅仅因为 Ruby 核心库中存在垃圾代码(相信我,有很多),并不意味着您也可以编写垃圾代码 :-)
在这种特殊情况下,
Float#to_int
完全是错误的,不应该存在。Float
不是Integer
的子类型。乍一看,情况恰恰相反,即 Integer#to_float 是有效的,但实际上也不是这样:在 Ruby 中,Integer 具有任意精度,但是Float
具有固定精度。实现 Fixnum#to_float 是有效的,但这将是一个坏主意,因为Integer
可以神奇地从Fixnum 转换
到BigInteger
并返回,因此#to_float
方法会“神奇地”出现和消失。最终帮助我理解
to_x
和to_xyz
之间的区别的是Array#join
:它打印出数组的元素,由分隔符对象分隔。它通过对数组的每个元素调用to_s
和对分隔符调用to_str
来实现此目的。一旦您了解了为什么它在一个上调用to_s
并在另一个上调用to_str
,您就基本上准备好了。(尽管您对
Float#to_int
的评论已经表明您确实理解。)旁注:对于代数上下文中的双重分派,Ruby实际上使用
#coerce
协议。因此,如果您希望1 + a_status
示例正常工作,则需要实现Status#coerce
。但是,请不要。This is a bad example, because concatenating strings is unidiomatic Ruby. String interpolation is the preferred way:
And this just works™, because string interpolation actually calls
to_s
on the result of the interpolated expression.I would argue that "can be represented as a string/int" isn't really behavior. IOW: "what the object can do" is about interesting behavior in a certain context, and "can be represented as a string/int" isn't really interesting behavior.
If you say that "Status IS-A Integer" (which is essentially what
to_int
means), then it follows that you can, for example, do arithmetic with it. But what does it even mean to "add 42 to file not found"? What is the logarithm of success? What is the square root of failure?In the above string interpolation example, the interesting behavior is "can be displayed". And that is basically indicated by implementing
#to_s
. Concatenating two strings, OTOH, requires, well, two strings.Again, this is a rather weak argument, because I actually agree with you: that's wrong.
In German Law, we have a principle which is hard to grasp and un-inituitive, but which I think applies here perfectly. It is called "Keine Gleichheit im Unrecht" (No Equality in Wrongness). It means that the basic right of Equaliy, which is granted in the Constitution, only applies within the Law. To put it another way: OJ doesn't make murder legal.
So, just because there is crap code in the Ruby core library (and believe me, there is a lot), doesn't mean you get to write crap, too :-)
In this particular case,
Float#to_int
is just plain wrong and shouldn't exist.Float
is not a subtype ofInteger
. At first glance, the opposite seems to be true, i.e.Integer#to_float
is valid, but actually that's not true, either: in Ruby,Integer
s have arbitrary precision, butFloat
s have fixed precision. It would be valid to implementFixnum#to_float
, but that would be a bad idea, sinceInteger
s can magically convert fromFixnum
toBigInteger
and back, and thus the#to_float
method would "magically" appear and vanish.The thing that finally helped me understand the difference between
to_x
andto_xyz
wasArray#join
: it prints out the elements of the array, separated by a separator object. It does this by callingto_s
on each element of the array andto_str
on the separator. Once you understand why it callsto_s
on one andto_str
on the other, you're basically set.(Although your comments on
Float#to_int
already indicate that you do understand.)Side note: for double dispatch in an algebraic context, Ruby actually uses the
#coerce
protocol. So, if you wanted the1 + a_status
example to work, you would need to implementStatus#coerce
. But, please don't.鸭子类型的精神肯定不会让人查找 Status 对象的源代码来找出返回的内容。
就我个人而言,我认为您应该通过两个实例方法公开文本结果和退出状态:
然后按如下方式使用它
,这对于任何阅读代码的人来说都立即有意义,恕我直言。
The spirit of duck-typing certainly wouldn't have someone look up the source code for the Status object to figure out what is getting returned.
Personally I think you should expose the text results and the exit status through two instance methods:
And then use it as follows
Which immediately makes sense to anyone reading the code, IMHO.