Swift và cấu trúc đột biến


96

Có điều gì đó mà tôi không hoàn toàn hiểu khi nói đến các kiểu giá trị thay đổi trong Swift.

Như iBook "Ngôn ngữ lập trình Swift" tuyên bố: Theo mặc định, không thể sửa đổi các thuộc tính của loại giá trị từ bên trong các phương thức thể hiện của nó.

Và vì vậy, để làm cho điều này khả thi, chúng ta có thể khai báo các phương thức với mutatingtừ khóa bên trong struct và enums.

Điều không hoàn toàn rõ ràng đối với tôi là: Bạn có thể thay đổi một var từ bên ngoài một cấu trúc, nhưng bạn không thể thay đổi nó từ các phương thức của chính nó. Điều này có vẻ phản trực quan đối với tôi, như trong các ngôn ngữ Hướng đối tượng, bạn thường cố gắng đóng gói các biến để chúng chỉ có thể được thay đổi từ bên trong. Với cấu trúc, điều này có vẻ ngược lại. Để giải thích, đây là một đoạn mã:

struct Point {
    var x = 0, y = 0
    mutating func moveToX(x: Int, andY y:Int) { //Needs to be a mutating method in order to work
        self.x = x
        self.y = y
    }
}

var p = Point(x: 1, y: 2)
p.x = 3 //Works from outside the struct!
p.moveToX(5, andY: 5) 

Có ai biết lý do tại sao các cấu trúc không thể thay đổi nội dung của chúng từ bên trong ngữ cảnh của chính chúng, trong khi nội dung có thể dễ dàng được thay đổi ở nơi khác không?

Câu trả lời:


80

Thuộc tính khả biến được đánh dấu trên một bộ lưu trữ (hằng số hoặc biến), không phải một kiểu. Bạn có thể nghĩ rằng struct có hai chế độ: có thể thay đổikhông thể thay đổi . Nếu bạn gán một giá trị struct cho một vùng lưu trữ không thay đổi (chúng tôi gọi nó lethoặc hằng số trong Swift) thì giá trị đó sẽ trở thành chế độ không thay đổi và bạn không thể thay đổi bất kỳ trạng thái nào trong giá trị. (bao gồm cả việc gọi bất kỳ phương thức đột biến nào)

Nếu giá trị được gán cho một vùng lưu trữ có thể thay đổi (chúng tôi gọi nó varhoặc biến trong Swift), bạn có thể tự do sửa đổi trạng thái của chúng và việc gọi phương thức thay đổi được phép.

Ngoài ra, các lớp không có chế độ bất biến / có thể thay đổi này. IMO, điều này là do các lớp thường được sử dụng để đại diện cho thực thể có thể tham chiếu . Và thực thể có thể tham chiếu thường có thể thay đổi được vì rất khó tạo và quản lý đồ thị tham chiếu của các thực thể theo cách bất biến với hiệu suất phù hợp. Họ có thể thêm tính năng này sau, nhưng ít nhất không phải bây giờ.

Đối với các lập trình viên Objective-C, khái niệm có thể thay đổi / bất biến là rất quen thuộc. Trong Objective-C, chúng tôi có hai lớp riêng biệt cho mỗi khái niệm, nhưng trong Swift, bạn có thể làm điều này với một cấu trúc. Một nửa công việc.

Đối với các lập trình viên C / C ++, đây cũng là khái niệm rất quen thuộc. Đây chính xác là những gì consttừ khóa làm trong C / C ++.

Ngoài ra, giá trị không thay đổi có thể được tối ưu hóa rất độc đáo. Về lý thuyết, trình biên dịch Swift (hoặc LLVM) có thể thực hiện sao chép-elision trên các giá trị được truyền qua let, giống như trong C ++. Nếu bạn sử dụng cấu trúc không thay đổi một cách khôn ngoan, nó sẽ hoạt động tốt hơn các lớp đã được đếm lại.

Cập nhật

Vì @Joseph tuyên bố điều này không cung cấp lý do tại sao , tôi đang bổ sung thêm một chút.

Cấu trúc có hai loại phương thức. phương pháp đơn giảnđột biến . Plain phương pháp ngụ ý bất biến (hoặc không mutating) . Sự tách biệt này chỉ tồn tại để hỗ trợ ngữ nghĩa bất biến . Một đối tượng ở chế độ không thay đổi sẽ không thay đổi trạng thái của nó.

Sau đó, các phương thức bất biến phải đảm bảo tính bất biến về ngữ nghĩa này . Có nghĩa là nó sẽ không thay đổi bất kỳ giá trị nội bộ nào. Vì vậy, trình biên dịch không cho phép bất kỳ thay đổi trạng thái nào của chính nó trong một phương thức bất biến. Ngược lại, các phương thức đột biến có thể tự do sửa đổi trạng thái.

Và sau đó, bạn có thể có câu hỏi tại sao không thay đổi được là mặc định? Đó là bởi vì rất khó để dự đoán trạng thái trong tương lai của các giá trị đột biến, và điều đó thường trở thành nguồn gốc chính gây đau đầu và lỗi. Nhiều người đã đồng ý rằng giải pháp này là tránh những thứ có thể thay đổi, và sau đó bất biến theo mặc định đã nằm trên danh sách mong muốn trong nhiều thập kỷ trong các ngôn ngữ họ C / C ++ và các dẫn xuất của nó.

Xem phong cách chức năng thuần túy để biết thêm chi tiết. Dù sao đi nữa, chúng ta vẫn cần những thứ có thể thay đổi bởi vì những thứ không thể thay đổi được có một số điểm yếu và việc thảo luận về chúng dường như là lạc đề.

Tôi hi vọng cái này giúp được.


2
Vì vậy, về cơ bản, tôi có thể giải thích nó như sau: Một cấu trúc không biết trước nó sẽ có thể thay đổi hoặc bất biến, vì vậy nó giả định là bất biến trừ khi được nêu khác đi. Nhìn như vậy quả thực rất có lý. Điều này có chính xác?
Mike Seghers

1
@MikeSeghers Tôi nghĩ điều đó có lý.
eonil

3
Mặc dù cho đến nay đây là câu trả lời tốt nhất trong số hai câu trả lời, tôi không nghĩ nó trả lời TẠI SAO, theo mặc định, các thuộc tính của một loại giá trị không thể được sửa đổi từ bên trong các phương thức thể hiện của nó. bạn đưa ra gợi ý rằng đó là vì cấu trúc mặc định là không thay đổi, nhưng tôi không nghĩ điều đó đúng
Joseph Knight

4
@JosephKnight Nó không phải là cấu trúc mặc định thành bất biến. Đó là các phương thức của cấu trúc được mặc định là không thể thay đổi. Hãy nghĩ nó giống như C ++ với giả định ngược lại. Trong các phương thức C ++ mặc định không phải là hằng số này, bạn phải thêm rõ ràng một constvào phương thức nếu bạn muốn nó có 'const this' và được phép trên các trường hợp hằng số (không thể sử dụng các phương thức bình thường nếu trường hợp là const). Swift làm điều tương tự với mặc định ngược lại: một phương thức có 'const this' theo mặc định và bạn đánh dấu nó theo cách khác nếu nó phải bị cấm đối với các trường hợp không đổi.
Tệp tương tự

3
@golddove Có, và bạn không thể. Bạn không nên. Bạn luôn cần tạo hoặc lấy một phiên bản mới của giá trị và chương trình của bạn cần được thiết kế để áp dụng theo cách này một cách có chủ ý. Tính năng tồn tại để ngăn chặn đột biến ngẫu nhiên như vậy. Một lần nữa, đây là một trong những khái niệm cốt lõi của lập trình kiểu hàm .
eonil

25

Cấu trúc là một tập hợp các trường; nếu một cá thể cấu trúc cụ thể có thể thay đổi, các trường của nó sẽ có thể thay đổi được; nếu một thể hiện là bất biến, các trường của nó sẽ là bất biến. Do đó, một kiểu cấu trúc phải được chuẩn bị cho khả năng các trường của bất kỳ trường hợp cụ thể nào có thể thay đổi hoặc bất biến.

Để một phương thức cấu trúc có thể thay đổi các trường của cấu trúc bên dưới, các trường đó phải có thể thay đổi được. Nếu một phương thức thay đổi các trường của cấu trúc bên dưới được gọi trên một cấu trúc bất biến, nó sẽ cố gắng thay đổi các trường bất biến. Vì không có điều gì tốt đẹp có thể xảy ra, nên cần phải cấm những lời kêu gọi như vậy.

Để đạt được điều đó, Swift chia các phương thức cấu trúc thành hai loại: những phương thức sửa đổi cấu trúc bên dưới và do đó chỉ có thể được gọi trên các trường hợp cấu trúc có thể thay đổi và những phương thức không sửa đổi cấu trúc bên dưới và do đó, bất khả xâm phạm trên cả hai trường hợp có thể thay đổi và bất biến . Việc sử dụng thứ hai có lẽ là thường xuyên hơn và do đó là mặc định.

Để so sánh, .NET hiện tại (vẫn còn!) Không cung cấp phương tiện phân biệt các phương thức cấu trúc sửa đổi cấu trúc với những phương thức không. Thay vào đó, việc gọi một phương thức cấu trúc trên một cá thể cấu trúc bất biến sẽ khiến trình biên dịch tạo một bản sao có thể thay đổi của cá thể cấu trúc, cho phép phương thức làm bất cứ điều gì nó muốn với nó và loại bỏ bản sao khi phương thức được thực hiện xong. Điều này có tác dụng buộc trình biên dịch lãng phí thời gian sao chép cấu trúc cho dù phương thức có sửa đổi nó hay không, mặc dù việc thêm thao tác sao chép hầu như sẽ không bao giờ biến những gì sẽ là mã không chính xác về mặt ngữ nghĩa thành mã đúng về mặt ngữ nghĩa; nó sẽ chỉ khiến mã sai ngữ nghĩa theo một cách (sửa đổi một giá trị "không thay đổi") theo một cách khác (cho phép mã nghĩ rằng nó đang sửa đổi cấu trúc, nhưng loại bỏ các thay đổi đã cố gắng). Việc cho phép các phương thức struct cho biết liệu chúng có sửa đổi cấu trúc bên dưới hay không có thể loại bỏ nhu cầu thực hiện thao tác sao chép vô ích và cũng đảm bảo rằng việc cố gắng sử dụng sai sẽ bị gắn cờ.


1
So sánh với .NET và một ví dụ không có từ khóa đột biến đã làm cho tôi thấy rõ. Lý do tại sao từ khóa đột biến sẽ tạo ra bất kỳ lợi ích nào.
Binarian

19

Thận trọng: điều khoản của giáo dân phía trước.

Lời giải thích này không đúng một cách nghiêm ngặt ở cấp mã nitty-gritty nhất. Tuy nhiên, nó đã được đánh giá bởi một người thực sự làm việc trên Swift và anh ấy nói rằng nó đủ tốt như một lời giải thích cơ bản.

Vì vậy, tôi muốn cố gắng trả lời đơn giản và trực tiếp câu hỏi "tại sao".

Nói chính xác: tại sao chúng ta phải đánh dấu các hàm struct mutatingkhi chúng ta có thể thay đổi các tham số struct mà không cần sửa đổi từ khóa nào?

Vì vậy, bức tranh toàn cảnh, nó liên quan rất nhiều đến triết lý giữ Swift nhanh chóng.

Bạn có thể nghĩ về nó giống như vấn đề quản lý các địa chỉ vật lý thực tế. Khi bạn thay đổi địa chỉ của mình, nếu có nhiều người có địa chỉ hiện tại của bạn, bạn phải thông báo cho tất cả họ rằng bạn đã chuyển đi. Nhưng nếu không ai có địa chỉ hiện tại của bạn, bạn có thể di chuyển đến bất cứ đâu bạn muốn và không ai cần biết.

Trong tình huống này, Swift giống như một bưu điện. Nếu nhiều người có nhiều mối quan hệ di chuyển nhiều nơi, nó có chi phí thực sự cao. Nó phải trả một lượng lớn nhân viên để xử lý tất cả các thông báo đó và quá trình này chiếm rất nhiều thời gian và công sức. Đó là lý do tại sao trạng thái lý tưởng của Swift là mọi người trong thị trấn của nó có càng ít liên hệ càng tốt. Sau đó, nó không cần một nhân viên lớn để xử lý các thay đổi địa chỉ và nó có thể làm mọi thứ khác nhanh hơn và tốt hơn.

Đây cũng là lý do tại sao những người dùng Swift đều say mê về các loại giá trị so với các loại tham chiếu. Về bản chất, các loại tham chiếu có nhiều "địa chỉ liên hệ" ở khắp nơi và các loại giá trị thường không cần nhiều hơn một vài. Loại giá trị là "Swift" -er.

Vì vậy, trở lại hình ảnh nhỏ: structs. Các cấu trúc là một vấn đề lớn trong Swift vì chúng có thể làm hầu hết những thứ mà các đối tượng có thể làm, nhưng chúng là các kiểu giá trị.

Hãy tiếp tục tương tự địa chỉ thực bằng cách tưởng tượng một địa chỉ misterStructsống ở đó someObjectVille. Ở đây có một chút tương tự, nhưng tôi nghĩ nó vẫn hữu ích.

Vì vậy, để mô hình hóa việc thay đổi một biến trên a struct, giả sử misterStructcó tóc màu xanh lá cây và nhận được lệnh chuyển sang tóc màu xanh lam. Như tôi đã nói, sự tương tự trở nên khó hiểu, nhưng điều xảy ra là thay vì thay misterStructtóc, người cũ chuyển ra ngoài và một người mới với mái tóc xanh chuyển đến, và người mới đó bắt đầu gọi họ misterStruct. Không ai cần nhận thông báo thay đổi địa chỉ, nhưng nếu ai nhìn vào địa chỉ đó, họ sẽ thấy một anh chàng tóc xanh.

Bây giờ hãy mô hình hóa những gì sẽ xảy ra khi bạn gọi một hàm trên a struct. Trong trường hợp này, nó giống như misterStructnhận được một đơn đặt hàng chẳng hạn changeYourHairBlue(). Vì vậy, bưu điện đưa ra hướng dẫn misterStruct"hãy thay đổi mái tóc của bạn thành màu xanh lam và cho tôi biết khi bạn hoàn thành."

Nếu anh ấy vẫn theo thói quen như trước đây, nếu anh ấy đang làm những gì anh ấy đã làm khi biến số bị thay đổi trực tiếp, điều misterStructsẽ làm là chuyển ra khỏi nhà của anh ấy và gọi cho một người mới với mái tóc xanh. Nhưng đó là vấn đề.

Mệnh lệnh là "hãy thay đổi mái tóc của bạn thành màu xanh lam và nói với tôi khi bạn hoàn thành," nhưng chính anh chàng màu xanh lá cây mới nhận được lệnh đó. Sau khi anh chàng da xanh chuyển đến, thông báo "công việc hoàn thành" vẫn phải được gửi lại. Nhưng anh chàng da xanh không biết gì về nó.

[Để thực sự hiểu ra điều tương tự này có điều gì đó khủng khiếp, điều về mặt kỹ thuật đã xảy ra với anh chàng tóc xanh là sau khi chuyển ra ngoài, anh ta ngay lập tức tự sát. Vì vậy, anh ta cũng không thể thông báo cho bất kỳ ai rằng nhiệm vụ đã hoàn thành ! ]

Để tránh vấn đề này, trong những trường hợp như thế này chỉ , Swift vừa đi vừa trực tiếp đến ngôi nhà tại địa chỉ đó và thực sự thay đổi mái tóc của cư dân hiện tại của . Đó là một quá trình hoàn toàn khác so với chỉ gửi một chàng trai mới.

Và đó là lý do tại sao Swift muốn chúng ta sử dụng mutatingtừ khóa!

Kết quả cuối cùng trông giống với bất kỳ thứ gì liên quan đến cấu trúc: cư dân của ngôi nhà giờ có mái tóc màu xanh lam. Nhưng các quá trình để đạt được nó thực sự hoàn toàn khác nhau. Có vẻ như nó đang làm điều tương tự, nhưng nó đang làm một điều rất khác. Nó đang làm một điều mà các cấu trúc Swift nói chung không bao giờ làm.

Vì vậy, để cung cấp cho trình biên dịch kém một chút trợ giúp, và không khiến nó phải tìm ra liệu một hàm có tự thay đổi structhay không, đối với mỗi hàm struct đơn lẻ, chúng tôi được yêu cầu bỏ qua và sử dụng mutatingtừ khóa.

Về bản chất, để giúp Swift nhanh chóng, tất cả chúng ta phải làm phần việc của mình. :)

BIÊN TẬP:

Này anh bạn / anh chàng đã phản đối tôi, tôi chỉ viết lại hoàn toàn câu trả lời của mình. Nếu nó phù hợp hơn với bạn, bạn sẽ loại bỏ phiếu phản đối?


1
Đây là <f-word-here> được viết tuyệt vời! Vì vậy, rất dễ hiểu. Tôi đã tìm kiếm trên internet cho điều này và đây là câu trả lời (tốt, mà không cần phải xem qua mã nguồn). Cảm ơn rất nhiều vì đã dành thời gian để viết nó.
Vaibhav

1
Tôi chỉ muốn xác nhận rằng tôi hiểu đúng: Chúng ta có thể nói rằng, khi một thuộc tính của cá thể Struct được thay đổi trực tiếp trong Swift, thì cá thể đó của Struct sẽ bị "kết thúc" và một cá thể mới với thuộc tính đã thay đổi đang được "tạo" đằng sau hậu trường? Và khi chúng ta sử dụng một phương thức đột biến trong instance để thay đổi thuộc tính của instance đó, quá trình kết thúc và khởi tạo lại này không diễn ra?
Ted

@Teo, tôi ước mình có thể trả lời dứt khoát, nhưng điều tốt nhất tôi có thể nói là tôi đã chạy phép loại suy này qua một nhà phát triển Swift thực tế và họ nói "về cơ bản là chính xác", ngụ ý rằng có nhiều điều hơn về nó theo nghĩa kỹ thuật. Nhưng với sự hiểu biết đó - có nhiều điều hơn là chỉ điều này - theo nghĩa rộng, vâng, đó là điều mà phép loại suy này đang nói.
Le Mot Juices

8

Cấu trúc Swift có thể được khởi tạo dưới dạng hằng số (qua let) hoặc biến (qua var)

Hãy xem xét Arraycấu trúc của Swift (vâng, đó là một cấu trúc).

var petNames: [String] = ["Ruff", "Garfield", "Nemo"]
petNames.append("Harvey") // ["Ruff", "Garfield", "Nemo", "Harvey"]

let planetNames: [String] = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
planetNames.append("Pluto") //Error, sorry Pluto. No can do

Tại sao phần phụ không hoạt động với tên hành tinh? Bởi vì phần phụ được đánh dấu bằng mutatingtừ khóa. Và kể từ khi planetNamesđược khai báo bằng cách sử dụng let, tất cả các phương pháp được đánh dấu như vậy đều bị giới hạn.

Trong ví dụ của bạn, trình biên dịch có thể cho biết bạn đang sửa đổi cấu trúc bằng cách gán cho một hoặc nhiều thuộc tính của nó bên ngoài một init. Nếu bạn thay đổi mã của mình một chút, bạn sẽ thấy điều đó xykhông phải lúc nào bạn cũng có thể truy cập được bên ngoài cấu trúc. Lưu ý lettrên dòng đầu tiên.

let p = Point(x: 1, y: 2)
p.x = 3 //error
p.moveToX(5, andY: 5) //error

5

Hãy xem xét một sự tương tự với C ++. Một phương thức struct trong Swift là mutating/ not- mutatingtương tự như một phương thức trong C ++ là non- const/ const. Một phương thức được đánh dấu consttrong C ++ tương tự không thể thay đổi cấu trúc.

Bạn có thể thay đổi một var từ bên ngoài một cấu trúc, nhưng bạn không thể thay đổi nó từ các phương thức của chính nó.

Trong C ++, bạn cũng có thể "thay đổi một var từ bên ngoài struct" - nhưng chỉ khi bạn có một constbiến không phải struct. Nếu bạn có một constbiến cấu trúc, bạn không thể gán cho var và bạn cũng không thể gọi một constphương thức không phải là . Tương tự, trong Swift, bạn chỉ có thể thay đổi thuộc tính của struct nếu biến struct không phải là hằng số. Nếu bạn có một hằng cấu trúc, bạn không thể gán cho một thuộc tính và bạn cũng không thể gọi một mutatingphương thức.


1

Tôi đã tự hỏi điều tương tự khi tôi bắt đầu học Swift, và mỗi câu trả lời trong số này, trong khi bổ sung một số thông tin chi tiết, có thể là dài dòng và khó hiểu theo đúng nghĩa của chúng. Tôi nghĩ câu trả lời cho câu hỏi của bạn thực sự khá đơn giản…

Phương thức đột biến được xác định bên trong cấu trúc của bạn muốn có quyền sửa đổi từng và mọi trường hợp của chính nó sẽ được tạo trong tương lai. Điều gì sẽ xảy ra nếu một trong những trường hợp đó được gán cho một hằng số bất biến với let? Ồ ồ. Để bảo vệ bạn khỏi chính mình (và để cho người biên tập và trình biên dịch biết những gì bạn đang cố gắng làm gì), bạn buộc phải rõ ràng khi muốn cung cấp cho một phương thức thể hiện sức mạnh này.

Ngược lại, cài đặt thuộc tính từ bên ngoài cấu trúc của bạn đang hoạt động trên một phiên bản đã biết của cấu trúc đó. Nếu nó được gán cho một hằng số, Xcode sẽ cho bạn biết về nó ngay khi bạn nhập vào lệnh gọi phương thức.

Đây là một trong những điều tôi yêu thích ở Swift khi tôi bắt đầu sử dụng nó nhiều hơn — được cảnh báo về lỗi khi tôi nhập chúng. Chắc chắn sẽ đánh bại được việc khắc phục các lỗi JavaScript khó hiểu!


1

SWIFT: Sử dụng hàm đột biến trong cấu trúc

Các lập trình viên Swift đã phát triển các Struct theo cách mà các thuộc tính của nó không thể được sửa đổi trong các phương thức Struct. Ví dụ: Kiểm tra mã được cung cấp bên dưới

struct City
{
  var population : Int 
  func changePopulation(newpopulation : Int)
  {
      population = newpopulation //error: cannot modify property "popultion"
  }
}
  var mycity = City(population : 1000)
  mycity.changePopulation(newpopulation : 2000)

Khi thực thi đoạn mã trên, chúng tôi gặp lỗi vì chúng tôi đang cố gắng gán một giá trị mới cho nhóm thuộc tính của Struct City. Theo mặc định, thuộc tính Structs không thể thay đổi bên trong các phương thức của chính nó. Đây là cách các Nhà phát triển của Apple đã xây dựng nó, do đó các Cấu trúc sẽ có tính chất tĩnh theo mặc định.

Làm thế nào để chúng tôi giải quyết nó? Giải pháp thay thế là gì?

Từ khóa thay đổi:

Khai báo hàm như biến đổi bên trong Struct cho phép chúng ta thay đổi các thuộc tính trong Structs. Dòng số: 5, của đoạn mã trên thay đổi thành như thế này,

mutating changePopulation(newpopulation : Int)

Bây giờ chúng ta có thể gán giá trị của newpopulation cho nhóm thuộc tính trong phạm vi của phương thức.

Ghi chú :

let mycity = City(1000)     
mycity.changePopulation(newpopulation : 2000)   //error: cannot modify property "popultion"

Nếu chúng ta sử dụng let thay vì var cho các đối tượng Struct, thì chúng ta không thể thay đổi giá trị của bất kỳ thuộc tính nào, Ngoài ra, đây là lý do tại sao chúng ta gặp lỗi khi cố gắng gọi hàm thay đổi bằng let instance. Vì vậy, tốt hơn là sử dụng var bất cứ khi nào bạn thay đổi giá trị của một thuộc tính.

Rất thích nghe ý kiến ​​và suy nghĩ của bạn… ..

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.