가변 대 불변 개체


173

변경 가능한 객체와 불변의 객체 주위에 머리를 갖으려고합니다. 변경 가능한 객체를 사용하면 프레스에서 많은 나쁜 프레스 (예 : 메소드에서 문자열 배열 반환)가 발생하지만 부정적인 영향이 무엇인지 이해하는 데 어려움을 겪고 있습니다. 가변 객체 사용에 대한 모범 사례는 무엇입니까? 가능할 때마다 피해야합니까?


string적어도 .NET에서는 불변이며 다른 많은 현대 언어로도 생각합니다.
Domenic

4
erlang 'strings'는 int의 배열 일 뿐이며 haskell "strings"는 문자의 배열입니다.
Chii

4
루비 문자열은 변경 가능한 바이트 배열입니다. 당신이 그 자리에서 바꿀 수있는 견과류를 절대적으로 운전합니다.
Daniel Spiewak

6
다니엘-왜? 우연히 그렇게하고 있습니까?

5
@DanielSpiewak 줄을 바꿀 수있는 너트를 구동하는 솔루션은 간단합니다.하지 마십시오. 현 위치를 바꿀 없기 때문에 견과를 견뎌내는 솔루션 은 그렇게 간단하지 않습니다.
Kaz

답변:


162

글쎄, 이것에는 몇 가지 측면이 있습니다.

  1. 참조 ID가없는 가변 객체는 이상한 시간에 버그를 일으킬 수 있습니다. 예를 들어, Person값 기반 equals메소드가 있는 Bean을 고려하십시오 .

    Map<Person, String> map = ...
    Person p = new Person();
    map.put(p, "Hey, there!");
    
    p.setName("Daniel");
    map.get(p);       // => null
    

    Person그 때문에 키로서 사용하는 경우 인스턴스는 맵에 "손실"됩니다 hashCode와 평등이 변경 가능한 값에 근거했다. 이러한 값은 맵 외부에서 변경되었으며 모든 해싱은 더 이상 사용되지 않습니다. 이론가들은이 시점에서 하프를 좋아하지만 실제로는 그다지 큰 문제가 아니라고 생각했습니다.

  2. 또 다른 측면은 코드의 논리적 "합리성"입니다. 가독성부터 흐름까지 모든 것을 포괄하는 정의하기 어려운 용어입니다. 일반적으로 코드 조각을보고 코드의 기능을 쉽게 이해할 수 있어야합니다. 그러나 그보다 더 중요한 것은 자신이 올바르게 작동한다는 것을 스스로에게 확신시킬 수 있어야 합니다 . 서로 다른 코드 "도메인"에서 개체가 독립적으로 변경 될 수있는 경우, 위치와 이유 ( " 원거리에서의 무시 무시한 동작 ")를 추적하기가 어려운 경우가 있습니다. 이것은 예시하기가 더 어려운 개념이지만, 더 크고 복잡한 아키텍처에서 종종 직면하는 것입니다.

  3. 마지막으로, 변경 가능한 객체는 동시 상황에서 킬러 입니다. 별도의 스레드에서 변경 가능한 객체에 액세스 할 때마다 잠금을 처리해야합니다. 이 처리량 감소하고 코드를 만드는 극적으로 더 어려워 유지 할 수 있습니다. 충분히 복잡한 시스템은이 문제를 일정 비율 이상으로 날려 유지하기가 거의 불가능 해집니다 (동시성 전문가 인 경우에도).

불변 개체 (특히 불변 컬렉션)는 이러한 모든 문제를 피합니다. 코드의 작동 방식에 대해 염두에두면 코드는 읽기 쉽고 유지 관리하기 쉽고 이상한 방식으로 예측할 수없는 방식으로 개발됩니다. 불변의 객체는 쉽게 조롱 할 수있을뿐만 아니라 실행하려는 코드 패턴으로 인해 테스트하기도 더 쉽습니다. 요컨대, 그들은 좋은 연습입니다!

그 말로, 나는이 문제에 열심이 아닙니다. 모든 것이 불변 인 경우 일부 문제는 잘 모델링되지 않습니다. 그러나 나는 당신이 이것을 가능한 견해로 만드는 언어를 사용한다고 가정 할 때 가능한 한 많은 방향으로 코드를 푸시하려고 노력해야한다고 생각합니다 (C / C ++은 Java와 마찬가지로 이것을 어렵게 만듭니다) . 요컨대, 장점은 문제에 다소 달려 있지만 불변성을 선호하는 경향이 있습니다.


11
훌륭한 반응. 그러나 하나의 작은 질문 : C ++은 불변성을 잘 지원하지 않습니까? 하지 않 CONST-정확성의 기능은 충분?
Dimitri C.

1
@DimitriC .: C ++에는 실제로 몇 가지 더 기본적인 기능이 있으며, 특히 객체 ID를 캡슐화하는 것과 공유되지 않은 상태를 캡슐화해야하는 저장소 위치를 더 잘 구분합니다.
supercat

2
Java 프로그래밍의 경우 Joshua Bloch는 그의 유명한 저서 Effective Java (Item 15)
dMathieuD에서

27

불변 객체와 불변 콜렉션

불변성 객체와 불변성 객체에 대한 토론에서 더 좋은 점 중 하나는 불변성의 개념을 컬렉션으로 확장 할 수 있다는 것입니다. 불변 객체는 종종 하나의 논리적 데이터 구조 (예 : 불변 문자열)를 나타내는 객체입니다. 불변 객체에 대한 참조가 있으면 객체의 내용이 변경되지 않습니다.

불변 컬렉션은 절대 변경되지 않는 컬렉션입니다.

변경 가능한 컬렉션에서 작업을 수행하면 컬렉션을 적절히 변경하고 컬렉션을 참조하는 모든 엔터티가 변경 사항을 볼 수 있습니다.

변경 불가능한 콜렉션에서 조작을 수행하면 변경 사항을 반영하는 참조가 새 콜렉션으로 리턴됩니다. 이전 버전의 컬렉션에 대한 참조가있는 모든 엔터티에는 변경 내용이 표시되지 않습니다.

영리한 구현은 불변성을 제공하기 위해 전체 컬렉션을 복사 (복제) 할 필요는 없습니다. 가장 간단한 예는 단일 연결 목록 및 푸시 / 팝 작업으로 구현 된 스택입니다. 새 컬렉션의 이전 컬렉션에서 모든 노드를 재사용하여 푸시에 단일 노드 만 추가하고 팝에 대한 노드는 복제하지 않을 수 있습니다. 반면에 단독으로 연결된 목록의 push_tail 작업은 그렇게 간단하지 않거나 효율적이지 않습니다.

불변 변수와 가변 변수 / 참조

일부 기능적 언어는 객체 참조 자체에 대한 불변성의 개념을 취하여 단일 참조 할당 만 허용합니다.

  • Erlang에서는 모든 "변수"에 해당됩니다. 객체를 참조에 한 번만 할당 할 수 있습니다. 컬렉션에서 작업 할 경우 새 컬렉션을 이전 참조 (변수 이름)에 다시 할당 할 수 없습니다.
  • 스칼라는 또한 모든 참조가 var 또는 val 으로 선언되고 , val 은 단일 지정이고 기능적 스타일을 홍보하지만보다 C 형 또는 Java 형 프로그램 구조를 허용하는 vars 로 언어에이를 빌드합니다 .
  • var / val 선언이 필요하지만 많은 전통적인 언어는 final in java 및 const const 와 같은 선택적 수정자를 사용합니다 .

손쉬운 개발 및 성능

거의 항상 불변의 객체를 사용하는 이유는 부작용이없는 프로그래밍과 코드에 대한 간단한 추론을 촉진하기위한 것입니다 (특히 동시 / 병렬 환경에서). 객체가 불변 인 경우 다른 엔터티가 기본 데이터를 변경하는 것에 대해 걱정할 필요가 없습니다.

주요 단점은 성능입니다. 다음은 장난감 문제에서 변경 불가능한 객체와 변경 불가능한 객체를 비교하여 Java 에서 수행 한 간단한 테스트 에 대한 글입니다.

성능 문제는 많은 응용 프로그램에서 문제가 될 수 있지만 전부는 아닙니다. 따라서 Python의 Numpy Array 클래스와 같은 많은 큰 숫자 패키지가 큰 배열의 내부 업데이트를 허용합니다. 이는 대규모 행렬 및 벡터 연산을 사용하는 응용 분야에 중요합니다. 이처럼 큰 데이터 병렬 및 계산 집약적 인 문제는 제자리에서 작동하여 속도를 크게 향상시킵니다.


12

이 블로그 게시물을 확인하십시오 : http://www.yegor256.com/2014/06/09/objects-should-be-immutable.html . 불변의 객체가 변경 가능한 것보다 나은 이유를 설명합니다. 한마디로 :

  • 불변 개체는 구성, 테스트 및 사용이 더 간단합니다.
  • 진정한 불변의 객체는 항상 스레드 안전
  • 그들은 시간적 결합을 피하는 데 도움이됩니다
  • 그들의 사용법은 부작용이 없습니다 (방어 사본 없음)
  • 신원 변경 가능성 문제를 피할 수 있습니다
  • 그들은 항상 실패 원 자성을 가지고
  • 그들은 캐시하기가 훨씬 쉽다

10

불변 개체는 매우 강력한 개념입니다. 모든 클라이언트에 대해 객체 / 변수를 일관성있게 유지해야하는 많은 부담을 제거합니다.

값 의미와 함께 주로 사용되는 CPoint 클래스와 같은 저수준의 다형성이 아닌 객체에 사용할 수 있습니다.

또는 객체 시맨틱과 함께 독점적으로 사용되는 수학 함수를 나타내는 IFunction과 같은 높은 수준의 다형성 인터페이스에 사용할 수 있습니다.

가장 큰 장점 : 불변성 + 객체 의미론 + 스마트 포인터는 객체 소유권을 문제가 아닌 것으로 만들며, 객체의 모든 클라이언트는 기본적으로 자체 사본을 가지고 있습니다. 암시 적으로 이것은 동시성이있을 때 결정적 동작을 의미합니다.

단점 : 많은 양의 데이터가 포함 된 객체와 함께 사용하면 메모리 소비가 문제가 될 수 있습니다. 이것에 대한 해결책은 객체에 대한 작업을 상징적으로 유지하고 게으른 평가를 수행하는 것입니다. 그러나 이는 인터페이스가 심볼 연산을 수용하도록 설계되지 않은 경우 성능 계산에 부정적인 영향을 줄 수있는 일련의 심볼 계산으로 이어질 수 있습니다. 이 경우 확실히 피해야 할 것은 메소드에서 엄청난 양의 메모리를 반환하는 것입니다. 체인화 된 심볼 작업과 함께 이로 인해 엄청난 메모리 소비와 성능 저하가 발생할 수 있습니다.

따라서 불변의 객체는 객체 지향 디자인에 대한 나의 주요 사고 방식이지만 분명히 교리가 아닙니다. 객체의 클라이언트에게는 많은 문제를 해결하지만 특히 구현 자에게는 많은 문제가 발생합니다.


나는 불변성 + 객체 의미론 + 스마트 포인터가 객체 소유권을 "매끄러운"점으로 만든다는 가장 큰 이점으로 섹션 4를 잘못 이해했다고 생각합니다. 논쟁의 여지가 있습니까? "moot"을 잘못 사용하고 있다고 생각합니다 ... 다음 문장을 보면 개체가 "moot"(설명 가능한) 동작에서 "결정적 동작"을 의미합니다.
Benjamin

당신 말이 맞아요, 'moot'을 잘못 사용했습니다. 변경 고려 :)
QBziZ

6

당신이 말하는 언어를 지정해야합니다. C 또는 C ++와 같은 저수준 언어의 경우 공간을 절약하고 메모리 변동을 줄이기 위해 가변 객체를 사용하는 것을 선호합니다. 더 높은 수준의 언어에서, 불변의 객체는 "먼 거리에서 멍청한 행동"이 없기 때문에 코드의 동작 (특히 멀티 스레드 코드)에 대해 추론하기가 더 쉽습니다.


스레드가 양자 얽혀 있다고 제안하고 있습니까? 그것은 꽤 신축입니다 :) 스레드는 실제로 생각하면 얽히는 것에 가깝습니다. 한 스레드가 변경하면 다른 스레드에 영향을줍니다. +1
ndrewxie

4

변경 가능한 객체는 단순히 만들거나 인스턴스화 한 후에 수정할 수있는 수정 가능한 객체와 수정할 수없는 변경 불가능한 객체입니다 ( 대상 의 Wikipedia 페이지 참조 ). 프로그래밍 언어에서 이에 대한 예는 Pythons 목록과 튜플입니다. 튜플은 할 수 없지만 목록을 수정할 수 있습니다 (예 : 새 항목을 만든 후 추가 할 수 있음).

나는 모든 상황에서 어느 것이 더 나은지에 대한 명확한 대답이 있다고 생각하지 않습니다. 둘 다 자리가 있습니다.


1

클래스 유형이 변경 가능한 경우 해당 클래스 유형의 변수는 여러 가지 다른 의미를 가질 수 있습니다. 예를 들어, 오브젝트 foo에 field int[] arr가 있고 int[3]숫자 {5, 7, 9} 를 보유한 것에 대한 참조를 보유 한다고 가정하십시오 . 필드 유형이 알려져 있지만 나타낼 수있는 최소한 네 가지가 있습니다.

  • 잠재적으로 공유 참조하는 경우, 그 소유자는 값을 캡슐화 것만 신경 모두 5, 7, 9 foo욕구가 arr다른 값을 캡슐화하기 위해서는, 원하는 값을 포함하는 다른 배열을 교체한다. 의 사본을 만들려면 {1,2,3} 값을 보유한 foo참조 arr또는 새로운 배열 중 더 편리한 사본을 사본에 제공 할 수 있습니다 .

  • 5, 7 및 9 값을 캡슐화하는 배열에 대한 유니버스의 유일한 참조는 현재 값 5, 7, 9를 보유하는 세 개의 저장 위치 세트입니다. 만약 foo이 숫자 5, 8, 9를 캡슐화하고자, 그것도 그 배열의 두 번째 항목을 변경하거나 숫자 5, 8 채 새로운 배열을 생성하고, (9) 및 이전을 포기할 수있다. 의 복사본을 만들려는 경우 유니버스의 모든 위치에서 해당 배열에 대한 유일한 참조로 남아 foo있으려면 복사본 arr에서 새 배열에 대한 참조로 바꿔야 foo.arr합니다.

  • 어떤 이유로 인해 노출 된 다른 객체 가 소유 한 배열에 대한 참조 foo(예 : foo데이터를 저장 하려는 경우) 이 시나리오에서는 arr배열의 내용을 캡슐화하지 않고 ID를 캡슐화합니다 . 교체 때문에 arr완전히 그 의미를 변경하는 새로운 배열에 참조하여이 복사의 foo동일한 어레이에 대한 참조를 보유한다.

  • 배열이 foo단독 소유자이지만 어떤 이유로 든 다른 객체에 의해 참조가 유지되는 배열에 대한 참조 (예 : 다른 개체가 데이터를 저장하기를 원함) (이전 사례의 뒤집 음). 이 시나리오에서는 arr배열의 ID와 해당 내용을 모두 캡슐화합니다. 교체 arr완전히 그 의미를 변경하는 새로운 배열에 대한 참조 만의 클론을 갖는 arr참조가 foo.arr가정 위반 foo단독 소유자입니다. 따라서 복사 할 방법이 없습니다 foo.

이론 상으로는 int[]잘 정의 된 단순하고 잘 정의 된 유형이어야하지만 네 가지 의미가 매우 다릅니다. 대조적으로, 불변 개체 (예를 들어 String)에 대한 언급 은 일반적으로 하나의 의미만을 갖는다. 불변 개체의 "파워"의 대부분은 그 사실에서 비롯됩니다.


1

가변 인스턴스는 참조로 전달됩니다.

불변 인스턴스는 값으로 전달됩니다.

추상적 인 예. 내 HDD에 txtfile 이라는 파일이 있다고 가정합니다 . 이제 txtfile 을 요청할 때 두 가지 모드로 반환 할 수 있습니다.

  1. txtfile에 대한 바로 가기 와 나에게 바로 가기 바로 가기를 만들거나
  2. txtfile 사본 과 pas 사본을 가져 오십시오 .

첫 번째 모드에서 리턴 된 txtfile 은 변경 가능한 파일입니다. 바로 가기 파일을 변경하면 원본 파일도 변경되기 때문입니다. 이 모드의 장점은 각각의 반환 된 바로 가기가 메모리 (RAM 또는 HDD)에 적은 메모리를 필요로하며 모든 사람 (나뿐만 아니라 소유자)도 파일 내용을 수정할 권한이 있다는 것입니다.

두 번째 모드에서, 수신 된 파일의 모든 변경 사항이 원본 파일을 참조하지 않기 때문에 리턴 된 txtfile 은 변경할 수없는 파일입니다. 이 모드의 장점은 나 (소유자) 만 원본 파일을 수정할 수 있다는 것입니다. 각 반환 사본은 필요한 메모리 (RAM 또는 HDD)가 필요합니다.


이것은 사실이 아닙니다. 불변의 인스턴스는 실제로 참조로 전달 될 수 있으며 종종 있습니다. 복사는 주로 객체 또는 데이터 구조를 업데이트해야 할 때 수행됩니다.
Jamon Holmgren

0

배열 또는 문자열의 참조를 반환하면 외부 세계가 해당 객체의 내용을 수정할 수 있으므로 변경 가능 (수정 가능) 객체로 만들 수 있습니다.


0

불변의 의미는 변경할 수 없으며, 변의의 의미는 변경할 수 있음을 의미합니다.

Java의 오브젝트는 기본 요소와 다릅니다. 프리미티브는 유형 (부울, int 등)으로 작성되며 객체 (클래스)는 사용자 작성 유형입니다.

프리미티브와 객체는 클래스 구현 내에서 멤버 변수로 정의 될 때 변경 가능하거나 변경 불가능할 수 있습니다.

많은 사람들이 최종 수정자를 갖는 프리미티브와 객체 변수가 불변이라고 생각하지만 이것은 사실이 아닙니다. 따라서 final은 변수에 대해 불변성을 의미하지는 않습니다.
http://www.siteconsortium.com/h/D0000F.php의 예제를 참조하십시오 .


0

Immutable object-작성 후 오브젝트 상태를 변경할 수 없습니다. 모든 필드가 불변 인 경우, 객체는 불변입니다

스레드 안전

불변 개체의 주요 장점은 동시 환경에 적합하다는 것입니다. 동시성에서 가장 큰 문제 shared resource는 스레드를 변경할 수 있다는 것입니다. 그러나 객체가 불변 인 경우 read-only스레드 안전 작업입니다. 원래의 불변 개체를 수정하면 복사본이 반환됩니다

부작용 무료

개발자는 불변 객체의 상태를 (의도적이든 아니든) 어느 곳에서나 변경할 수 없다는 것을 완전히 확신합니다.

컴파일 최적화

성능 향상

불리:

변경 가능한 객체를 변경하는 것보다 객체를 복사하는 것이 더 많은 작업이므로 성능에 영향을주는 이유

immutable객체 를 만들려면 다음을 사용해야합니다.

  1. 언어 수준. 각 언어에는 도움이되는 도구가 있습니다. 예를 들어 Java는 finaland primitives, Swift has letstruct[About] 입니다. 언어는 변수 유형을 정의합니다. 예를 들어 Java has primitivereferencetype, Swift has valuereferencetype [About] 입니다. 불변의 객체의 경우 기본적으로 복사하는 것이 더 편리 primitives하고 value유형이 좋습니다. 에 관해서 reference만 가능 (당신이 그것을 객체의 상태를 변경할 수 있기 때문에) 형태가 더 어렵습니다. 예를 들어 clone개발자 수준에서 패턴을 사용할 수 있습니다

  2. 개발자 수준. 개발자는 상태 변경을위한 인터페이스를 제공하지 않아야합니다

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.