一个数组同时拥有多个变量名的意义在哪?(注意语义)

我随便翻了一下Julia的文档

“变量”章节,第一行

变量

Julia 语言中,变量是与某个值相关联(或绑定)的名字。你可以用它来保存一个值(例如某些计算得到的结果),供之后的代码使用。

“与其它语言的显著差异”章节,MATLAB段

Julia 的数组在赋值给另一个变量时不发生复制。执行 A = B 后,改变 B 中元素也会修改 A
Julia 的值在向函数传递时不发生复制。如果某个函数修改了数组,这一修改对调用者是可见的。

要不你试试Rust,一个Vec根本没法有两个名字,是否符合你的要求(手动狗头)

让你的Rust代码能编译过可是一门技术活

你这句讲的最明白

我觉得这个设计可能和实际工程实现有关吧。

大部分时候我们其实并不需要额外复制一份数组,所以在C里面往往都是通过传指针或引用来共享数据。在Julia里面默认做的就是类似的传引用的工作,而在确实需要共享数据的情况下,则明确地通过copy来实现。

a=b=[0]; a===b # true
a=b=0; a===b # true
a=0; b=0; a===b # true
前面三个一样

a=[0]; b=[0]; a===b # false

确定 x 和 y 是否相同,因为没有任何程序可以区分它们。首先比较 x 和 y 的类型。如果这些是
相同的可变对象按内存中的地址进行比较,不可变对象(如数字)按位级别的内容进行比较。这
功能是某个时候
这是我从REPL中找到的关于===的文档,注意内存地址比较

== 不好吗?

a=b=[0]; a==b # true
a=b=0; a==b # true
a=0; b=0; a==b # true    
a=[0]; b=[0]; a==b # true

一个求相等,一个求相同

上面回复也说了,文档里关于 === 的叙述如下

===(x,y) -> Bool
≡(x,y) -> Bool

Determine whether x and y are identical, in the sense that no
program could distinguish them. First the types of x and y are
compared. If those are identical, mutable objects are compared by
address in memory and immutable objects (such as numbers) are compared
by contents at the bit level.

x == y 先比较 xy 的类型;如果类型一样,则:

  • Mutable 变量比较地址;
  • Immutable 变量比较内存内容。

所以:
a = [0]; b = [0]; a === b 返回 false,因为 ab 是 mutable 变量,不是同一个对象,自然地址不同。
a = 0; b = 0; a === b 返回 true,因为 ab 是 Immutable 变量。但是这种情况也不说明 ab 在内存中地址一样,只能确定内容一样。
至于 a = b = [0] 这种连等的写法,编译器的解读是 a = (b = [0]);所以 ab 指向同一数组没问题。

楼上说了,不用赘述(而且你还有一个小错)。:grin:

就是啊,感觉都赘述这么久了,所以看不懂你这究竟是哪里有问题……:sunglasses:

你又拷贝一段英文出来想说明啥

还请您不吝赐教?

不懂你的用意???

[0] 是堆上的数组,a=[0]; b=[0]; 相当于 a = malloc(); b = malloc(); a === b 比较地址,当然不同。

(0,)这个才相当于是栈上的数组:

julia> a = (0,)
(0,)

julia> b = (0,)
(0,)

julia> a === b
true
2 个赞

我觉得题主是想说给一个可变的数组起两个名字没有什么意义吧,因为实践中用不到一个东西两个不同的名字。
但是第一,你用不到不代表别人用不到,有些情况下确实别名这个东西可以用做缩写。第二,正如前面的人所说的,除了Rust和Haskell实现了线性类型系统可以满足你这种没有别名的需求以外,其他的语言应该是不符合你的要求的。。。所以你到底在大惊小怪什么。。。
也确实有部分语言能把数组等部分内置类型做成值传递的(特殊处理),不过嘛,这样子内置数组就很特别了,这样的类型系统是不能扩展的。
一般来说动态类型的语言都采用引用模型(或者,如上面所说,也有一些语言把数组特殊处理),不管怎么样,从引用模型得到值模型更加容易一些,因为我们总是可以用copy确保传值。但是从值模型到引用模型要困难一些,因为如果默认传值,我们就需要一个构造来提取对某个值的引用(然后把这个引用作为值传递),这意味着引用本身也要有自己的类型(例如Julia中有Ref),考虑到科学计算往往我们都是把一个向量,数组传进一个函数里面修改一下(例如说把一个数组清零)得到一个新的答案,所以要求程序员手动构造Ref太麻烦。(一般来说函数的传递和赋值的语义是一致的,因为函数可以视为一个形式参数用实际参数赋值的let语句)

你的这个说法是不正确的,在Julia里只有给值取名(Julia中只有引用语义没有值语义),没有给变量赋值。所以到底要讲清楚什么。。。明明都是引用,没有一点歧义。
真正的问题在于可变与不可变,这个和是不是值还是引用没有关系,所以我就不说了。。。不可变+引用可以相等于值传递(反正你也改不了,所以怎么传递也无所谓)。
而且我觉得值传递是一个ill-defined的东西,很多东西都没有自然定义的值传递,大多数情况下,值传递都是指浅层拷贝(数组拷贝第一层?如果是数组的数组呢?),但是如果用一个树来赋值,难道我们不应该深层拷贝吗?所以说这个东西牵扯到很多很复杂的东西,很多动态语言设计的时候,直接就全部引用传递了(或者也可以向Haskell学习,变成纯函数的,这样就可以等价于全部值引用了)
另外你举的这个例子已经被 Gnimuc说清楚了,x=y=[0]x=[0];y=x的简写,就相当于只调用了malloc一次,x=[0];y=[0]构造了数组两次,所以是两个不同的数组。
如果你觉得这个语法有歧义的话(你怕出错的话),那么你就不应该使用这个语法。我看这个回答下很多人貌似都没觉得这到底有什么问题(虽然他们的回答也不完全严谨就是了),我觉得你不能纯粹把别的语言的东西搬过来,然后语法上一样就觉得是一样的东西。按照你这个道理,C语言里的voidvoid*还有歧义,因为void*不是指向void的指针。。。

2 个赞

我再总结一下这个问题:
这个问题本质上是关于变量和赋值语义的互相作用。
Julia的变量其实是对值的引用,例如说:

x=1
y=[1,2,3]

x是一个对整数的引用,而y是一个对数组的引用
当对一个表达式求值的时候,每个(右值)变量被解引用得到其值(所以x解引用得到1,y解引用得到一个数组,但是数组其实是用一个指向第一个元素的指针表示的,所以y的值其实是一个int*指针),这都好理解
问题出在赋值上,Julia的赋值就是一个变量重绑定的过程(更换引用)
所以

x=1
y=2
y=x+3

y先是一个对2的引用,然后变成对5的引用。同理:

x = [0]
y = x

x是一个对int的引用。y赋值后也得到这个对int的引用,所以是指向同一个东西。
所以我们应该把这个过程分为两步:变量总是引用的,赋值总是引用重绑定的(因为例如数组传的其实是数组的指针。。。一个变量引用一个指针,所以一个变量其实是一个二级指针int**),不管可变不变。Julia中只有这个语义。

真正tricky的地方在于,我们会认为[1,2,3]作为数组的值。应该是这个数组的整体(整个[1,2,3]),而非指向这个数组指针。所以我们会认为

x=[1,2,3]
y=x

如果是数组整个作为值进行值传递的话,那么就应该拷贝一份数组。但是我们已经看到了,Julia的变量总是引用,而赋值是重绑定的过程。这和怎么传递根本就没有关系,语言选择了这个语义,就必然要导致这个结果。

为什么一些别的语言需要这个值传递还是引用传递的区分?以C为例,这是因为C可以对一个变量取引用,注意在严格求值的语言中,仅仅用取引用函数是不能做到这一点的!
考虑两个程序:

#julia
x = 0
y = Ref(x)
y[]+=1
#C
int x = 0
int* y = &x
*y += 1 

区别在于Julia的程序中,尽管我们写着Ref(x)但是我们没法得到对于x所指向0的指针(因为Ref(x)的x先被求值为0,所以Ref(0)重新构造了指向另外一个0的指针),从而我们不能改动对于x的值。
所以在一些(静态)语言中,赋值才要做出这种区分,因为可以对变量取指针,因此可以随便改动这个变量所指内存区域的值,迫使我们要区分赋值是产生别名还是产生拷贝(要不然的话我们可能会不小心把本来不同的变量弄成是别名的)。但因为Julia中你反正也不能这么做,所以区分到底怎么传递也毫无意义。

5 个赞