Câu hỏi Các đối tượng không thể thay đổi tham chiếu lẫn nhau?


Hôm nay tôi đã cố gắng quấn đầu mình quanh những vật thể bất biến có liên quan đến nhau. Tôi đi đến kết luận rằng bạn không thể làm điều đó mà không sử dụng đánh giá lười biếng nhưng trong quá trình tôi đã viết điều này (theo ý kiến ​​của tôi) mã thú vị.

public class A
{
    public string Name { get; private set; }
    public B B { get; private set; }
    public A()
    {
        B = new B(this);
        Name = "test";
    }
}

public class B
{
    public A A { get; private set; }
    public B(A a)
    {
        //a.Name is null
        A = a;
    }
}

Điều tôi thấy thú vị là tôi không thể nghĩ ra một cách khác để quan sát đối tượng của loại A trong trạng thái chưa được xây dựng hoàn chỉnh và bao gồm các luồng. Tại sao điều này thậm chí còn hợp lệ? Có cách nào khác để quan sát trạng thái của một đối tượng không được xây dựng hoàn chỉnh không?


76
2017-10-05 12:49


gốc


Tại sao bạn mong đợi nó không hợp lệ? - leppie
Bởi vì sự hiểu biết của tôi là một hàm tạo được cho là đảm bảo rằng mã nó chứa được thực hiện trước khi mã bên ngoài có thể quan sát trạng thái của đối tượng. - Stilgar
Mã này hợp lệ nhưng không đáng tin cậy. Stilgar là đúng - dụ của lớp A đã vượt qua B.ctor không được khởi tạo đầy đủ. Bạn phải tạo phiên bản mới của B trong A sau khi A được khởi tạo hoàn toàn - nó phải là dòng cuối cùng trong .ctor. - Karel Frajták


Các câu trả lời:


Tại sao điều này thậm chí còn hợp lệ?

Tại sao bạn mong đợi nó không hợp lệ?

Bởi vì một hàm tạo được cho là đảm bảo rằng mã nó chứa được thực hiện trước khi mã bên ngoài có thể quan sát trạng thái của đối tượng.

Chính xác. Nhưng trình biên dịch không chịu trách nhiệm duy trì sự bất biến đó. Bạn là. Nếu bạn viết mã mà phá vỡ bất biến đó, và nó đau khi bạn làm điều đó, sau đó ngừng làm việc đó.

Có cách nào khác để quan sát trạng thái của một đối tượng không được xây dựng hoàn chỉnh không?

Chắc chắn rồi. Đối với các kiểu tham chiếu, tất cả chúng liên quan đến việc bằng cách nào đó truyền "this" này ra khỏi hàm tạo, rõ ràng, vì mã người dùng duy nhất giữ tham chiếu đến bộ nhớ là hàm tạo. Một số cách mà hàm tạo có thể bị rò rỉ "this" là:

  • Đặt "this" trong một trường tĩnh và tham chiếu nó từ một luồng khác
  • thực hiện cuộc gọi phương thức hoặc gọi hàm tạo và chuyển "this" làm đối số
  • thực hiện một cuộc gọi ảo - đặc biệt khó chịu nếu phương thức ảo bị ghi đè bởi một lớp dẫn xuất, bởi vì sau đó nó chạy trước khi cơ sở ctor lớp dẫn xuất chạy.

Tôi nói rằng chỉ Mã người dùng giữ một tham chiếu là ctor, nhưng tất nhiên người thu gom rác cũng giữ một tham chiếu. Do đó, một cách thú vị khác trong đó một đối tượng có thể được quan sát trong trạng thái nửa được xây dựng là nếu đối tượng có một destructor, và constructor ném một ngoại lệ (hoặc nhận một ngoại lệ không đồng bộ như hủy bỏ thread; ) Trong trường hợp đó, đối tượng sắp chết và do đó cần phải được hoàn thành, nhưng luồng finalizer có thể thấy trạng thái nửa khởi tạo của đối tượng. Và bây giờ chúng tôi đang trở lại trong mã người dùng có thể nhìn thấy một nửa đối tượng được xây dựng!

Các cấu trúc được yêu cầu phải mạnh mẽ khi đối mặt với kịch bản này. Một destructor không được phụ thuộc vào bất kỳ bất biến nào của đối tượng được thiết lập bởi constructor đang được duy trì, bởi vì đối tượng bị phá hủy có thể chưa bao giờ được xây dựng đầy đủ.

Một cách điên rồ khác mà một đối tượng được xây dựng một nửa có thể được quan sát bởi mã bên ngoài là tất nhiên nếu destructor thấy đối tượng được khởi tạo một nửa trong kịch bản trên, và sau đó sao chép tham chiếu đối tượng đó vào một trường tĩnh, do đó đảm bảo rằng đối tượng nửa được xây dựng, nửa hoàn thiện được cứu thoát khỏi cái chết. Xin đừng làm điều đó.Như tôi đã nói, nếu nó đau, đừng làm thế.

Nếu bạn đang ở trong hàm tạo của một kiểu giá trị thì mọi thứ về cơ bản giống nhau, nhưng có một số khác biệt nhỏ trong cơ chế. Ngôn ngữ yêu cầu một lời gọi hàm tạo trên một kiểu giá trị tạo ra một biến tạm thời mà chỉ có ctor có quyền truy cập, biến đổi biến đó, và sau đó thực hiện một bản sao cấu trúc của giá trị bị biến đổi sang lưu trữ thực tế. Điều đó đảm bảo rằng nếu các nhà xây dựng ném, sau đó lưu trữ cuối cùng không ở trong trạng thái nửa biến đổi.

Lưu ý rằng vì các bản sao cấu trúc không được đảm bảo là nguyên tử, nó  có thể cho một luồng khác để xem lưu trữ ở trạng thái nửa biến đổi; sử dụng khóa chính xác nếu bạn đang ở trong tình huống đó. Ngoài ra, nó có thể cho một ngoại lệ không đồng bộ như một sợi abort được ném nửa chừng thông qua một bản sao struct. Những vấn đề không nguyên tử phát sinh bất kể bản sao là từ một ctor tạm thời hoặc một bản sao "thường xuyên". Và nói chung, rất ít bất biến được duy trì nếu có ngoại lệ không đồng bộ.

Trong thực tế, trình biên dịch C # sẽ tối ưu hóa việc phân bổ tạm thời và sao chép nếu nó có thể xác định rằng không có cách nào cho kịch bản đó phát sinh. Ví dụ, nếu giá trị mới đang khởi tạo một địa phương không được đóng bởi một lambda và không phải trong một khối lặp, thì S s = new S(123); chỉ biến đổi s trực tiếp.

Để biết thêm thông tin về cách các hàm tạo kiểu giá trị hoạt động, hãy xem:

Debunking một huyền thoại về các loại giá trị

Và để biết thêm thông tin về cách ngữ nghĩa ngôn ngữ C # cố gắng cứu bạn khỏi chính mình, hãy xem:

Tại sao các Khởi tạo lại chạy theo thứ tự đối lập với tư cách là người xây dựng? Phần một

Tại sao các Khởi tạo lại chạy theo thứ tự đối lập với tư cách là người xây dựng? Phần hai

Tôi dường như đã lạc lối khỏi chủ đề này. Trong một cấu trúc, bạn có thể quan sát một đối tượng được xây dựng một nửa theo cùng một cách - sao chép đối tượng được xây dựng một nửa vào một trường tĩnh, gọi một phương thức với "this" làm đối số, v.v. Và, như tôi đã nói, bản sao từ lưu trữ tạm thời đến lưu trữ cuối cùng không phải là nguyên tử và do đó một luồng khác có thể quan sát cấu trúc được sao chép một nửa.


Bây giờ chúng ta hãy xem xét nguyên nhân gốc rễ của câu hỏi của bạn: làm thế nào để bạn làm cho các đối tượng bất biến tham chiếu lẫn nhau?

Thông thường, như bạn đã khám phá, bạn không biết. Nếu bạn có hai đối tượng bất biến tham chiếu lẫn nhau thì hợp lý chúng tạo thành một đồ thị theo chu kỳ. Bạn có thể xem xét việc xây dựng một đồ thị có hướng bất biến! Làm như vậy là khá dễ dàng. Biểu đồ có hướng bất biến bao gồm:

  • Một danh sách bất biến của các nút bất biến, mỗi nút chứa một giá trị.
  • Một danh sách bất biến của các cặp nút bất biến, mỗi cặp có điểm bắt đầu và điểm kết thúc của cạnh đồ thị.

Bây giờ cách bạn thực hiện các nút A và B "tham chiếu" với nhau là:

A = new Node("A");
B = new Node("B");
G = Graph.Empty.AddNode(A).AddNode(B).AddEdge(A, B).AddEdge(B, A);

Và bạn đã hoàn thành, bạn có một đồ thị trong đó A và B "tham chiếu" lẫn nhau.

Vấn đề, tất nhiên, là bạn không thể đến B từ A mà không có G trong tay. Có thêm mức độ vô hướng có thể là không thể chấp nhận được.


105
2017-10-05 14:42



Cảm ơn rất nhiều. Tôi đã đọc bài báo về loại giá trị và chúng là một phần lý do tại sao tôi nghĩ ngôn ngữ cố gắng đảm bảo việc xây dựng hoàn toàn đối tượng trước khi nó có thể được quan sát. Sau khi tất cả điều này là lý do tại sao giá trị được sao chép xung quanh. - Stilgar
@ Stilgar: Chúng tôi cố gắng trở thành một ngôn ngữ "chất lượng", nơi bạn thực sự phải làm việc chăm chỉ để viết một chương trình làm điều gì đó điên rồ. Thật không may, rất khó để thiết kế một ngôn ngữ hữu ích mà nó là được đảm bảo một đối tượng sẽ không bao giờ được quan sát thấy ở trạng thái không nhất quán, vì vậy chúng tôi không cố gắng Bảo hành cái đó. Chúng tôi chỉ cố gắng đẩy bạn mạnh mẽ theo hướng đó. (Đây là lý do tại sao các kiểu tham chiếu không nullable không hoạt động trong .NET, rất khó đảm bảo trong hệ thống kiểu rằng một trường kiểu tham chiếu không thể rỗng là không bao giờ quan sát được là rỗng.) - Eric Lippert
Có vẻ như bạn đã làm rất tốt công việc mà tôi mong đợi rằng bạn sẽ ngăn tôi viết mã trên. - Stilgar
@ Stilgar: Vấn đề là nếu chúng ta làm, thì chúng tôi cũng ngăn cản bạn viết rất nhiều mã hữu ích. Đôi khi rất hữu ích để có thể truyền "this" cho một phương thức hoặc hàm tạo của một lớp khác, đặc biệt là trong các kiểu kịch bản khởi tạo này. Tôi viết mã như thế mỗi ngày: trong trình biên dịch, chúng ta thường trong những tình huống mà "máy phân tích mã" bất biến đang xây dựng những "ký hiệu" bất biến, và chúng phải có khả năng tham khảo lẫn nhau. - Eric Lippert
@EricLippert: Đó là trong thực tế hữu ích. Tôi có những trường hợp hữu ích khi vượt qua this tham chiếu đến các đối tượng khác từ bên trong hàm tạo. Nhưng tại sao trình biên dịch không cho phép sử dụng this từ khóa từ trình khởi tạo trường? Cả hai đều có thể thấy các đối tượng được xây dựng một phần. Câu hỏi này đã không thực sự cung cấp một hợp lý cho giới hạn này. Nếu bạn tình cờ biết lý do, hãy chia sẻ. - Allon Guralnek


Vâng, đây là cách duy nhất để hai vật không thay đổi có thể tham khảo lẫn nhau - ít nhất một trong số chúng phải nhìn thấy vật kia theo cách không được xây dựng hoàn chỉnh.

nó là nói chung là một ý tưởng tồi để cho this thoát khỏi nhà xây dựng của bạn nhưng trong trường hợp bạn tự tin về những gì cả hai nhà thầu đều làm, và đó là lựa chọn duy nhất để thay đổi, tôi không nghĩ rằng quá xấu.


47
2017-10-05 12:54



Tôi đưa ra một ví dụ về tham khảo lẫn nhau bằng cách sử dụng this trong câu trả lời này sang một câu hỏi khác. - Brian


"Hoàn toàn được xây dựng" được xác định bởi mã của bạn, không phải bằng ngôn ngữ.

Đây là một biến thể khi gọi một phương thức ảo từ hàm tạo,
hướng dẫn chung là: đừng làm thế.

Để thực hiện chính xác khái niệm "được xây dựng hoàn chỉnh", đừng vượt qua this ra khỏi nhà xây dựng của bạn.


22
2017-10-05 12:54





Thật vậy, rò rỉ this tham chiếu trong quá trình khởi tạo sẽ cho phép bạn thực hiện điều này; nó có thể gây ra vấn đề nếu các phương thức được gọi trên đối tượng không đầy đủ, rõ ràng. Đối với "các cách khác để quan sát trạng thái của đối tượng không được xây dựng hoàn toàn":

  • gọi một virtual phương thức trong một hàm tạo; constructor của lớp con sẽ chưa được gọi, vì vậy override có thể thử truy cập trạng thái không đầy đủ (các trường được khai báo hoặc khởi tạo trong lớp con, v.v.)
  • phản ánh, có lẽ sử dụng FormatterServices.GetUninitializedObject (tạo ra một đối tượng mà không cần gọi hàm tạo ở tất cả)

8
2017-10-05 12:54



Phản ánh không được tính :) - Stilgar
@Stilgar nếu nó cho phép tôi quan sát trạng thái của một đối tượng không được xây dựng hoàn chỉnh, sau đó .... meh - Marc Gravell♦


Nếu bạn xem xét thứ tự khởi tạo

  • Các trường tĩnh có nguồn gốc
  • Hàm khởi tạo tĩnh có nguồn gốc
  • Các trường cá thể có nguồn gốc
  • Trường tĩnh cơ bản
  • Hàm tạo tĩnh cơ sở
  • Trường cá thể cơ bản
  • Hàm khởi tạo cơ sở
  • Khởi tạo cá thể thể hiện

rõ ràng thông qua up-casting bạn có thể truy cập vào lớp TRƯỚC KHI constructor instance được gọi (đây là lý do bạn không nên sử dụng các phương thức ảo từ các constructors. không thể đưa lớp dẫn xuất vào trạng thái "nhất quán")


6
2017-10-05 12:53





Bạn có thể tránh được vấn đề bằng cách căn chỉnh B cuối cùng trong constuctor của bạn:

 public A() 
    { 
        Name = "test"; 
        B = new B(this); 
    } 

Nếu những gì bạn đề xuất là không thể, thì A sẽ không thay đổi.

Chỉnh sửa: cố định, nhờ leppie.


4
2017-10-05 12:54



Rất khác với mã ví dụ của bạn ... - leppie
Bạn viết để khởi tạo B cuối cùng trong hàm tạo, nhưng trong ví dụ bạn khởi tạo nó trước, giống như trong mã từ OP. Typo? - Avada Kedavra
Cảm ơn, các bạn, bài đăng đã được sửa. Một lỗi đánh máy thực sự. - Nick
Tôi nghĩ rằng OP biết điều này và đã hỏi một câu hỏi cơ bản hơn. - Henk Holterman
@Nick: Wery tốt cho đến khi bạn có 3 lớp bất biến :) `public A () {Name =" test "; B = B mới (điều này); C = new C (điều này); } ` - VMykyt


Nguyên tắc là không để cho bạn điều này thoát khỏi đối tượng từ thân của hàm tạo.

Một cách khác để quan sát vấn đề này là bằng cách gọi các phương thức ảo bên trong hàm tạo.


3
2017-10-05 12:53





Như đã lưu ý, trình biên dịch không có phương tiện để biết tại điểm nào một đối tượng đã được xây dựng đủ tốt để có ích; do đó giả định rằng một lập trình viên đã vượt qua this từ một nhà xây dựng sẽ biết liệu một đối tượng đã được xây dựng đủ tốt để đáp ứng nhu cầu của mình chưa.

Tuy nhiên, tôi sẽ thêm rằng đối với các đối tượng được dự định thực sự không thay đổi được, người ta phải tránh đi qua this cho bất kỳ mã nào sẽ kiểm tra trạng thái của một trường trước khi nó được gán giá trị cuối cùng của nó. Điều này ngụ ý rằng thiskhông được chuyển tới mã bên ngoài tùy ý, nhưng không ngụ ý rằng có bất kỳ điều gì sai trái khi có một đối tượng đang được xây dựng truyền cho đối tượng khác với mục đích lưu trữ một tham chiếu ngược mà sẽ không thực sự được sử dụng cho đến sau khi hàm tạo đầu tiên hoàn thành.

Nếu người ta đã thiết kế một ngôn ngữ để tạo điều kiện thuận lợi cho việc xây dựng và sử dụng các vật thể không thay đổi, thì có thể hữu ích khi tuyên bố các phương pháp chỉ có thể sử dụng trong khi xây dựng, chỉ sau khi xây dựng, hoặc một trong hai; các trường có thể được khai báo là không thể bỏ qua trong quá trình xây dựng và chỉ đọc sau đó; các tham số tương tự có thể được gắn thẻ để cho biết rằng không nên tham chiếu. Theo một hệ thống như vậy, một trình biên dịch có thể cho phép xây dựng các cấu trúc dữ liệu liên kết với nhau, nhưng không có tài sản nào có thể thay đổi sau khi nó được quan sát. Để xem liệu lợi ích của việc kiểm tra tĩnh như vậy có lớn hơn chi phí không, tôi không chắc chắn, nhưng nó có thể là thú vị.

Ngẫu nhiên, một tính năng có liên quan sẽ hữu ích sẽ là khả năng khai báo các tham số và hàm trả về là tạm thời, trả về hoặc (mặc định) có thể tồn tại được. Nếu một tham số hoặc hàm trả về được khai báo là tạm thời, nó không thể được sao chép vào bất kỳ trường nào và cũng không được chuyển thành tham số bền vững cho bất kỳ phương thức nào. Ngoài ra, chuyển giá trị trả về hoặc không trả về như một tham số trả về cho phương thức sẽ làm cho giá trị trả về của hàm thừa hưởng các hạn chế của giá trị đó (nếu hàm có hai tham số trả về, giá trị trả về của nó sẽ thừa hưởng ràng buộc hạn chế hơn) thông số). Một điểm yếu lớn với Java và .net là tất cả các tham chiếu đối tượng đều không liên quan; một khi mã bên ngoài đưa tay lên một, không có ai nói với ai có thể kết thúc nó. Nếu các tham số có thể được khai báo tạm thời, nó sẽ thường xuyên hơn có thể cho mã mà tổ chức chỉ tham chiếu đến một cái gì đó để biết nó tổ chức tham chiếu duy nhất, và do đó tránh các hoạt động sao chép phòng thủ không cần thiết. Ngoài ra, những thứ như đóng cửa có thể được tái chế nếu trình biên dịch có thể biết rằng không có tham chiếu đến chúng tồn tại sau khi chúng trở về.


1
2017-07-25 00:57