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 Weather
z 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 Weather
z API, a następnie modyfikowanie go byłoby dziwne: zmiana Temperature
lub Sky
nie zmieniłaby pogody w prawdziwym świecie, a zmiana CorrespondingCity
ró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; } }
}
źródło
{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.Odpowiedzi:
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:
StringBuilder
lubUriBuilder
w języku C #, czyWeatherBuilder
w Twoim przypadku. To dla mnie główny powód, ponieważ wiele zajęć, które projektuję, nie są warte takiego wysiłku.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.
źródło
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:
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ć:
Teraz funkcja nie zadziera
x
ani 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:
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ć
transform
funkcja wolna od efektów ubocznych, takich jak:I w tych przypadkach, gdy kopiowanie czegoś w całości byłoby zwykle epickim narzutem, przydało mi się zmienić
Mesh
w 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ą,
Mesh
aby 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
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
źródło
Car
obiekt 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. GdybyCar
były niezmienne, znalezienie obecnego miejsca pobytu byłoby prawdopodobnie znacznie trudniejsze.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ż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ść.
źródło
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 Zbędzie sposób na stwierdzenie, że wyniku określonego wywołania metody (tj.
ImmutableList<T>.Add
) nie można odrzucić lub przynajmniej wygeneruje ostrzeżenieA 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 )
źródło
MustUseReturnValueAttribute
niestandardowy atrybut, który dokładnie to robi.PureAttribute
ma ten sam efekt i jest do tego jeszcze lepszy.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.
źródło