가변 구조체는 왜 "악"입니까?


485

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

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


12
가변 구조체를 악하다고 주장하는 것은 가변 int, bools를 주장하는 것과 같 으며 다른 모든 값 유형은 악입니다. 변경 가능성과 불변성에 대한 경우가 있습니다. 이러한 경우는 메모리 할당 / 공유 유형이 아니라 데이터의 역할에 달려 있습니다.
슬립 D. 톰슨

41
@slipp int이며 변경 bool불가능 합니다.
Blorgbeard가

2
.-구문, 참조 유형 데이터와 값 유형 데이터를 사용하는 작업은 분명히 다르더라도 동일하게 보입니다. 이것은 구조체가 아닌 C # 속성의 결함입니다. 일부 언어는 내부 a[V][X] = 3.14변경을위한 대체 구문을 제공합니다 . C #에서, 당신은 (뮤 테이터 <Vector2 심판> 액션) 'MutateV 같은 제안 구조체 멤버 뮤 테이터 방법에 더 잘 할 것'과 같이 사용할 수 a.MutateV((v) => { v.X = 3; }) 했다 관한 예를 때문에 제한 사항의 C #의 과도한 단순화 ( ref하지만 일부와, 키워드 해결 방법이 가능해야합니다) .
슬립 D. 톰슨

2
@Slipp 글쎄, 나는 이런 종류의 구조체에 대해 정확히 반대라고 생각합니다. DateTime 또는 TimeSpan과 같은 .NET 라이브러리에 이미 구현 된 구조체는 변경할 수 없다고 생각하는 이유는 무엇입니까? 아마도 그러한 구조체의 var 멤버 중 하나만 변경하는 것이 유용 할 수 있지만 너무 불편하여 너무 많은 문제가 발생합니다. 실제로 C #은 어셈블러로 컴파일되지 않기 때문에 IL로 컴파일하기 때문에 프로세서 calc가 무엇인지 잘못 알고 있습니다. IL에서 (우리는 이미 이름의 변수가 제공하는 x이 단일 작업하는) 4 설명입니다 ldloc.0... 0 인덱스 변수로 (로드
Sushi271

1
... 유형. T유형입니다. Ref는 변수가 복사본이 아닌 메서드 자체에 전달되는 키워드입니다. 변수를 변경할 수 있기 때문에 참조 유형에 대해서도 의미가 있습니다 . 즉, 메소드 외부의 참조는 메소드 내에서 변경된 후 다른 객체를 가리 킵니다. 때문에 ref T유형,하지만 방법 매개 변수를 전달하는 방식으로하지 않습니다, 당신은에 넣어 수 없습니다 <>만 종류가 넣을 수 있습니다 원인. 그래서 그것은 틀 렸습니다. 아마도 그렇게하는 것이 편리 할 것입니다. 아마도 C # 팀이 새로운 버전을 위해 이것을 만들 수 있을지 모르지만 지금은 일부 버전에서 작업하고 있습니다.
Sushi271

답변:


290

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

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

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

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


82
"구조가 변경 불가능한 경우 모든 사본은 동일합니다." 아니요, 다른 가치를 원한다면 의식적으로 사본을 만들어야한다는 것을 의미합니다. 원본을 수정한다고 생각하면 사본을 수정하지 않아도됩니다.
Lucas

19
@Lucas 나는 당신이 다른 종류의 사본에 대해 이야기하고 있다고 생각합니다. 나는 가치에 의해 전달 된 결과로 만들어진 자동 사본에 대해 이야기하고 있습니다. 실제로는 다른 데이터를 포함하는 고의적 인 새로운 순간을 복사하는 것이 아닙니다.
trampster

3
16 개월 후 수정하면 조금 더 명확 해집니다. 그래도 여전히 "(불변 구조체)는 원본을 수정하고 있다고 생각하는 사본을 수정하는 데 걸리지 않음을 의미합니다."
Lucas

6
@ 루카스 : 구조체의 복사본을 만들고 수정하고 어떤 식 으로든 원본을 수정한다고 생각하는 경우 (스트럭처 필드를 작성 한다는 사실이 자신의 사본을 작성한다는 사실을 자명 하게 만드는 경우) 클래스 객체를 포함하는 정보를 보유하는 수단으로 클래스 객체를 보유한 사람이 객체를 변경하여 자체 정보를 업데이트하고 다른 객체가 보유한 정보를 손상시키는 위험에 비해 매우 작습니다.
supercat

2
세 번째 단락이 잘못 들리거나 불분명합니다. 구조체가 변경 불가능한 경우 해당 필드 또는 사본의 필드를 수정할 수 없습니다. "당신이 그것을 변경하려는 경우 당신이 가지고 ..." 너무 오해의 소지가있어, 당신은 변경할 수 없습니다 그것을 지금까지 , 아니 의식적이나 무의식적으로. 원하는 데이터로 새 인스턴스를 만들면 동일한 데이터 구조를 갖는 것 외에 원본 복사본과 아무 ​​관련이 없습니다.
Saeb Amini

167

시작 위치 ;-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, 당신은 매우 신속하게 많은 메모리를 구울 수 있습니다. 클래스를 사용하면 여러 호출자가 동일한 인스턴스에 대한 참조를 공유 할 수 있습니다 (참조는 작음).


5
예, 그러나 컴파일러는 그런 식으로 바보입니다. IMHO는 속성 구조 멤버에게 할당을 허용하지 않는 것은 운영자 허용하기 때문에 어리석은 디자인 결정이었습니다 ++. 이 경우 컴파일러는 프로그래머를 방해하지 않고 명시 적 할당 자체를 작성합니다.
Konrad Rudolph

12
@Konrad : myObj.SomeProperty.Size = 22는 myObj.SomeProperty의 사본을 수정합니다. 컴파일러는 명백한 버그로부터 당신을 구하고 있습니다. 그리고 ++에는 허용되지 않습니다.
Lucas

@Lucas : 모노 C # 컴파일러는 확실히 허용합니다. Windows가 없기 때문에 Microsoft의 컴파일러를 확인할 수 없습니다.
Konrad Rudolph

6
@ Konrad-간접적으로 하나만 작동하면 작동합니다. 막힌 경우는 "스택에서 일시적인 값으로 만 존재하며 무 (無)로 증발하려고하는 무언가의 값을 변경하는 것"입니다.
Marc Gravell

2
@Marc Gravell : 앞의 코드에서, 이름이 "abc"이고 다른 속성은 List [0]을 방해하지 않으면 서 "Foo"로 끝납니다. Foo가 클래스 인 경우이를 복제 한 다음 사본을 변경해야합니다. 내 생각에, 가치 유형과 클래스 구분의 큰 문제는 "."의 사용이다. 두 가지 목적을위한 연산자. 내가 진실을 알면 수업은 "." 메소드와 속성에 대해서는 "->"를 사용하지만 "."에 대한 일반적인 의미는 속성은 해당 필드를 수정하여 새 인스턴스를 만드는 것입니다.
supercat

71

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

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

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


1
에릭 리퍼 트 ​​(Eric Lippert)는 ...
Marc Gravell

42
내가 Eric Lippert를 존중하는 한 그는 신이 아니었다. 링크 된 블로그 게시물과 위의 게시물은 물론 구조체를 불변으로 만드는 데 합리적인 논쟁이지만 실제로는 절대로 가변 구조체를 사용 하지 않는 논쟁으로 매우 약 합니다. 그러나이 게시물은 +1입니다.
Stephen Martin

2
C #에서 개발할 때는 일반적으로 현재 변경이 필요합니다. 특히 스트리밍 등이 기존 솔루션과 원활하게 작동하기를 원하는 비즈니스 모델에서는 특히 그러합니다. : 나는 가변 및 불변의 데이터로 작업, 가변성 주위에 대부분의 문제를 해결 (I 희망) 방법에 대한 기사를 쓴 rickyhelgesson.wordpress.com/2012/07/17/...
리키 Helgesson

3
@StephenMartin : 단일 값을 캡슐화하는 Struct는 종종 불변이어야하지만 구조체는 "동일성"이없는 독립적이지만 관련 변수 (점의 X 및 Y 좌표와 같은)의 고정 세트를 캡슐화하는 데 가장 적합한 매체입니다. 그룹. 이러한 목적으로 사용되는 구조 는 일반적으로 변수를 공용 필드로 노출해야합니다. 나는 그러한 목적을 위해 구조체보다 클래스를 사용하는 것이 더 잘못되었다는 생각을 고려할 것입니다. 불변 클래스는 종종 비효율적이며, 가변 클래스는 종종 두려운 의미를 갖습니다.
supercat

2
@StephenMartin : 예를 들어 float그래픽 변환 의 6 가지 구성 요소 를 반환하는 메서드 나 속성을 고려하십시오 . 그러한 메소드가 6 개의 컴포넌트를 가진 노출 된 필드 구조체를 반환한다면, 구조체의 필드를 수정해도 수신 된 그래픽스 개체는 수정되지 않습니다. 이러한 메소드가 변경 가능한 클래스 객체를 반환하는 경우 속성을 변경하면 기본 그래픽 객체가 변경 될 수도 있고 그렇지 않을 수도 있습니다.
supercat 2016 년

48

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

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

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

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

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

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


8
또한 덕트 테이프와 함께 고정 변수 세트를 고정하여 값을 개별적으로 또는 단위로 처리하거나 저장할 수 있다면 컴파일러에 고정 세트를 고정하도록 요청하는 것이 훨씬 합리적입니다 struct엉뚱하게 같은 목적을 달성하기 위해 클래스를 정의하거나 구조체에 정크를 추가하여 클래스를 에뮬레이트하도록 만드는 것보다 변수를 함께 (즉, 공개 필드로 선언 ) 덕트 테이프와 함께 붙어있는 일련의 변수처럼 행동하십시오. 이것은 처음에 실제로 원하는 것입니다)
supercat

41

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

.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의 값 유형 지원에 대한 일부 제한 사항에도 불구하고 가변 값 유형이 다른 유형의 엔티티보다 더 적합한 경우가 많이 있습니다.


1
@Ron Warholic : SomeRect가 Rectangle이라는 것은 자명하지 않습니다. Rectangle에서 암시 적으로 타입 캐스트 할 수있는 다른 유형 일 수 있습니다. Rectangle에서 암시 적으로 타입 캐스트 할 수있는 유일한 시스템 정의 유형은 RectangleF이며 RectangleF의 필드를 Rectangle의 생성자에 전달하려고하면 컴파일러가 스 쿼크됩니다 (이전은 Single이고 후자는 정수이므로) 이러한 암시 적 타입 캐스트를 허용하는 사용자 정의 구조체가있을 수 있습니다. BTW에서 첫 번째 문은 SomeRect가 Rectangle인지 RectangleF인지에 상관없이 동일하게 작동합니다.
supercat

당신이 보여준 모든 것은 당신이 한 가지 방법이 더 명확하다고 생각되는 예에서 당신이 보여줍니다. 우리가 당신의 모범을 Rectangle보이면, 당신이 매우 불분명 한 행동을 취하는 일반적인 상황을 쉽게 떠 올릴 수 있습니다 . WinForms Rectangle는 폼의 Bounds속성에 사용되는 가변 타입을 구현한다고 생각하십시오 . 범위를 변경하려면 멋진 구문을 사용하고 싶습니다. form.Bounds.X = 10;그러나 이것은 양식의 아무것도 변경 하지 않습니다 (그리고 그러한 것을 알려주는 멋진 오류가 발생합니다). 불일치는 프로그래밍의 단점이며 불변성이 요구되는 이유입니다.
Ron Warholic

3
Warholic @Ron : BTW, 나는 것 같은 말을 할 수있는 "form.Bounds.X = 10;" 제대로 작동하되 시스템은 깔끔한 방법을 제공하지 않습니다. 콜백을 수락하는 메소드로 값 유형 특성을 노출하는 규칙은 클래스를 사용하는 접근 방식보다 훨씬 깨끗하고 효율적이며 확인 가능한 올바른 코드를 제공 할 수 있습니다.
supercat

4
이 답변은 가장 많이 투표 된 답변보다 훨씬 더 통찰력이 있습니다. 변경 가능한 값 형식에 대한 인수는 앨리어싱과 돌연변이를 혼합 할 때 발생하는 "예상치"라는 개념에 의존한다는 것은 터무니없는 것입니다. 어쨌든 그것은 끔찍한 일입니다 !
Eamon Nerbonne

2
@ supercat : C # 7에 대해 이야기하고있는 Ref-Return 기능은 그베이스를 다룰 수 있습니다 (실제로 자세히 보지는 않았지만 표면적으로 비슷하게 들립니다).
Eamon Nerbonne

24

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

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

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


글쎄, 그것들 최소한 불변의 개념 을 나타내야한다 ; -p
Marc Gravell

3
글쎄, 그들이 System.Drawing.Point를 불변으로 만들 수 있다고 생각하지만 심각한 디자인 오류 IMHO 일 것입니다. 포인트는 실제로 전형적인 값 유형이며 변경 가능하다고 생각합니다. 그리고 그들은 101 명의 초보자를 초창기 프로그래밍하는 것 외에는 아무 문제도 일으키지 않습니다.
Stephen Martin

3
원칙적으로 포인트도 불변해야한다고 생각하지만 타입을 사용하기 어렵거나 덜 우아하게 만들면 물론 고려해야합니다. 아무도 그것을 사용하고 싶지 않으면 최고의 원리를지지하는 코드 구조를 가질 필요는 없습니다.)
Morten Christiansen

3
값 유형은 단순한 불변 개념을 나타내는 데 유용하지만 노출 된 필드 구조는 작은 고정 된 관련이지만 독립적 인 값 집합 (예 : 점 좌표)을 유지하거나 전달하는 데 사용하는 가장 좋은 유형입니다. 이러한 값 유형의 저장 위치는 필드 값을 캡슐화하고 다른 값은 없습니다. 반대로, 변경 가능한 참조 유형의 저장 위치는 변경 가능한 객체의 상태를 유지하기 위해 사용될 수 있지만, 동일한 객체에 존재하는 유니버스 전체의 다른 모든 참조의 ID를 캡슐화합니다.
supercat 2016 년

4
"값 유형은 기본적으로 불변의 개념을 나타냅니다". 아닙니다. 값 유형 변수의 가장 오래되고 가장 유용한 응용 프로그램 중 하나는 int이터레이터이며, 변경 불가능한 경우에는 완전히 쓸모가 없습니다. "값 유형 유형의 컴파일러 / 런타임 구현"을 "값 유형으로 유형이 지정된 변수"로 혼동한다고 생각합니다. 후자는 가능한 모든 값으로 변경 가능합니다.
슬립 D. 톰슨

19

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

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

    // 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);

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


5
.net 언어가 약간 더 나은 값 유형 지원을 제공하는 경우 struct 메소드는 명시 적으로 선언되지 않은 한 'this'를 변경하지 않아야하며 그렇게 선언 된 메소드는 읽기 전용으로 금지되어야합니다. 문맥. 가변 구조체의 배열은 다른 의미로는 효율적으로 달성 할 수없는 유용한 의미론을 제공합니다.
supercat

2
이들은 가변 구조체에서 발생할 수있는 매우 미묘한 문제의 좋은 예입니다. 나는이 행동을 기대하지 않았을 것이다. 왜 배열이 당신에게 참조를 주지만 인터페이스는 당신에게 가치를 줄까요? 나는 항상 가치가있는 것 외에도 (내가 정말로 기대하는 것), 적어도 다른 방법이 될 것이라고 생각했을 것이다. 값을주는 배열 ...
Dave Cousineau

@Sahuagin : 불행히도, 인터페이스가 참조를 노출시킬 수있는 표준 메커니즘은 없습니다. 특별한을 정의하여 같은 것들을 안전하고 유용하게 (예를 수행 할 수 있도록 할 수 .NET 가지 방법이있다 "ArrayRef <T>의"포함하는 구조체 T[]및 정수 인덱스 및 유형의 속성에 대한 액세스가 있음을 제공 ArrayRef<T>int로서 해석됩니다 적절한 배열 요소에 액세스) (클래스가 ArrayRef<T>다른 목적 으로 노출을 원한다면 속성과 반대로 메소드를 제공하여 검색 할 수 있습니다). 불행히도 그러한 조항은 존재하지 않습니다.
supercat

2
오 마이 .. 이것은 가변 구조체를 망하게 만듭니다!
nawfal

1
나는 분명하지 않은 매우 귀중한 정보를 포함하고 있기 때문에이 답변을 좋아합니다. 그러나 실제로 이것은 일부 주장과 같은 가변 구조체에 대한 논쟁이 아닙니다. 예, Eric이 말한 것처럼 "절망의 구덩이"가 있습니다. 그러나이 절망의 원인은 변할 수 없습니다. 절망의 근원은 구조체 자체 돌연변이 방법 입니다. 배열과 목록이 다르게 동작하는 이유는 하나는 기본적으로 메모리 주소를 계산하는 연산자이고 다른 하나는 속성이기 때문입니다. 일반적으로 "참조"가 주소 이라는 것을 이해하면 모든 것이 명확 해 집니다 .
AnorZaken

18

C / C ++와 같은 언어로 프로그래밍 한 적이 있다면 구조체를 변경 가능하게 사용하는 것이 좋습니다. 심판과 함께 그들을지나 치면 잘못 될 수있는 것은 없습니다. 내가 찾은 유일한 문제는 C # 컴파일러의 제한 사항이며 경우에 따라 Copy 대신 멍청한 일이 구조체에 대한 참조를 사용하도록 강요 할 수 없습니다 (구조가 C # 클래스의 일부 인 경우와 같습니다) ).

그래서, 가변 구조체 악, C #을하고있다되지 않습니다 만들어 이를 악을. 나는 C ++에서 항상 가변 구조체를 사용하며 매우 편리하고 직관적입니다. 대조적으로, C #에서는 객체를 처리하는 방식 때문에 클래스의 멤버로서 구조체를 완전히 포기했습니다. 그들의 편의는 우리를 희생시켰다.


구조 유형의 클래스 필드를 갖는 것은 종종 매우 유용한 패턴 일 수 있지만 몇 가지 제한 사항이 있습니다. 필드 대신 속성을 사용하거나을 사용하면 성능이 저하 readonly되지만 구조 유형의 클래스 필드는 그럴 수 없습니다. 구조의 유일한 근본적인 한계는 변경 가능한 클래스 유형의 구조체 필드가 int[]ID 또는 변경되지 않는 값 세트를 캡슐화 할 수 있지만 원치 않는 ID를 캡슐화하지 않으면 서 변경 가능한 값을 캡슐화하는 데 사용할 수 없다는 것입니다.
supercat

13

구조체가 의도 한 것 (포인터로 사용되지 않을 때 C #, Visual Basic 6, Pascal / Delphi, C ++ 구조체 유형 (또는 클래스))을 고수하면 구조체가 복합 변수 이상이 아님을 알 수 있습니다 . 즉, 공통 이름 (멤버를 참조하는 레코드 변수)으로 묶은 변수 세트로 취급합니다.

나는 많은 사람들이 OOP에 깊이 익숙해 져 있다는 것을 혼동 할 것임을 알고 있지만, 올바르게 사용한다면 그러한 것들이 본질적으로 악하다고 말할 충분한 이유는 아닙니다. 일부 구조는 의도 한대로 변경할 수 없지만 (Python의 경우 namedtuple) 고려해야 할 또 다른 패러다임입니다.

예 : 구조체에는 많은 메모리가 필요하지만 다음을 수행하여 메모리가 더 많지는 않습니다.

point.x = point.x + 1

다음과 비교 :

point = Point(point.x + 1, point.y)

메모리 소비량은 변하지 않는 경우에 적어도 동일하거나 훨씬 더 클 것입니다 (언어에 따라 현재 스택의 경우 일시적 임에도 불구하고).

그러나 마지막으로 구조는 객체가 아닌 구조 입니다. POO에서 ​​객체의 주요 속성은 그들의 정체성 이며, 대부분의 경우 메모리 주소를 넘지 않습니다. Struct는 데이터 구조를 나타내며 (적절한 개체가 아니므로 ID가 없음) 데이터를 수정할 수 있습니다. 다른 언어에서, 레코드 ( 파스칼의 경우와 마찬가지로 struct 대신 )는 단어이며 동일한 목적을 가지고 있습니다 : 데이터 레코드 변수, 파일에서 읽고 수정하고 파일로 덤프하기위한 것입니다. 많은 언어에서 레코드에서 데이터 정렬을 정의 할 수도 있지만, 반드시 개체라고하는 것은 아닙니다.

좋은 예를 원하십니까? Structs는 파일을 쉽게 읽는 데 사용됩니다. 파이썬은 이 라이브러리를 가지고 있습니다. 왜냐하면 객체 지향적이고 구조체를 지원하지 않기 때문에 다른 방법으로 구현해야했기 때문에 다소 추한 것입니다. 구조체를 구현하는 언어에는 내장 기능이 있습니다. Pascal 또는 C와 같은 언어로 적절한 구조체로 비트 맵 헤더를 읽으십시오. 구조체가 올바르게 작성되고 정렬되면 Pascal에서는 레코드 기반 액세스를 사용하지 않고 임의의 이진 데이터를 읽는 기능을 사용합니다. 따라서 파일 및 직접 (로컬) 메모리 액세스의 경우 구조체가 객체보다 낫습니다. 오늘날 우리는 JSON과 XML에 익숙하기 때문에 이진 파일의 사용 (및 부작용으로 구조체 사용)을 잊어 버렸습니다. 그러나 그렇습니다. 그것들은 존재하며 목적이 있습니다.

그들은 악하지 않습니다. 올바른 목적으로 사용하십시오.

망치로 생각하면 나사를 못으로 취급하고 나사가 벽에 빠지기가 더 어려워 나사의 잘못이되어 악의적 인 사람이 될 것입니다.


12

1,000,000 개의 구조체가 있다고 상상해보십시오. bid_price, offer_price (아마도 십진수) 등과 같은 항목으로 주식을 나타내는 각 구조체는 C # / VB에 의해 생성됩니다.

관리되지 않는 힙에 할당 된 메모리 블록에 배열이 생성되어 다른 원시 코드 스레드가 동시에 배열에 액세스 할 수 있다고 가정합니다 (어쩌면 수학을 수행하는 일부 고성능 코드).

C # / VB 코드가 가격 변동에 대한 시장 피드를 듣고 있다고 상상해보십시오.이 코드는 어레이의 일부 요소 (어떤 보안을 위해)에 액세스 한 다음 일부 가격 필드를 수정해야 할 수 있습니다.

이것이 초당 수십 또는 수십만 번 수행되고 있다고 상상해보십시오.

사실, 사실을 직시하자.이 경우에 우리는이 구조체가 변경 가능하기를 원합니다. 다른 네이티브 코드와 공유되기 때문에 복사본을 만드는 것이 도움이되지 않기 때문입니다. 이 업데이트 속도로 약 120 바이트 구조체의 복사본을 만드는 것은 미치므로, 특히 업데이트가 실제로 1 ~ 2 바이트에만 영향을 미칠 수 있기 때문입니다.

휴고


3
사실이지만이 경우 구조체를 사용하는 이유는 외부 제약 조건 (네이티브 코드 사용으로)에 의해 응용 프로그램 디자인에 적용되기 때문입니다. 이러한 개체에 대해 설명하는 다른 모든 항목은 C # 또는 VB.NET의 클래스 여야합니다.
Jon Hanna

6
왜 어떤 사람들은 물건이 클래스 객체라고 생각하는지 잘 모르겠습니다. 모든 배열 슬롯이 참조 고유 인스턴스로 채워져 있으면 클래스 유형을 사용하면 메모리 요구 사항에 12 또는 24 바이트가 추가되며 클래스 객체 참조 배열의 순차적 액세스는 순차적 액세스보다 훨씬 느리게 진행됩니다. 구조체의 배열.
supercat

9

무언가가 변이 될 수 있다면, 그것은 정체성의 감각을 얻는다.

struct Person {
    public string name; // mutable
    public Point position = new Point(0, 0); // mutable

    public Person(string name, Point position) { ... }
}

Person eric = new Person("Eric Lippert", new Point(4, 2));

Person변경 가능 하기 때문에 Eric 복제, 복제 이동 및 원본 파괴 보다 Eric의 위치 변경 에 대해 생각 하는 것이 더 자연 스럽습니다 . 두 작업 모두의 내용을 변경하는 데 성공 하지만 하나는 다른 것보다 직관적입니다. 마찬가지로 Eric을 수정하는 방법에 대해 Eric을 참조로 사용하는 것이 더 직관적입니다. 에릭의 복제품을 제공하는 것은 거의 항상 놀라운 일이 될 것입니다. 돌연변이 를 원하는 사람 은 참조를 요청해야합니다. 그렇지 않으면 잘못된 일을합니다.eric.positionPersonPerson

유형을 불변으로 만들면 문제가 사라집니다. 내가 수정할 수 없다면 eric, eric또는의 복제본을 받든 나에게 아무런 영향을 미치지 않습니다 eric. 보다 일반적으로, 관찰 가능한 모든 상태가 다음 중 하나 인 멤버에 유지되는 경우 유형에 따라 값을 전달하는 것이 안전합니다.

  • 불변의
  • 참조 유형
  • 가치를 지킬 안전

이러한 조건이 충족되면 변경 가능한 값 형식은 얕은 복사본으로 인해 수신자가 원본 데이터를 수정할 수 있기 때문에 참조 형식처럼 동작합니다.

불변의 직관성은 Person당신이하려는 일에 달려 있습니다. 사람에 대한 일련의 데이터Person 만 나타내는 경우 에는 직관적이지 않습니다. 변수는 객체가 아닌 추상적 인 값을 나타냅니다 . (이 경우 이름을으로 바꾸는 것이 더 적절할 것입니다 .) 실제로 사람 자체를 모델링하는 경우 수정하려는 생각의 함정을 피한 경우에도 복제본을 지속적으로 생성 및 이동한다는 아이디어는 어리 석습니다. 원래. 이 경우 단순히 참조 유형 (즉, 클래스)을 만드는 것이 더 자연 스럽습니다 .PersonPersonDataPersonPerson

물론, 함수형 프로그래밍이 우리에게 모든 것을 불변 으로 만드는 데 이점이 있다는 것을eric 알았지 만 (누구도 그를 참조 하고 돌연변이 시킬 수는 없습니다 ), OOP에서는 관용적이지 않기 때문에 다른 사람과 일하는 사람에게는 여전히 직관적이지 않습니다. 암호.


3
정체성 에 대한 당신의 요점은 좋은 것입니다. ID가 무언가에 대한 여러 참조가 존재하는 경우에만 관련성이 있음을 주목할 가치가 있습니다. 경우 foo우주에서의 목표 어디서나에 대한 유일한 참조를 보유하고 있으며 아무것도 객체의 아이덴티티 해시 값은 다음 돌연변이 필드가 있음을 포착하지 않았다 foo.X만들기 위해 의미 적으로 동일하다 foo그냥 이전에 언급 한 것처럼하는 새로운 객체에 지점을하지만,과 X원하는 값을 유지합니다. 클래스 유형을 사용하면 일반적으로 무언가에 대한 여러 참조가 있는지 알기가 어렵지만 구조체를 사용하면 쉽지 않습니다.
supercat 2009 년

1
경우 Thing변경 가능한 클래스 유형이, a는 Thing[] 것이다 오브젝트 ID를 캡슐화 - 하나는가 여부를 원하는지 - 하나 더 보장 할 수 없습니다하지 않는 한 Thing어떠한 외부 참조가되는 배열의 적 변이 될 것이다. 배열 요소가 아이덴티티를 캡슐화하는 것을 원하지 않는 경우 일반적으로 참조를 보유하는 항목이 변경되지 않도록하거나 외부 참조가 보유하고있는 항목에 대해 어떠한 참조도 존재하지 않도록해야합니다. ]. 두 가지 접근 방식 모두별로 편리하지 않습니다. 경우 Thing구조가하는 Thing[]캡슐화 값만.
supercat

물체의 경우, 정체성은 위치에서 비롯됩니다. 참조 유형의 인스턴스는 메모리에서의 위치 덕분에 ID를 가지며 데이터 유형이 아닌 ID 만 참조를 전달하지만 값 유형은 저장된 외부 위치에 ID가 있습니다. Eric 값 유형의 ID는 그가 저장된 변수에서만 나옵니다. 당신이 그를 통과하면, 그는 그의 정체성을 잃게됩니다.
IllidanS4가 Monica를

6

구조체와 관련이 없으며 C # 과도 관련이 없지만 Java에서는 해시 맵의 키와 같은 가변 객체에 문제가 발생할 수 있습니다. 맵에 추가 한 후 변경하고 해시 코드를 변경 하면 악의적 인 일이 발생할 수 있습니다.


3
지도에서 클래스를 키로 사용하는 경우에도 마찬가지입니다.
Marc Gravell

6

개인적으로 코드를 볼 때 다음은 꽤 어색해 보입니다.

data.value.set (data.value.get () + 1);

단순히보다는

data.value ++; 또는 data.value = data.value + 1;

데이터 캡슐화는 클래스를 전달할 때 유용하며 값이 제어 된 방식으로 수정되도록합니다. 그러나 공개 세트를 가지고 있고 전달 된 값으로 값을 설정하는 것 이상의 기능을 수행 할 때 단순히 공개 데이터 구조를 단순히 전달하는 것보다 어떻게 개선됩니까?

클래스 내에서 개인 구조를 만들 때 변수 구조를 하나의 그룹으로 구성하기 위해 해당 구조를 만들었습니다. 클래스 범위 내에서 해당 구조를 수정하고 해당 구조의 사본을 얻지 않고 새 인스턴스를 만들 수 있기를 원합니다.

나에게 이것은 공용 변수를 구성하는 데 사용되는 구조의 유효한 사용을 막습니다. 액세스 제어를 원한다면 클래스를 사용합니다.


1
바로 포인트! 구조는 액세스 제어 제한이없는 조직 단위입니다! 불행히도 C #은 이러한 목적으로 쓸모가 없었습니다!
ThunderGr

이것은 완전히 당신의 예는 변경할 수 구조체를 보여 모두로 포인트를 벗어났습니다.
vidstige

C #은 구조의 목적이 아니기 때문에 이러한 목적으로 쓸모가 없었습니다.
Luiz Felipe

6

Eric Lippert 씨의 사례에는 몇 가지 문제가 있습니다. 구조체가 복사되는 시점과 조심하지 않으면 어떻게 문제가 될 수 있는지 설명하기 위해 고안되었습니다. 예제를 보면 프로그래밍 습관이 나쁜 결과 struct 나 클래스에 문제가 아니라는 것을 알 수 있습니다.

  1. 구조체에는 공개 멤버 만 있어야하며 캡슐화가 필요하지 않습니다. 그렇다면 실제로 유형 / 클래스 여야합니다. 같은 것을 말하기 위해 두 개의 구성이 실제로 필요하지 않습니다.

  2. 구조체를 묶는 클래스가 있으면 클래스의 메서드를 호출하여 멤버 구조체를 변경합니다. 이것이 좋은 프로그래밍 습관으로 할 것입니다.

적절한 구현은 다음과 같습니다.

struct Mutable {
public int x;
}

class Test {
    private Mutable m = new Mutable();
    public int mutate()
    { 
        m.x = m.x + 1;
        return m.x;
    }
  }
  static void Main(string[] args) {
        Test t = new Test();
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
    }

구조체 자체의 문제가 아닌 프로그래밍 습관의 문제 인 것 같습니다. Structs는 변경 가능해야합니다. 즉 아이디어와 의도입니다.

변경 결과는 예상대로 동작합니다.

1 아무 키나 눌러 계속하십시오. . .


4
불변 클래스 개체처럼 동작하도록 작은 불투명 구조를 설계하는 데 아무런 문제가 없습니다. MSDN 지침은 개체처럼 동작하는 것을 만들려고 할 때 합리적 입니다. 물체는 물체처럼 행동하는 가벼운 물건이 필요한 경우와 덕트 테이프와 함께 붙어있는 많은 변수가 필요한 경우에 구조가 적합합니다. 그러나 어떤 이유로 든 많은 사람들은 구조가 두 가지 용도로 사용되며 하나에 적합한 지침이 다른 하나에는 적합하지 않다는 것을 인식하지 못합니다.
supercat

5

변경 가능한 데이터에는 많은 장점과 단점이 있습니다. 백만 달러의 단점은 앨리어싱입니다. 여러 장소에서 동일한 값을 사용하고 있고 그 중 하나가 값을 변경하면 해당 값을 사용하는 다른 장소로 마술처럼 변경된 것으로 보입니다. 이것은 경쟁 조건과 관련이 있지만 동일하지는 않습니다.

백만 달러의 이점은 때로는 모듈성입니다. 가변 상태를 사용하면 알 필요가없는 코드에서 변경 정보를 숨길 수 있습니다.

통역사 기술 은 이러한 트레이드 오프에 대해 자세히 설명하고 몇 가지 예를 제공합니다.


구조체는 C #에서 별칭이 지정되지 않습니다. 모든 구조체 할당은 사본입니다.
재귀

@ 재귀 : 어떤 경우에는 변경 가능한 구조체의 주요 이점이며, 구조체를 변경할 수 없다는 개념에 의문을 제기하는 경우가 있습니다. 컴파일러가 때때로 암시 적으로 구조체를 복사한다는 사실은 가변 구조체의 유용성을 감소시키지 않습니다.
supercat

1

올바르게 사용하면 악하다고 생각하지 않습니다. 나는 그것을 프로덕션 코드에 넣지 않을 것이지만 구조체의 수명이 비교적 작은 구조적 단위 테스트 모의와 같은 것을 원할 것입니다.

Eric 예제를 사용하면 해당 Eric의 두 번째 인스턴스를 작성하지만 테스트의 특성 (예 : 복제, 수정)으로 조정합니다. 테스트 스크립트의 나머지 부분에 Eric2를 사용하려는 경우가 아니라면 테스트 스크립트의 나머지 부분에 Eric2 만 사용하는 경우 Eric의 첫 번째 인스턴스에서 어떤 일이 발생하는지는 중요하지 않습니다.

이것은 주로 특정 객체 (구조 점)를 정의하는 레거시 코드를 테스트하거나 수정하는 데 유용하지만 변경 불가능한 구조를 사용하면 성가신 사용을 막을 수 있습니다.


내가 알다시피, 구조체는 핵심으로 덕트 테이프와 함께 붙어있는 많은 변수입니다. .NET에서 구조체가 덕트 테이프와 함께 붙어있는 많은 변수 이외의 것으로 가장 할 수도 있습니다. 실제로 많은 변수가 아닌 다른 것으로 가장하는 유형이 함께 붙어 있다고 제안합니다. 덕트 테이프를 사용하면 구조체의 경우 불변성을 암시하는 단일 객체로 작동해야하지만 덕트 테이프와 함께 여러 변수를 묶는 것이 유용합니다. 프로덕션 코드에서도 유형을 갖는 것이 더 낫다고 생각합니다.
supercat

... "각 필드에는 마지막으로 쓰여진 내용이 들어 있습니다"라는 의미 이상의 의미가 없으며, 모든 의미를 구조 를 사용 하는 코드로 밀어 넣는 것보다 더 많은 의미를 가지려고합니다. 예를 들어, Range<T>멤버 MinimumMaximum필드 유형이 있는 유형 T과 코드가 주어진 경우, 포함 및 포함 여부를 Range<double> myRange = foo.getRange();보증 합니다 . 데이 노출 된 필드 구조체는 자신의 동작을 추가 않을거야 명확한 것을 만들 것이다합니다. MinimumMaximumfoo.GetRange();Range
supercat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.