변경 가능한 객체와 불변의 객체 주위에 머리를 갖으려고합니다. 변경 가능한 객체를 사용하면 프레스에서 많은 나쁜 프레스 (예 : 메소드에서 문자열 배열 반환)가 발생하지만 부정적인 영향이 무엇인지 이해하는 데 어려움을 겪고 있습니다. 가변 객체 사용에 대한 모범 사례는 무엇입니까? 가능할 때마다 피해야합니까?
변경 가능한 객체와 불변의 객체 주위에 머리를 갖으려고합니다. 변경 가능한 객체를 사용하면 프레스에서 많은 나쁜 프레스 (예 : 메소드에서 문자열 배열 반환)가 발생하지만 부정적인 영향이 무엇인지 이해하는 데 어려움을 겪고 있습니다. 가변 객체 사용에 대한 모범 사례는 무엇입니까? 가능할 때마다 피해야합니까?
답변:
글쎄, 이것에는 몇 가지 측면이 있습니다.
참조 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
와 평등이 변경 가능한 값에 근거했다. 이러한 값은 맵 외부에서 변경되었으며 모든 해싱은 더 이상 사용되지 않습니다. 이론가들은이 시점에서 하프를 좋아하지만 실제로는 그다지 큰 문제가 아니라고 생각했습니다.
또 다른 측면은 코드의 논리적 "합리성"입니다. 가독성부터 흐름까지 모든 것을 포괄하는 정의하기 어려운 용어입니다. 일반적으로 코드 조각을보고 코드의 기능을 쉽게 이해할 수 있어야합니다. 그러나 그보다 더 중요한 것은 자신이 올바르게 작동한다는 것을 스스로에게 확신시킬 수 있어야 합니다 . 서로 다른 코드 "도메인"에서 개체가 독립적으로 변경 될 수있는 경우, 위치와 이유 ( " 원거리에서의 무시 무시한 동작 ")를 추적하기가 어려운 경우가 있습니다. 이것은 예시하기가 더 어려운 개념이지만, 더 크고 복잡한 아키텍처에서 종종 직면하는 것입니다.
마지막으로, 변경 가능한 객체는 동시 상황에서 킬러 입니다. 별도의 스레드에서 변경 가능한 객체에 액세스 할 때마다 잠금을 처리해야합니다. 이 처리량 감소하고 코드를 만드는 극적으로 더 어려워 유지 할 수 있습니다. 충분히 복잡한 시스템은이 문제를 일정 비율 이상으로 날려 유지하기가 거의 불가능 해집니다 (동시성 전문가 인 경우에도).
불변 개체 (특히 불변 컬렉션)는 이러한 모든 문제를 피합니다. 코드의 작동 방식에 대해 염두에두면 코드는 읽기 쉽고 유지 관리하기 쉽고 이상한 방식으로 예측할 수없는 방식으로 개발됩니다. 불변의 객체는 쉽게 조롱 할 수있을뿐만 아니라 실행하려는 코드 패턴으로 인해 테스트하기도 더 쉽습니다. 요컨대, 그들은 좋은 연습입니다!
그 말로, 나는이 문제에 열심이 아닙니다. 모든 것이 불변 인 경우 일부 문제는 잘 모델링되지 않습니다. 그러나 나는 당신이 이것을 가능한 견해로 만드는 언어를 사용한다고 가정 할 때 가능한 한 많은 방향으로 코드를 푸시하려고 노력해야한다고 생각합니다 (C / C ++은 Java와 마찬가지로 이것을 어렵게 만듭니다) . 요컨대, 장점은 문제에 다소 달려 있지만 불변성을 선호하는 경향이 있습니다.
불변성 객체와 불변성 객체에 대한 토론에서 더 좋은 점 중 하나는 불변성의 개념을 컬렉션으로 확장 할 수 있다는 것입니다. 불변 객체는 종종 하나의 논리적 데이터 구조 (예 : 불변 문자열)를 나타내는 객체입니다. 불변 객체에 대한 참조가 있으면 객체의 내용이 변경되지 않습니다.
불변 컬렉션은 절대 변경되지 않는 컬렉션입니다.
변경 가능한 컬렉션에서 작업을 수행하면 컬렉션을 적절히 변경하고 컬렉션을 참조하는 모든 엔터티가 변경 사항을 볼 수 있습니다.
변경 불가능한 콜렉션에서 조작을 수행하면 변경 사항을 반영하는 참조가 새 콜렉션으로 리턴됩니다. 이전 버전의 컬렉션에 대한 참조가있는 모든 엔터티에는 변경 내용이 표시되지 않습니다.
영리한 구현은 불변성을 제공하기 위해 전체 컬렉션을 복사 (복제) 할 필요는 없습니다. 가장 간단한 예는 단일 연결 목록 및 푸시 / 팝 작업으로 구현 된 스택입니다. 새 컬렉션의 이전 컬렉션에서 모든 노드를 재사용하여 푸시에 단일 노드 만 추가하고 팝에 대한 노드는 복제하지 않을 수 있습니다. 반면에 단독으로 연결된 목록의 push_tail 작업은 그렇게 간단하지 않거나 효율적이지 않습니다.
일부 기능적 언어는 객체 참조 자체에 대한 불변성의 개념을 취하여 단일 참조 할당 만 허용합니다.
거의 항상 불변의 객체를 사용하는 이유는 부작용이없는 프로그래밍과 코드에 대한 간단한 추론을 촉진하기위한 것입니다 (특히 동시 / 병렬 환경에서). 객체가 불변 인 경우 다른 엔터티가 기본 데이터를 변경하는 것에 대해 걱정할 필요가 없습니다.
주요 단점은 성능입니다. 다음은 장난감 문제에서 변경 불가능한 객체와 변경 불가능한 객체를 비교하여 Java 에서 수행 한 간단한 테스트 에 대한 글입니다.
성능 문제는 많은 응용 프로그램에서 문제가 될 수 있지만 전부는 아닙니다. 따라서 Python의 Numpy Array 클래스와 같은 많은 큰 숫자 패키지가 큰 배열의 내부 업데이트를 허용합니다. 이는 대규모 행렬 및 벡터 연산을 사용하는 응용 분야에 중요합니다. 이처럼 큰 데이터 병렬 및 계산 집약적 인 문제는 제자리에서 작동하여 속도를 크게 향상시킵니다.
이 블로그 게시물을 확인하십시오 : http://www.yegor256.com/2014/06/09/objects-should-be-immutable.html . 불변의 객체가 변경 가능한 것보다 나은 이유를 설명합니다. 한마디로 :
불변 개체는 매우 강력한 개념입니다. 모든 클라이언트에 대해 객체 / 변수를 일관성있게 유지해야하는 많은 부담을 제거합니다.
값 의미와 함께 주로 사용되는 CPoint 클래스와 같은 저수준의 다형성이 아닌 객체에 사용할 수 있습니다.
또는 객체 시맨틱과 함께 독점적으로 사용되는 수학 함수를 나타내는 IFunction과 같은 높은 수준의 다형성 인터페이스에 사용할 수 있습니다.
가장 큰 장점 : 불변성 + 객체 의미론 + 스마트 포인터는 객체 소유권을 문제가 아닌 것으로 만들며, 객체의 모든 클라이언트는 기본적으로 자체 사본을 가지고 있습니다. 암시 적으로 이것은 동시성이있을 때 결정적 동작을 의미합니다.
단점 : 많은 양의 데이터가 포함 된 객체와 함께 사용하면 메모리 소비가 문제가 될 수 있습니다. 이것에 대한 해결책은 객체에 대한 작업을 상징적으로 유지하고 게으른 평가를 수행하는 것입니다. 그러나 이는 인터페이스가 심볼 연산을 수용하도록 설계되지 않은 경우 성능 계산에 부정적인 영향을 줄 수있는 일련의 심볼 계산으로 이어질 수 있습니다. 이 경우 확실히 피해야 할 것은 메소드에서 엄청난 양의 메모리를 반환하는 것입니다. 체인화 된 심볼 작업과 함께 이로 인해 엄청난 메모리 소비와 성능 저하가 발생할 수 있습니다.
따라서 불변의 객체는 객체 지향 디자인에 대한 나의 주요 사고 방식이지만 분명히 교리가 아닙니다. 객체의 클라이언트에게는 많은 문제를 해결하지만 특히 구현 자에게는 많은 문제가 발생합니다.
당신이 말하는 언어를 지정해야합니다. C 또는 C ++와 같은 저수준 언어의 경우 공간을 절약하고 메모리 변동을 줄이기 위해 가변 객체를 사용하는 것을 선호합니다. 더 높은 수준의 언어에서, 불변의 객체는 "먼 거리에서 멍청한 행동"이 없기 때문에 코드의 동작 (특히 멀티 스레드 코드)에 대해 추론하기가 더 쉽습니다.
변경 가능한 객체는 단순히 만들거나 인스턴스화 한 후에 수정할 수있는 수정 가능한 객체와 수정할 수없는 변경 불가능한 객체입니다 ( 대상 의 Wikipedia 페이지 참조 ). 프로그래밍 언어에서 이에 대한 예는 Pythons 목록과 튜플입니다. 튜플은 할 수 없지만 목록을 수정할 수 있습니다 (예 : 새 항목을 만든 후 추가 할 수 있음).
나는 모든 상황에서 어느 것이 더 나은지에 대한 명확한 대답이 있다고 생각하지 않습니다. 둘 다 자리가 있습니다.
클래스 유형이 변경 가능한 경우 해당 클래스 유형의 변수는 여러 가지 다른 의미를 가질 수 있습니다. 예를 들어, 오브젝트 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
)에 대한 언급 은 일반적으로 하나의 의미만을 갖는다. 불변 개체의 "파워"의 대부분은 그 사실에서 비롯됩니다.
가변 인스턴스는 참조로 전달됩니다.
불변 인스턴스는 값으로 전달됩니다.
추상적 인 예. 내 HDD에 txtfile 이라는 파일이 있다고 가정합니다 . 이제 txtfile 을 요청할 때 두 가지 모드로 반환 할 수 있습니다.
첫 번째 모드에서 리턴 된 txtfile 은 변경 가능한 파일입니다. 바로 가기 파일을 변경하면 원본 파일도 변경되기 때문입니다. 이 모드의 장점은 각각의 반환 된 바로 가기가 메모리 (RAM 또는 HDD)에 적은 메모리를 필요로하며 모든 사람 (나뿐만 아니라 소유자)도 파일 내용을 수정할 권한이 있다는 것입니다.
두 번째 모드에서, 수신 된 파일의 모든 변경 사항이 원본 파일을 참조하지 않기 때문에 리턴 된 txtfile 은 변경할 수없는 파일입니다. 이 모드의 장점은 나 (소유자) 만 원본 파일을 수정할 수 있다는 것입니다. 각 반환 사본은 필요한 메모리 (RAM 또는 HDD)가 필요합니다.
배열 또는 문자열의 참조를 반환하면 외부 세계가 해당 객체의 내용을 수정할 수 있으므로 변경 가능 (수정 가능) 객체로 만들 수 있습니다.
불변의 의미는 변경할 수 없으며, 변의의 의미는 변경할 수 있음을 의미합니다.
Java의 오브젝트는 기본 요소와 다릅니다. 프리미티브는 유형 (부울, int 등)으로 작성되며 객체 (클래스)는 사용자 작성 유형입니다.
프리미티브와 객체는 클래스 구현 내에서 멤버 변수로 정의 될 때 변경 가능하거나 변경 불가능할 수 있습니다.
많은 사람들이 최종 수정자를 갖는 프리미티브와 객체 변수가 불변이라고 생각하지만 이것은 사실이 아닙니다. 따라서 final은 변수에 대해 불변성을 의미하지는 않습니다.
http://www.siteconsortium.com/h/D0000F.php의 예제를 참조하십시오 .
Immutable object
-작성 후 오브젝트 상태를 변경할 수 없습니다. 모든 필드가 불변 인 경우, 객체는 불변입니다
스레드 안전
불변 개체의 주요 장점은 동시 환경에 적합하다는 것입니다. 동시성에서 가장 큰 문제 shared resource
는 스레드를 변경할 수 있다는 것입니다. 그러나 객체가 불변 인 경우 read-only
스레드 안전 작업입니다. 원래의 불변 개체를 수정하면 복사본이 반환됩니다
부작용 무료
개발자는 불변 객체의 상태를 (의도적이든 아니든) 어느 곳에서나 변경할 수 없다는 것을 완전히 확신합니다.
컴파일 최적화
성능 향상
불리:
변경 가능한 객체를 변경하는 것보다 객체를 복사하는 것이 더 많은 작업이므로 성능에 영향을주는 이유
immutable
객체 를 만들려면 다음을 사용해야합니다.
언어 수준. 각 언어에는 도움이되는 도구가 있습니다. 예를 들어 Java는 final
and primitives
, Swift has let
및 struct
[About] 입니다. 언어는 변수 유형을 정의합니다. 예를 들어 Java has primitive
및 reference
type, Swift has value
및 reference
type [About] 입니다. 불변의 객체의 경우 기본적으로 복사하는 것이 더 편리 primitives
하고 value
유형이 좋습니다. 에 관해서 reference
만 가능 (당신이 그것을 객체의 상태를 변경할 수 있기 때문에) 형태가 더 어렵습니다. 예를 들어 clone
개발자 수준에서 패턴을 사용할 수 있습니다
개발자 수준. 개발자는 상태 변경을위한 인터페이스를 제공하지 않아야합니다
string
적어도 .NET에서는 불변이며 다른 많은 현대 언어로도 생각합니다.