一个新手应该警惕什么?

我最近学习了 Ruby 编程语言,总的来说,它是一种很好的语言。但是我很惊讶地发现事情并不像我想象的那么简单。更准确地说,“最小惊喜法则”在我看来似乎并没有得到很好的尊重(当然这是相当主观的)。例如:

x = true and false
puts x  # displays true!

还有著名的:

puts "zero is true!" if 0  # zero is true!

你还会警告一个 Ruby 新手什么?

15518 次浏览

One that's caught me out in the past is that the newline character (\n) escape sequence—amongst others—isn't supported by strings within single quotes. The backslash itself gets escaped. You have to use double quotes for the escaping to work as expected.

x = (true and false) # x is false

0 and '' are true, as you pointed out.

You can have a method and a module/class by the same name (which makes sense, because the method actually gets added to Object and thus has its own namespace).

There is no multiple inheritance, but frequently "mixin modules" are used to add common methods to multiple classes.

Understanding the difference between Time and Date class. Both are different and have created issues while using them in rails. The Time class sometimes conflicts with other Time class libraries present in standard ruby/rails library. It personally took me a lot of time to understand what was exactly going on in my rails app. Later, I figured when I did

Time.new

It was referring to some library in a location that I was not even aware of.

Sorry if I am not clear with what I want to say exactly. If others have faced similar problems, please re-explain.

  • Blocks are really important to understand, they're used everywhere.

  • You don't need parentheses around method parameters. Whether you use them or not is up to you. Some say you should always use them.

  • Use raise and rescue for exception handling, not throw and catch.

  • You can use ; but you don't have to unless you want to put multiple things on one line.

When calling super with no arguments, the overridden method is actually called with the same arguments as the overriding method.

class A
def hello(name="Dan")
puts "hello #{name}"
end
end


class B < A
def hello(name)
super
end
end


B.new.hello("Bob") #=> "hello Bob"

To actually call super with no arguments, you need to say super().

I think "and" and "or" are nods to Perl, which is one of Ruby's more obvious "parents" (the most prominent other being Smalltalk). They both have much lower precedence (lower than assignment, in fact, which is where the behaviour noted comes from) than && and || which are the operators you should be using.

Other things to be aware of that aren't immediately obvious:

You don't really call methods/functions, although it kinda looks that way. Instead, as in Smalltalk, you send a message to an object. So method_missing is really more like message_not_understood.

some_object.do_something(args)

is equivalent to

some_object.send(:do_something, args) # note the :

Symbols are very widely used. That's those things that start with : and they're not immediately obvious (well they weren't to me) but the earlier you get to grips with them the better.

Ruby is big on "duck-typing", following the principal that "if it walks like a duck and quacks like a duck..." that allows informal substitution of objects with a common subset of methods without any explicit inheritance or mixin relationship.

I had a lot of trouble understanding class variables, class attributes and class methods. This code might help a newbie:

class A
@@classvar = "A1"
@classattr = "A2"
def self.showvars
puts "@@classvar => "+@@classvar
puts "@classattr => "+@classattr
end
end


A.showvars
# displays:
# @@classvar => A1
# @classattr => A2


class B < A
@@classvar = "B1"
@classattr = "B2"
end


B.showvars
# displays:
# @@classvar => B1
# @classattr => B2


A.showvars
# displays:
# @@classvar => B1   #Class variables are shared in a class hierarchy!
# @classattr => A2   #Class attributes are not

I'm new to ruby, and on my first round I hit an issue regarding changing floats/strings to an integer. I started with the floats and coded everything as f.to_int. But when I continued on and used the same method for strings I was thrown a curve when it came to run the program.

Aparently a string doesn't have a to_int method, but floats and ints do.

irb(main):003:0* str_val = '5.0'
=> "5.0"
irb(main):006:0> str_val.to_int
NoMethodError: undefined method `to_int' for "5.0":String
from (irb):6
irb(main):005:0* str_val.to_i
=> 5




irb(main):007:0> float_val = 5.0
=> 5.0
irb(main):008:0> float_val.to_int
=> 5
irb(main):009:0> float_val.to_i
=> 5
irb(main):010:0>

Arbitrary parenthesis threw me at first too. I saw some code with and some without. It took me awhile to realize that either styles are accepted.

I had trouble with mixins which contain instance methods and class methods. This code might help a newbie:

module Displayable
# instance methods here
def display
puts name
self.class.increment_displays
end
def self.included(base)
# This module method will be called automatically
# after this module is included in a class.
# We want to add the class methods to the class.
base.extend Displayable::ClassMethods
end
module ClassMethods
# class methods here
def number_of_displays
@number_of_displays # this is a class attribute
end
def increment_displays
@number_of_displays += 1
end
def init_displays
@number_of_displays = 0
end
# this module method will be called automatically
# after this module is extended by a class.
# We want to perform some initialization on a
# class attribute.
def self.extended(base)
base.init_displays
end
end
end


class Person
include Displayable
def name; @name; end
def initialize(name); @name=name; end
end


puts Person.number_of_displays # => 0
john = Person.new "John"
john.display # => John
puts Person.number_of_displays # => 1
jack = Person.new "Jack"
jack.display # => Jack
puts Person.number_of_displays # => 2

At first, I thought I could have modules with both instance methods and class methods by simply doing this:

module Displayable
def display
puts name
self.class.increment_displays
end
def self.number_of_displays  # WRONG!
@number_of_displays
end
[...]
end

Unfortunately, method number_of_displays will never be included or extended because it is a "module class method". Only "module instance methods" can be included into a class (as instance methods) or extended into a class (as class methods). This is why you need to put your mixin's instance methods into a module, and your mixin's class methods into another module (you usually put the class methods into a "ClassMethods" submodule). Thanks to the included magic method, you can make it easy to include both instance methods and class methods in just one simple "include Displayable" call (as shown in the example above).

This mixin will count each display on a per-class basis. The counter is a class attribute, so each class will have its own (your program will probably fail if you derive a new class from the Person class since the @number_of_displays counter for the derived class will never be initialized). You may want to replace @number_of_displays by @@number_of_displays to make it a global counter. In this case, each class hierarchy will have its own counter. If you want a global and unique counter, you should probably make it a module attribute.

All of this was definitely not intuitive for me when I started with Ruby.

I still can't figure out how to cleanly make some of these mixin methods private or protected though (only the display and number_of_displays method should be included as public methods).

I think it is always good to use .length on things... since size is supported by nearly everything and Ruby has dynamic types you can get really weird results calling .size when you have the wrong type... I would much rather get a NoMethodError: undefined method `length', so I generally never call size on objects in Ruby.

bit me more than once.

Also remember objects have ids, so I try not to use variables call id or object_id just to avoid confusion. If I need an id on a Users object it is best to call it something like user_id.

Just my two cents

one thing i learned was to use the operator ||= carefully. and take special care if you are dealing with booleans. i usually used a ||= b as a catch all to give 'a' a default value if everything else failed and 'a' remained nil. but if a is false and b is true, then a will be assigned true.

Newbies will have trouble with equality methods:

  • a == b : checks whether a and b are equal. This is the most useful.
  • a.eql? b : also checks whether a and b are equal, but it is sometimes more strict (it might check that a and b have the same type, for example). It is mainly used in Hashes.
  • a.equal? b : checks whether a and b are the same object (identity check).
  • a === b : used in case statements (I read it as "a matches b").

These examples should clarify the first 3 methods:

a = b = "joe"


a==b       # true
a.eql? b   # true
a.equal? b # true (a.object_id == b.object_id)


a = "joe"
b = "joe"


a==b       # true
a.eql? b   # true
a.equal? b # false (a.object_id != b.object_id)


a = 1
b = 1.0


a==b       # true
a.eql? b   # false (a.class != b.class)
a.equal? b # false

Note that ==, eql? and equal? should always be symmetrical : if a==b then b==a.

Also note that == and eql? are both implemented in class Object as aliases to equal?, so if you create a new class and want == and eql? to mean something else than plain identity, then you need to override them both. For example:

class Person
attr_reader name
def == (rhs)
rhs.name == self.name  # compare person by their name
end
def eql? (rhs)
self == rhs
end
# never override the equal? method!
end

The === method behaves differently. First of all it is not symmetrical (a===b does not imply that b===a). As I said, you can read a===b as "a matches b". Here are a few examples:

# === is usually simply an alias for ==
"joe" === "joe"  # true
"joe" === "bob"  # false


# but ranges match any value they include
(1..10) === 5        # true
(1..10) === 19       # false
(1..10) === (1..10)  # false (the range does not include itself)


# arrays just match equal arrays, but they do not match included values!
[1,2,3] === [1,2,3] # true
[1,2,3] === 2       # false


# classes match their instances and instances of derived classes
String === "joe"   # true
String === 1.5     # false (1.5 is not a String)
String === String  # false (the String class is not itself a String)

The case statement is based on the === method:

case a
when "joe": puts "1"
when 1.0  : puts "2"
when (1..10), (15..20): puts "3"
else puts "4"
end

is equivalent to this:

if "joe" === a
puts "1"
elsif 1.0 === a
puts "2"
elsif (1..10) === a || (15..20) === a
puts "3"
else
puts "4"
end

If you define a new class whose instances represent some sort of container or range (if it has something like an include? or a match? method), then you might find it useful to override the === method like this:

class Subnet
[...]
def include? (ip_address_or_subnet)
[...]
end
def === (rhs)
self.include? rhs
end
end


case destination_ip
when white_listed_subnet: puts "the ip belongs to the white-listed subnet"
when black_listed_subnet: puts "the ip belongs to the black-listed subnet"
[...]
end

Wikipedia Ruby gotchas

From the article:

  • Names which begin with a capital letter are treated as constants, so local variables should begin with a lowercase letter.
  • The characters $ and @ do not indicate variable data type as in Perl, but rather function as scope resolution operators.
  • To denote floating point numbers, one must follow with a zero digit (99.0) or an explicit conversion (99.to_f). It is insufficient to append a dot (99.), because numbers are susceptible to method syntax.
  • Boolean evaluation of non-boolean data is strict: 0, "" and [] are all evaluated to true. In C, the expression 0 ? 1 : 0 evaluates to 0 (i.e. false). In Ruby, however, it yields 1, as all numbers evaluate to true; only nil and false evaluate to false. A corollary to this rule is that Ruby methods by convention — for example, regular-expression searches — return numbers, strings, lists, or other non-false values on success, but nil on failure (e.g., mismatch). This convention is also used in Smalltalk, where only the special objects true and false can be used in a boolean expression.
  • Versions prior to 1.9 lack a character data type (compare to C, which provides type char for characters). This may cause surprises when slicing strings: "abc"[0] yields 97 (an integer, representing the ASCII code of the first character in the string); to obtain "a" use "abc"[0,1] (a substring of length 1) or "abc"[0].chr.
  • The notation statement until expression, unlike other languages' equivalent statements (e.g. do { statement } while (not(expression)); in C/C++/...), actually never runs the statement if the expression is already true. This is because statement until expression is actually syntactic sugar over

    until expression
    statement
    end
    

    , the equivalent of which in C/C++ is while (not(expression)) statement; just like statement if expression is an equivalent to

    if expression
    statement
    end
    

    However, the notation

    begin
    statement
    end until expression
    

    in Ruby will in fact run the statement once even if the expression is already true.

  • Because constants are references to objects, changing what a constant refers to generates a warning, but modifying the object itself does not. For example, Greeting << " world!" if Greeting == "Hello" does not generate an error or warning. This is similar to final variables in Java, but Ruby does also have the functionality to "freeze" an object, unlike Java.

Some features which differ notably from other languages:

  • The usual operators for conditional expressions, and and or, do not follow the normal rules of precedence: and does not bind tighter than or. Ruby also has expression operators || and && which work as expected.

  • def inside def doesn't do what a Python programmer might expect:

    def a_method
    x = 7
    def print_x; puts x end
    print_x
    end
    

    This gives an error about x not being defined. You need to use a Proc.

Language features

  • Omission of parentheses around method arguments may lead to unexpected results if the methods take multiple parameters. The Ruby developers have stated that omission of parentheses on multi-parameter methods may be disallowed in future Ruby versions; the current (November 2007) Ruby interpreter throws a warning which encourages the writer not to omit (), to avoid ambiguous meaning of code. Not using () is still common practice, and can be especially nice to use Ruby as a human readable domain-specific programming language itself, along with the method called method_missing().

Related to monkut's response, Ruby's to_foo methods hint at how strict a conversion they'll do.

Short ones like to_i, to_s tell it to be lazy, and convert them to the target type even if they're not able to be represented accurately in that format. For example:

"10".to_i == 10
:foo.to_s == "foo"

The longer explicit functions like to_int, to_s mean that the object can be natively represented as that type of data. For example, the Rational class represents all rational numbers, so it can be directly represented as a Fixnum (or Bignum) integer by calling to_int.

Rational(20,4).to_int == 5

If you can't call the longer method, it means the object can't be natively represented in that type.

So basically, when converting, if you're lazy with the method names, Ruby will be lazy with the conversion.

Pay attention to the Range notation.

(At least, pay more attention than I initially did!)

There is a difference between 0..10 (two dots) and 0...10 (three dots).

I enjoy Ruby a great deal. But this dot-dot versus dot-dot-dot thing bugs me. I think that such a subtle dual-syntax "feature" that is:

  • easy to mistype, and
  • easy to miss with your eyes while glancing over the code

should not be able to cause devastating off-by-one bugs in my programs.

Iteration over ruby hashes aren't guaranteed to happen in any particular order. (It's not a bug, it's a feature)

Hash#sort is useful if you need a particular order.

Related question: Why are Ruby’s array of 1000 hashes' key and value pairs always in a particular order?

Blocks and methods return the value of the last line by default. Adding puts statements to the end for debugging purposes can cause unpleasant side effects

The following code surprised me. I think it's a dangerous gotcha: both easy to run into, and hard to debug.

(1..5).each do |number|
comment = " is even" if number%2==0
puts number.to_s + comment.to_s
end

This prints:

1
2 is even
3
4 is even
5

But if I just add comment =anything before the block...

comment = nil
(1..5).each do |number|
comment = " is even" if number%2==0
puts number.to_s + comment.to_s
end

Then I get:

1
2 is even
3 is even
4 is even
5 is even

Basically, when a variable is only defined inside a block, then it is destroyed at the end of the block, and then it gets reset to nil upon every iteration. That's usually what you expect. But if the variable is defined before the block, then the outer variable is used inside the block, and its value is therefore persistent between iterations.

One solution would be to write this instead:

comment = number%2==0 ? " is even" : nil

I think a lot of people (including me) tend to write "a = b if c" instead of "a = (c ? b : nil)", because it's more readable, but obviously it has side-effects.

1..5.each {|x| puts x}

doesn't work. You have to put the range into parentheses, like

(1..5).each {|x| puts x}

so it doesn't think you're calling 5.each. I think this is a precedence issue, just like the x = true and false gotcha.

From In Ruby why won't foo = true unless defined?(foo) make the assignment?

foo = true unless defined?(foo) #Leaves foo as nil

Because foo is defined as nil when defined?(foo) is called.

If you declare a setter (aka mutator) using attr_writer or attr_accessor (or def foo=), be careful of calling it from inside the class. Since variables are implicitly declared, the interpreter always has to resolve foo = bar as declaring a new variable named foo, rather than calling the method self.foo=(bar).

class Thing
attr_accessor :foo
def initialize
@foo = 1      # this sets @foo to 1
self.foo = 2  # this sets @foo to 2
foo = 3       # this does *not* set @foo
end
end


puts Thing.new.foo #=> 2

This also applies to Rails ActiveRecord objects, which get accessors defined based on fields in the database. Since they're not even @-style instance variables, the proper way to set those values individually is with self.value = 123 or self['value'] = 123.

Methods can be redefined and can become a mind-scratcher until you discover the cause. (Admittedly, this error is probably a bit "harder" to detect when a Ruby on Rails controller's action is re-defined by mistake!)

#demo.rb
class Demo


def hello1
p "Hello from first definition"
end


# ...lots of code here...
# and you forget that you have already defined hello1


def hello1
p "Hello from second definition"
end


end
Demo.new.hello1

Run:

$ ruby demo.rb
=> "Hello from second definition"

But call it with warnings enabled and you can see the reason:

$ ruby -w demo.rb
demo.rb:10: warning: method redefined; discarding old hello1
=> "Hello from second definition"

This one made me mad once:

1/2 == 0.5 #=> false
1/2 == 0   #=> true