Sao chép, copy-on-write, và cái giá của việc dùng chung
Quyết định thiết kế trung tâm của Swift là: gần như mọi thứ đều là giá trị. Int, String,
Array, Dictionary, mọi struct và mọi enum — gán một cái là bên nhận có bản sao của riêng nó.
Class là ngoại lệ: gán một class là bạn có thêm một cái tên trỏ vào cùng một đối tượng.
Hệ quả người ta hay nhắc là “struct thì sao chép, class thì dùng chung”. Hệ quả thật sự quan trọng lại nằm ở chỗ lập luận: với một giá trị, không phần nào khác của chương trình có thể đổi thứ bạn đang cầm. Không ai có tham chiếu để thò tay vào.
struct Point { var x: Int, y: Int }
var a = Point(x: 0, y: 0)
var b = a
b.x = 10
print(a.x) // 0 — không thể chạm tới `a` thông qua `b`
Đổi struct thành class thì a.x in ra 10. Mọi hàm nhận a trước đó giờ đang giữ một thứ khác
với thứ nó được trao, mà chẳng có gì trong hệ thống kiểu đánh dấu khả năng ấy.
“Sao chép mảng là sao chép từng phần tử” sẽ khiến Swift không dùng được — truyền một mảng triệu phần
tử vào hàm hoá ra là memcpy một triệu phần tử. Thực tế không phải vậy. Array, String,
Dictionary và Set là kiểu giá trị bọc quanh một tham chiếu. Bản thân struct chỉ là phần đầu
mỏng chứa con trỏ tới vùng đệm trên heap. Sao chép struct là sao chép con trỏ và tăng bộ đếm tham
chiếu.
Vùng đệm chỉ bị nhân đôi khi có ai đó ghi vào một bản sao — copy-on-write.
var first = [1, 2, 3]
var second = first // không cấp phát; cả hai trỏ chung một vùng đệm
second.append(4) // giờ mới nhân đôi, vì vùng đệm đang được dùng chung
Bạn cũng cài được cơ chế đó, và với kiểu nào ôm dữ liệu lớn thì nên làm:
struct Pixels {
private final class Buffer {
var data: [UInt8]
init(data: [UInt8]) { self.data = data }
}
private var buffer: Buffer
init(data: [UInt8]) { buffer = Buffer(data: data) }
var data: [UInt8] { buffer.data }
mutating func setPixel(at index: Int, to value: UInt8) {
if !isKnownUniquelyReferenced(&buffer) {
buffer = Buffer(data: buffer.data)
}
buffer.data[index] = value
}
}
isKnownUniquelyReferenced chính là toàn bộ cơ chế. Nếu giá trị này đang giữ tham chiếu duy nhất
tới vùng đệm thì sửa tại chỗ. Nếu còn ai khác đang nhìn thì nhân đôi trước đã. Để ý lớp buffer là
final và private: tính duy nhất chỉ có nghĩa khi không lớp con nào và không code bên ngoài nào
giữ được một tham chiếu mà bạn không lường trước.
Cảnh báo
isKnownUniquelyReferenced luôn trả về false với đối tượng Objective-C. Một lớp bọc
copy-on-write quanh lớp con NSObject nhập từ Objective-C sẽ sao chép ở từng lần ghi một.
Một phương thức mutating trên struct không sửa giá trị tại chỗ theo cách phương thức của class
làm. Nó nhận giá trị dưới dạng inout, mà inout nghĩa là “chép vào, rồi chép ngược ra” — xét về
mặt ngữ nghĩa. Trình tối ưu thường biến việc đó thành cập nhật tại chỗ, nhưng ngữ nghĩa mới là thứ
ngôn ngữ bảo đảm, và nó giải thích những luật trông rất khó hiểu:
struct Counter {
var count = 0
mutating func increment() { count += 1 }
}
let fixed = Counter()
// fixed.increment() // lỗi: `fixed` là `let`
Lời gọi bị từ chối không phải vì phương thức chạm vào bộ nhớ, mà vì gọi nó đồng nghĩa với gán một
Counter mới ngược lại cho fixed.
Đây cũng là lý do phương thức mutating không dùng được qua một existential đang nằm trong let,
và vì sao nó không thể để self thoát ra ngoài vào một closure.
Mặc định hãy dùng struct. Dùng class khi rơi vào một trong các trường hợp sau:
User giống hệt nhau từng trường là cùng một người dùng;
hai đối tượng URLSession thì không thể thay cho nhau dù giống nhau đến đâu.Lỗi tôi gặp nhiều nhất lại ngược với thứ các quy tắc phong cách hay cảnh báo. Nó không phải “lạm
dụng class”. Nó là một struct ôm bên trong một tham chiếu class rồi âm thầm mang ngữ nghĩa tham
chiếu vào bằng cửa sau:
final class Session { var token: String? }
struct Client { // nhìn như một giá trị
let session: Session // thật ra thì không
}
Sao chép một Client là bạn có hai client dùng chung một token. Kiểu nói rằng đây là giá trị; hành
vi nói rằng đây là tham chiếu. Nếu đó đúng là điều bạn muốn, hãy khai báo hẳn thành class để người
đọc nhìn thấy.