그렇다면 어떤 시점에서 클래스가 불변하기에는 너무 복잡합니까?
제 생각에는 작은 클래스를 당신이 보여주는 언어와 같은 언어로 변경할 수 없게 만드는 것은 가치가 없습니다 . 나는 여기에 작고 복잡 하지 않습니다. 해당 클래스에 10 개의 필드를 추가하고 실제로 멋진 작업을 수행하더라도 메가 바이트는 물론 기가 바이트는 물론 킬로바이트를 취할 것이므로 의심 할 여지가 없습니다. 클래스는 단순히 외부 부작용을 피하기 위해 원본을 수정하지 않도록 전체 객체의 저렴한 사본을 만들 수 있습니다.
지속적인 데이터 구조
불변성을 개인적으로 사용하는 곳은 백만을 저장하는 클래스와 같이 보여주고있는 클래스의 인스턴스와 같은 많은 십대 데이터를 집계하는 큰 중앙 데이터 구조입니다 NamedThings
. 변경 불가능한 영구 데이터 구조에 속하고 읽기 전용 액세스 만 허용하는 인터페이스 뒤에 있으면 컨테이너에 속하는 요소는 요소 클래스 ( NamedThing
)를 처리 하지 않고도 변경할 수 없습니다 .
저렴한 사본
지속적인 데이터 구조는 데이터 구조를 전체적으로 복사하지 않고도 원본의 수정을 피하면서 영역을 변형하고 고유하게 만들 수 있습니다. 그것이 진정한 아름다움입니다. 기가 바이트의 메모리를 차지하고 메가 바이트의 메모리 만 수정하는 데이터 구조를 입력하는 부작용을 피하는 함수를 순진하게 작성하려면 입력을 건드리지 않고 새로운 기능을 반환하기 위해 전체 괴물을 복사해야합니다. 산출. 부작용을 피하기 위해 기가 바이트를 복사하거나 해당 시나리오에서 부작용을 일으키므로 두 가지 불쾌한 선택 중에서 선택해야합니다.
영구적 인 데이터 구조를 사용하면 이러한 함수를 작성하고 전체 데이터 구조를 복사하지 않아도되며, 함수가 메가 바이트의 메모리 만 변환 한 경우 출력에 약 메가 바이트의 추가 메모리 만 있으면됩니다.
부담
부담은 적어도 내 경우에는 즉각적인 문제가 있습니다. 사람들이 거대한 데이터 구조에 대한 변형을 건드리지 않고 효과적으로 표현할 수 있으려면 사람들이 말하고있는 "일시적인"빌더가 필요합니다. 다음과 같은 코드 :
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
... 다음과 같이 작성해야합니다.
ImmList<Stuff> transform_stuff(ImmList<Stuff> stuff, int first, int last)
{
// Grab a "transient" (builder) list we can modify:
TransientList<Stuff> transient(stuff);
// Transform stuff in the range, [first, last)
// for the transient list.
for (; first != last; ++first)
transform(transient[first]);
// Commit the modifications to get and return a new
// immutable list.
return stuff.commit(transient);
}
그러나이 두 줄의 코드를 교체하면 동일한 원래 목록을 가진 스레드를 호출해도 안전하고 부작용 등이 발생하지 않습니다. 또한이 작업을 실행 취소 할 수없는 사용자 작업으로 만드는 것이 매우 쉽습니다. 실행 취소는 이전 목록의 저렴한 얕은 사본을 저장할 수 있습니다.
예외 안전 또는 오류 복구
모든 사람들이 이와 같은 맥락에서 영구적 인 데이터 구조에서 얻은 것만 큼 많은 이점을 얻을 수는 없습니다 (VFX 도메인의 중심 개념 인 실행 취소 시스템 및 비파괴 편집에서 많은 용도로 사용됨). 고려해야 할 모든 사람은 예외 안전 또는 오류 복구 입니다.
원래의 변경 기능을 예외로부터 안전하게 만들려면 롤백 로직이 필요합니다. 롤백 로직이 가장 간단한 구현에는 전체 목록을 복사해야 합니다.
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Make a copy of the whole massive gigabyte-sized list
// in case we encounter an exception and need to rollback
// changes.
MutList<Stuff> old_stuff = stuff;
try
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
catch (...)
{
// If the operation failed and ran into an exception,
// swap the original list with the one we modified
// to "undo" our changes.
stuff.swap(old_stuff);
throw;
}
}
이 시점에서 예외 안전 변경 가능 버전은 "빌더"를 사용하여 변경 불가능한 버전보다 계산 비용이 훨씬 비싸고 정확하게 작성하기가 훨씬 더 어렵습니다. 그리고 많은 C ++ 개발자는 예외 안전을 무시하고 도메인에 적합 할 수도 있지만 예외가 발생하는 경우에도 코드가 올바르게 작동하는지 확인하고 싶습니다 (심지어 예외를 테스트하기 위해 예외를 던지는 테스트 작성) 안전), 그래서 그것은 무언가가 던져지면 함수가 함수에 반쯤 발생하는 부작용을 롤백 할 수 있어야합니다.
예외 안전하고 응용 프로그램 충돌 및 굽기없이 오류를 정상적으로 복구하려면 오류 / 예외가 발생했을 때 기능으로 인해 발생할 수있는 부작용을 되돌 리거나 취소해야합니다. 그리고 빌더는 실제로 계산 시간과 함께 비용보다 더 많은 프로그래머 시간을 절약 할 수 있습니다.
아무런 영향을 미치지 않는 함수에서 부작용을 롤백하는 것에 대해 걱정할 필요가 없습니다!
근본적인 질문으로 돌아가십시오.
불변 클래스는 어떤 시점에서 부담이 되는가?
불변성보다 변이성을 중심으로하는 언어에서는 항상 부담이되므로 이점이 비용보다 훨씬 큰 곳에서 사용해야합니다. 그러나 충분히 큰 데이터 구조를 수용 할 수있을만큼 넓은 수준에서 가치있는 트레이드 오프가되는 경우가 많다고 생각합니다.
또한 필자는 몇 가지 불변의 데이터 유형 만 가지고 있으며 막대한 수의 요소 (이미지 / 텍스처의 픽셀, ECS의 엔티티 및 구성 요소, 정점 / 가장자리 / 다각형)를 저장하려는 거대한 데이터 구조입니다. 메쉬).