简体   繁体   中英

Reading in from file in Ruby with while-loop

I've found the following bit of code used frequently when you want to read in a file line-by-line in Ruby:

while (line = fileobject.gets)
  # code block such as 'puts line' or something
end

I just need some help understanding what is going on there. I know that 'while' is to be followed by a boolean expression, and then the code block will be repeated until the expression returns 'false'.

So here, the boolean expression is line = fileobject.gets ...but how is that evaluated as true or false? To me it looks like an assignment statement, that is, you're assigning 'line' to be whatever the next line of the fileobject is.

I understand that this WILL work for reading in from text files line by line, but I'm not comfortable using it until I know WHY it works. Maybe I'm just too used to C++ with its counters and incrementing. Thanks!

The first principle that will help you understand this is that in Ruby, conditions aren't expected to be just true or false, they can be any value. The value is then considered 'truthy' or 'falsey' - that is - like being true of like being false. In Ruby, there are only two values that are falsey (act like false): false itself and nil .

So for example:

if nil  # nil is 'falsey'
  # Won't go here!
else
  # Will go here!
end

if 'randomstring'   # any string is 'truthy'
  # will go here!
end  

This idea applies the same way to while loops.

Next up is the gets method. If you check out the documentation, you can see that the IO::gets method returns nil when it reaches the end of the file. So, when that happens, line is set to nil , which is a fasley value, and the loop exits.


Short version: nil is like false and gets returns nil when it reaches the end of the file.

I've found the following bit of code used frequently when you want to read in a file line-by-line

I don't know what articles you've been reading, but no well grounded rubyist would ever read a file like that. Instead they would do something like this:

IO.foreach('data1.txt') do |line|
  print line
end

As for this:

line = fileobject.gets

In order for ruby to execute that assignment, ruby has to first execute fileobject.gets . And fileobject.gets either returns a string or nil(when end of file is reached). And any string is considered true in ruby--even blank strings. For example:

str = ""

if str
  puts "true"
else
  puts "false"
end

--output:--
true

So the code:

line = fileobject.gets

is equivalent to:

line = "some string"

or

line = nil

Finally, an assignment returns the right hand side, so you are left with:

while "some string"

or

while nil

and ruby evaluates while "some string" as while true ; and ruby evaluates while nil as while false because in ruby only nil and false are false in a boolean context, eg in an if or while conditional.

The Analysis

while (line = fileobject.gets); end

This isn't really idiomatic Ruby; it's more of a Perl-ish way of doing things, although it certainly works. The reason it works is that almost everything in Ruby is an expression that returns some sort of value. For example:

# Open some file for reading.
fileobject = File.open '/etc/passwd'

# As long as File#gets evaluates as truthy, keep going.
while line = fileobject.gets
  puts line
end

The "magic" here is that the expression line = fileobject.gets returns nil when it reaches EOF, and so the while condition evaluates as falsey. Until then, every time fileobject.gets returns a string the expression evaluates as truthy, so the while-loop just keeps chugging along and assigning successive lines to your line variable. See IO#gets for more on this useful method.

A Better Way with File Objects

A more idiomatic way to do this would be to do this in a self-closing block. for example:

File.open '/etc/passwd' do |file|
  file.each_line { |line| puts line }
end

Self-closing blocks are (possibly) just as "magical" as while-loops that leverage the truthiness of IO#gets, but it certainly seems clearer and more Ruby-like than the original code sample. YMMV.

Even More Compact Using IO Objects

As another answer reminded me, the following is also very idiomatic in Ruby:

IO.foreach('/etc/passwd') { |line| puts line }

This leverages IO::foreach to implicitly open the named file, pass each line through the block for processing, and then closing the file automatically when the block ends. It is very similar to the previous example above, but has the advantage of being shorter and (in some cases) semantically clearer. Whether or not the implicit file handling is too "magical" or not for your taste, it's definitely worth knowing this particular construct too.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM