[C#] 좋은 습관입니까? 게터에서 객체 초기화

동료에 따르면 적어도 이상한 습관이 있습니다. 우리는 작은 프로젝트를 함께 진행하고 있습니다. 내가 수업을 쓴 방법은 (단순화 된 예)입니다.

[Serializable()]
public class Foo
{
    public Foo()
    { }

    private Bar _bar;

    public Bar Bar
    {
        get
        {
            if (_bar == null)
                _bar = new Bar();

            return _bar;
        }
        set { _bar = value; }
    }
}

따라서 기본적으로 getter가 호출되고 필드가 여전히 null 일 때만 필드를 초기화합니다. 어디서나 사용되지 않는 속성을 초기화하지 않으면 과부하가 줄어 듭니다.

ETA : 내가 한 이유는 내 클래스에 다른 클래스의 인스턴스를 반환하는 여러 속성이 있기 때문에 더 많은 클래스가있는 속성도 있습니다. 최상위 클래스의 생성자를 호출하면 나중에이 클래스가 아닌 모든 클래스의 모든 생성자를 호출합니다. 항상 필요한 아니지만 모든 합니다.

개인적 취향 외에이 관행에 대한 반대 의견이 있습니까?

업데이트 : 나는이 질문에 관해 많은 다른 의견을 고려했으며 받아 들인 대답을 기다릴 것입니다. 그러나 이제는 개념에 대해 훨씬 더 잘 이해하게되었으며 사용시기와 사용하지 않을시기를 결정할 수 있습니다.

단점 :

  • 스레드 안전 문제
  • 전달 된 값이 null 일 때 “세터”요청을 따르지 않음
  • 마이크로 최적화
  • 예외 처리는 생성자에서 발생해야합니다
  • 클래스 코드에서 null을 확인해야합니다.

장점 :

  • 마이크로 최적화
  • 속성은 null을 반환하지 않습니다
  • “무거운”오브젝트로드 지연 또는 방지

대부분의 단점은 현재 라이브러리에 적용 할 수 없지만 “마이크로 최적화”가 실제로 어떤 것을 최적화하고 있는지 테스트해야합니다.

마지막 업데이트:

좋아, 나는 대답을 바꿨다. 내 원래의 질문은 이것이 좋은 습관인지 아닌지였습니다. 그리고 나는 그것이 아니라고 확신합니다. 어쩌면 나는 현재 코드의 일부 부분에서 여전히 사용하지만 무조건적이고 확실하지는 않습니다. 그래서 나는 습관을 잃고 그것을 사용하기 전에 그것에 대해 생각할 것입니다. 모두 감사합니다!



답변

여기에 “게으른 초기화”의 순진한 구현이 있습니다.

짧은 답변:

지연 초기화를 무조건 사용 하는 것은 좋은 생각이 아닙니다. 그것은 그 자리를 가지고 있지만이 솔루션의 영향을 고려해야합니다.

배경과 설명 :

구체적 구현 :
먼저 구체적인 샘플을 살펴보고 구현 순진한 이유를 살펴 보겠습니다.

  1. 그것은 최소한의 서프라이즈 원칙 (POLS)을 위반합니다 . 값이 속성에 할당되면이 값이 반환 될 것으로 예상됩니다. 귀하의 구현에서는 다음과 같은 경우가 아닙니다 null.

    foo.Bar = null;
    Assert.Null(foo.Bar); // This will fail
  2. 스레딩 문제가 많이 발생합니다. foo.Bar다른 스레드 의 두 호출자 는 잠재적으로 두 개의 다른 인스턴스를 얻을 수 Bar있으며 그 중 하나는 인스턴스에 연결되지 않습니다 Foo. 해당 Bar인스턴스에 대한 모든 변경 사항 은 자동으로 손실됩니다.
    이것은 POLS 위반의 또 다른 경우입니다. 속성의 저장된 값만 액세스하면 스레드로부터 안전해야합니다. 클래스가 단순히 스레드 안전하지 않다고 주장 할 수는 있지만 속성의 getter를 포함하여 일반적인 경우가 아니라는 것을 올바르게 문서화해야합니다. 또한 우리가 곧 보게 되겠지만이 문제의 도입은 불필요하다.

일반적으로 :
이제는 초기화 지연을 살펴볼 차례입니다.
지연 초기화는 일반적으로 구성 하는 데 시간이 오래 걸리거나 완전히 구성된 후 많은 메모리를 사용하는 개체의 구성을 지연시키는 데 사용됩니다 .
이것이 게으른 초기화를 사용하는 매우 유효한 이유입니다.

그러나 이러한 속성에는 일반적으로 세터가 없으므로 위에서 지적한 첫 번째 문제를 제거합니다.
또한 Lazy<T>두 번째 문제를 피하기 위해 스레드 안전 구현이 사용됩니다 .

게으른 속성을 구현할 때이 두 가지 사항을 고려하더라도이 패턴의 일반적인 문제는 다음과 같습니다.

  1. 객체 생성에 실패하여 속성 getter에서 예외가 발생할 수 있습니다. 이것은 POLS에 대한 또 다른 위반이므로 피해야합니다. “클래스 라이브러리 개발을위한 디자인 지침”의 속성 섹션 에서도 속성 게터가 예외를 발생시키지 않아야한다고 명시하고 있습니다.

    속성 게터에서 예외를 발생시키지 마십시오.

    속성 게터는 사전 조건이없는 간단한 작업이어야합니다. getter가 예외를 throw 할 수있는 경우 특성을 메소드로 재 설계하는 것이 좋습니다.

  2. 컴파일러에 의한 자동 최적화, 즉 인라인 및 분기 예측이 손상됩니다. 자세한 설명 은 Bill K의 답변 을 참조하십시오 .

이러한 사항의 결론은 다음과 같습니다.
느리게 구현 된 각 단일 속성에 대해 이러한 사항을 고려해야합니다.
즉, 결정은 사례마다 다르며 일반적인 모범 사례로 간주 할 수 없습니다.

이 패턴은 그 자리에 있지만 클래스를 구현할 때 일반적인 모범 사례는 아닙니다. 위에 언급 한 이유 때문에 무조건 사용해서는 안됩니다 .


이 섹션에서는 다른 사람들이 지연 초기화를 무조건적으로 사용하기위한 인수로 제시 한 몇 가지 사항에 대해 논의하고자합니다.

  1. 직렬화 :
    EricJ는 한 의견에서 다음과 같이 말합니다.

    직렬화 될 수있는 객체는 직렬화 해제 될 때 생성자가 호출되지 않습니다 (직렬화기에 따라 다르지만 많은 일반 객체는 이와 같이 동작 함). 생성자에 초기화 코드를 넣으면 역 직렬화에 대한 추가 지원을 제공해야합니다. 이 패턴은 특별한 코딩을 피합니다.

    이 주장에는 몇 가지 문제가 있습니다.

    1. 대부분의 객체는 직렬화되지 않습니다. 필요하지 않을 때 지원을 추가하면 YAGNI를 위반 합니다 .
    2. 클래스가 직렬화를 지원해야 할 때 직렬화와 관련이없는 해결 방법없이 클래스를 활성화하는 방법이 있습니다.
  2. 미세 최적화 : 주된 논점은 누군가가 실제로 액세스 할 때만 오브젝트를 구성한다는 것입니다. 따라서 실제로 메모리 사용 최적화에 대해 이야기하고 있습니다.
    다음과 같은 이유로이 주장에 동의하지 않습니다.

    1. 대부분의 경우 메모리에 몇 개의 객체가 더 이상 영향을 미치지 않습니다. 최신 컴퓨터에는 충분한 메모리가 있습니다. 프로파일 러가 확인한 실제 문제가없는 경우 이는 조기 최적화 이므로 이에 대한 충분한 이유가 있습니다.
    2. 나는 때때로 이런 종류의 최적화가 정당하다는 사실을 인정한다. 그러나 이러한 경우에도 게으른 초기화는 올바른 해결책이 아닌 것 같습니다. 그것에 대해 말하는 두 가지 이유가 있습니다.

      1. 지연 초기화는 잠재적으로 성능을 저하시킵니다. 어쩌면 미미할 지 모르지만 Bill의 답변에서 알 수 있듯이 영향은 언뜻보기에 생각하는 것보다 큽니다. 따라서이 방법은 기본적으로 성능과 메모리를 교환합니다.
      2. 클래스의 일부만 사용하는 일반적인 유스 케이스 인 디자인이있는 경우 이는 디자인 자체의 문제점을 암시합니다. 문제의 클래스는 둘 이상의 책임이 있습니다. 해결책은 클래스를 몇 가지 더 집중된 클래스로 나누는 것입니다.

답변

좋은 디자인 선택입니다. 라이브러리 코드 또는 핵심 클래스에 강력히 권장됩니다.

“지연된 초기화”또는 “지연된 초기화”에 의해 호출되며 일반적으로 모두 좋은 디자인 선택으로 간주됩니다.

먼저 클래스 수준 변수 또는 생성자의 선언에서 초기화하면 객체가 생성 될 때 절대 사용할 수없는 리소스를 만드는 오버 헤드가 발생합니다.

둘째, 필요한 경우에만 리소스가 생성됩니다.

셋째, 사용되지 않은 객체를 가비지 수집하지 않도록합니다.

마지막으로, 속성에서 발생할 수있는 초기화 예외와 클래스 수준 변수 또는 생성자의 초기화 중에 발생하는 예외를 처리하는 것이 더 쉽습니다.

이 규칙에는 예외가 있습니다.

“get”속성에서 초기화에 대한 추가 검사의 성능 인수와 관련하여 중요하지 않습니다. 객체를 초기화하고 배치하는 것은 간단한 null 포인터 확인으로 점프하는 것보다 훨씬 중요한 성능 저하입니다.

디자인 지침 클래스 라이브러리 개발을위한 에서 http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx

에 관해서 Lazy<T>

일반 Lazy<T>클래스는 포스터가 원하는대로 정확하게 작성되었습니다 . http://msdn.microsoft.com/en-us/library/dd997286(v=vs.100).aspx의 Lazy Initialization 을 참조 하십시오 . 이전 버전의 .NET을 사용하는 경우 질문에 설명 된 코드 패턴을 사용해야합니다. 이 코드 패턴은 매우 일반화되어 패턴을보다 쉽게 ​​구현할 수 있도록 최신 .NET 라이브러리에 클래스를 포함시키는 데 적합했습니다. 또한 구현에 스레드 안전성이 필요한 경우이를 추가해야합니다.

기본 데이터 유형 및 단순 클래스

명백히, 기본 데이터 유형이나 간단한 클래스 사용에 지연 초기화를 사용하지 않을 것 List<string>입니다.

Lazy에 대해 언급하기 전에

Lazy<T> .NET 4.0에 도입되었으므로이 클래스에 대한 또 다른 의견을 추가하지 마십시오.

마이크로 최적화에 대해 언급하기 전에

라이브러리를 구축 할 때는 모든 최적화를 고려해야합니다. 예를 들어, .NET 클래스에는 코드 전체에서 부울 클래스 변수에 사용되는 비트 배열이 표시되어 메모리 소비 및 메모리 조각화를 줄이고 두 개의 “마이크로 최적화”라고합니다.

사용자 인터페이스에 대하여

사용자 인터페이스에서 직접 사용하는 클래스에는 지연 초기화를 사용하지 않습니다. 지난 주에는 콤보 상자의 뷰 모델에 사용되는 8 개의 컬렉션을 지연 로딩하여 하루 중 더 좋은 시간을 보냈습니다. LookupManager사용자 인터페이스 요소에 필요한 컬렉션의 느린로드 및 캐싱을 처리 하는 을 가지고 있습니다 .

“세터”

게으른로드 된 속성에 대해 set-property ( “setters”)를 사용한 적이 없습니다. 따라서을 허용하지 않습니다 foo.Bar = null;. 설정 해야하는 경우 lazy-initialization을 사용하지 않고 Bar라는 메소드를 작성합니다.SetBar(Bar value)

컬렉션

클래스 컬렉션 속성은 null이 아니 어서 선언 될 때 항상 초기화됩니다.

복잡한 수업

이것을 다르게 반복하겠습니다. 복잡한 클래스에는 지연 초기화를 사용합니다. 일반적으로 제대로 설계되지 않은 클래스입니다.

마지막으로

나는 모든 수업이나 모든 경우에 이것을하지 않았다. 나쁜 습관입니다.


답변

Lazy<T>?를 사용하여 이러한 패턴을 구현하는 것을 고려하고 있습니까?

지연로드 된 객체를 쉽게 만들 수있을뿐만 아니라 객체가 초기화되는 동안 스레드 안전성을 얻을 수 있습니다.

다른 사람들이 말했듯이 실제로 리소스가 많거나 객체 생성 시간 동안 객체를로드하는 데 시간이 걸리는 경우 지연로드하십시오.


답변

나는 그것이 당신이 초기화하는 것에 달려 있다고 생각합니다. 건설 비용이 상당히 적기 때문에 목록을 작성하지 않을 것이므로 생성자에 들어갈 수 있습니다. 그러나 그것이 미리 채워진 목록이라면 처음으로 필요할 때까지는 아마 없을 것입니다.

기본적으로 건설 비용이 각 액세스에 대해 조건부 검사를 수행하는 비용보다 큰 경우 게으른 생성합니다. 그렇지 않은 경우 생성자에서 수행하십시오.


답변

내가 볼 수있는 단점은 Bars가 null인지 묻기를 원한다면 결코 존재하지 않으며 거기에 목록을 작성한다는 것입니다.


답변

게으른 인스턴스화 / 초기화는 완벽하게 실행 가능한 패턴입니다. 그러나 API의 일반적인 규칙으로서 게터와 세터가 최종 사용자 POV로부터 식별 가능한 시간을 갖거나 실패 할 것으로 예상하지 않습니다.


답변

나는 다니엘의 대답에 대한 의견을 말하려고했지만 솔직히 그것이 충분하다고 생각하지 않습니다.

특정 상황 (예 : 데이터베이스에서 객체가 초기화 될 때)에서 사용하기에는 매우 좋은 패턴이지만, 들어가는 것은 나쁜 습관입니다.

개체의 가장 좋은 점 중 하나는 안전하고 신뢰할 수있는 환경을 제공한다는 것입니다. 가장 좋은 경우는 가능한 한 많은 필드를 “최종”으로 만들어 생성자로 채우는 것입니다. 이것은 당신의 수업을 방탄하게 만듭니다. setter를 통해 필드를 변경하는 것은 약간 덜하지만 끔찍한 것은 아닙니다. 예를 들어 :

SafeClass 클래스
{
    문자열 이름 = "";
    정수 연령 = 0;

    공공 무효 setName (문자열 newName)
    {
        assert (newName! = null)
        name = newName;
    } // 연령에 따라이 패턴을 따릅니다.
    ...
    공개 문자열 toString () {
        String s = "세이프 클래스는 이름 :"+ name + "및 연령 :"+ age입니다.
    }
}

패턴을 사용하면 toString 메소드는 다음과 같습니다.

    if (이름 == null)
        새로운 IllegalStateException을 던집니다 ( "SafeClass가 잘못된 상태가되었습니다! 이름이 null입니다").
    if (나이 == null)
        새로운 IllegalStateException을 던집니다 ( "SafeClass가 잘못된 상태가되었습니다! 나이가 null입니다").

    공개 문자열 toString () {
        String s = "세이프 클래스는 이름 :"+ name + "및 연령 :"+ age입니다.
    }

이뿐 만 아니라 클래스에서 해당 객체를 사용할 수있는 모든 곳에서 null 검사가 필요합니다 (게터의 null 검사로 인해 클래스 외부에서는 안전하지만 대부분 클래스 내부에서 클래스 멤버를 사용해야합니다)

또한 클래스가 불확실한 상태에 있습니다. 예를 들어 몇 가지 주석을 추가하여 해당 클래스를 최대 절전 모드 클래스로 설정 한 경우 어떻게해야합니까?

요구 사항과 테스트없이 일부 미세 광학 화를 기반으로 결정을 내리는 것은 거의 잘못된 결정입니다. 실제로 if 문이 CPU에서 분기 예측 실패를 일으켜서 여러 번 더 느려질 수 있기 때문에 가장 이상적인 상황에서도 패턴이 실제로 시스템 속도를 늦출 가능성이 매우 높습니다. 생성하는 객체가 상당히 복잡하거나 원격 데이터 소스에서 오는 경우가 아니면 생성자에 값을 할당하면됩니다.

brance 예측 문제의 예 (한 번도 반복적으로 발생하지 않음)는이 멋진 질문에 대한 첫 번째 답변을 참조하십시오 . 정렬되지 않은 배열보다 정렬 된 배열을 처리하는 것이 왜 더 빠릅니까?