- The Zen of Python
Just like Ruby, Rails offers multiple ways to do the same thing. Unlike The Zen of Python, there's no one obvious way to do it. A good example is finding the number of elements in a collection. You can use the length
, size
, and count
methods to accomplish this.
However, it can be confusing to understand when to use which method, as there are a few subtle differences between them, and your choice may have performance implications. Before understanding the difference between these methods, I was never really sure if I had used the correct one.
This post explores the difference between these methods and explains how you should choose which method to use according to the needs of your application.
TL;DR Here's a handy flowchart to simplify your decision-making. It may look daunting, but it really isn't.
Let's start with a brief overview of these methods in plain Ruby, specifically in the context of an Array
. Then we'll move on to Rails.
length
This is the simplest of them all. It's also the method you're most familiar with.
The length
method returns the number of elements in an array. It runs in O(1)
constant time.
numbers = [1, 2, 3, 4, 5]
numbers.length # 5
It can't get any simpler than this.
size
This is an alias to the length
method. If you try to read the Ruby docs for size
, it simply redirects you to the length
method.
numbers = [1, 2, 3, 4, 5]
numbers.length # 5
Should you use length
or size
? It's a matter of personal preference, depending on the context in which you need the number of elements in the collection. Choose the one that is most readable for your context.
For example, if I have a collection named classroom
that contains a bunch of students
, I'd use classroom.size
instead of classroom.length
. On the other hand, if I am dealing with an array of numbers, I might use length
. Both these methods accomplish the same thing, but the flow and focus is subtly different.
count
This method counts the number of specified elements in the array. Use it when you need to find the count of elements matching specific criteria. There are four variations of this method.
1. With no argument and no block, returns the count of all elements
[10, 20, 30].count # => 3
[].count # => 0
2. When an argument is provided, returns the count of array elements that are equal to the argument.
[0, 1, 2, 0.0].count(0) # => 2
[0, 1, 2].count(3) # => 0
3. When a block is provided without any argument, calls the block with each array element and returns the count of elements for which the block returns a truthy
value.
[0, 1, 2, 3].count { |e| e > 1 } # => 2
4. If you pass both an argument and a block, it ignores the block and returns the count of elements that are equal to the argument. It also issues a warning.
[1, 2, 3].count(2) { |e| e == 0 }
(irb):2: warning: given block not used
=> 1
That's it for Ruby. Now let's turn our attention to Rails, where things get quite interesting.
Ruby on Rails
We will examine these methods in the context of the ActiveRecord::Associations::CollectionProxy
class, which inherits from the ActiveRecord::Relation
class.
class Post < ActiveRecord::Base
has_many :comments
end
post = Post.last
comments = post.comments
# Comment::ActiveRecord_Associations_CollectionProxy
comments.class
In this example, comments
is a collection proxy, delegating to a collection of posts. Now let's understand how these methods work on a collection proxy.
size
If the collection hasn’t been loaded, the size
method executes a SELECT COUNT(*)
query. Otherwise, it calls collection.size
.
Post.all.size
# SELECT COUNT(*) FROM `posts`
Behind the scenes, this is how the size
method works.
length
Returns the size of the collection by calling the size
method on it.
Post.all.length
# SELECT `posts`.* FROM `posts`
That means, if the collection has been already loaded, length
and size
are equivalent.
When to use length
vs. size
?
When the collection is already loaded, the size
and length
methods are equal.
If the collection is not loaded from the database:
- If you will need the records anyway, use the
length
method. It will take one less query, since Rails will use the cached records for the later query. - If you won't need the records, use the
size
method as it's more efficient. It will run aCOUNT
SQL query to fetch the count directly from the database, without loading the records in memory.
post.comments.length
Post Load (2.4ms) SELECT `posts`.* FROM `posts` ORDER BY `posts`.`id` ASC LIMIT 1
Comment Load (0.5ms) SELECT `comments`.* FROM `comments` WHERE `comments`.`post_id` = 1
post.comments.size
Post Load (0.4ms) SELECT `posts`.* FROM `posts` ORDER BY `posts`.`id` ASC LIMIT 1
Comment Count (1.9ms) SELECT COUNT(*) FROM `comments` WHERE `comments`.`post_id` = 1
count
Just like Ruby's count
method, the count
method in Rails counts all the records in a collection that satisfy a given condition. There are two variations of this method:
1. When a block is not provided, returns the count of all records in the collection
posts = Post.all
# SELECT COUNT(*) FROM `posts`
posts.count
2. When a block is provided, calls the block with each collection element and returns the count of elements for which the block returns a truthy
value.
Post.all.count { |p| p.title.include?('-') }
I use the count
method when I need the count of elements that match a certain criteria. Otherwise, I just stick to one of the length
or size
methods.
That's a wrap. I hope you found this article helpful and you learned something new.
As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.
If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.