Jakie są wady niezmiennych typów?

12

Widzę, że używam coraz więcej niezmiennych typów, gdy nie oczekuje się, że instancje klasy zostaną zmienione . Wymaga więcej pracy (patrz przykład poniżej), ale ułatwia korzystanie z typów w środowisku wielowątkowym.

Jednocześnie rzadko widzę niezmienne typy w innych aplikacjach, nawet jeśli zmienność nie przyniosłaby nikomu korzyści.

Pytanie: Dlaczego niezmienne typy są tak rzadko używane w innych aplikacjach?

  • Czy to dlatego, że dłużej jest pisać kod dla niezmiennego typu,
  • A może coś mi brakuje i istnieją pewne ważne wady przy stosowaniu niezmiennych typów?

Przykład z prawdziwego życia

Powiedzmy, że otrzymujesz Weatherz RESTful API takiego:

public Weather FindWeather(string city)
{
    // TODO: Load the JSON response from the RESTful API and translate it into an instance
    // of the Weather class.
}

To, co ogólnie widzimy, to (nowe wiersze i komentarze zostały usunięte, aby skrócić kod):

public sealed class Weather
{
    public City CorrespondingCity { get; set; }
    public SkyState Sky { get; set; } // Example: SkyState.Clouds, SkyState.HeavySnow, etc.
    public int PrecipitationRisk { get; set; }
    public int Temperature { get; set; }
}

Z drugiej strony, napisałbym to w ten sposób, biorąc pod uwagę, że pobieranie Weatherz API, a następnie modyfikowanie go byłoby dziwne: zmiana Temperaturelub Skynie zmieniłaby pogody w prawdziwym świecie, a zmiana CorrespondingCityrównież nie ma sensu.

public sealed class Weather
{
    private readonly City correspondingCity;
    private readonly SkyState sky;
    private readonly int precipitationRisk;
    private readonly int temperature;

    public Weather(City correspondingCity, SkyState sky, int precipitationRisk,
        int temperature)
    {
        this.correspondingCity = correspondingCity;
        this.sky = sky;
        this.precipitationRisk = precipitationRisk;
        this.temperature = temperature;
    }

    public City CorrespondingCity { get { return this.correspondingCity; } }
    public SkyState Sky { get { return this.sky; } }
    public int PrecipitationRisk { get { return this.precipitationRisk; } }
    public int Temperature { get { return this.temperature; } }
}
Arseni Mourzenko
źródło
3
„Wymaga więcej pracy” - wymagane cytowanie. Z mojego doświadczenia wynika, że ​​wymaga mniej pracy.
Konrad Rudolph
1
@KonradRudolph: przez więcej pracy mam na myśli więcej kodu do napisania, aby stworzyć niezmienną klasę. Ilustruje to przykład z mojego pytania, z 7 liniami dla zmiennej klasy i 19 dla niezmiennej.
Arseni Mourzenko
Możesz ograniczyć pisanie kodu za pomocą funkcji Fragmenty kodu w programie Visual Studio, jeśli go używasz. Możesz utworzyć niestandardowe fragmenty i pozwolić IDE zdefiniować pole i właściwość jednocześnie za pomocą kilku kluczy. Niezmienne typy są niezbędne do wielowątkowości i są szeroko stosowane w językach takich jak Scala.
Mert Akcakaya
@Mert: Fragmenty kodu świetnie nadają się do prostych rzeczy. Napisanie fragmentu kodu, który zbuduje pełną klasę z komentarzami pól i właściwości oraz poprawnym uporządkowaniem, nie będzie łatwym zadaniem.
Arseni Mourzenko
5
Nie zgadzam się z podanym przykładem, niezmienna wersja robi więcej i różne rzeczy. Możesz usunąć zmienne na poziomie instancji, deklarując właściwości za pomocą akcesorów {get; private set;}, a nawet zmienna powinna mieć konstruktor, ponieważ wszystkie te pola powinny być zawsze ustawione i dlaczego nie miałbyś tego wymuszać? Dokonanie tych dwóch całkowicie uzasadnionych zmian powoduje, że stają się one równe i parzystości LoC.
Phoshi

Odpowiedzi:

16

Programuję w C # i Objective-C. Bardzo lubię niezmienne pisanie, ale w rzeczywistości zawsze byłem zmuszony ograniczać jego użycie, głównie do typów danych, z następujących powodów:

  1. Wysiłek implementacyjny w porównaniu do typów zmiennych. W przypadku typu niezmiennego potrzebujesz konstruktora wymagającego argumentów dla wszystkich właściwości. Twój przykład jest dobry. Spróbuj sobie wyobrazić, że masz 10 klas, z których każda ma 5-10 właściwości. Żeby było łatwiej, może trzeba mieć klasę budowniczy na budowę lub tworzenie zmodyfikowanych niezmienne instancji w sposób podobny do StringBuilderlub UriBuilderw języku C #, czy WeatherBuilderw Twoim przypadku. To dla mnie główny powód, ponieważ wiele zajęć, które projektuję, nie są warte takiego wysiłku.
  2. Użyteczność konsumencka . Niezmienny typ jest trudniejszy w użyciu w porównaniu z typem zmiennym. Tworzenie instancji wymaga zainicjowania wszystkich wartości. Niezmienność oznacza również, że nie możemy przekazać instancji metodzie w celu zmodyfikowania jej wartości bez użycia konstruktora, a jeśli potrzebujemy konstruktora, to wada leży po stronie mojej (1).
  3. Zgodność ze strukturą języka. Wiele ram danych wymaga do działania typów zmiennych i domyślnych konstruktorów. Na przykład nie można wykonywać zagnieżdżonych zapytań LINQ-SQL dla typów niezmiennych i nie można wiązać właściwości do edycji w edytorach, takich jak TextBox Windows Forms.

Krótko mówiąc, niezmienność jest dobra dla obiektów, które zachowują się jak wartości lub mają tylko kilka właściwości. Przed uczynieniem czegokolwiek niezmiennym należy wziąć pod uwagę wysiłek i użyteczność samej klasy po uczynieniu go niezmiennym.

tia
źródło
11
„Instancja potrzebuje wcześniej wszystkich wartości.”: Także typ zmienny, chyba że zaakceptujesz ryzyko, że obiekt z niezainicjowanymi polami uniesie się wokół ...
Giorgio
@Giorgio W przypadku typu mutable domyślny konstruktor powinien zainicjować instancję do stanu domyślnego, a stan instancji można zmienić później po utworzeniu instancji.
tia
8
W przypadku typu niezmiennego możesz mieć tego samego domyślnego konstruktora i wykonać kopię później, używając innego konstruktora. Jeśli wartości domyślne są poprawne dla typu zmiennego, powinny być również poprawne dla typu niezmiennego, ponieważ w obu przypadkach modelujesz ten sam byt. A jaka jest różnica?
Giorgio 30.04.13
1
Jeszcze jedną rzeczą do rozważenia jest to, co reprezentuje typ. Kontrakty danych nie stanowią dobrych niezmiennych typów z powodu wszystkich tych punktów, ale typy usług, które są inicjowane z zależnościami lub odczytują tylko dane, a następnie wykonują operacje, doskonale nadają się do niezmienności, ponieważ operacje będą działały konsekwentnie i nie można zmienić stanu usługi zmieniłem, aby zaryzykować.
Kevin
1
Obecnie koduję w F #, gdzie niezmienność jest domyślna (stąd łatwiejsza do wdrożenia). Uważam, że twój punkt 3 jest dużą przeszkodą. Jak tylko użyjesz większości standardowych bibliotek .Net, będziesz musiał skakać przez obręcze. (A jeśli używają odbicia i tym samym omijają niezmienność ... argh!)
Guran
5

Po prostu ogólnie niezmienne typy tworzone w językach, które nie obracają się wokół niezmienności, będą kosztować więcej czasu programisty, aby je stworzyć, a także potencjalnie wykorzystać, jeśli wymagają jakiegoś typu „konstruktora” do wyrażenia pożądanych zmian (nie oznacza to, że ogólny praca będzie większa, ale w takich przypadkach koszty są z góry). Niezależnie od tego, czy język naprawdę ułatwia tworzenie niezmiennych typów, czy nie, zawsze będzie wymagał trochę przetwarzania i narzutu pamięci w przypadku nietrywialnych typów danych.

Tworzenie funkcji pozbawionych efektów ubocznych

Jeśli pracujesz w językach, które nie obracają się wokół niezmienności, myślę, że pragmatycznym podejściem nie jest dążenie do tego, aby każdy typ danych był niezmienny. Potencjalnie znacznie bardziej produktywny sposób myślenia, który daje wiele takich samych korzyści, polega na maksymalizacji liczby funkcji w systemie, które powodują zerowe skutki uboczne .

Jako prosty przykład, jeśli masz funkcję, która powoduje taki efekt uboczny:

// Make 'x' the absolute value of itself.
void make_abs(int& x);

Zatem nie potrzebujemy niezmiennego typu danych całkowitoliczbowych, który zabrania operatorom takich jak przypisanie po inicjalizacji, aby funkcja ta unikała skutków ubocznych. Możemy to po prostu zrobić:

// Returns the absolute value of 'x'.
int abs(int x);

Teraz funkcja nie zadziera xani nie wykracza poza jej zakres, aw tym trywialnym przypadku moglibyśmy nawet ogolić niektóre cykle, unikając narzutu związanego z pośrednim / aliasingiem. Przynajmniej druga wersja nie powinna być droższa pod względem obliczeniowym niż pierwsza.

Rzeczy, których kopiowanie w całości jest drogie

Oczywiście większość przypadków nie jest tak trywialna, jeśli chcemy uniknąć sytuacji, w której funkcja powoduje skutki uboczne. Złożony przypadek użycia w świecie rzeczywistym może być mniej więcej taki:

// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);

W tym momencie siatka może wymagać kilkuset megabajtów pamięci z ponad sto tysiącami wielokątów, jeszcze większą liczbą wierzchołków i krawędzi, wieloma mapami tekstur, zmiennymi celami itp. Kopiowanie całej siatki byłoby bardzo drogie, aby to zrobić transformfunkcja wolna od efektów ubocznych, takich jak:

// Returns a new version of the mesh whose vertices been 
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

I w tych przypadkach, gdy kopiowanie czegoś w całości byłoby zwykle epickim narzutem, przydało mi się zmienić Meshw trwałą strukturę danych i niezmienny typ z analogicznym „konstruktorem” do tworzenia zmodyfikowanych wersji, aby to można po prostu płytko skopiować i zliczyć części, które nie są unikalne. Wszystko to skupia się na możliwości pisania funkcji siatki, które są wolne od skutków ubocznych.

Trwałe struktury danych

A w tych przypadkach, w których kopiowanie wszystkiego jest tak niewiarygodnie drogie, starałem się zaprojektować niezmienną, Meshaby naprawdę się spłaciła, mimo że z góry miała nieco stromy koszt, ponieważ nie tylko uprościła bezpieczeństwo wątków. Uprościło to również nieniszczącą edycję (pozwalającą użytkownikowi na warstwowanie operacji siatki bez modyfikowania oryginalnej kopii), cofanie systemów (teraz system cofania może po prostu przechowywać niezmienną kopię siatki przed zmianami dokonanymi przez operację bez wysadzania pamięci use) oraz bezpieczeństwo wyjątków (teraz, jeśli w powyższej funkcji wystąpi wyjątek, funkcja nie musi cofać i cofać wszystkich swoich skutków ubocznych, ponieważ nie spowodowała żadnych).

Mogę śmiało powiedzieć w tych przypadkach, że czas potrzebny do uczynienia tych potężnych struktur danych niezmiennymi zaoszczędził więcej czasu niż koszt, ponieważ porównałem koszty utrzymania tych nowych projektów z poprzednimi, które obracały się wokół zmienności i funkcji powodujących skutki uboczne, a poprzednie modyfikowalne projekty kosztowały znacznie więcej czasu i były znacznie bardziej podatne na ludzkie błędy, szczególnie w obszarach, które naprawdę kuszą deweloperów do zaniedbania w czasie kryzysu, takich jak bezpieczeństwo wyjątkowe.

Sądzę więc, że niezmienne typy danych naprawdę się opłacają w takich przypadkach, ale nie wszystko musi być niezmienne, aby większość funkcji w twoim systemie była wolna od skutków ubocznych. Wiele rzeczy jest na tyle tanie, że można je w całości skopiować. Również wiele aplikacji w świecie rzeczywistym będzie musiało wywoływać tu i tam pewne skutki uboczne (przynajmniej jak zapisywanie pliku), ale zazwyczaj jest o wiele więcej funkcji, które mogą być pozbawione skutków ubocznych.

Chodzi o to, aby mieć dla mnie pewne niezmienne typy danych, aby upewnić się, że możemy napisać maksymalną liczbę funkcji, które będą wolne od skutków ubocznych, bez ponoszenia epickich kosztów ogólnych w postaci głębokiego kopiowania ogromnych struktur danych w lewo i prawo w całości, gdy tylko małe porcje z nich należy zmodyfikować. Mając w tych przypadkach trwałe struktury danych, stają się one szczegółami optymalizacji, dzięki czemu możemy napisać nasze funkcje, aby były wolne od skutków ubocznych, nie ponosząc przy tym ogromnych kosztów.

Niezmienny napowietrzny

Teraz, pod względem koncepcyjnym, modyfikowalne wersje zawsze będą miały przewagę wydajności. Zawsze istnieje taki narzut obliczeniowy związany z niezmiennymi strukturami danych. Ale uznałem, że jest to warta wymiany w przypadkach, które opisałem powyżej, i możesz skupić się na tym, aby koszty ogólne były wystarczająco minimalne. Wolę takie podejście, w którym poprawność staje się łatwa, a optymalizacja trudniejsza niż optymalizacja łatwiejsza, ale poprawność staje się trudniejsza. Nie jest to tak demoralizujące, że kod, który działa idealnie poprawnie, wymaga kilku ulepszeń w stosunku do kodu, który nie działa poprawnie w pierwszej kolejności, bez względu na to, jak szybko osiąga niepoprawne wyniki.


źródło
3

Jedyną wadą, o której mogę myśleć, jest to, że teoretycznie wykorzystanie niezmiennych danych może być wolniejsze niż zmienne - wolniej jest tworzyć nowe wystąpienie i zbierać poprzednie, niż modyfikować istniejące.

Innym „problemem” jest to, że nie można używać tylko typów niezmiennych. Na koniec musisz opisać stan i musisz użyć do tego zmiennych typów - bez zmiany stanu nie możesz wykonać żadnej pracy.

Ale nadal ogólną zasadą jest używanie typów niezmiennych gdziekolwiek możesz i modyfikowanie typów tylko wtedy, gdy naprawdę jest ku temu powód ...

I aby odpowiedzieć na pytanie „ Dlaczego typy niezmienne są tak rzadko używane w innych aplikacjach? ” - Naprawdę nie sądzę, że znajdują się… gdziekolwiek spojrzysz, wszyscy zalecają uczynienie twoich klas tak niezmiennymi, jak to tylko możliwe… na przykład: http://www.javapractices.com/topic/TopicAction.do?Id=29

mrpyo
źródło
1
Oba twoje problemy nie są w Haskell.
Florian Margaine
@FlorianMargaine Czy mógłbyś opracować?
mrpyo
Powolność nie jest prawdziwa dzięki inteligentnemu kompilatorowi. W Haskell nawet I / O odbywa się przez niezmienny interfejs API.
Florian Margaine
2
Bardziej podstawowym problemem niż szybkość jest to, że niezmiennym obiektom trudno jest utrzymać tożsamość, gdy zmienia się ich stan. Jeśli zmienny Carobiekt jest stale aktualizowany o lokalizację konkretnego fizycznego samochodu, to jeśli mam odniesienie do tego obiektu, mogę szybko i łatwo dowiedzieć się, gdzie znajduje się ten samochód. Gdyby Carbyły niezmienne, znalezienie obecnego miejsca pobytu byłoby prawdopodobnie znacznie trudniejsze.
supercat
Czasami trzeba dość sprytnie napisać kod, aby kompilator stwierdził, że nie ma odniesienia do poprzedniego obiektu, a tym samym może go zmodyfikować lub przekształcić wylesianie itp. Zwłaszcza w większych programach. I jak mówi @supercat, tożsamość może rzeczywiście stać się problemem.
Macke
0

Aby modelować dowolny rzeczywisty system, w którym rzeczy mogą się zmienić, stan zmienny będzie musiał być gdzieś zakodowany. Istnieją trzy główne sposoby, w jakie obiekt może utrzymać stan zmienny:

  • Używanie zmiennego odniesienia do niezmiennego obiektu
  • Używanie niezmiennego odwołania do obiektu podlegającego mutacji
  • Używanie zmiennego odwołania do modyfikowalnego obiektu

Użycie pierwszego ułatwia obiektowi wykonanie niezmiennej migawki bieżącego stanu. Użycie drugiego ułatwia obiektowi utworzenie podglądu na żywo bieżącego stanu. Użycie trzeciego może czasem sprawić, że niektóre działania będą bardziej wydajne w przypadkach, w których nie ma oczekiwanej potrzeby niezmiennych migawek lub widoków na żywo.

Poza faktem, że aktualizacja stanu przechowywanego przy użyciu zmiennego odwołania do niezmiennego obiektu jest często wolniejsza niż aktualizacja stanu przechowywanego przy użyciu zmiennego obiektu, użycie zmiennego odniesienia będzie wymagało rezygnacji z możliwości zbudowania taniego podglądu stanu na żywo. Jeśli nie trzeba tworzyć podglądu na żywo, nie stanowi to problemu; gdyby jednak trzeba było utworzyć podgląd na żywo, niemożność użycia niezmiennego odwołania spowoduje wykonanie wszystkich operacji z widokiem - zarówno do odczytu, jak i do zapisu- znacznie wolniej niż w innym przypadku. Jeśli potrzeba niezmiennych migawek przewyższa potrzebę podglądów na żywo, poprawiona wydajność niezmiennych migawek może uzasadniać spadek wydajności w widokach na żywo, ale jeśli ktoś potrzebuje widoków na żywo i nie potrzebuje migawek, sposobem jest użycie niezmiennych odniesień do obiektów zmiennych iść.

supercat
źródło
0

W twoim przypadku odpowiedź jest głównie dlatego, że C # ma słabe wsparcie dla niezmienności ...

Byłoby świetnie, gdyby:

  • wszystko będzie domyślnie niezmienne, chyba że zaznaczono inaczej (tzn. ze słowem kluczowym „mutable”), mieszanie typów niezmiennych i mutable jest mylące

  • metody mutacji ( With) będą automatycznie dostępne - chociaż można to już osiągnąć, patrz Z

  • będzie sposób na stwierdzenie, że wyniku określonego wywołania metody (tj. ImmutableList<T>.Add) nie można odrzucić lub przynajmniej wygeneruje ostrzeżenie

  • A przede wszystkim, jeśli kompilator może w jak największym stopniu zapewnić niezmienność tam, gdzie jest to wymagane (patrz https://github.com/dotnet/roslyn/issues/159 )

Kofifus
źródło
1
Jeśli chodzi o trzeci punkt, ReSharper ma MustUseReturnValueAttributeniestandardowy atrybut, który dokładnie to robi. PureAttributema ten sam efekt i jest do tego jeszcze lepszy.
Sebastian Redl,
-1

Dlaczego typy niezmienne są tak rzadko używane w innych aplikacjach?

Ignorancja? Brak doświadczenia?

Niezmienne obiekty są dziś powszechnie uważane za lepsze, ale jest to stosunkowo nowy rozwój. Inżynierowie, którzy nie aktualizowali się lub po prostu utknęli w „tym, co wiedzą”, nie będą ich używać. Efektywne ich wykorzystanie wymaga nieco zmian w projekcie. Jeśli aplikacje są stare lub inżynierowie mają słabe umiejętności projektowe, korzystanie z nich może być niewygodne lub w inny sposób kłopotliwe.

Telastyn
źródło
„Inżynierowie, którzy nie aktualizowali się na bieżąco”: Można powiedzieć, że inżynier powinien również dowiedzieć się o technologiach nie należących do głównego nurtu. Idea niezmienności dopiero niedawno stała się głównym nurtem, ale jest to dość stara idea i jest wspierana (jeśli nie jest egzekwowana) przez starsze języki, takie jak Scheme, SML, Haskell. Tak więc każdy, kto jest przyzwyczajony do patrzenia poza języki głównego nurtu, mógł się o tym dowiedzieć nawet 30 lat temu.
Giorgio
@Giorgio: w niektórych krajach wielu inżynierów nadal pisze kod C # bez LINQ, bez FP, bez iteratorów i bez generycznych, więc właściwie jakoś przegapili wszystko, co stało się z C # od 2003 roku. Jeśli nawet nie znają swojego preferowanego języka , Nie wyobrażam sobie, by znali jakiś język spoza głównego nurtu.
Arseni Mourzenko
@MainMa: Dobrze, że napisałeś słowo inżynieria kursywą.
Giorgio
@Giorgio: w moim kraju nazywani są również architektami , konsultantami i wieloma innymi haniebnymi terminami, nigdy nie pisanymi kursywą. W firmie, w której obecnie pracuję, nazywam się programistą analityków i spodziewam się, że będę spędzać czas na pisaniu CSS na kiepskim starszym kodzie HTML. Nazwy stanowisk są niepokojące na tak wielu poziomach.
Arseni Mourzenko
1
@MainMa: Zgadzam się. Tytuły takie jak inżynier czy konsultant są często tylko modnymi słowami bez powszechnie akceptowanego znaczenia. Często są używane, aby uczynić kogoś lub jego pozycję ważniejszą / bardziej prestiżową niż jest w rzeczywistości.
Giorgio