Be cautious with Ruby coercion methods

栏目: IT技术 · 发布时间: 4年前

内容简介:Coercion is a tricky problem to solve and it’s something we need pretty much everywhere. Whether you’re building a web application or a CLI tool, you will have to coerce values in some way. I say it’s a tricky problem because Ruby comes with many builtin c

Coercion is a tricky problem to solve and it’s something we need pretty much everywhere. Whether you’re building a web application or a CLI tool, you will have to coerce values in some way. I say it’s a tricky problem because Ruby comes with many builtin coercion methods but it’s not enough. Furthermore, the builtin methods may actually lead to bugs or surprising behaviors.

In this article we’ll take a look at the builtin coercion methods, various caveats that come with their usage and ways how you can handle coercion in a more predictable and strict way.

Instance coercion methods

Let’s start with the most problematic type of coercion methods – instance coercion methods that start with #to_* . They feel very idiomatic and people use them quite often. That’s why it is important to understand how they work because you may end up with results you did not expect. Another reason is to be sure that what you’re doing is intentional.

Here’s a list of various caveats that I’m sure many of you either didn’t know about or you don’t remember about them:

Calling coercion methods on nil

I consider this to be a classic mistake in Ruby code. Here’s how it can bite you:

nil.to_i
# 0
nil.to_f
# 0.0

I know that instinctively you may think “this makes sense” – after all, nil represents “nothing”, and in the realm of numbers 0 is “nothing”. In a way, there’s some reasoning behind this behavior. The problem is that this can easily hide bugs in your code.

Here’s a typical scenario:

def total_price(line_items)
  line_items.map { |line_item| line_item[:price].to_i }.reduce(:+)
end

Can you see the problem? Correct! If the line_item[:price] returns nil then the code will still work. The bug here is that it shouldn’t be possible for the line item to have nil price, yet if that’s what happened, this method will happily perform the calculation.

The solution is to already have the price represented as an integer. In such core domain methods there should be no place for coercions. However, if you’re working with a legacy code base and you can’t refactor it just yet, then I recommend using Integer(line_item[:price]) instead. It’s more intentional and it will raise an error in case of a value that cannot be coerced, including nil .

There are more caveats in case of nil and coercion methods. Remember about them:

  1. nil.to_h => {}
  2. nil.to_a => []
  3. nil.to_f => 0.0
  4. nil.to_r => (0/1)
  5. nil.to_c => (0+0i)

Accidental string => number coercions

Another caveat is that a string that starts with a number may be mistakenly coerced into a number. How? Here’s how:

"312.to_i
# 312

"312 oh hai".to_i
# 312

As you can probably imagine, this may easily lead to bugs where a completely unexpected value ends up being coerced into a number without anybody noticing it.

Same advice as before – use Kernel coercions instead.

Handling of empty strings

This is a really weird one and I always wonder what the reasoning behind this behavior is. Using #to_* number coercions with an empty string actually returns zero (represented by different types of numbers). Here are a couple of common examples:

"".to_i
# 0

"".to_f
# 0.0

Array <=> Hash coercions

Beware that using #to_a and #to_h on empty hashes or arrays works and it can bite you. Look:

[].to_h
# {}

{}.to_a
# []

This works because it is possible to convert Array<=>Hash only when an array is a list of key/value pairs . The problem is that if you’re blindly coercing a bunch of values into, let’s say, hashes, and you don’t check anywhere that each value is a key/value pair, then you may get unexpected results.

This one’s a bit messy because even with Kernel coercions the behavior feels inconsistent:

Array({})
# []

Array({a:1})
# [[:a, 1]]

Hash([])
# TypeError (can't convert Array into Hash)

Hash([[:a, 1]])
# TypeError (can't convert Array into Hash)

In case of complex data structures it’s better to properly validate the input, check types and structure of individual values, before coercing anything.

Kernel coercion methods

A much stricter way of coercing values in Ruby is to use Kernel coercion methods, like it’s mentioned above you can use them to avoid various common caveats.

Here’s what’s available:

Integer
Float
String
Array
Hash
BigDecimal
Rational
Complex

There is a big difference between kernel coercion methods and #to_* instance methods – kernel coercions raise TypeError in case of a value that cannot be coerced. This makes them much stricter and a better fit in case of domain logic.

Furthermore, number coercions do not coerce nil – which is something that you typically want to avoid. Here’s an example with integers:

Integer(nil)
# TypeError (can't convert nil into Integer)

Float(nil)
# TypeError (can't convert nil into Float))

Libraries to the rescue

Despite the fact that Kernel coercions are stricter and safer to use in general, it won’t solve the coercion problem!

Given the complexity of coercions and various “gotchas”, I recommend using dedicated libraries that can be used to handle type conversion more safely. Long time ago I wrote virtus to help with this, eventually I created dry-types that is a far better solution if you need coercions. One of the biggest issues with virtus was that it was based on the idea of “generic coercions” – this means that its coercion mechanism would accept *a lot of different value types* causing not only performance issues but also ambiguity. When dealing with coercions you want to be as intentional and explicit as possible.

If you treat an empty string and nil the same way, it’s not duck-typing – it’s writing code that’s prone to bugs and security issues.

There are plenty of ruby gems that can help you with coercions – check out results on ruby-toolbox.com and pick up whatever works best for you.

It’s also worth to mention that coercion should be typically handled along with validation. In a perfect-world scenario you want to validate that an input can be coerced , then coerce it and validate that the output meets requirements and is valid . That’s a topic for another article.

Let me know what you think. Do you have similar experience? Do you know more caveats related to coercions? I’d love to know them!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

若为自由故

若为自由故

[美] Sam Williams / 邓楠、李凡希 / 人民邮电出版社 / 2015-4 / 49

理查德·马修·斯托曼(Richard Matthew Stallman,简称RMS)是自由软件之父,他是自由软件运动的精神领袖、GNU计划以及自由软件基金会的创立者。作为一个著名的黑客,他的主要成就包括Emacs及后来的GNU Emacs、GNU C 编译器及GDB 调试器。他编写的GNU通用公共许可证(GNU GPL)是世上最广为采用的自由软件许可证,为copyleft观念开拓出一条崭新的道路。......一起来看看 《若为自由故》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

MD5 加密
MD5 加密

MD5 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换