求值策略和内容类型
求值策略 Evaluation_strategy, 如值传递/引用传递, 属于函数调用时参数的求值策略, 这是对调用函数时, 求值和传值的方式的描述, 和传递的内容的类型没有直接关系.
传递的内容类型(Value Content), 如值类型/引用类型,是用于区分两种内存分配方式,值类型在调用栈上分配,引用类型在堆上分配。
一个描述参数求值策略, 一个描述内存分配方式, 两者之间无任何依赖或约束关系。
求值策略的区别
求值策略(值传递和引用传递)的关注的点在于:
-
求值的时机: 传入的参数(表达式)在调用函数的过程中,求值的时机、值的形式的选取等问题
求值时机可能是在调用前, 调用后(用到才求值, 类似Lazy Loading)
-
传递形式: 主要关注有无副本
求值策略 | 求值时机 | 传递方式 | 根本区别 | 影响 |
---|---|---|---|---|
值传递(Pass by value) | 调用前 | 原值的副本 | 会创建副本 | 函数无法改变(change)原值 |
引用传递(Pass by reference) | 调用前 | 原值(无副本) | 不会创建副本 | 函数可以改变(change)原值 |
名传递(Pass by name) | 调用后 | 与值无关的一个名 | - | - |
值传递+mutate
Go 是值传递:
package main
import "fmt"
type Person struct {
name string
}
func change(person Person) {
person.name = "zhong"
}
func main() {
me := Person{"fox"}
change(me)
fmt.Println(me.name) //fox; 没被改变
}
可以看到me.name经过change后没有变化, 看起来证明了「值传递无法改变原值」, 其实不然, 见下文.
Ruby 是也是值传递, 我们来看一个容易误解的例子:
def change(person)
person[:name] = 'zhong'
end
me = { name: 'fox' }
change(me)
puts(me[:name]) # zhong; 被改变了
同样是值传递, Ruby代码和go代码基本一致, 但是为什么Ruby对象me被改变了呢?
理解值传递
「改变」一词在函数中通常有2种不同的含义:
- mutate: 表示内容属性的修改, 但是内容引用不变, 如在ruby中:
person[:name] = 'zhong'
- change: 表示直接改变量指向, 表现通常是进行重新赋值:
person = { name: 'zhong' }
值传递无法改变原值, 说的「改变」是指的「change」, 而不是「mutate」, 上面Go 和 Ruby 代码中me的name改变属于内容变化(mutate), 函数中的mutate能否改变原值指向的原始对象还和传递的内容类型(Content Value)有关:
求值策略 | Content Value | 函数内mutate(如果对象可变) | 函数内change | 代表 |
---|---|---|---|---|
值传递 | 值类型(被复制) | 不影响原始对象 | 不影响原始对象 | Go 传递struct,array等 Javascript传递基础类型 Ruby立即值Symbol,Fixnum |
值传递 | 引用类型(被复制) | 影响原始对象 | 不影响原始对象 | Go 传递map,slice,channel Javascript传递Object Ruby传递非立即值 Java |
上面的例子中, Go传递的struct是Content Value是值类型, 而Ruby传递的hash是引用类型.
因为Ruby 是一门非常纯粹的面向对象的语言, 当我们在Ruby中使用对象时, 其实是在使用对象的引用(这个引用和引用传递没有关系, 文章最开始进行了澄清), Ruby的求值策略是值传递, 但是传递的值是对象引用. 以上Ruby例子中, 从实参me到形参person, 存在副本复制, 只是这个复制的内容是对象引用, 在change函数中, 无法改变这个对象引用, 改变的只是这个对象引用对应的对象属性, 因此上面的例子还是值传递.
其实在Go中, 也存在「值传递」时, 传递的值是类似「引用」的情况, 那就是传递map, slice, channel时, 可以认为是引用(其实还是值, 只是数据结构有很多指针), 我们用map重写上面Golang的例子:
package main
import "fmt"
type Person map[string]string
func change(person Person) {
person["name"] = "zhong"
}
func main() {
me := Person{"name": "fox"}
change(me)
fmt.Println(me["name"]) //zhong, 被改变了
}
值传递(Evaluation)一定会复制副本, 不会影响原值, 这个副本是引用类型还是值类型是由传递的对象决定(Value Content), Value Content如果是引用类型, 在函数mutate时会影响原始对象(因为副本本身就是一个对象引用)
由于这个描述太绕, 于是对于Java, Ruby, JavaScript, Python等语言使用的这种求值策略(值传递+引用类型),起了一个更贴切名字,叫Call by sharing
值传递+change
值传递无法改变原值, 说的「改变」是指的「change」, 来看看change的实例:
def change(person)
person = { name: 'zhong' }
end
me = { name: 'fox' }
change(me)
puts(me[:name]) # fox; 没被改变
对于值传递的Golang, Javascript等也有同上的例子, 可以自行试试.
对比引用传递
C# 里既支持值传递, 又支持引用传递, 比对起来看看:
通过值传递调用:
using System;
public class Person
{
public Person(string name)
{
Name = name;
}
public string Name { get; set; }
}
public class testEvaluation
{
public static void Change(Person person) //值传递方式, 存在复制副本
{
person = new Person("zhong"); //仅副本变化, 原始实参没有变化
}
public static void Main(string[] args)
{
Person me = new Person("fox");
testEvaluation.Change(me);
Console.WriteLine(me.Name); //fox, 值传递, 原始实参没有变化
}
}
通过引用传递调用:
using System;
public class Person
{
public Person(string name)
{
Name = name;
}
public string Name { get; set; }
}
public class testEvaluation
{
public static void Change(ref Person person) //引用传递方式, 没有复制副本
{
person = new Person("zhong"); //改变了原始值
}
public static void Main(string[] args)
{
Person me = new Person("fox");
testEvaluation.Change(ref me);
Console.WriteLine(me.Name); //zhong, 原始值被改变
}
}
总结
Evaluation 影响的是传入的「原值」, 而对原值指向的原始对象的影响, 还需要结合Value Content和改变方式:
Evaluation | 对原值的影响 | Value Content | 实例 | 改变方式 | 对原值指向的原始对象的影响 |
---|---|---|---|---|---|
值传递 | 对原值复制 无法改变原值 |
值类型 | Go JavaScript_primitive Ruby_Symbol_Fixnum |
change | 无法改变 |
Go | mutate | 无法改变 | |||
引用类型 Call by sharing |
Ruby Javascript_Object Go_map_channel_slice |
change | 无法改变 | ||
mutate | 可以改变 | ||||
引用传递 | 没有复制 可以改变原值 |
值类型/引用 | C# | change/mutate | 可以改变 |