【问题标题】:Does 'upcase!' not mutate a variable in Ruby?'大写!不在Ruby中改变变量?
【发布时间】:2020-06-28 16:19:26
【问题描述】:

我只是想确保我了解这里发生的事情。我知道 += 是重新分配,所以也许这就是为什么 str 没有被修改但为什么 upcase! 也不在这里修改 str ?

def change_me(str)
  str += "?"
  str.upcase!
end

question = "whats your name"
change_me(question)

puts question

'whats your name'
=> nil

【问题讨论】:

  • 这是因为 Ruby 在传递参数时使用了pass by value
  • 这不是真的@Rajagopalan,如果您删除str += "?" 行,该方法实际上会更改传递的字符串
  • 有两种方法可以做你想做的事。一种是写def change_me(str); str.replace(str.upcase + "?"); end,使用方法String#replace。另一种是将方法的操作行改为str[0..-1] = str.upcase + "?",即使用String#[]=
  • @MarioF 不! Ruby 是按值传递的,请阅读此答案stackoverflow.com/a/10330589/9043475,非常重要的是阅读此答案下方的评论。
  • 你可以运行我之前评论中建议的方法和@Mario 的“副作用”方法here

标签: ruby


【解决方案1】:

“大写!”不在 Ruby 中改变变量?

方法不可能改变变量。 Ruby 是一种面向对象的语言,因此方法可以改变对象(更准确地说,方法可以改变它的接收者),但变量不是 Ruby 中的对象。 (与大多数其他语言一样,几乎没有一种语言将变量作为对象。)

改变变量的唯一方法是通过赋值。请注意,我们通常不谈论“变异”变量,而是谈论“重新绑定”或“重新分配”。

我只是想确保我了解这里发生的事情。我知道 += 是重新分配,所以也许这就是为什么 str 没有被修改但为什么 upcase! 也不在这里修改 str ?

再次,您混淆了变量和对象。 upcase! 修改str 引用的对象,但它修改str

听起来您希望 Ruby 是一种通过引用传递的语言,但事实并非如此。 Ruby 是纯粹的按值传递,始终没有例外。更准确地说,传递的值是指向对象的不可修改、不可伪造的指针

以下是执行流程之后发生的情况:

question = "whats your name"

  • 对字符串文字 "whats your name" 求值,生成一个 String 对象,其内容为 whats your name
  • 局部变量 question 使用一个不可变的、不可伪造的指针初始化,该指针指向在步骤 #1 中创建的字符串对象。

change_me(question)

  • 局部变量 question 被取消引用,导致指向在步骤 #1 中创建的字符串对象的不可变、不可伪造的指针。
  • 制作了该指针的副本。
  • 第 4 步的副本被放入调用 change_me 的参数列表中

str += "?"

  • change_me 方法体内部,参数绑定str 绑定到从步骤#4 和#5 复制的不可变不可伪造指针。
  • 这一行脱糖成str = str + "?",所以会发生什么:
    • str 被取消引用,导致从步骤 #4、#5 和 #6 复制的不可变、不可伪造的指针。
    • 我们跟随指针并将消息+ 发送到对象,该对象带有一个不可变、不可伪造的指针,该指针指向通过评估字符串文字"?" 作为参数而创建的字符串对象。
    • String#+ 返回一个新字符串(或者,更准确地说,一个指向新字符串的不可变、不可伪造的指针)。
    • str 重新绑定到调用 str+("?") 返回的新的不可变、不可伪造的指针。

str.upcase!

  • str 被取消引用,从而产生来自步骤 #7c #7d 的新的不可变、不可伪造的指针。
  • 我们跟随指针,向对象发送消息upcase!
  • String#upcase!变异接收者对象(在这种情况下,新创建的字符串来自步骤#7c)以使所有字母大写。
  • String#upcase!返回一个不可变的、不可伪造的指向接收器对象本身的指针(即用于调用方法的指针),如果它对接收器进行了任何更改,或者它将返回一个不可变的, 指向对象 nil 的不可伪造指针,如果字符串已经是大写字母或不包含任何字母。

返回change_me(question)

  • 然而,这个返回值只是被忽略了,它被丢弃了,它没有被打印出来,没有分配给变量,没有作为参数传递给不同的方法,也没有存储在数据结构中。

puts question

好的,现在我将保存详细信息,因为该变量已被取消引用等。

关键部分是:变量question 从未被触及,从未被重新分配,因此它仍然包含完全相同的东西,它始终包含:不可变、不可伪造的指针到步骤 #1 和 #2 中的字符串对象。

我们将此对象分配给变量,然后:

  • 从未重新分配变量,因此变量仍然指向同一个对象
  • 从不要求对象自己变异,所以对象的内容还是一样的

因此,对象仍然没有改变,变量仍然指向同一个对象,因此我们得到什么都没有改变的结果。

我们更改了change_me 方法内str 参数绑定的绑定,但该绑定对于该方法是本地。 (参数绑定实际上等同于局部变量。)因此,在方法返回的那一刻,它就不再存在了。

并且我们更改了新创建的字符串对象,但是由于我们从未获得指向该对象的指针,因此我们无法访问它。一个指针存储在str 中,但它已经消失了。另一个指针从change_me 返回,但我们把它扔掉了,所以它也消失了。由于没有引用这个字符串对象,所以该对象是不可访问的。

事实上,change_me 方法并没有做任何可以从外部观察到的事情。它创建一个字符串对象,然后对其进行变异,但对该对象的任何引用都不会离开该方法。因此,就好像突变从未发生过一样好,字符串对象从一开始就不存在。

事实上,一个足够聪明的编译器可以优化你的整个代码:

puts "whats your name"

【讨论】:

【解决方案2】:

当您执行str += "?" 时,您正在创建一个新字符串,因此str 指向的字符串与您作为参数传递的字符串不同。

你所做的基本上是这样的:

def change_me(str)
  new_str = str + "?"
  new_str.upcase!
end

这就是为什么您之前的字符串没有被更改的原因。如果你想让函数有副作用,你应该这样做:

def change_me(str)
  str << "?"
  str.upcase!
end

但是,我认为就地修改字符串有点问题。我认为如果需要返回一个新字符串并覆盖您的引用会更安全。

【讨论】:

  • 但是,不应该大写!然后也改变新的字符串对象?我仍然期待一个新的 str 对象,其值为“你叫什么名字?”。
  • 它正在这样做。如果您将新字符串存储在一个额外的变量中,您会看到它已被修改
  • 他不是在寻求实现这一目标的方法!
  • @SubscriptionServices:是什么让你认为它没有返回一个改变的 str 应该说“你叫什么名字?”你从不查看返回值,你不打印它,你不将它分配给一个变量,你不将它作为参数传递,你忽略它并把它扔掉。你怎么知道它做什么和不返回?
  • 感谢您的回答,我真的只需要找出我的逻辑缺陷在哪里。让这项工作成为现实对我来说并不重要。
【解决方案3】:

让我们看看我是否可以为您简化所有这些。首先,仔细看一下马里奥的“你所做的本质上就是这个”的代码示例。了解您正在对一个全新的对象调用 #upcase! 方法,因为当您尝试将 ? 附加到新对象上时,您将 str 重新分配给它。

现在,看看这个:

def change_me(str)
  str.upcase!
  42
end

x = 'hello'
puts x # => hello
change_me(x)
puts x # => HELLO

如您所见,此代码返回42。现在,正如道格拉斯亚当斯告诉我们的那样,42 岁就是生命的意义。但如果是这样,生命的意义在这里就完全无关紧要了,因为正如 Jörg 一直试图向您解释的那样,您不会对方法调用的返回值做任何事情。

您还会看到您的str 对象确实在这里发生了变异。这是因为在这种情况下,您没有像您的代码那样将 str 变量重新分配给方法内的不同对象。 (再次仔细看看马里奥的第一个例子。)

现在,如果在您的方法中,您想在发送到方法中的对象的末尾添加一些东西,您需要使用&lt;&lt; 而不是+。看看马里奥的第二个代码示例,试试看。

要深入研究并彻底了解它,#object_id 方法非常有用。尝试运行此代码:

def change_me(str)
  p str.object_id
  str += "?"
  p str.object_id
  str.upcase!
  p str.object_id
end

def change_me_2(str)
  p str.object_id
  str << "?"
  p str.object_id
  str.upcase!
  p str.object_id
end

如果您花一些时间评估对象 ID,您会很快自己解决这个问题。

最后,我赞同马里奥的观点,即修改字符串在实践中有点问题。除非有某种原因你不能这样做,否则我会这样做:

def change_me(str)
  str.upcase + '?'
end

然后:

question = "what's your name"
question = change_me(question)

或者简单地说:

question = change_me("what's your name")

最后,这是一个小测验。获取您的代码并更改您的调用方式:

def change_me(str)
  str += "?"
  str.upcase!
end

question = "whats your name"
puts change_me(question)

为什么这会达到你的预期?现在,将str.upcase! 更改为str.upcase,您会发现它也符合您的预期。为什么使用! 没有任何区别?

【讨论】:

  • 非常感谢,这真的有助于澄清事情:)
  • @SubscriptionServices 非常欢迎您。我在帖子中添加了一个小“测验”,您可能也想看看。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-03-20
  • 2022-11-21
  • 1970-01-01
  • 2016-12-19
  • 1970-01-01
  • 2022-01-27
相关资源
最近更新 更多