[C#] 가변 구조체는 왜 “악”입니까?

SO에 대한 토론에 이어 이미 가변 구조체가 “악”이라는 말을 여러 번 읽었습니다 (이 답변에 대한 것처럼) 질문 .

C #의 가변성 및 구조체의 실제 문제는 무엇입니까?



답변

Structs는 값 유형으로, 전달 될 때 복사됩니다.

따라서 사본을 변경하면 해당 사본 만 변경되고 원본이 아닌 다른 사본은 변경되지 않습니다.

구조체가 변경 불가능한 경우 값을 전달하여 생성 된 모든 자동 사본은 동일합니다.

변경하려면 수정 된 데이터로 구조체의 새 인스턴스를 만들어 의식적으로 변경해야합니다. (사본이 아님)


답변

시작 위치 ;-p

Eric Lippert의 블로그 는 항상 인용문으로 좋습니다.

이것이 가변 값 유형이 악한 또 다른 이유입니다. 항상 값 유형을 변경할 수 없도록하십시오.

먼저 변경 사항을 쉽게 잃는 경향이 있습니다. 예를 들어 목록에서 항목을 가져 오는 경우 :

Foo foo = list[0];
foo.Name = "abc";

그게 뭐에요? 유용한 것은 없습니다 …

속성과 동일 :

myObj.SomeProperty.Size = 22; // the compiler spots this one

당신이 강제로 :

Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;

덜 중요한 것은 크기 문제입니다. 가변 객체 는 여러 속성을 갖는 경향 이 있습니다. 당신이 가진 구조체 경우 아직 intS, A string하는 DateTime과를 bool, 당신은 매우 신속하게 많은 메모리를 구울 수 있습니다. 클래스를 사용하면 여러 호출자가 동일한 인스턴스에 대한 참조를 공유 할 수 있습니다 (참조는 작음).


답변

나는 악의를 말하지 는 않지만, 가변성은 종종 최대 기능을 제공하기 위해 프로그래머 측의 지나치게 열망의 신호입니다. 실제로, 이것은 종종 필요하지 않으며, 결과적으로 인터페이스를 더 작고 사용하기 쉽고 잘못 사용하기 어렵게 만듭니다 (= 더 견고 함).

경쟁 조건에서의 읽기 / 쓰기 및 쓰기 / 쓰기 충돌이 이에 해당합니다. 쓰기는 유효한 작업이 아니기 때문에 변경 불가능한 구조에서는 이러한 오류가 발생할 수 없습니다.

또한 변경이 거의 필요하지 않다고 주장합니다 . 프로그래머는 단지 미래에 있을 것이라고 생각 합니다 . 예를 들어 날짜를 변경하는 것은 의미가 없습니다. 오히려 이전 날짜를 기준으로 새 날짜를 만드십시오. 이것은 저렴한 작업이므로 성능을 고려하지 않습니다.


답변

가변적 인 구조체는 악하지 않습니다.

고성능 환경에서는 절대적으로 필요합니다. 예를 들어 캐시 라인 및 가비지 수집이 병목 현상이되는 경우.

나는 완벽하게 유효한 유스 케이스 “evil”에서 불변 구조체의 사용을 호출하지 않을 것이다.

C #의 구문이 값 형식 또는 참조 형식의 멤버 액세스를 구별하는 데 도움이되지 않는 점을 알 수 있으므로 변경 불가능한 구조체 보다 불변성을 강제 하는 불변 구조체 를 선호 합니다.

그러나 불변 구조체를 단순히 “악”으로 표시하는 대신 언어를 포용하고보다 유용하고 건설적인 엄지 손가락 규칙을 옹호하는 것이 좋습니다.

예를 들어 “struct는 값 유형이며 기본적으로 복사됩니다. 복사하지 않으려면 참조가 필요합니다” 또는
“readonly structs를 먼저 사용하십시오” .


답변

공개적으로 변경 가능한 필드 또는 속성을 가진 구조는 악하지 않습니다.

.net이 그렇지 않은 메소드와 구별 할 수있는 방법을 제공하지 않기 때문에 “this”를 변경하는 구조 메소드 (특성 설정자와는 구별됨)는 다소 악의적입니다. “this”를 변경하지 않는 구조 메소드는 방어 적 복사가 필요없는 읽기 전용 구조체에서도 호출 할 수 있어야합니다. “this”를 변경하는 메소드는 읽기 전용 구조체에서 호출 할 수 없어야합니다. .net은 읽기 전용 구조체에서 “this”가 호출되는 것을 수정하지 않는 구조체 메소드를 금지하고 싶지 않지만 읽기 전용 구조체의 변경을 허용하지 않으려면 구조체를 읽기 전용으로 복사합니다. 두 세계에서 최악의 상황이 될 수 있습니다.

그러나 읽기 전용 컨텍스트에서 자체 변경 메소드를 처리하는 데 문제가 있음에도 불구하고 변경 가능한 구조체는 종종 변경 가능한 클래스 유형보다 훨씬 우수한 의미를 제공합니다. 다음 세 가지 메소드 서명을 고려하십시오.

struct PointyStruct {public int x, y, z;};
PointyClass 클래스 {public int x, y, z;};

void Method1 (PointyStruct foo);
void Method2 (참조 PointyStruct foo);
void Method3 (PointyClass foo);

각 방법에 대해 다음 질문에 답하십시오.

  1. 메소드가 “안전하지 않은”코드를 사용하지 않는다고 가정하면 foo를 수정할 수 있습니까?
  2. 메소드가 호출되기 전에 ‘foo’에 대한 외부 참조가 없으면 외부 참조가 존재할 수 있습니까?

대답:

질문 1 :
Method1(): 아니오 (명확한 의도)
Method2() : 예 (명확한 의도)
Method3() : 예 (불확실한 의도)
질문 2 :
Method1(): 아니오
Method2(): 아니오 ( 아니요 안전하지 않은 경우)
Method3() : 예

Method1은 foo를 수정할 수 없으며 참조를 얻지 않습니다. Method2는 foo에 대한 짧은 참조를 가져옵니다. foo는 반환 될 때까지 순서에 상관없이 foo 필드를 여러 번 수정할 수 있지만 해당 참조를 유지할 수는 없습니다. 안전하지 않은 코드를 사용하지 않으면 Method2가 리턴되기 전에 ‘foo’참조로 작성된 모든 사본이 사라집니다. Method2는 Method2와 달리 foo에 대해 무차별 적으로 공유 할 수있는 참조를 얻습니다. foo를 전혀 변경하지 않거나 foo를 변경 한 다음 반환하거나 다른 스레드에 대한 foo에 대한 참조를 제공하여 나중에 임의의 방식으로 임의의 방식으로 변경시킬 수 있습니다.

구조의 배열은 훌륭한 의미를 제공합니다. Rectangle 유형의 RectArray [500]이 주어진 경우, 예를 들어 요소 123을 요소 456에 복사 한 다음 나중에 요소 456을 방해하지 않고 요소 123의 너비를 555로 설정하는 방법이 명확하고 분명합니다. “RectArray [432] = RectArray [321 ]; …; RectArray [123] .Width = 555; “. Rectangle이 Width라는 정수 필드를 가진 구조체라는 것을 알면 위의 문장에 대해 모두 알아야 할 것입니다.

이제 RectClass가 Rectangle과 동일한 필드를 가진 클래스이고 RectClass 유형의 RectClassArray [500]에서 동일한 작업을 수행하려고한다고 가정하십시오. 아마도 배열은 변경 가능한 RectClass 객체에 대해 사전 초기화 된 500 개의 불변 참조를 보유해야합니다. 이 경우 올바른 코드는 “RectClassArray [321] .SetBounds (RectClassArray [456]); …; RectClassArray [321] .X = 555;”와 같은 코드입니다. 아마도 배열은 변경되지 않을 인스턴스를 보유한다고 가정하므로 적절한 코드는 “RectClassArray [321] = RectClassArray [456]; …; RectClassArray [321] = New RectClass (RectClassArray [321) ]); RectClassArray [321] .X = 555; ” 무엇을 해야하는지 알기 위해서는 RectClass에 대해 더 많이 알아야합니다 (예 : 복사 생성자, copy-from 메소드 등을 지원합니까). ) 및 배열의 ​​의도 된 사용법. 구조체를 사용하는 것만 큼 깨끗한 곳은 없습니다.

확실히 배열 이외의 컨테이너 클래스가 구조체 배열의 깨끗한 의미를 제공하는 좋은 방법은 없습니다. 컬렉션으로 문자열을 색인화하기를 원한다면 가장 좋은 방법은 색인에 대한 문자열, 일반 매개 변수 및 전달되는 대리자를 허용하는 일반 “ActOnItem”메소드를 제공하는 것입니다. 제네릭 매개 변수와 컬렉션 항목을 모두 참조하십시오. 그것은 구조체 배열과 거의 동일한 의미를 허용하지만 vb.net 및 C # 사람들이 멋진 구문을 제공하도록 설득 할 수 없다면 코드가 성능이 우수하더라도 (일반 매개 변수를 전달해도 코드가 어색해 보입니다) 정적 대리자를 사용할 수 있으며 임시 클래스 인스턴스를 만들 필요가 없습니다.

개인적으로, 나는 증오 Eric Lippert et al. 가변 값 유형과 관련하여 분출합니다. 그들은 모든 곳에서 사용되는 무차별 참조 유형보다 훨씬 더 명확한 의미를 제공합니다. .net의 값 유형 지원에 대한 일부 제한 사항에도 불구하고 가변 값 유형이 다른 유형의 엔티티보다 더 적합한 경우가 많이 있습니다.


답변

값 유형은 기본적으로 불변의 개념을 나타냅니다. Fx, 정수, 벡터 등과 같은 수학적 값을 가지고 수정할 수 없습니다. 그것은 가치의 의미를 재정의하는 것과 같습니다. 값 유형을 변경하는 대신 다른 고유 한 값을 할당하는 것이 더 합리적입니다. 속성의 모든 값을 비교하여 값 유형이 비교된다는 사실에 대해 생각하십시오. 요점은 속성이 동일하면 해당 값의 동일한 범용 표현이라는 것입니다.

Konrad가 언급했듯이 값은 상태 또는 컨텍스트 의존성을 가진 시간 객체의 인스턴스가 아닌 고유 한 시점을 나타내므로 날짜를 변경하는 것이 의미가 없습니다.

이것이 당신에게 의미가 있기를 바랍니다. 실제 세부 사항보다 값 유형으로 캡처하려는 개념에 대한 자세한 내용입니다.


답변

프로그래머의 관점에서 예측할 수없는 행동을 유발할 수있는 몇 가지 다른 경우가 있습니다.

변경할 수없는 값 유형 및 읽기 전용 필드

    // Simple mutable structure. 
    // Method IncrementI mutates current state.
    struct Mutable
    {
        public Mutable(int i) : this()
        {
            I = i;
        }

        public void IncrementI() { I++; }

        public int I { get; private set; }
    }

    // Simple class that contains Mutable structure
    // as readonly field
    class SomeClass
    {
        public readonly Mutable mutable = new Mutable(5);
    }

    // Simple class that contains Mutable structure
    // as ordinary (non-readonly) field
    class AnotherClass
    {
        public Mutable mutable = new Mutable(5);
    }

    class Program
    {
        void Main()
        {
            // Case 1. Mutable readonly field
            var someClass = new SomeClass();
            someClass.mutable.IncrementI();
            // still 5, not 6, because SomeClass.mutable field is readonly
            // and compiler creates temporary copy every time when you trying to
            // access this field
            Console.WriteLine(someClass.mutable.I);

            // Case 2. Mutable ordinary field
            var anotherClass = new AnotherClass();
            anotherClass.mutable.IncrementI();

            // Prints 6, because AnotherClass.mutable field is not readonly
            Console.WriteLine(anotherClass.mutable.I);
        }
    }

가변 값 유형 및 배열

Mutable구조체 배열이 있고 해당 배열의 IncrementI첫 번째 요소에 대한 메소드를 호출 한다고 가정하십시오 . 이 전화에서 어떤 행동을 기대하십니까? 배열의 값을 변경해야합니까 아니면 사본 만 변경해야합니까?

    Mutable[] arrayOfMutables = new Mutable[1];
    arrayOfMutables[0] = new Mutable(5);

    // Now we actually accessing reference to the first element
    // without making any additional copy
    arrayOfMutables[0].IncrementI();

    // Prints 6!!
    Console.WriteLine(arrayOfMutables[0].I);

    // Every array implements IList<T> interface
    IList<Mutable> listOfMutables = arrayOfMutables;

    // But accessing values through this interface lead
    // to different behavior: IList indexer returns a copy
    // instead of an managed reference
    listOfMutables[0].IncrementI(); // Should change I to 7

    // Nope! we still have 6, because previous line of code
    // mutate a copy instead of a list value
    Console.WriteLine(listOfMutables[0].I);

따라서 변경 가능한 구조체는 자신과 팀의 나머지 구성원이 자신이하는 일을 명확하게 이해하는 한 악한 것이 아닙니다. 그러나 프로그램 동작이 예상 한 것과 다를 경우 코너 생성이 너무 어려워서 오류를 생성하기 어렵고 이해하기 어려울 수 있습니다.