IT TIP

서로를 참조하는 불변 객체?

itqueen 2020. 10. 15. 22:07
반응형

서로를 참조하는 불변 객체?


오늘 저는 서로를 참조하는 불변의 객체를 머리로 감싸려고했습니다. 게으른 평가를 사용하지 않고는 할 수 없다는 결론에 도달했지만 그 과정에서 흥미로운 코드를 작성했습니다.

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;
    }
}

흥미로운 점은 아직 완전히 구성되지 않고 스레드를 포함하는 상태에서 유형 A의 객체를 관찰하는 다른 방법을 생각할 수 없다는 것입니다. 왜 이것이 유효합니까? 완전히 구성되지 않은 물체의 상태를 관찰하는 다른 방법이 있습니까?


왜 이것이 유효합니까?

왜 유효하지 않을 것으로 예상합니까?

생성자는 외부 코드가 객체의 상태를 관찰하기 전에 포함 된 코드가 실행되도록 보장해야하기 때문입니다.

옳은. 그러나 컴파일러 는 그 불변성을 유지할 책임이 없습니다. 당신은 입니다. 그 불변을 깨는 코드를 작성하고 그렇게 할 때 아프면 그만하십시오 .

완전히 구성되지 않은 물체의 상태를 관찰하는 다른 방법이 있습니까?

확실한. 참조 유형의 경우 저장소에 대한 참조를 보유하는 유일한 사용자 코드가 생성자이기 때문에 모든 유형이 생성자에서 "this"를 전달하는 작업을 포함합니다. 생성자가 "this"를 유출 할 수있는 몇 가지 방법은 다음과 같습니다.

  • 정적 필드에 "this"를 넣고 다른 스레드에서 참조
  • 메서드 호출 또는 생성자 호출을 수행하고 "this"를 인수로 전달합니다.
  • 가상 호출을 만듭니다. 특히 가상 메서드가 파생 클래스에 의해 재정의 된 경우에는 파생 클래스 ctor 본문이 실행되기 전에 실행되기 때문에 특히 불쾌합니다.

참조를 보유하는 유일한 사용자 코드 는 ctor이지만 물론 가비지 수집기참조를 보유 한다고 말했습니다 . 따라서 객체가 절반으로 구성된 상태로 관찰 될 수있는 또 다른 흥미로운 방법은 객체에 소멸자가 있고 생성자가 예외를 throw하는 경우 (또는 스레드 중단과 같은 비동기 예외가 발생하는 경우입니다. )이 경우 객체가 죽기 직전이므로 종료해야하지만 종료 자 스레드는 객체의 절반 초기화 상태를 볼 수 있습니다. 이제 절반으로 구성된 객체를 볼 수있는 사용자 코드로 돌아 왔습니다!

이 시나리오에서 소멸자는 강력해야합니다. 소멸자는 유지되는 생성자가 설정 한 개체의 불변성에 의존해서는 안됩니다. 소멸중인 개체가 완전히 생성되지 않았을 수 있기 때문입니다.

외부 코드에서 절반으로 구성된 개체를 관찰 할 수있는 또 다른 미친 방법은 물론 소멸자가 위 시나리오에서 절반으로 초기화 된 개체를보고 해당 개체에 대한 참조 를 정적 필드에 복사 하여 절반이 -건설 된 반쯤 완성 된 물체는 죽음에서 구출됩니다. 하지 마세요. 내가 말했듯이 아프면하지 마십시오.

값 유형의 생성자에있는 경우 기본적으로 동일하지만 메커니즘에 약간의 차이가 있습니다. 언어는 값 유형에 대한 생성자 호출이 액터 만 액세스 할 수있는 임시 변수를 생성하고 해당 변수를 변경 한 다음 변경된 값의 구조체 복사본을 실제 저장소에 수행하도록 요구합니다. 이렇게하면 생성자가 던지면 최종 저장소가 반 돌연변이 상태가되지 않습니다.

구조체 복사 원자 보장되지 않기 때문에주의, 그 다른 스레드가 반 돌연변이 상태의 저장을 표시 가능한; 그러한 상황에 있다면 잠금 장치를 올바르게 사용하십시오. 또한 스레드 중단과 같은 비동기 예외가 구조체 복사본 중간에 throw 될 수 있습니다. 이러한 비원 자성 문제는 복사본이 ctor 임시 복사본이든 "일반"복사본이든 상관없이 발생합니다. 그리고 일반적으로 비동기 예외가있는 경우 유지되는 불변은 거의 없습니다.

실제로 C # 컴파일러는 해당 시나리오가 발생할 방법이 없다고 판단 할 수있는 경우 임시 할당 및 복사를 최적화합니다. 예를 들어 새 값이 반복기 블록이 아니라 람다에 의해 닫히지 않은 로컬을 초기화하는 경우 직접 S s = new S(123);변경 s합니다.

값 형식 생성자가 작동하는 방식에 대한 자세한 내용은 다음을 참조하세요.

가치 유형에 대한 또 다른 신화 폭로

C # 언어 의미 체계가 사용자를 스스로로부터 구하는 방법에 대한 자세한 내용은 다음을 참조하세요.

이니셜 라이저가 생성자와 반대 순서로 실행되는 이유는 무엇입니까? 파트 1

이니셜 라이저가 생성자와 반대 순서로 실행되는 이유는 무엇입니까? 두 번째 부분

나는 당면한 주제에서 벗어난 것 같다. struct에서는 물론 동일한 방식으로 반으로 구성된 객체를 관찰 할 수 있습니다. 반으로 구성된 객체를 정적 필드에 복사하고 "this"를 인수로 사용하여 메서드를 호출하는 등의 작업을 수행 할 수 있습니다. (분명히 더 파생 된 형식에서 가상 메서드를 호출하는 것은 구조체에서 문제가되지 않습니다.) 그리고 제가 말했듯이 임시 저장소에서 최종 저장소로의 복사본은 원자 적이 지 않으므로 다른 스레드가 절반으로 복사 된 구조체를 관찰 할 수 있습니다.


이제 질문의 근본 원인을 고려해 봅시다. 서로를 참조하는 불변 객체를 어떻게 만들까요?

Typically, as you've discovered, you don't. If you have two immutable objects that reference each other then logically they form a directed cyclic graph. You might consider simply building an immutable directed graph! Doing so is quite easy. An immutable directed graph consists of:

  • An immutable list of immutable nodes, each of which contains a value.
  • An immutable list of immutable node pairs, each of which has the start and end point of a graph edge.

Now the way you make nodes A and B "reference" each other is:

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

And you're done, you've got a graph where A and B "reference" each other.

The problem, of course, is that you cannot get to B from A without having G in hand. Having that extra level of indirection might be unacceptable.


Yes, this is the only way for two immutable objects to refer to each other - at least one of them must see the other in a not-fully-constructed way.

It's generally a bad idea to let this escape from your constructor but in cases where you're confident of what both constructors do, and it's the only alternative to mutability, I don't think it's too bad.


"Fully constructed" is defined by your code, not by the language.

This is a variation on calling a virtual method from the constructor,
the general guideline is: don't do that.

To correctly implement the notion of "fully constructed", don't pass this out of your constructor.


Indeed, leaking the this reference out during the constructor will allow you to do this; it may cause problems if methods get invoked on the incomplete object, obviously. As for "other ways to observe the state of an object that is not fully constructed":

  • invoke a virtual method in a constructor; the subclass constructor will not have been called yet, so an override may try to access incomplete state (fields declared or initialized in the subclass, etc)
  • reflection, perhaps using FormatterServices.GetUninitializedObject (which creates an object without calling the constructor at all)

If you consider the initialization order

  • Derived static fields
  • Derived static constructor
  • Derived instance fields
  • Base static fields
  • Base static constructor
  • Base instance fields
  • Base instance constructor
  • Derived instance constructor

clearly through up-casting you can access the class BEFORE the derived instance constructor is called (this is the reason you shouldn't use virtual methods from constructors. They could easily access derived fields not initialized by the constructor/the constructor in the derived class could not have brought the derived class in a "consistent" state)


You can avoid the problem by instancing B last in your constuctor:

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

If what you suggest was not possible, then A would not be immutable.

Edit: fixed, thanks to leppie.


The principle is that don't let your this object escape from the constructor body.

Another way to observe such problem is by calling virtual methods inside the constructor.


As noted, the compiler has no means of knowing at what point an object has been constructed well enough to be useful; it therefore assumes that a programmer who passes this from a constructor will know whether an object has been constructed well enough to satisfy his needs.

I would add, however, that for objects which are intended to be truly immutable, one must avoid passing this to any code which will examine the state of a field before it has been assigned its final value. This implies that this not be passed to arbitrary outside code, but does not imply that there is anything wrong with having an object under construction pass itself to another object for the purpose of storing a back-reference which will not actually be used until after the first constructor has completed.

If one were designing a language to facilitate the construction and use of immutable objects, it may be helpful for it to declare methods as being usable only during construction, only after construction, or either; fields could be declared as being non-dereferenceable during construction and read-only afterward; parameters could likewise be tagged to indicate that should be non-dereferenceable. Under such a system, it would be possible for a compiler to allow the construction of data structures which referred to each other, but where no property could ever change after it was observed. As to whether the benefits of such static checking would outweigh the cost, I'm not sure, but it might be interesting.

Incidentally, a related feature which would be helpful would be the ability to declare parameters and function returns as ephemeral, returnable, or (the default) persistable. If a parameter or function return were declared ephemeral, it could not be copied to any field nor passed as a persistable parameter to any method. Additionally, passing an ephemeral or returnable value as a returnable parameter to a method would cause the return value of the function to inherit the restrictions of that value (if a function has two returnable parameters, its return value would inherit the more restrictive constraint from its parameters). A major weakness with Java and .net is that all object references are promiscuous; once outside code gets its hands on one, there's no telling who may end up with it. If parameters could be declared ephemeral, it would more often be possible for code which held the only reference to something to know it held the only reference, and thus avoid needless defensive copy operations. Additionally, things like closures could be recycled if the compiler could know that no references to them existed after they returned.

참고URL : https://stackoverflow.com/questions/7661538/immutable-objects-that-reference-each-other

반응형