【问题标题】:What are the Ruby Gotchas a newbie should be warned about? [closed]新手应该注意哪些 Ruby 陷阱? [关闭]
【发布时间】:2010-09-27 05:22:13
【问题描述】:

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

x = true and false
puts x  # displays true!

和著名的:

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

您会警告 Ruby 新手的其他“陷阱”是什么?

【问题讨论】:

  • @phrases.insert(0, p) 好的 @phrases.insert(p) 什么都不会发生 @phrases
  • 为什么true and false 返回true?
  • 因为“x = true and false”实际上被解释为“(x = true) and false”。这是运算符优先级的问题:“and”的优先级低于“=”。大多数其他语言都有相反的优先级,我不知道他们为什么在 Rails 中选择这个顺序,我觉得很混乱。如果您想要“正常”的行为,只需输入“x = (true and false)”,那么 x 将为 false。
  • 另一种解决方案是使用“&&”和“||”而不是“and”和“or”:它们的行为符合预期。例如:“x = true && false”导致 x 为假。
  • “最小意外原则意味着最小我的意外原则。”来自en.wikipedia.org/wiki/Ruby_(programming_language)#Philosophy Python 也是如此。我有类似的关于 Python 的创建者的引用,但我忘记了它在哪里。

标签: ruby


【解决方案1】:

Wikipedia Ruby gotchas

来自文章:

  • 以大写字母开头的名称被视为常量,因此局部变量应以小写字母开头。
  • 字符 $@ 不像 Perl 中那样指示可变数据类型,而是用作范围解析运算符。
  • 要表示浮点数,后面必须跟一个零位 (99.0) 或显式转换 (99.to_f)。附加一个点 (99.) 是不够的,因为数字容易受到方法语法的影响。
  • 非布尔数据的布尔求值是严格的:0""[] 都被求值为 true。在 C 中,表达式 0 ? 1 : 0 的计算结果为 0(即 false)。然而,在 Ruby 中,它产生1,因为所有数字的计算结果都是true;只有 nilfalse 评估为 false。这条规则的一个推论是 Ruby 方法按照惯例——例如,正则表达式搜索——成功时返回数字、字符串、列表或其他非 false 值,但失败时返回 nil(例如,不匹配)。此约定也用于 Smalltalk,其中只有特殊对象 truefalse 可以在布尔表达式中使用。
  • 1.9 之前的版本缺少字符数据类型(与 C 相比,它为字符提供类型 char)。这可能会在对字符串进行切片时引起意外:"abc"[0] 产生 97(一个整数,表示字符串中第一个字符的 ASCII 码);要获取"a",请使用"abc"[0,1](长度为1 的子字符串)或"abc"[0].chr
  • 符号 statement until expression 与其他语言的等效语句(例如 C/C++/...中的 do { statement } while (not(expression));)不同,如果表达式已经是 true,则实际上永远不会运行该语句。这是因为statement until expression 实际上是语法糖

    until expression
      statement
    end
    

    ,在 C/C++ 中相当于 while (not(expression)) statement; 就像 statement if expression 相当于

    if expression
      statement
    end
    

    但是,符号

    begin
      statement
    end until expression
    

    在 Ruby 中,即使表达式已经为真,实际上也会运行该语句一次。

  • 因为常量是对对象的引用,所以更改常量引用的内容会产生警告,但修改对象本身不会。例如,Greeting << " world!" if Greeting == "Hello" 不会生成错误或警告。这类似于 Java 中的 final 变量,但与 Java 不同,Ruby 也具有“冻结”对象的功能。

与其他语言明显不同的一些功能:

  • 条件表达式的常用运算符andor 不遵循正常的优先规则:and 的绑定不比or 更紧密。 Ruby 还有表达式运算符||&&,它们可以按预期工作。

  • def 内的def 不会像 Python 程序员所期望的那样:

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

    这给出了一个关于 x 未定义的错误。您需要使用Proc

语言特征

  • 如果方法采用多个参数,则在方法参数周围省略括号可能会导致意外结果。 Ruby 开发人员表示,在未来的 Ruby 版本中可能不允许省略多参数方法的括号;当前(2007 年 11 月)Ruby 解释器抛出警告,鼓励作者不要省略 (),以避免代码含义模糊。不使用() 仍然是常见的做法,并且使用Ruby 作为一种人类可读的特定于领域的编程语言本身以及称为method_missing() 的方法会特别好。

【讨论】:

  • Ruby 1.9 也缺少字符数据类型。在 1.8 中,索引运算符返回一个 Fixnum;在 1.9 中,相当于对一个字符串进行切片。
【解决方案2】:

新手会遇到平等方法的问题:

  • a == b :检查 a 和 b 是否相等。这是最有用的。
  • a.eql? b :还检查 a 和 b 是否相等,但有时更严格(例如,它可能检查 a 和 b 是否具有相同的类型)。主要用于Hashes。
  • a.等于? b :检查 a 和 b 是否是同一个对象(身份检查)。
  • a === b :用于 case 语句(我将其理解为“a 匹配 b”)。

这些示例应该阐明前 3 种方法:

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

注意 ==eql?equal? 应该始终是对称的:如果 a==b 那么 b==a。

还要注意 ==eql? 都在 Object 类中作为 equal? 的别名实现,所以如果你创建一个新的类并希望 ==eql? 意味着除了普通身份之外的其他内容,那么您需要覆盖它们。例如:

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

=== 方法的行为不同。首先它是 not 对称的(a===b 确实 not 暗示 b===a )。正如我所说,您可以将 a===b 解读为“a 匹配 b”。以下是几个例子:

# === 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)

case 语句基于 === 方法:

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

等价于:

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

如果你定义一个新类,它的实例代表某种容器或范围(如果它有类似 include?ma​​tch? 方法),那么你可能会发现像这样覆盖 === 方法很有用:

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

【讨论】:

  • 还有:a = 'строка'; b = 'строка'; p a == b; a = a.force_encoding 'ASCII-8BIT'; b = b.force_encoding 'UTF-8'; p a == b; p a === b; p a.eql?乙; p a.等于? b
【解决方案3】:

【讨论】:

    【解决方案4】:

    下面的代码让我吃惊。我认为这是一个危险的陷阱:既容易遇到,也很难调试。

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

    打印出来:

    1
    2 is even
    3
    4 is even
    5
    

    但如果我只是在块之前添加comment =anything...

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

    然后我得到:

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

    基本上,当一个变量只在一个块内定义时,它会在块的末尾被销毁,然后在每次迭代时重置为nil。这通常是您所期望的。但是如果变量在块之前定义的,那么外部变量在块内部使用,因此它的值在迭代之间是持久的。

    一种解决方案是改为这样写:

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

    我认为很多人(包括我)倾向于写“a = b if c”而不是“a = (c ? b : nil)”,因为它更具可读性,但显然它有副作用。

    【讨论】:

    • 你也可以通过 (1..5) do |number;comment| 隐藏外部范围变量.....阅读这里stackoverflow.com/questions/1654637/…
    • 这对我来说似乎是合乎逻辑的。这种作用域是其他语言的典型,只是语法不同。
    • 但是,您可以写a = (b if c) 以获得所需的效果,无需三元。这是因为如果 c 为假,b if c 的计算结果为 nil。
    【解决方案5】:

    当不带参数调用super 时,被覆盖的方法实际上是用与覆盖方法相同的参数调用的。

    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"
    

    要真正调用super而不带参数,你需要说super()

    【讨论】:

    • 如果B#hellosuper 之前有name = 42,那么它会说“hello 42”。
    【解决方案6】:

    块和方法默认返回最后一行的值。将puts 语句添加到末尾以进行调试可能会导致令人不快的副作用

    【讨论】:

      【解决方案7】:

      【讨论】:

      • 哇,这是一个我不知道的问题。谢谢!
      • 在受保护方法的情况下会以相当复杂的方式进行。
      【解决方案8】:

      我在理解类变量、类属性和类方法时遇到了很多麻烦。此代码可能对新手有所帮助:

      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
      

      【讨论】:

      • 是的,类变量可能很棘手。我认为大多数有经验的 Ruby 专家会说避免它们是明智的,因为通常有其他方法可以在没有它们的情况下解决问题。一些语言爱好者甚至会说 Ruby 的类变量在语言层面设计得很糟糕。
      【解决方案9】:

      我学到的一件事是小心使用运算符 ||=。如果您正在处理布尔值,请特别注意。我通常使用 a ||= b 作为全部捕获,如果其他一切都失败并且“a”保持为零,则为“a”提供默认值。但如果 a 为假且 b 为真,则 a 将被赋值为真。

      【讨论】:

      • 您可以使用a = b if a.nil?@a = b unless defined?(@a)
      【解决方案10】:
      • 理解块非常重要,它们无处不在。

      • 方法参数不需要括号。是否使用它们取决于您。 Some say you should always use them.

      • 使用 raise 和 rescue 来处理异常,而不是 throw 和 catch。

      • 您可以使用;,但除非您想将多个内容放在一行中,否则您不必这样做。

      【讨论】:

      • 如果您不打算超越 Ruby 1.8.6,请尽可能忽略括号。否则,您最好使用它们。
      【解决方案11】:

      我在使用包含实例方法类方法的 mixin 时遇到了麻烦。此代码可能对新手有所帮助:

      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
      

      起初,我认为我可以通过简单地执行以下操作来拥有具有实例方法类方法的模块:

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

      不幸的是,方法 number_of_displays 永远不会被包含或扩展,因为它是一个“模块类方法”。只有“模块实例方法”可以包含在类中(作为实例方法)或扩展到类中(作为类方法)。这就是为什么你需要把你的 mixin 的实例方法放到一个模块中,而你的 mixin 的类方法放到另一个模块中(你通常把类方法放到一个“ClassMethods”子模块中)。多亏了 included 魔术方法,您可以轻松地在一个简单的“include Displayable”调用中同时包含实例方法和类方法(如上例所示)。

      这个 mixin 将在 per-class 的基础上计算每个显示。计数器是一个类属性,所以每个类都有自己的(如果你从 Person 类派生一个新类,你的程序可能会失败,因为派生类的 @number_of_displays 计数器永远不会被初始化)。您可能希望将 @number_of_displays 替换为 @@number_of_displays 以使其成为全局计数器。在这种情况下,每个类层次结构都有自己的计数器。如果您想要一个全局且唯一的计数器,您可能应该将其设为模块属性。

      当我开始使用 Ruby 时,所有这些对我来说绝对不直观。

      我仍然不知道如何干净地将这些 mixin 方法中的一些设为私有或受保护(只有 displaynumber_of_displays 方法应该作为公共方法包含在内)。

      【讨论】:

        【解决方案12】:

        注意范围符号。

        (至少,比最初做的更关注!)

        0..10(两个点)和 0...10(三个点)是有区别的。

        我非常喜欢 Ruby。但是这个点点与点点的事情让我很烦恼。我认为这样一个微妙的双语法“功能”是:

        • 容易打错,而且
        • 浏览代码时很容易被您的眼睛忽略

        应该不能在我的程序中造成毁灭性的错误。

        【讨论】:

        • for (i=0; i&lt;max; i++)for (i=0; i&lt;=max; i++)没有太大区别
        • 我一直在尝试找出 0..10 和 0...10 之间的区别。
        【解决方案13】:

        我认为“and”和“or”是对 Perl 的致敬,Perl 是 Ruby 最明显的“父母”之一(最突出的另一个是 Smalltalk)。它们的优先级都比您应该使用的运算符&amp;&amp;|| 的优先级低得多(实际上低于赋值,这是所指出的行为的来源)。

        其他需要注意但不是立即显而易见的事情:

        你并没有真正调用方法/函数,尽管它看起来有点像。相反,就像在 Smalltalk 中一样,您向对象发送消息。所以method_missing 真的更像message_not_understood

        some_object.do_something(args)
        

        等价于

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

        符号被广泛使用。那是那些以: 开头的东西,它们并不是很明显(对我来说不是),但你越早掌握它们越好。

        Ruby 非常重视“鸭子类型”,遵循“如果它像鸭子一样走路,像鸭子一样嘎嘎叫……”的原则,它允许使用通用方法子集非正式地替换对象,而无需任何显式继承或混合关系。

        【讨论】:

        • 谢谢。我讨厌 send 方法的一件事:它让你甚至可以在类外调用私有方法!哎哟。
        • @MiniQuark:这就是我喜欢 send 方法的原因!
        【解决方案14】:

        如果您使用attr_writerattr_accessor(或def foo=)声明一个setter(又名mutator),请小心从类内部调用它。由于变量是隐式声明的,解释器总是必须将 foo = bar 解析为声明一个名为 foo 的新变量,而不是调用方法 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
        

        这也适用于 Rails ActiveRecord 对象,它获取基于数据库中的字段定义的访问器。由于它们甚至不是 @ 样式的实例变量,因此单独设置这些值的正确方法是使用 self.value = 123self['value'] = 123

        【讨论】:

          【解决方案15】:

          了解 Time 和 Date 类之间的区别。两者都是不同的,并且在 Rails 中使用它们时产生了问题。 Time 类有时会与标准 ruby​​/rails 库中的其他 Time 类库发生冲突。我个人花了很多时间来了解我的 Rails 应用程序中到底发生了什么。后来,我想我什么时候做的

          Time.new

          它指的是位于我什至不知道的位置的某个图书馆。

          对不起,如果我不清楚我想说什么。如果其他人遇到过类似问题,请重新解释。

          【讨论】:

            【解决方案16】:

            过去让我感到震惊的是,单引号内的字符串不支持换行符 (\n) 转义序列以及其他转义序列。反斜杠本身被转义。您必须使用双引号才能使转义按预期工作。

            【讨论】:

            • 这和其他语言有什么不同?
            • Java 就是其中之一。 Java 中的单引号只能用来括住单个字符,不能用来括起字符串。
            • 这与任何允许您对字符串使用单引号的语言保持一致,这也是它们这样做的原因。
            • @John: 是的,但是 Java 中的 '\n' 仍然是换行符。
            • 但在 Java 中,单引号只创建 char 类型的值。不是字符串。这就是区别。
            【解决方案17】:
            x = (true and false) # x is false
            

            0 和 '' 是真的,正如你所指出的。

            您可以拥有同名的方法和模块/类(这是有道理的,因为该方法实际上被添加到 Object 并因此具有自己的命名空间)。

            没有多重继承,但经常使用“mixin模块”为多个类添加通用方法。

            【讨论】:

            • 0==true // 啊,我脑子里的 c 编译器爆炸了!!
            • 0==true 在 Ruby 中给出 false。 0 为 true 是有道理的,因为 true 是 Ruby 中的一个对象。在 C 0 中恰好与 false 具有相同的表示。
            • 在 Ruby 的条件下,只有 falsenil 是错误的。其他都是真实的价值观。
            【解决方案18】:

            方法可以重新定义,并且在您发现原因之前可能会让人费解。 (诚然,当 Ruby on Rails 控制器的操作被错误地重新定义时,这个错误可能有点“难以”检测到!

            #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
            

            运行:

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

            但是在启用警告的情况下调用它,您可以看到原因:

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

            【讨论】:

            • 如果可以的话,我会 +100 使用警告。
            【解决方案19】:

            我认为在事物上使用 .length 总是好的...因为几乎所有东西都支持 size 并且 Ruby 具有动态类型,当您的类型错误时调用 .size 会得到非常奇怪的结果...我会宁可得到 NoMethodError: undefined method `length',所以我通常从不在 Ruby 中的对象上调用 size。

            不止一次咬我。

            还要记住对象有 id,所以我尽量不要使用变量调用 id 或 object_id 以避免混淆。如果我需要用户对象上的 id,最好将其命名为 user_id。

            只要我的两分钱

            【讨论】:

              【解决方案20】:

              我是 ruby​​ 新手,在第一轮我遇到了一个关于将浮点数/字符串更改为整数的问题。我从浮点数开始,并将所有内容编码为 f.to_int。但是当我继续对字符串使用相同的方法时,我在运行程序时遇到了一条曲线。

              显然,字符串没有 to_int 方法,但浮点数和整数有。

              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>
              

              一开始我也被任意括号吓到了。我看到了一些代码和一些没有。我花了一段时间才意识到这两种风格都可以接受。

              【讨论】:

                【解决方案21】:

                与蒙库特的回应有关,Ruby 的 to_foo 方法暗示了他们将进行的转换有多严格。

                to_ito_s 这样的短句告诉它是惰性的,即使它们不能以那种格式准确表示,也将它们转换为目标类型。例如:

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

                to_intto_s 等较长的显式函数意味着对象可以在本地表示为该类型的数据。比如Rational类代表所有有理数,所以可以直接调用to_int表示为Fixnum(或Bignum)整数。

                Rational(20,4).to_int == 5
                

                如果不能调用 long 方法,则表示对象无法以该类型原生表示。

                所以基本上,在转换时,如果你对方法名称很懒惰,Ruby 也会对转换很懒惰。

                【讨论】:

                • “懒惰”这个词在这里合适吗?
                【解决方案22】:

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

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

                因为在调用defined?(foo) 时,foo 被定义为nil

                【讨论】:

                  【解决方案23】:

                  不能保证以任何特定顺序对 ruby​​ 哈希进行迭代。 (这不是一个错误,这是一个功能)

                  如果您需要特定订单,Hash#sort 很有用。

                  相关问题:Why are Ruby’s array of 1000 hashes' key and value pairs always in a particular order?

                  【讨论】:

                  • 这在 1.9 中无效:“然而,在 Ruby 1.9 中,哈希元素按插入顺序迭代”来自 Ruby 编程语言
                  【解决方案24】:

                  这一次让我很生气:

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

                  【讨论】:

                  • 我相信这在 Java、C 和 C++ 中的行为方式完全相同。
                  • 这很有趣,我什至没有考虑过,但是如果你打开 irb 并尝试一下,这是有道理的:所以 (1/2) 是 Fixnum 而 (0.5) 是 Fl​​oat .我们知道 Fixnim != Float。
                  • @DemitryT 我认为更简单的原因是1/2 的计算结果为0,它不等于0.5,无论类型如何。但是,Rational(1, 2) == 0.51.0 == 1
                  • 通用语言在这里打嗝。这是 ruby​​ 和编程新手应该知道的。
                  【解决方案25】:
                  1..5.each {|x| puts x}
                  

                  不起作用。您必须将范围放在括号中,例如

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

                  所以它认为你不是在打电话给5.each。我认为这是一个优先问题,就像x = true and false 陷阱一样。

                  【讨论】:

                  • 我将其称为括号。其次,如果任何代码看起来有返回值/优先级问题,无论如何都应该用括号括起来。所以,对我来说,这个“陷阱”并没有什么特别之处。您可以继续编写每个组合“陷阱”,但这会浪费时间。坦率地说,即使你对此有预期的结果,我仍然更喜欢用括号括起来。
                  猜你喜欢
                  • 2011-08-31
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2010-10-04
                  • 1970-01-01
                  • 1970-01-01
                  • 2010-10-30
                  • 1970-01-01
                  相关资源
                  最近更新 更多