Vậy tại thời điểm nào một lớp học trở nên quá phức tạp để trở thành bất biến?
Theo tôi, không đáng bận tâm để làm cho các lớp nhỏ không thay đổi trong các ngôn ngữ như ngôn ngữ bạn đang hiển thị. Tôi đang sử dụng nhỏ ở đây và không phức tạp , bởi vì ngay cả khi bạn thêm mười trường vào lớp đó và nó thực sự hoạt động rất thú vị với chúng, tôi nghi ngờ rằng nó sẽ mất kilobyte chứ đừng nói đến megabyte, vì vậy, bất kỳ chức năng nào cũng sử dụng các thể hiện của bạn lớp chỉ có thể tạo một bản sao rẻ tiền của toàn bộ đối tượng để tránh sửa đổi bản gốc nếu nó muốn tránh gây ra tác dụng phụ bên ngoài.
Cấu trúc dữ liệu liên tục
Nơi tôi tìm thấy việc sử dụng cá nhân cho tính bất biến là cho các cấu trúc dữ liệu trung tâm lớn, tổng hợp một loạt dữ liệu tuổi teen như các thể hiện của lớp bạn đang hiển thị, giống như một cấu trúc lưu trữ một triệu NamedThings
. Bằng cách thuộc cấu trúc dữ liệu liên tục không thay đổi và nằm sau giao diện chỉ cho phép truy cập chỉ đọc, các phần tử thuộc về bộ chứa trở nên bất biến mà không cần lớp phần tử ( NamedThing
) phải xử lý.
Bản sao giá rẻ
Cấu trúc dữ liệu liên tục cho phép các vùng của nó được chuyển đổi và tạo thành duy nhất, tránh sửa đổi thành bản gốc mà không phải sao chép toàn bộ cấu trúc dữ liệu. Đó là vẻ đẹp thực sự của nó. Nếu bạn muốn viết các hàm một cách ngây thơ để tránh các tác dụng phụ nhập vào cấu trúc dữ liệu chiếm bộ nhớ gigabyte và chỉ sửa đổi giá trị bộ nhớ của một megabyte, thì bạn phải sao chép toàn bộ thứ kỳ dị để tránh chạm vào đầu vào và trả lại một cái mới đầu ra. Đó là sao chép gigabyte để tránh tác dụng phụ hoặc gây ra tác dụng phụ trong kịch bản đó, khiến bạn phải lựa chọn giữa hai lựa chọn khó chịu.
Với cấu trúc dữ liệu liên tục, nó cho phép bạn viết một hàm như vậy và tránh tạo một bản sao của toàn bộ cấu trúc dữ liệu, chỉ cần khoảng một megabyte bộ nhớ bổ sung cho đầu ra nếu chức năng của bạn chỉ chuyển đổi bộ nhớ trị giá một megabyte.
Gánh nặng
Đối với gánh nặng, ít nhất là ngay lập tức trong trường hợp của tôi. Tôi cần những người xây dựng mà mọi người đang nói đến hoặc "tạm thời" khi tôi gọi họ để có thể diễn đạt hiệu quả các biến đổi cho cấu trúc dữ liệu khổng lồ đó mà không cần chạm vào nó. Mã như thế này:
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
... sau đó phải được viết như thế này:
ImmList<Stuff> transform_stuff(ImmList<Stuff> stuff, int first, int last)
{
// Grab a "transient" (builder) list we can modify:
TransientList<Stuff> transient(stuff);
// Transform stuff in the range, [first, last)
// for the transient list.
for (; first != last; ++first)
transform(transient[first]);
// Commit the modifications to get and return a new
// immutable list.
return stuff.commit(transient);
}
Nhưng để đổi lấy hai dòng mã bổ sung đó, chức năng giờ đây an toàn để gọi qua các luồng có cùng danh sách gốc, nó không gây ra tác dụng phụ, v.v. Nó cũng giúp thực hiện thao tác này một cách dễ dàng cho người dùng kể từ khi hoàn tác chỉ có thể lưu trữ một bản sao nông giá rẻ của danh sách cũ.
Phục hồi ngoại lệ hoặc an toàn
Không phải ai cũng có thể hưởng lợi nhiều như tôi đã làm từ các cấu trúc dữ liệu liên tục trong các bối cảnh như thế này (tôi thấy sử dụng chúng rất nhiều trong các hệ thống hoàn tác và chỉnh sửa không phá hủy vốn là khái niệm trung tâm trong miền VFX của tôi), nhưng một điều có thể áp dụng cho tất cả mọi người để xem xét là ngoại lệ - an toàn hoặc phục hồi lỗi .
Nếu bạn muốn làm cho chức năng đột biến ban đầu trở nên an toàn ngoại lệ, thì nó cần logic rollback, trong đó việc thực hiện đơn giản nhất đòi hỏi phải sao chép toàn bộ danh sách:
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Make a copy of the whole massive gigabyte-sized list
// in case we encounter an exception and need to rollback
// changes.
MutList<Stuff> old_stuff = stuff;
try
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
catch (...)
{
// If the operation failed and ran into an exception,
// swap the original list with the one we modified
// to "undo" our changes.
stuff.swap(old_stuff);
throw;
}
}
Tại thời điểm này, phiên bản có thể thay đổi an toàn ngoại lệ thậm chí còn đắt hơn về mặt tính toán và thậm chí còn khó viết hơn so với phiên bản không thay đổi khi sử dụng "trình tạo". Và rất nhiều nhà phát triển C ++ chỉ bỏ qua an toàn ngoại lệ và có thể điều đó tốt cho miền của họ, nhưng trong trường hợp của tôi, tôi muốn đảm bảo mã của mình hoạt động chính xác ngay cả trong trường hợp ngoại lệ (thậm chí viết các bài kiểm tra cố tình ném ngoại lệ để kiểm tra ngoại lệ an toàn), và điều đó khiến tôi phải có khả năng phục hồi bất kỳ tác dụng phụ nào mà chức năng gây ra giữa chừng cho chức năng nếu có bất cứ điều gì ném.
Khi bạn muốn được an toàn ngoại lệ và phục hồi từ các lỗi một cách duyên dáng mà không bị hỏng và cháy ứng dụng, thì bạn phải hoàn nguyên / hoàn tác bất kỳ tác dụng phụ nào mà một chức năng có thể gây ra trong trường hợp xảy ra lỗi / ngoại lệ. Và ở đó, người xây dựng thực sự có thể tiết kiệm nhiều thời gian lập trình hơn chi phí cùng với thời gian tính toán vì: ...
Bạn không phải lo lắng về việc đẩy lùi tác dụng phụ trong một chức năng không gây ra bất kỳ!
Vì vậy, trở lại câu hỏi cơ bản:
Tại thời điểm nào các lớp bất biến trở thành một gánh nặng?
Chúng luôn là gánh nặng trong các ngôn ngữ xoay quanh khả năng biến đổi hơn là bất biến, đó là lý do tại sao tôi nghĩ bạn nên sử dụng chúng khi lợi ích vượt xa đáng kể chi phí. Nhưng ở mức đủ rộng cho các cấu trúc dữ liệu đủ lớn, tôi tin rằng có nhiều trường hợp đó là một sự đánh đổi xứng đáng.
Ngoài ra, tôi chỉ có một vài loại dữ liệu bất biến và tất cả chúng đều có cấu trúc dữ liệu khổng lồ nhằm lưu trữ số lượng lớn các yếu tố (pixel của hình ảnh / kết cấu, thực thể và các thành phần của ECS, và đỉnh / cạnh / đa giác của lưới).