为啥你可以在函数中使用另一种方法而不是赋值运算符来更改 Ruby 中局部变量的值?

Posted

技术标签:

【中文标题】为啥你可以在函数中使用另一种方法而不是赋值运算符来更改 Ruby 中局部变量的值?【英文标题】:Why can you change the value of a local variable in Ruby in a function using another method but not with an assignment operator?为什么你可以在函数中使用另一种方法而不是赋值运算符来更改 Ruby 中局部变量的值? 【发布时间】:2021-02-10 05:06:11 【问题描述】:

我正在尝试将 Ruby 的概念理解为按引用值传递的语言。使用我在this 网站上找到的示例...

def uppercase(value)
  value.upcase!
end

name = 'William'
uppercase(name)
puts name

我们得到输出“WILLIAM”。因此 value 和 name 都指向同一个对象,该对象最初持有一个值“William”,然后 .upcase 将其更改为“WILLIAM”。我会将此理解为传递引用。

但是如果我将.upcase 更改为=

def uppercase2(value)
  value = "WILLIAM"
end

name = 'William'
uppercase2(name)
puts name

输出变为“William”。这是按值传递。

为什么赋值运算符会影响 Ruby 中变量的处理方式?

【问题讨论】:

经验法则是你永远不能改变self。如果value = "WILLIAM" 确实 发生了变异,那实际上是在更改self 的值,对吗?因此这是不可能的,Ruby 会重新分配变量。 【参考方案1】:

这里的关键是你永远不能改变一个Ruby对象的核心self,一旦它被创建,在垃圾收集和销毁之前它永远是同一个对象。只能更改对象的属性

但是,您可以更改变量或对象引用,例如实例变量、常量、attr_accessor 属性等。

所以在这种情况下:

def uppercase2(value)
  value = "WILLIAM"
end

这将重新分配 value 局部变量。它对原始对象没有任何作用。

如果您想替换该文本,您需要使用对象上的方法来实现它(如果支持)。在这种情况下有一个方法:

def uppercase2(value)
  value.replace("WILLIAM")
end

这些通常被称为就地修改,因为对象本身是被操纵的,而不是被替换为另一个对象。

【讨论】:

...或value[0..-1] = "WILLIAM" 我对你想说的话感到困惑。您的第一句话是“您永远无法更改 Ruby 对象”,然后在您的第二个代码 sn-p 中,您演示了如何更改对象,这似乎与您的第一句话直接矛盾。 @JörgWMittag 我的意思是你不能改变对象本身,它的基本selfobject_id 一样,但是你可以改变对象属性,比如@987654329 @的内容。【参考方案2】:

我正在尝试将 Ruby 的概念理解为按引用值传递的语言。

Ruby 是按值传递的。总是。它永远不是通过引用传递的。被传递的值是一个不可变的不可伪造的指针,但它与传递引用非常不同。

C 是按值传递。 C 有指针。这并不意味着如果你在 C 中传递一个指针,它就会神奇地变成按引用传递。它仍然是按值传递。

C++ 有指针,它支持按值传递和按引用传递。在 C++ 中,可以按值或按引用传递指针,也可以按值或按引用传递非指针。这两个概念是完全正交的。

如果我们可以同意 C 是按值传递的,并且我们可以同意 C 有指针,并且我们可以同意当我们在 C 中传递一个指针时,它仍然是按值传递,那么我们必须也同意 Ruby 是按值传递的,因为 Ruby 的行为类似于 C 的假设版本,其中唯一允许的类型是“指向某物的指针”,访问值的唯一方法是取消引用指针,并且唯一的方法是传值就是取指针。

这不会以任何方式改变C中的参数传递,这意味着它仍然是按值传递,这意味着如果我们在C中调用它是按值传递,那么调用它没有任何意义其他在 Ruby 中。

def uppercase(value)
  value.upcase!
end

name = 'William'
uppercase(name)
puts name

我们得到输出“WILLIAM”。因此 value 和 name 都指向同一个对象,该对象最初持有一个值“William”,然后 .upcase 将其更改为“WILLIAM”。我会将此理解为传递引用。

同样,这不是传递引用。引用传递意味着您可以更改调用者范围内的引用,而 Ruby 不允许您这样做。

这不过是简单的突变。 Ruby 不是一种纯粹的函数式语言,它确实允许您改变对象。当你改变一个对象时,无论你叫什么名字,你都可以观察到它的变化状态。

我的朋友称我为“Jörg”,但我的理发师称我为“Mittag 先生”。当我的理发师剪掉我的头发时,它不会在我遇到我的朋友时神奇地长回来,即使他们没有像我的理发师那样称呼我,它仍然会消失。

你对同一个对象有两个名字,然后你改变了那个对象。无论您使用哪个名称来引用该对象,您都将观察到新状态。

那只是“可变状态”,它与传递引用无关。

但是如果我将.upcase 更改为=

def uppercase2(value)
  value = "WILLIAM"
end

name = 'William'
uppercase2(name)
puts name

输出变为“William”。这是按值传递。

为什么赋值运算符会影响 Ruby 中变量的处理方式?

它没有。这两种情况都是按值传递的。在第二种情况下,您创建了一个新对象并将其分配给uppercase2 方法内的局部变量value。 (从技术上讲,它不是一个局部变量,它是一个参数绑定,但它可以在方法体内反弹,正是因为 Ruby 是按值传递的。如果是按值传递-引用,那么这name局部变量重新分配给新创建的对象。)

有时,这种特定按值传递的情况,其中传递的值是指向潜在可变对象的不可变指针,称为按对象共享调用 em>、共享调用对象调用。但这 不是 与传递值不同的东西。 仍然是值传递。这是传值的一种特殊情况,其值不能是“任何值”,而始终是“一个不可变的不可伪造的指针”。

有时,您会听到这样的描述:“Ruby 是按值传递,其中传递的值是引用”或“Ruby 是按引用值传递”或“Ruby 是按值传递” -reference”或“Ruby 是传递对象引用”。我不太喜欢这些术语,因为它们听起来非常接近“pass-by-reference”,但实际上是“pass-by-reference”中的“reference”一词和“reference”一词" 在“传递对象引用”中 表示两个不同的东西

在“pass-by-reference”中,术语“reference”是一个技术术语,可以被认为是对“变量”、“存储位置”等概念的概括。在“pass-by”中-value-reference”,我们说的是“对象引用”,它更像是指针,但不能制造或改变。

我也不喜欢上面我使用的术语“按值传递,其中传递的值是不可变的不可伪造的指针”,因为“指针”一词具有一定的含义,尤其是对于来自 C 的人。在C 中,您可以进行指针运算,并且可以将数字转换为指针,即可以“凭空变出指针”。在 Ruby 中你什么都做不了。这就是为什么我添加形容词“不可变”(没有算术)和“不可伪造”(你不能创建指针,只能由系统传递一个),但人们忽略或忽略它们或低估它们的重要性。

在某些语言中,这些指向对象的不可变、不可伪造的指针被称为“对象引用”(这又很危险,因为它会引起与“传递引用”的混淆)或 OOPS(面向对象的指针),它具有不幸的含义大部分不受限制的免费“C”指针。 (例如,Go 有更多限制性的指针,但是当你简单地说“指针”这个词时,没有人会想到 Go。)

请注意,这些都不是 Ruby 特有的。 Python、ECMAScript、Java 和许多其他的行为方式相同。默认情况下,C# 的行为方式相同,但也支持通过引用作为 explicit 选择加入。 (您必须在方法定义和方法调用时显式请求 pass-by-reference。)默认情况下,Scala 的行为方式相同,但可选择支持按名称调用。

C# 实际上是一种很好的展示区别的方式,因为 C# 支持按值传递和按引用传递,同时支持值类型和引用类型,显然,您可以将类型编写为可变和不可变类型,因此您实际上获得了所有 8 种可能的不同组合,并且您可以研究它们的行为方式。

我想出了这个简单的测试代码,您也可以轻松地将其翻译成其他语言:

def is_ruby_pass_by_value?(foo)
  foo.replace('More precisely, it is call-by-object-sharing!')
  foo = 'No, Ruby is pass-by-reference.'
end

bar = 'Yes, of course, Ruby *is* pass-by-value!'

is_ruby_pass_by_value?(bar)

p bar
# 'More precisely, it is call-by-object-sharing!'

Here is the slightly more involved example in C#:

struct MutableCell  public string value; 

static void ArgumentPassing(string[] foo, MutableCell bar, ref string baz, ref MutableCell qux)

    foo[0] = "More precisely, for reference types it is call-by-object-sharing, which is a special case of pass-by-value.";
    foo = new string[]  "C# is not pass-by-reference." ;

    bar.value = "For value types, it is *not* call-by-sharing.";
    bar = new MutableCell  value = "And also not pass-by-reference." ;

    baz = "It also supports pass-by-reference if explicitly requested.";

    qux = new MutableCell  value = "Pass-by-reference is supported for value types as well." ;


var quux = new string[]  "Yes, of course, C# *is* pass-by-value!" ;
var corge = new MutableCell  value = "For value types it is pure pass-by-value." ;
var grault = "This string will vanish because of pass-by-reference.";
var garply = new MutableCell  value = "This string will vanish because of pass-by-reference." ;

ArgumentPassing(quux, corge, ref grault, ref garply);

Console.WriteLine(quux[0]);
// More precisely, for reference types it is call-by-object-sharing, which is a special case of pass-by-value.

Console.WriteLine(corge.value);
// For value types it is pure pass-by-value.

Console.WriteLine(grault);
// It also supports pass-by-reference if explicitly requested.

Console.WriteLine(garply.value);
// Pass-by-reference is supported for value types as well.

【讨论】:

【参考方案3】:

让我们开始把它分解成更容易理解的东西。

您可以将name(或value)之类的变量与路标进行比较。

假设我将它指向一个米色的房子,这将是变量的值。

考虑到以上内容,让我们看一下第一个示例:

# don't worry about this line for now
House = Struct.new(:color)
def color_red(value)
  value.color = :red
end

house = House.new(:beige)
color_red(house)
puts house
# prints: #<struct House color=:red>

那么这里会发生什么?当我们将house 作为参数传递给color_red 时,Ruby 将复制我们的路标并将其分配给value。现在两个路标都指向同一所房子。然后我们按照路标value 的指示走到房子前,把它涂成红色。

因此,house 的颜色最终会变为红色。

现在让我们看看另一个例子:

def color_red(value)
  value = House.new(:red)
end

house = House.new(:beige)
color_red(house)
puts house
# prints: #<struct House color=:beige>

从这里开始,我们复制我们的路标house 并将副本分配给value。然而,我们不会走到房子前画它,而是要修改路标并将其指向街道更远处的一座红色房子。由于我们的路标valuehouse 的副本,因此将其指向新方向不会影响house


您的代码也在做同样的事情。当您致电value.upcase! 时,您是在对她说字符串。嘿你,大写你所有的角色! (类似于粉刷房子。)

当您重新签名 value (value = "WILLIAM") 时,您实际上只是在修改路标并将其指向新方向。但是,路标是作为副本传递的,因此不会影响原件。

【讨论】:

【参考方案4】:
def uppercase(value)
  value.upcase!
end

name = 'William'
uppercase(name)
puts name #WILLIAM

在这种情况下,您正在改变原始对象。 name 指向 William value 也是如此。当你传入参数时,Ruby 将分配 参数变量value 指向name 指向的同一对象。

def uppercase2(value)
  value = "WILLIAM"
end

name = 'William'
uppercase2(name)
puts name

在这种情况下,您将重新分配 value。也就是说,你正在改变哪个 对象value 指向。它指向同一个字符串对象 名字所指。但是,现在,您要求 value 引用 不同的对象。

因此,总而言之,upcase! 会改变对象,而= 将重新分配。 你可以想到 3 个圈子,value 在一个圈子里,name 在另一个圈子里,William 在第三个。 valuename 都指向字符串对象 William

在第一种情况下,你改变了 valuename 的字符串对象 正在指向。

在第二种情况下,您正在创建第 4 个圆圈,其中包含 WILLIAM。然后您将删除线从valueWilliam 并创建一条线 从valueWILLIAM

如果我这样做,你就会明白:

def uppercase2(value)
  value = "WILLIAM"
  puts value
end

name = 'William'
uppercase2(name) # => “WILLIAM”
puts name           # William

【讨论】:

以上是关于为啥你可以在函数中使用另一种方法而不是赋值运算符来更改 Ruby 中局部变量的值?的主要内容,如果未能解决你的问题,请参考以下文章

为啥将 Collections.emptySet() 与泛型一起使用在赋值中而不是作为方法参数?

为啥在直接初始化和赋值中传递 lambda 而不是复制初始化时会编译?

为啥定义了移动构造函数而隐式删除了赋值运算符?

c++中为啥赋值运算符重载返回类型是引用

Python为啥要self

在C语言中能否直接给指针指向的数据赋值?为啥?