Có một lý do nào đó mà việc gán mảng Swift không nhất quán (không phải là tài liệu tham khảo hay bản sao sâu)?


216

Tôi đang đọc tài liệu và tôi liên tục lắc đầu trước một số quyết định thiết kế của ngôn ngữ. Nhưng điều thực sự khiến tôi bối rối là cách xử lý mảng.

Tôi vội vã đến sân chơi và thử những thứ này. Bạn cũng có thể thử chúng. Vì vậy, ví dụ đầu tiên:

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b

Đây ablà cả hai [1, 42, 3], mà tôi có thể chấp nhận. Mảng được tham chiếu - OK!

Bây giờ hãy xem ví dụ này:

var c = [1, 2, 3]
var d = c
c.append(42)
c
d

c[1, 2, 3, 42]NHƯNG d[1, 2, 3]. Đó là, dđã thấy sự thay đổi trong ví dụ trước nhưng không thấy nó trong ví dụ này. Các tài liệu nói rằng đó là vì chiều dài thay đổi.

Bây giờ, làm thế nào về điều này:

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e
f

e[4, 5, 3], đó là mát mẻ. Thật tuyệt khi có một sự thay thế đa chỉ mục, nhưng fVẪN không thấy sự thay đổi mặc dù độ dài không thay đổi.

Vì vậy, để tổng hợp nó, các tham chiếu phổ biến đến một mảng sẽ thay đổi nếu bạn thay đổi 1 phần tử, nhưng nếu bạn thay đổi nhiều phần tử hoặc chắp thêm các mục, một bản sao sẽ được tạo.

Đây có vẻ như là một thiết kế rất kém với tôi. Tôi có đúng trong suy nghĩ này không? Có một lý do tôi không thấy tại sao các mảng nên hành động như thế này?

EDIT : Mảng đã thay đổi và bây giờ có ngữ nghĩa giá trị. Mạnh mẽ hơn nhiều!


95
Đối với hồ sơ, tôi không nghĩ câu hỏi này nên được đóng lại. Swift là một ngôn ngữ mới, vì vậy sẽ có những câu hỏi như thế này trong một thời gian khi tất cả chúng ta đều học. Tôi thấy câu hỏi này rất thú vị và tôi hy vọng rằng ai đó sẽ có một trường hợp hấp dẫn về phòng thủ.
Joel Berger

4
@Joel Fine, hãy hỏi nó về lập trình viên, Stack Overflow dành cho các vấn đề lập trình không được đề cập cụ thể.
bjb568

21
@ bjb568: Tuy nhiên, đó không phải là ý kiến. Câu hỏi này nên được trả lời với sự thật. Nếu một số nhà phát triển Swift đến và trả lời "Chúng tôi đã làm như vậy cho X, Y và Z", thì đó là sự thật. Bạn có thể không đồng ý với X, Y và Z, nhưng nếu quyết định được đưa ra cho X, Y và Z thì đó chỉ là sự thật lịch sử của thiết kế ngôn ngữ. Giống như khi tôi hỏi tại sao std::shared_ptrkhông có phiên bản phi nguyên tử, đã có câu trả lời dựa trên thực tế chứ không phải ý kiến (thực tế là ủy ban đã xem xét nhưng không muốn vì nhiều lý do).
Bắp ngô

7
@JasonMArcher: Chỉ đoạn cuối cùng dựa trên ý kiến ​​(mà vâng, có lẽ nên được loại bỏ). Tiêu đề thực tế của câu hỏi (mà tôi lấy như chính câu hỏi thực tế) có thể trả lời được bằng thực tế. Có một lý do các mảng được thiết kế để làm việc theo cách họ làm việc.
Bắp ngô

7
Đúng, như API-Beast đã nói, điều này thường được gọi là "Sao chép trên một nửa ngôn ngữ-thiết kế ngôn ngữ".
R. Martinho Fernandes

Câu trả lời:


109

Lưu ý rằng ngữ nghĩa mảng và cú pháp đã được thay đổi trong phiên bản Xcode beta 3 ( bài đăng trên blog ), vì vậy câu hỏi không còn được áp dụng. Câu trả lời sau đây áp dụng cho beta 2:


Đó là vì lý do hiệu suất. Về cơ bản, họ cố gắng tránh sao chép mảng miễn là họ có thể (và yêu cầu "hiệu suất giống như C"). Để trích dẫn cuốn sách ngôn ngữ :

Đối với mảng, sao chép chỉ diễn ra khi bạn thực hiện một hành động có khả năng sửa đổi độ dài của mảng. Điều này bao gồm nối thêm, chèn hoặc xóa các mục hoặc sử dụng một chỉ mục có phạm vi để thay thế một loạt các mục trong mảng.

Tôi đồng ý rằng điều này hơi khó hiểu, nhưng ít nhất có một mô tả rõ ràng và đơn giản về cách thức hoạt động của nó.

Phần đó cũng bao gồm thông tin về cách đảm bảo một mảng được tham chiếu duy nhất, cách buộc các mảng sao chép và cách kiểm tra xem hai mảng có chia sẻ lưu trữ hay không.


61
Tôi thấy thực tế là bạn có cả unshare và sao chép một lá cờ đỏ LỚN trong thiết kế.
Cthutu

9
Chính xác. Một kỹ sư mô tả với tôi rằng đối với thiết kế ngôn ngữ, điều này là không mong muốn và là điều họ hy vọng sẽ "sửa chữa" trong các bản cập nhật sắp tới cho Swift. Bỏ phiếu với radar.
Erik Kerber

2
Nó chỉ giống như sao chép trên ghi (COW) trong quản lý bộ nhớ con của Linux, phải không? Có lẽ chúng ta có thể gọi nó là thay đổi độ dài sao chép (COLA). Tôi thấy đây là một thiết kế tích cực.
cần

3
@justhalf Tôi có thể dự đoán một loạt những người mới bối rối đến với SO và hỏi tại sao mảng của họ không được chia sẻ (chỉ trong một cách ít rõ ràng hơn).
John Dvorak

11
@justhalf: COW là một sự bi quan trong thế giới hiện đại, và thứ hai, COW là một kỹ thuật chỉ thực hiện và công cụ COLA này dẫn đến chia sẻ hoàn toàn ngẫu nhiên và không chia sẻ.
Cún con

25

Từ tài liệu chính thức của ngôn ngữ Swift :

Lưu ý rằng mảng không được sao chép khi bạn đặt một giá trị mới bằng cú pháp đăng ký, bởi vì việc đặt một giá trị duy nhất với cú pháp đăng ký không có khả năng thay đổi độ dài của mảng. Tuy nhiên, nếu bạn nối một mục mới vào mảng, bạn sẽ sửa đổi độ dài của mảng . Điều này nhắc Swift tạo một bản sao mới của mảng tại điểm bạn nối thêm giá trị mới. Do đó, a là một bản sao độc lập, riêng biệt của mảng .....

Đọc toàn bộ phần Chuyển nhượng và Sao chép Hành vi cho Mảng trong tài liệu này. Bạn sẽ thấy rằng khi bạn thay thế một loạt các mục trong mảng thì mảng sẽ lấy một bản sao của chính nó cho tất cả các mục.


4
Cảm ơn. Tôi đã đề cập đến văn bản đó một cách mơ hồ trong câu hỏi của tôi. Nhưng tôi đã chỉ ra một ví dụ trong đó việc thay đổi phạm vi đăng ký không thay đổi độ dài và nó vẫn được sao chép. Vì vậy, nếu bạn không muốn có một bản sao, bạn phải thay đổi từng yếu tố một.
Cthutu

21

Hành vi đã thay đổi với Xcode 6 beta 3. Mảng không còn là kiểu tham chiếu và có cơ chế sao chép khi ghi , nghĩa là ngay khi bạn thay đổi nội dung của một mảng từ một hoặc biến khác, mảng sẽ được sao chép và chỉ một bản sao sẽ được thay đổi.


Câu trả lời cũ:

Như những người khác đã chỉ ra, Swift cố gắng tránh sao chép mảng nếu có thể, kể cả khi thay đổi giá trị cho các chỉ mục duy nhất tại một thời điểm.

Nếu bạn muốn chắc chắn rằng một biến mảng (!) Là duy nhất, tức là không được chia sẻ với một biến khác, bạn có thể gọi unsharephương thức. Điều này sao chép mảng trừ khi nó chỉ có một tham chiếu. Tất nhiên bạn cũng có thể gọi copyphương thức, phương thức này sẽ luôn tạo một bản sao, nhưng unshare được ưu tiên để đảm bảo không có biến nào khác giữ trên cùng một mảng.

var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a               // [1, 42, 3]
b               // [1, 2, 3]

hmm, đối với tôi, unshare()phương pháp đó là không xác định.
Hlung

1
@Hlung Nó đã bị xóa trong phiên bản beta 3, tôi đã cập nhật câu trả lời của mình.
Pascal

12

Hành vi cực kỳ giống với Array.Resizephương thức trong .NET. Để hiểu những gì đang diễn ra, có thể hữu ích khi xem lịch sử của .mã thông báo trong C, C ++, Java, C # và Swift.

Trong C, một cấu trúc không có gì khác hơn là tổng hợp các biến. Áp dụng .một biến của kiểu cấu trúc sẽ truy cập vào một biến được lưu trữ trong cấu trúc. Con trỏ tới các đối tượng không giữ tập hợp các biến, nhưng xác định chúng. Nếu một con trỏ xác định cấu trúc, ->toán tử có thể được sử dụng để truy cập vào một biến được lưu trữ trong cấu trúc được xác định bởi con trỏ.

Trong C ++, các cấu trúc và các lớp không chỉ tổng hợp các biến mà còn có thể đính kèm mã vào chúng. Sử dụng .để gọi một phương thức sẽ trên một biến yêu cầu phương thức đó hành động theo chính nội dung của biến đó ; sử dụng ->trên một biến xác định một đối tượng sẽ yêu cầu phương thức đó hành động theo đối tượng được xác định bởi biến đó.

Trong Java, tất cả các loại biến tùy chỉnh chỉ cần xác định các đối tượng và gọi một phương thức trên một biến sẽ cho phương thức biết đối tượng nào được xác định bởi biến. Các biến không thể trực tiếp giữ bất kỳ loại dữ liệu tổng hợp nào, cũng như không có bất kỳ phương tiện nào mà một phương thức có thể truy cập vào một biến mà nó được gọi. Những hạn chế này, mặc dù hạn chế về mặt ngữ nghĩa, đơn giản hóa rất nhiều thời gian chạy và tạo điều kiện cho việc xác thực mã byte; sự đơn giản hóa như vậy đã làm giảm chi phí tài nguyên của Java tại thời điểm thị trường nhạy cảm với các vấn đề như vậy và do đó đã giúp nó có được lực kéo trên thị trường. Chúng cũng có nghĩa là không cần mã thông báo tương đương với mã .được sử dụng trong C hoặc C ++. Mặc dù Java có thể đã được sử dụng ->giống như C và C ++, nhưng những người tạo đã chọn sử dụng ký tự đơn. vì nó không cần thiết cho bất kỳ mục đích nào khác.

Trong C # và các ngôn ngữ .NET khác, các biến có thể xác định các đối tượng hoặc giữ các kiểu dữ liệu tổng hợp trực tiếp. Khi được sử dụng trên một biến của kiểu dữ liệu tổng hợp, sẽ .tác động đến nội dung của biến đó; khi được sử dụng trên một biến của kiểu tham chiếu, .tác động lên đối tượng được xác địnhbởi nó. Đối với một số loại hoạt động, sự khác biệt về ngữ nghĩa không đặc biệt quan trọng, nhưng đối với các hoạt động khác thì không. Các tình huống có vấn đề nhất là những tình huống trong đó phương thức của kiểu dữ liệu tổng hợp sẽ sửa đổi biến mà nó được gọi, được gọi trên một biến chỉ đọc. Nếu một nỗ lực được thực hiện để gọi một phương thức trên một giá trị hoặc biến chỉ đọc, trình biên dịch thường sẽ sao chép biến đó, hãy để phương thức hành động theo đó và loại bỏ biến đó. Điều này thường an toàn với các phương thức chỉ đọc biến, nhưng không an toàn với các phương thức ghi vào nó. Thật không may, .does vẫn chưa có bất kỳ phương tiện nào cho biết phương pháp nào có thể được sử dụng một cách an toàn với sự thay thế đó và phương pháp nào không thể.

Trong Swift, các phương thức trên tổng hợp có thể chỉ ra rõ ràng liệu chúng có sửa đổi biến mà chúng được gọi hay không và trình biên dịch sẽ cấm sử dụng các phương thức biến đổi trên các biến chỉ đọc (thay vì chúng biến đổi các bản sao tạm thời của biến đó bị loại bỏ). Do sự khác biệt này, sử dụng .mã thông báo để gọi các phương thức sửa đổi các biến mà chúng được gọi sẽ an toàn hơn nhiều trong Swift so với .NET. Thật không may, thực tế là cùng một .mã thông báo được sử dụng cho mục đích đó để hành động theo một đối tượng bên ngoài được xác định bởi một biến có nghĩa là khả năng gây nhầm lẫn vẫn còn.

Nếu có một cỗ máy thời gian và quay trở lại việc tạo ra C # và / hoặc Swift, người ta có thể tránh được nhiều sự nhầm lẫn xung quanh các vấn đề như vậy bằng cách sử dụng các ngôn ngữ .->mã thông báo theo cách gần gũi hơn với việc sử dụng C ++. Các phương thức của cả hai loại tổng hợp và tham chiếu có thể sử dụng .để hành động theo biến mà chúng được gọi và ->hành động theo một giá trị (đối với vật liệu tổng hợp) hoặc vật được xác định qua đó (đối với loại tham chiếu). Không ngôn ngữ nào được thiết kế theo cách đó, tuy nhiên.

Trong C #, cách thực hành thông thường đối với một phương thức để sửa đổi một biến mà nó được gọi là truyền biến đó dưới dạng reftham số cho một phương thức. Do đó, việc gọi Array.Resize(ref someArray, 23);khi someArrayxác định một mảng gồm 20 phần tử sẽ gây ra someArrayviệc xác định một mảng mới gồm 23 phần tử, mà không ảnh hưởng đến mảng ban đầu. Việc sử dụng reflàm rõ rằng phương thức nên được dự kiến ​​sẽ sửa đổi biến mà nó được gọi. Trong nhiều trường hợp, thật thuận lợi khi có thể sửa đổi các biến mà không phải sử dụng các phương thức tĩnh; Địa chỉ Swift có nghĩa là bằng cách sử dụng .cú pháp. Nhược điểm là nó mất đi sự rõ ràng về phương thức nào tác động đến các biến và phương thức nào tác động đến các giá trị.


5

Đối với tôi điều này có ý nghĩa hơn nếu lần đầu tiên bạn thay thế hằng số của mình bằng các biến:

a[i] = 42            // (1)
e[i..j] = [4, 5]     // (2)

Dòng đầu tiên không bao giờ cần thay đổi kích thước a. Đặc biệt, nó không bao giờ cần thực hiện bất kỳ phân bổ bộ nhớ. Bất kể giá trị của i, đây là một hoạt động nhẹ. Nếu bạn tưởng tượng rằng dưới mui xe alà một con trỏ, nó có thể là một con trỏ không đổi.

Dòng thứ hai có thể phức tạp hơn nhiều. Tùy thuộc vào các giá trị của ij, bạn có thể cần thực hiện quản lý bộ nhớ. Nếu bạn tưởng tượng đó elà một con trỏ trỏ đến nội dung của mảng, bạn không còn có thể cho rằng đó là một con trỏ không đổi; bạn có thể cần phân bổ một khối bộ nhớ mới, sao chép dữ liệu từ khối bộ nhớ cũ sang khối bộ nhớ mới và thay đổi con trỏ.

Có vẻ như các nhà thiết kế ngôn ngữ đã cố gắng giữ (1) càng nhẹ càng tốt. Vì (2) dù sao cũng có thể liên quan đến việc sao chép, họ đã dùng đến giải pháp mà nó luôn hoạt động như thể bạn đã sao chép.

Điều này rất phức tạp, nhưng tôi rất vui vì họ không làm cho nó phức tạp hơn nữa, ví dụ như các trường hợp đặc biệt như "nếu trong (2) i và j là các hằng số thời gian biên dịch và trình biên dịch có thể suy ra rằng kích thước của e sẽ không xảy ra để thay đổi, sau đó chúng tôi không sao chép " .


Cuối cùng, dựa trên sự hiểu biết của tôi về các nguyên tắc thiết kế của ngôn ngữ Swift, tôi nghĩ các quy tắc chung là:

  • Sử dụng hằng số ( let) luôn ở mọi nơi theo mặc định và sẽ không có bất ngờ lớn nào.
  • Chỉ sử dụng các biến ( var) nếu thực sự cần thiết và cẩn thận thay đổi trong những trường hợp đó, vì sẽ có những bất ngờ [ở đây: các bản sao ngầm ẩn của mảng trong một số nhưng không phải tất cả các tình huống].

5

Những gì tôi đã tìm thấy là: Mảng sẽ là một bản sao có thể thay đổi của tham chiếu khi và chỉ khi hoạt động có khả năng thay đổi độ dài của mảng . Trong ví dụ cuối cùng của bạn, f[0..2]lập chỉ mục với nhiều người, thao tác có khả năng thay đổi độ dài của nó (có thể là trùng lặp không được phép), do đó, nó sẽ bị sao chép.

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e // 4,5,3
f // 1,2,3


var e1 = [1, 2, 3]
var f1 = e1

e1[0] = 4
e1[1] = 5

e1 //  - 4,5,3
f1 // - 4,5,3

8
"được coi là chiều dài đã thay đổi" Tôi có thể hiểu rằng nó sẽ bị sao chép nếu chiều dài bị thay đổi, nhưng kết hợp với trích dẫn ở trên, tôi nghĩ đây là một "tính năng" thực sự đáng lo ngại và tôi nghĩ nhiều người sẽ hiểu sai
Joel Berger

25
Chỉ vì một ngôn ngữ mới không có nghĩa là nó ổn để chứa những mâu thuẫn nội bộ rõ ràng.
Các cuộc đua nhẹ nhàng trong quỹ đạo

Điều này đã được "sửa" trong phiên bản beta 3, varcác mảng giờ hoàn toàn có thể thay đổi và letcác mảng hoàn toàn không thay đổi.
Pascal

4

Các chuỗi và mảng của Delphi có cùng "tính năng". Khi bạn nhìn vào việc thực hiện, nó có ý nghĩa.

Mỗi biến là một con trỏ tới bộ nhớ động. Bộ nhớ đó chứa số tham chiếu theo sau là dữ liệu trong mảng. Vì vậy, bạn có thể dễ dàng thay đổi một giá trị trong mảng mà không cần sao chép toàn bộ mảng hoặc thay đổi bất kỳ con trỏ nào. Nếu bạn muốn thay đổi kích thước mảng, bạn phải phân bổ thêm bộ nhớ. Trong trường hợp đó, biến hiện tại sẽ trỏ đến bộ nhớ mới được phân bổ. Nhưng bạn không thể dễ dàng theo dõi tất cả các biến khác chỉ vào mảng ban đầu, vì vậy bạn để chúng một mình.

Tất nhiên, sẽ không khó để thực hiện nhất quán hơn. Nếu bạn muốn tất cả các biến để xem thay đổi kích thước, hãy làm điều này: Mỗi biến là một con trỏ tới một thùng chứa được lưu trữ trong bộ nhớ động. Container chứa chính xác hai thứ, một số tham chiếu và con trỏ tới dữ liệu mảng thực tế. Dữ liệu mảng được lưu trữ trong một khối bộ nhớ động riêng biệt. Bây giờ chỉ có một con trỏ tới dữ liệu mảng, vì vậy bạn có thể dễ dàng thay đổi kích thước đó và tất cả các biến sẽ thấy sự thay đổi.


4

Rất nhiều người chấp nhận sớm Swift đã phàn nàn về ngữ nghĩa mảng dễ bị lỗi này và Chris Lattner đã viết rằng ngữ nghĩa mảng đã được sửa đổi để cung cấp ngữ nghĩa giá trị đầy đủ ( liên kết Nhà phát triển của Apple cho những người có tài khoản ). Chúng tôi sẽ phải chờ ít nhất cho phiên bản beta tiếp theo để xem chính xác điều này có nghĩa là gì.


1
Hành vi Array mới hiện có sẵn kể từ SDK đi kèm với iOS 8 / Xcode 6 Beta 3.
smileyborg

0

Tôi sử dụng .copy () cho việc này.

    var a = [1, 2, 3]
    var b = a.copy()
     a[1] = 42 

1
Tôi nhận được "Giá trị của loại '[Int]' không có thành viên 'bản sao'" khi tôi chạy mã của bạn
jreft56

0

Có điều gì thay đổi trong hành vi mảng trong các phiên bản Swift sau này không? Tôi chỉ chạy ví dụ của bạn:

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b

Và kết quả của tôi là [1, 42, 3] và [1, 2, 3]

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.