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?


16

Khi thiết kế các lớp để giữ mô hình dữ liệu của bạn, tôi đã đọc nó có thể hữu ích để tạo các đối tượng bất biến nhưng tại thời điểm nào thì gánh nặng của danh sách tham số hàm tạo và các bản sao sâu trở nên quá nhiều và bạn phải từ bỏ giới hạn bất biến?

Ví dụ, đây là một lớp không thay đổi để biểu diễn một thứ được đặt tên (Tôi đang sử dụng cú pháp C # nhưng nguyên tắc áp dụng cho tất cả các ngôn ngữ OO)

class NamedThing
{
    private string _name;    
    public NamedThing(string name)
    {
        _name = name;
    }    
    public NamedThing(NamedThing other)
    {
         this._name = other._name;
    }
    public string Name
    {
        get { return _name; }
    }
}

Những thứ được đặt tên có thể được xây dựng, truy vấn và sao chép vào những thứ được đặt tên mới nhưng tên không thể thay đổi.

Điều này là tốt nhưng điều gì xảy ra khi tôi muốn thêm một thuộc tính khác? Tôi phải thêm một tham số cho hàm tạo và cập nhật hàm tạo sao chép; đó không phải là quá nhiều công việc nhưng vấn đề bắt đầu, theo như tôi có thể thấy, khi tôi muốn làm cho một đối tượng phức tạp không thể thay đổi.

Nếu lớp chứa các thuộc tính và bộ sưu tập có thể chứa các lớp phức tạp khác, thì đối với tôi, danh sách tham số của hàm tạo sẽ trở thành một cơn ác mộng.

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?


Tôi luôn nỗ lực để làm cho các lớp trong mô hình của tôi trở nên bất biến. Nếu bạn đang có một danh sách tham số lớn, dài, thì có lẽ lớp của bạn quá lớn và nó có thể bị tách ra không? Nếu các đối tượng cấp thấp hơn của bạn cũng không thay đổi và theo cùng một mẫu thì các đối tượng cấp cao hơn của bạn không nên chịu đựng (quá nhiều). Tôi thấy việc thay đổi một lớp hiện có trở nên bất biến hơn là làm cho một mô hình dữ liệu trở nên bất biến khi tôi bắt đầu từ đầu.
Không ai

1
Bạn có thể xem mẫu Builder được đề xuất trong câu hỏi này: stackoverflow.com/questions/1304154/
Ant

Bạn đã xem MemberwiseClone chưa? Bạn không phải cập nhật hàm tạo sao chép cho mỗi thành viên mới.
kevin cline

3
@Tony Nếu bộ sưu tập của bạn và mọi thứ chúng chứa cũng không thay đổi, bạn không cần một bản sao sâu, một bản sao nông là đủ.
mjcopple

2
Bên cạnh đó, tôi thường sử dụng các trường "set-once" trong các lớp trong đó lớp cần phải "khá" bất biến, nhưng không hoàn toàn. Tôi thấy điều này giải quyết vấn đề của các nhà xây dựng lớn, nhưng cung cấp hầu hết các lợi ích của các lớp bất biến. (cụ thể là mã lớp nội bộ của bạn không phải lo lắng về việc thay đổi giá trị)
Earlz

Câu trả lời:


23

Khi họ trở thành gánh nặng? Rất nhanh chóng (đặc biệt nếu ngôn ngữ bạn chọn không cung cấp đủ hỗ trợ cú pháp cho tính bất biến.)

Tính bất biến đang được bán như viên đạn bạc cho tình huống khó xử đa lõi và tất cả những thứ đó. Nhưng tính bất biến trong hầu hết các ngôn ngữ OO buộc bạn phải thêm các tạo tác và thực hành nhân tạo vào mô hình và quy trình của mình. Đối với mỗi lớp bất biến phức tạp, bạn phải có một trình xây dựng phức tạp như nhau (ít nhất là bên trong). Cho dù bạn thiết kế nó như thế nào, nó vẫn giới thiệu khớp nối mạnh mẽ (vì vậy chúng tôi tốt hơn là có lý do để giới thiệu chúng.)

Không nhất thiết có thể mô hình hóa mọi thứ trong các lớp nhỏ không phức tạp. Vì vậy, đối với các lớp và cấu trúc lớn, chúng tôi phân vùng chúng một cách giả tạo - không phải vì điều đó có ý nghĩa trong mô hình miền của chúng tôi, mà bởi vì chúng tôi phải xử lý việc khởi tạo và xây dựng phức tạp của chúng trong mã.

Điều tồi tệ hơn nữa là khi mọi người đưa ý tưởng về sự bất biến quá xa trong một ngôn ngữ có mục đích chung như Java hoặc C #, khiến mọi thứ trở nên bất biến. Sau đó, kết quả là, bạn thấy mọi người buộc các cấu trúc biểu thức s trong các ngôn ngữ không hỗ trợ những thứ đó một cách dễ dàng.

Kỹ thuật là hành động của mô hình hóa thông qua thỏa hiệp và đánh đổi. Làm cho mọi thứ trở nên bất biến bởi sắc lệnh bởi vì ai đó đọc rằng mọi thứ đều bất biến trong ngôn ngữ chức năng X hoặc Y (một mô hình lập trình hoàn toàn khác), điều đó không được chấp nhận. Đó không phải là kỹ thuật tốt.

Những điều nhỏ, có thể đơn nhất có thể được thực hiện bất biến. Những điều phức tạp hơn có thể được thực hiện bất biến khi nó có ý nghĩa . Nhưng bất biến không phải là viên đạn bạc. Khả năng giảm lỗi, để tăng khả năng mở rộng và hiệu suất, đó không phải là chức năng duy nhất của tính bất biến. Nó là một chức năng của thực hành kỹ thuật thích hợp . Rốt cuộc, mọi người đã viết phần mềm tốt, có thể mở rộng mà không bất biến.

Tính bất biến trở thành một gánh nặng rất nhanh (nó làm tăng thêm sự phức tạp tình cờ) nếu nó được thực hiện mà không có lý do, khi nó được thực hiện bên ngoài những gì nó có ý nghĩa trong bối cảnh của một mô hình miền.

Tôi, trước hết, cố gắng tránh nó (trừ khi tôi đang làm việc trong một ngôn ngữ lập trình với sự hỗ trợ cú pháp tốt cho nó.)


6
luis, bạn có nhận thấy làm thế nào các câu trả lời được viết tốt, thực tế được viết với một lời giải thích về các nguyên tắc kỹ thuật đơn giản nhưng hợp lý có xu hướng không nhận được nhiều phiếu như những người sử dụng mốt mã hóa hiện đại không? Đây là một câu trả lời tuyệt vời, tuyệt vời.
Huperniketes

3
Cảm ơn :) Bản thân tôi đã nhận thấy xu hướng đại diện, nhưng không sao. Fad fanboys mã churn mà sau này chúng tôi có thể sửa chữa với mức giá tốt hơn hàng giờ, hahah :) jk ...
luis.espinal

4
Viên đạn bạc? Không. Đáng giá một chút vụng về trong C # / Java (nó không thực sự tệ đến vậy)? Chắc chắn rồi. Ngoài ra, vai trò của đa lõi trong tính bất biến là khá nhỏ ... lợi ích thực sự là dễ dàng lý luận.
Mauricio Scheffer

@Maurermo - nếu bạn nói như vậy (tính bất biến trong Java không phải là xấu). Đã làm việc trên Java từ năm 1998 đến năm 2011, tôi xin được khác biệt, nó không được miễn trừ trong các cơ sở mã đơn giản. Tuy nhiên, mọi người có những trải nghiệm khác nhau và tôi thừa nhận rằng POV của tôi không có tính chủ quan. Xin lỗi, không thể đồng ý ở đó. Tôi đồng ý, tuy nhiên, với sự dễ dàng lý luận là điều quan trọng nhất khi nói đến sự bất biến.
luis.espinal

13

Tôi đã trải qua một giai đoạn khăng khăng về các lớp học là bất biến nếu có thể. Có phải các nhà xây dựng cho hầu hết mọi thứ, các mảng bất biến, v.v., tôi đã tìm thấy câu trả lời cho câu hỏi của bạn rất đơn giản: Tại thời điểm nào các lớp bất biến trở thành gánh nặng? Rất nhanh. Ngay khi bạn muốn tuần tự hóa một cái gì đó, bạn phải có khả năng giải tuần tự hóa, điều đó có nghĩa là nó phải có thể thay đổi được; ngay khi bạn muốn sử dụng ORM, hầu hết trong số họ nhấn mạnh vào các thuộc tính có thể thay đổi. Và như thế.

Cuối cùng tôi đã thay thế chính sách đó bằng các giao diện bất biến thành các đối tượng có thể thay đổi.

class NamedThing : INamedThing
{
    private string _name;    
    public NamedThing(string name)
    {
        _name = name;
    }    

    public NamedThing(NamedThing other)
    {
        this._name = other._name;
    }

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

interface INamedThing
{
    string Name { get; }
}

Bây giờ đối tượng đã linh hoạt nhưng bạn vẫn có thể nói mã gọi rằng nó không nên chỉnh sửa các thuộc tính này.


8
Bỏ qua những vấn đề nhỏ nhặt, tôi đồng ý rằng các vật thể bất biến có thể trở thành một cơn đau ở mông rất nhanh khi lập trình bằng ngôn ngữ bắt buộc, nhưng tôi không thực sự chắc chắn liệu một giao diện bất biến có thực sự giải quyết được vấn đề tương tự hay bất kỳ vấn đề nào không. Lý do chính để sử dụng một đối tượng bất biến là vì vậy bạn có thể ném nó xung quanh bất cứ nơi nào bất cứ lúc nào và không bao giờ phải lo lắng về việc ai đó làm hỏng trạng thái của bạn. Nếu đối tượng cơ bản là có thể thay đổi thì bạn không có sự đảm bảo đó, đặc biệt nếu lý do bạn giữ nó có thể thay đổi là vì nhiều thứ cần phải biến đổi nó.
Aaronaught

1
@Aaronaught - Điểm thú vị. Tôi đoán đó là một điều tâm lý nhiều hơn là một sự bảo vệ thực sự. Tuy nhiên, dòng cuối cùng của bạn có một tiền đề sai. Lý do để giữ cho nó có thể thay đổi là nhiều thứ khác nhau cần phải khởi tạo nó và sinh ra thông qua sự phản chiếu, không biến đổi một khi được tạo ra ngay lập tức.
pdr

1
@Aaronaught: Cách tương tự IComparable<T>đảm bảo rằng nếu X.CompareTo(Y)>0Y.CompareTo(Z)>0, sau đó X.CompareTo(Z)>0. Giao diện có hợp đồng . Nếu hợp đồng IImmutableList<T>xác định rằng các giá trị của tất cả các vật phẩm và thuộc tính phải được "đặt trong đá" trước khi bất kỳ trường hợp nào được đưa ra thế giới bên ngoài, thì tất cả các triển khai hợp pháp sẽ làm như vậy. Không có gì ngăn cản IComparableviệc thực hiện vi phạm tính siêu việt, nhưng việc thực hiện là không hợp pháp. Nếu SortedDictionarytrục trặc khi được đưa ra một cách bất hợp pháp IComparable, ...
supercat

1
@Aaronaught: Tại sao tôi nên tin tưởng rằng việc triển khai IReadOnlyList<T>sẽ là bất biến, vì (1) không có yêu cầu nào được nêu trong tài liệu giao diện và (2) cách triển khai phổ biến nhất List<T>, thậm chí không chỉ đọc ? Tôi không rõ ràng những gì mơ hồ về các điều khoản của tôi: một bộ sưu tập có thể đọc được nếu dữ liệu trong đó có thể được đọc. Nó chỉ đọc nếu có thể hứa rằng dữ liệu chứa trong đó không thể thay đổi trừ khi một số tham chiếu bên ngoài được giữ bởi mã sẽ thay đổi nó. Thật bất biến nếu nó có thể đảm bảo rằng nó không thể thay đổi, theo thời gian.
supercat

1
@supercat: Ngẫu nhiên, Microsoft đồng ý với tôi. Họ đã phát hành một gói bộ sưu tập bất biến và lưu ý rằng chúng đều là các loại cụ thể, bởi vì bạn không bao giờ có thể đảm bảo rằng một lớp trừu tượng hoặc giao diện là thực sự bất biến.
Aaronaught

8

Tôi không nghĩ rằng có một câu trả lời chung cho vấn đề này. Một lớp càng phức tạp thì càng khó lý luận về sự thay đổi trạng thái của nó và càng tốn kém hơn để tạo ra các bản sao mới của nó. Vì vậy, trên mức độ phức tạp (cá nhân) nào đó, nó sẽ trở nên quá đau đớn để làm cho / giữ một lớp bất biến.

Lưu ý rằng một lớp quá phức tạp hoặc một danh sách tham số phương thức dài là mùi thiết kế mỗi se, bất kể tính bất biến.

Vì vậy, thường thì giải pháp ưa thích sẽ là chia một lớp như vậy thành nhiều lớp riêng biệt, mỗi lớp có thể được tạo thành có thể thay đổi hoặc bất biến theo cách riêng của nó. Nếu điều này là không khả thi, nó có thể được biến đổi.


5

Bạn có thể tránh vấn đề sao chép nếu bạn lưu trữ tất cả các trường không thay đổi của bạn trong một bên trong struct. Đây về cơ bản là một biến thể của mô hình vật lưu niệm. Sau đó, khi bạn muốn tạo một bản sao, chỉ cần sao chép phần lưu niệm:

class MyClass
{
    struct Memento
    {
        public int field1;
        public string field2;
    }

    private readonly Memento memento;

    public MyClass(int field1, string field2)
    {
        this.memento = new Memento()
            {
                field1 = field1,
                field2 = field2
            };
    }

    private MyClass(Memento memento) // for copying
    {
        this.memento = memento;
    }

    public int Field1 { get { return this.memento.field1; } }
    public string Field2 { get { return this.memento.field2; } }

    public MyClass WithNewField1(int newField1)
    {
        Memento newMemento = this.memento;
        newMemento.field1 = newField1;
        return new MyClass(newMemento);
    }
}

Tôi nghĩ rằng cấu trúc bên trong là không cần thiết. Đó chỉ là một cách khác để làm MemberwiseClone.
Codism

@Codism - có và không. Đôi khi bạn có thể cần các thành viên khác mà bạn không muốn sao chép. Điều gì sẽ xảy ra nếu bạn đang sử dụng đánh giá lười biếng trong một trong các getters của mình và lưu trữ kết quả trong một thành viên? Nếu bạn thực hiện MemberwiseClone, bạn sẽ sao chép giá trị được lưu trong bộ nhớ cache, sau đó bạn sẽ thay đổi một trong các thành viên của mình mà giá trị được lưu trong bộ nhớ cache phụ thuộc vào. Nó sạch hơn để tách trạng thái khỏi bộ đệm.
Scott Whitlock

Có thể đáng nói đến một lợi thế khác của cấu trúc bên trong: nó giúp đối tượng dễ dàng sao chép trạng thái của nó sang một đối tượng mà các tham chiếu khác tồn tại . Một nguồn mơ hồ phổ biến trong OOP là liệu một phương thức trả về tham chiếu đối tượng có trả về một khung nhìn của một đối tượng có thể thay đổi ngoài tầm kiểm soát của người nhận hay không. Nếu thay vì trả về một tham chiếu đối tượng, một phương thức chấp nhận một tham chiếu đối tượng từ người gọi và sao chép trạng thái vào nó, quyền sở hữu đối tượng sẽ rõ ràng hơn nhiều. Cách tiếp cận như vậy không hoạt động tốt với các kiểu thừa kế tự do, nhưng ...
supercat

... nó có thể rất hữu ích với những người nắm giữ dữ liệu có thể thay đổi. Cách tiếp cận cũng làm cho nó rất dễ dàng để có các lớp có thể thay đổi và bất biến "song song" (xuất phát từ một cơ sở "có thể đọc" trừu tượng) và các nhà xây dựng của chúng có thể sao chép dữ liệu từ nhau.
supercat

3

Bạn có một vài điều trong công việc ở đây. Bộ dữ liệu bất biến rất tốt cho khả năng mở rộng đa luồng. Về cơ bản, bạn có thể tối ưu hóa bộ nhớ của mình khá nhiều để một bộ tham số là một thể hiện của lớp - ở mọi nơi. Bởi vì các đối tượng không bao giờ thay đổi, bạn không phải lo lắng về việc đồng bộ hóa xung quanh việc truy cập các thành viên của nó. Đó là một điều tốt. Tuy nhiên, như bạn chỉ ra, đối tượng càng phức tạp thì bạn càng cần một số khả năng biến đổi. Tôi sẽ bắt đầu với lý luận dọc theo những dòng này:

  • Có bất kỳ lý do kinh doanh tại sao một đối tượng có thể thay đổi trạng thái của nó? Ví dụ, một đối tượng người dùng được lưu trữ trong cơ sở dữ liệu là duy nhất dựa trên ID của nó, nhưng nó phải có khả năng thay đổi trạng thái theo thời gian. Mặt khác, khi bạn thay đổi tọa độ trên một lưới, nó không còn là tọa độ ban đầu và do đó, nó có ý nghĩa để làm cho tọa độ không thay đổi. Tương tự với chuỗi.
  • Một số thuộc tính có thể được tính toán? Nói tóm lại, nếu các giá trị khác trong bản sao mới của một đối tượng là một hàm của một số giá trị cốt lõi mà bạn truyền vào, bạn có thể tính toán chúng trong hàm tạo hoặc theo yêu cầu. Điều này làm giảm số lượng bảo trì vì bạn có thể khởi tạo các giá trị đó theo cùng một cách khi sao chép hoặc tạo.
  • Có bao nhiêu giá trị tạo nên đối tượng bất biến mới? Tại một số điểm, sự phức tạp của việc tạo ra một đối tượng trở nên không tầm thường và tại thời điểm đó có nhiều phiên bản của đối tượng có thể trở thành một vấn đề. Các ví dụ bao gồm các cấu trúc cây bất biến, các đối tượng có nhiều hơn ba tham số được truyền vào, v.v. Càng nhiều tham số thì càng có nhiều khả năng làm rối trật tự của các tham số hoặc loại bỏ sai tham số.

Trong các ngôn ngữ chỉ hỗ trợ các đối tượng bất biến (như Erlang), nếu có bất kỳ thao tác nào có vẻ như sửa đổi trạng thái của một đối tượng bất biến, kết quả cuối cùng là một bản sao mới của đối tượng có giá trị được cập nhật. Ví dụ: khi bạn thêm một mục vào một vectơ / danh sách:

myList = lists:append([[1,2,3], [4,5,6]])
% myList is now [1,2,3,4,5,6]

Đó có thể là một cách làm việc lành mạnh với các đối tượng phức tạp hơn. Khi bạn thêm một nút cây chẳng hạn, kết quả là một cây mới với nút được thêm vào. Phương thức trong ví dụ trên trả về một danh sách mới. Trong ví dụ trong đoạn này, tree.add(newNode)sẽ trả về một cây mới với nút được thêm vào. Đối với người dùng, nó trở nên dễ dàng để làm việc với. Đối với các nhà văn thư viện, nó trở nên tẻ nhạt khi ngôn ngữ không hỗ trợ sao chép ngầm. Ngưỡng đó là tùy thuộc vào sự kiên nhẫn của bạn. Đối với người dùng thư viện của bạn, giới hạn lành mạnh nhất mà tôi tìm thấy là khoảng ba đến bốn đỉnh tham số.


Nếu người ta có xu hướng sử dụng một tham chiếu đối tượng có thể thay đổi làm giá trị [có nghĩa là không có tham chiếu nào được chứa trong chủ sở hữu của nó và không bao giờ được hiển thị], thì việc xây dựng một đối tượng bất biến mới chứa nội dung "đã thay đổi" mong muốn tương đương với việc sửa đổi trực tiếp đối tượng, mặc dù có khả năng chậm hơn Các đối tượng có thể thay đổi, tuy nhiên, cũng có thể được sử dụng như các thực thể . Làm thế nào người ta có thể tạo ra những thứ hoạt động như các thực thể mà không có các đối tượng có thể thay đổi?
supercat

0

Nếu bạn có nhiều thành viên lớp cuối cùng và không muốn họ tiếp xúc với tất cả các đối tượng cần tạo nó, bạn có thể sử dụng mẫu trình tạo:

class NamedThing
{
    private string _name;    
    private string _value;
    private NamedThing(string name, string value)
    {
        _name = name;
        _value = value;
    }    
    public NamedThing(NamedThing other)
    {
        this._name = other._name;
        this._value = other._value;
    }
    public string Name
    {
        get { return _name; }
    }

    public static class Builder {
        string _name;
        string _value;

        public void setValue(string value) {
            _value = value;
        }
        public void setName(string name) {
            _name = name;
        }
        public NamedThing newObject() {
            return new NamedThing(_name, _value);
        }
    }
}

lợi thế là bạn có thể dễ dàng tạo một đối tượng mới chỉ với một giá trị khác nhau của một tên khác nhau.


Tôi nghĩ rằng trình xây dựng của bạn là tĩnh là không chính xác. Một luồng khác có thể thay đổi tên tĩnh hoặc giá trị tĩnh sau khi bạn đặt chúng, nhưng trước khi bạn gọi newObject.
ErikE 24/07/2015

Chỉ có lớp xây dựng là tĩnh, nhưng các thành viên của nó thì không. Điều này có nghĩa là với mỗi trình tạo bạn tạo, họ có tập hợp thành viên riêng với các giá trị tương ứng. Lớp cần phải tĩnh để có thể được sử dụng và khởi tạo bên ngoài lớp chứa ( NamedThingtrong trường hợp này)
Salandur

Tôi thấy những gì bạn đang nói, tôi chỉ hình dung một vấn đề với điều này bởi vì nó không khiến nhà phát triển "rơi vào hố thành công". Việc nó sử dụng các biến tĩnh có nghĩa là nếu a Builderđược sử dụng lại, có nguy cơ thực sự xảy ra mà tôi đã đề cập. Ai đó có thể đang xây dựng rất nhiều đối tượng và quyết định rằng vì hầu hết các thuộc tính đều giống nhau, chỉ đơn giản là sử dụng lại Builder, và trên thực tế, hãy biến nó thành một đơn vị toàn cầu được tiêm phụ thuộc! Rất tiếc. Giới thiệu lỗi lớn. Vì vậy, tôi nghĩ rằng mô hình hỗn hợp ngay lập tức so với tĩnh là xấu.
ErikE 29/07/2015

1
@Salandur Các lớp bên trong trong C # luôn "tĩnh" theo nghĩa lớp bên trong Java.
Sebastian Redl

0

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).

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.