我正在阅读文档,我经常对语言的一些设计决策摇头。但真正让我困惑的是数组是如何处理的。
我冲到操场上试了试。你也可以试试。第一个例子:
var a = [1, 2, 3]
var b = a
a[1] = 42
a
b
这里a和b都是[1,42,3],我可以接受。数组被引用-好的!
现在来看这个例子:
var c = [1, 2, 3]
var d = c
c.append(42)
c
d
c是[1,2,3,42]但d是[1,2,3]。也就是说,d在上一个例子中看到了变化,但在这个例子中没有看到。文档上说这是因为长度变了。
现在,看看这个:
var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e
f
E是[4,5,3],很酷。有一个多索引替换是很好的,但是f STILL没有看到变化,即使长度没有改变。
总而言之,如果更改了一个元素,对数组的常见引用就会看到变化,但如果更改了多个元素或附加项,则会生成一个副本。
在我看来,这是一个非常糟糕的设计。我这样想对吗?有什么原因让我不明白数组为什么应该这样做吗?
编辑:数组已经改变,现在有值语义。理智多了!
对我来说,如果你先用变量替换常量,这更有意义:
a[i] = 42 // (1)
e[i..j] = [4, 5] // (2)
第一行永远不需要改变a的大小,特别是,它永远不需要做任何内存分配。不管i的值是多少,这都是一个轻量级操作。如果你把a想象成一个指针,它可以是一个常量指针。
第二行可能要复杂得多。根据i和j的值,您可能需要进行内存管理。如果你想象e是一个指向数组内容的指针,你不能再假设它是一个常量指针;您可能需要分配一个新的内存块,将数据从旧的内存块复制到新的内存块,并更改指针。
看起来语言设计者们已经试图保持(1)尽可能的轻量级。因为(2)可能涉及复制,他们已经采取了解决方案,它总是表现得好像你做了一个复制。
这很复杂,但我很高兴他们没有让它变得更复杂,例如,特殊情况下,如“如果在(2)I和j是编译时常数,编译器可以推断出e的大小不会改变,那么我们不复制”。
最后,基于我对Swift语言设计原则的理解,我认为一般规则如下:
默认情况下,在所有地方都使用常量(let) always,这样就不会有什么大的意外了。
只有在绝对必要的情况下才使用变量(var),并在这些情况下要小心,因为会出现令人惊讶的情况[这里:在某些情况下,但不是所有情况下,数组的奇怪隐式副本]。
其行为与Array极其相似。. net中的Resize方法。要了解发生了什么,看看历史可能会有所帮助。C, c++, Java, c#和Swift中的token。
在C语言中,结构只不过是变量的聚合。应用。结构类型的变量将访问存储在结构中的变量。指向对象的指针不包含变量的聚合,而是标识它们。如果指针标识一个结构,->操作符可用于访问存储在该指针标识的结构中的变量。
在c++中,结构和类不仅可以聚合变量,还可以将代码附加到变量上。使用。调用一个方法将对一个变量要求该方法对变量本身的内容进行操作;在标识对象的变量上使用->将要求该方法对该变量标识的对象进行操作。
In Java, all custom variable types simply identify objects, and invoking a method upon a variable will tell the method what object is identified by the variable. Variables cannot hold any kind of composite data type directly, nor is there any means by which a method can access a variable upon which it is invoked. These restrictions, although semantically limiting, greatly simplify the runtime, and facilitate bytecode validation; such simplifications reduced the resource overhead of Java at a time when the market was sensitive to such issues, and thus helped it gain traction in the marketplace. They also meant that there was no need for a token equivalent to the . used in C or C++. Although Java could have used -> in the same way as C and C++, the creators opted to use single-character . since it was not needed for any other purpose.
In C# and other .NET languages, variables can either identify objects or hold composite data types directly. When used on a variable of a composite data type, . acts upon the contents of the variable; when used on a variable of reference type, . acts upon the object identified by it. For some kinds of operations, the semantic distinction isn't particularly important, but for others it is. The most problematical situations are those in which a composite data type's method which would modify the variable upon which it is invoked, is invoked on a read-only variable. If an attempt is made to invoke a method on a read-only value or variable, compilers will generally copy the variable, let the method act upon that, and discard the variable. This is generally safe with methods that only read the variable, but not safe with methods that write to it. Unfortunately, .does has not as yet have any means of indicating which methods can safely be used with such substitution and which can't.
In Swift, methods on aggregates can expressly indicate whether they will modify the variable upon which they are invoked, and the compiler will forbid the use of mutating methods upon read-only variables (rather than having them mutate temporary copies of the variable which will then get discarded). Because of this distinction, using the . token to call methods that modify the variables upon which they are invoked is much safer in Swift than in .NET. Unfortunately, the fact that the same . token is used for that purpose as to act upon an external object identified by a variable means the possibility for confusion remains.
如果有一台时光机,回到c#和/或Swift的创建时期,人们就可以通过让语言使用。和->令牌更接近c++的用法。可以使用聚合类型和引用类型的方法。作用于调用它们的变量,->作用于一个值(对于组合)或由此标识的东西(对于引用类型)。然而,这两种语言都不是这样设计的。
In C#, the normal practice for a method to modify a variable upon which it is invoked is to pass the variable as a ref parameter to a method. Thus calling Array.Resize(ref someArray, 23); when someArray identifies an array of 20 elements will cause someArray to identify a new array of 23 elements, without affecting the original array. The use of ref makes clear that the method should be expected to modify the variable upon which it is invoked. In many cases, it's advantageous to be able to modify variables without having to use static methods; Swift addresses that means by using . syntax. The disadvantage is that it loses clarify as to what methods act upon variables and what methods act upon values.