为什么可以使用另一种方法而不是赋值运算符来更改函数中 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 的概念理解为一种传递引用值的语言。使用我在 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 中的处理方式?

这里的关键是您永远无法更改 Ruby 对象的核心 self,一旦它被创建,它将始终是同一个对象,直到被垃圾收集和销毁。只能更改对象的 属性

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

所以在这种情况下:

def uppercase2(value)
  value = "WILLIAM"
end

这会重新分配 value 局部变量。它对原始 object.

没有任何作用

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

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

这些通常被称为 in-place 修改 因为对象本身是被操纵的而不是交换为另一个对象。

让我们开始用更容易理解的方式来分解它。

您可以将 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") 时,您实际上只是在修改路标并将其指向新的方向。但是,路标作为副本传递,因此不会影响原始路标。

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! 会改变对象,而 = 会 re-assign。
你可以想到 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

I'm trying to understand the concept of Ruby as a pass-by-reference-value language.

Ruby 是 pass-by-value。总是。它永远不会 pass-by-reference。传递的值是一个不可变的不可伪造的指针,但非常不同于pass-by-reference。

C 是 pass-by-value。 C有指针。这并不意味着如果你在 C 中传递一个指针,它会神奇地变成 pass-by-reference。还是pass-by-value.

C++有指针,它同时支持pass-by-value和pass-by-reference。在 C++ 中,您可以按值或按引用传递指针,也可以按值或按引用传递 non-pointers。这两个概念是完全正交的。

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

这不会以任何方式改变在 C 中传递的参数,这意味着它仍然是 pass-by-value,这意味着如果我们在 C pass-by-value 中调用它,调用它没有意义Ruby.

中的任何其他内容
def uppercase(value)
  value.upcase!
end

name = 'William'
uppercase(name)
puts name

we get the output "WILLIAM". So value and name are both pointing at the same object, which is initially holding a value of "William" and then .upcase changes it to "WILLIAM". I would understand this as pass-by-reference.

同样,这是 不是 pass-by-reference。 Pass-by-reference 意味着您可以 更改调用方范围内的引用 ,而 Ruby 不允许您这样做。

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

我的朋友叫我“Jörg”,但我的理发师叫我“Mittag 先生”。当我的理发师给我剪头发时,当我遇到我的朋友时它不会神奇地长回来,它仍然会消失,即使他们提到我的名字和我的理发师不一样。

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

那只是“可变状态”,与pass-by-reference无关。

But then if I change .upcase to =:

def uppercase2(value)
  value = "WILLIAM"
end

name = 'William'
uppercase2(name)
puts name

The output becomes "William". Which is pass by value.

Why does an assignment operator make the difference in how a variable is treated in Ruby?

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

有时,pass-by-value 的这种 特定 情况称为 [=,其中传递的值是指向潜在可变对象的不可变指针118=]call-by-sharingcall-by-object。但那是 而不是 不同于 pass-by-value 的东西。还是还是pass-by-value。它是 pass-by-value 的特例,其中值不能是“任何值”,而始终是“不可变的不可伪造的指针”。

有时,您会听到这样的描述“Ruby 是 pass-by-value,其中传递的值是一个引用”或“Ruby 是 pass-by-reference-值”或“Ruby 是 pass-by-value-参考”或“Ruby 是 pass-by-object-reference”。我不太喜欢这些术语,因为它们听起来非常接近“pass-by-reference”,但实际上“pass-by-reference”中的术语“参考”和“pass-by-object-reference”中的术语“参考”表示两个不同的事物

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

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

在某些语言中,这些指向对象的不可变不可伪造指针被称为“对象引用”(这又是危险的,因为它会引起与“pass-by-reference”的混淆)或 OOPS(Object-Oriented PointerS)具有大多数不受限制的free-for-all“C”指针的不幸含义。 (例如,Go 有更多的限制性指针,但是 whn你只是说“指针”这个词,没有人会想到围棋。)

请注意,其中 none 确实特定于 Ruby。 Python、ECMAScript、Java 和许多其他语言的行为方式相同。 C# 的默认行为方式相同,但也支持 pass-by-reference 作为 显式 opt-in。 (您 必须 在方法定义和方法调用时显式请求 pass-by-reference。)Scala 默认情况下的行为方式相同,但可以选择支持 call-by-name。

C# 实际上是展示这些区别的一种很好的方式,因为 C# 同时支持 pass-by-value 和 pass-by-reference,值类型和引用类型,显然,您可以将类型都编写为可变类型和不可变类型,所以你实际上得到了所有 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.