Dlaczego zdecydowali się na String
niezmienność w Javie i .NET (i niektórych innych językach)? Dlaczego nie zmienili go?
String
jest w rzeczywistości wewnętrznie zmienny. StringBuilder
w .NET 2.0 mutuje ciąg . Zostawię to tutaj.
Dlaczego zdecydowali się na String
niezmienność w Javie i .NET (i niektórych innych językach)? Dlaczego nie zmienili go?
String
jest w rzeczywistości wewnętrznie zmienny. StringBuilder
w .NET 2.0 mutuje ciąg . Zostawię to tutaj.
Odpowiedzi:
Według Effective Java , rozdział 4, strona 73, wydanie drugie:
„Jest wiele dobrych powodów: niezmienne klasy są łatwiejsze do zaprojektowania, wdrożenia i użytkowania niż klasy zmienne. Są mniej podatne na błędy i są bardziej bezpieczne.
[...]
„ Niezmienne obiekty są proste. obiekt może znajdować się dokładnie w jednym stanie, w którym został utworzony. Jeśli upewnisz się, że wszystkie konstruktory ustanawiają niezmienniki klas, to gwarantuje się, że niezmienniki te pozostaną prawdziwe przez cały czas, z bez wysiłku z twojej strony.
[...]
Niezmienne przedmioty są z natury bezpieczne dla nici; nie wymagają synchronizacji. Nie można ich zepsuć, gdy wiele wątków uzyskuje do nich dostęp jednocześnie. Jest to zdecydowanie najłatwiejsze podejście do osiągnięcia bezpieczeństwa nici. W rzeczywistości żaden wątek nigdy nie może zaobserwować żadnego wpływu innego wątku na niezmienny obiekt. Dlatego niezmienne obiekty można swobodnie udostępniać
[...]
Inne małe punkty z tego samego rozdziału:
Możesz nie tylko udostępniać niezmienne obiekty, ale także udostępniać ich elementy wewnętrzne.
[...]
Niezmienne obiekty stanowią świetne elementy konstrukcyjne dla innych obiektów, zarówno zmiennych, jak i niezmiennych.
[...]
Jedyną prawdziwą wadą niezmiennych klas jest to, że wymagają one osobnego obiektu dla każdej odrębnej wartości.
report2.Text = report1.Text;
. Potem, gdzieś indziej, modyfikując tekst: report2.Text.Replace(someWord, someOtherWord);
. Zmieniłoby to zarówno pierwszy raport, jak i drugi.
Istnieją co najmniej dwa powody.
Po pierwsze - bezpieczeństwo http://www.javafaq.nu/java-article1060.html
Głównym powodem, dla którego String stał się niezmienny, było bezpieczeństwo. Spójrz na ten przykład: mamy metodę otwierania pliku z kontrolą logowania. Do tej metody przekazujemy ciąg znaków w celu przetworzenia uwierzytelnienia, które jest konieczne przed przekazaniem połączenia do systemu operacyjnego. Jeśli String można było modyfikować, możliwe było zmodyfikowanie jego zawartości po sprawdzeniu uwierzytelnienia, zanim OS otrzyma żądanie od programu, wówczas można zażądać dowolnego pliku. Więc jeśli masz prawo do otwierania pliku tekstowego w katalogu użytkownika, ale następnie w locie, gdy jakoś uda ci się zmienić nazwę pliku, możesz poprosić o otwarcie pliku „passwd” lub dowolnego innego. Następnie można zmodyfikować plik i będzie można zalogować się bezpośrednio do systemu operacyjnego.
Po drugie - wydajność pamięci http://hikrish.blogspot.com/2006/07/why-string-class-is-immutable.html
JVM wewnętrznie utrzymuje „Pula ciągów”. Aby osiągnąć wydajność pamięci, JVM skieruje obiekt String z puli. Nie utworzy nowych obiektów String. Tak więc, za każdym razem, gdy utworzysz nowy literał łańcuchowy, JVM sprawdzi w puli, czy już istnieje. Jeśli jest już obecny w puli, po prostu podaj odniesienie do tego samego obiektu lub utwórz nowy obiekt w puli. Będzie wiele odniesień do tych samych obiektów String, jeśli ktoś zmieni wartość, wpłynie to na wszystkie odniesienia. Więc słońce postanowiło uczynić go niezmiennym.
W rzeczywistości powody, dla których ciąg jest niezmienny w java, nie mają wiele wspólnego z bezpieczeństwem. Dwa główne powody są następujące:
Ciągi to niezwykle szeroko stosowany typ obiektu. Dlatego jest mniej więcej gwarantowane do użycia w środowisku wielowątkowym. Ciągi są niezmienne, aby zapewnić bezpieczne dzielenie ciągów między wątkami. Posiadanie niezmiennych ciągów gwarantuje, że podczas przekazywania ciągów z wątku A do innego wątku B, wątek B nie może nieoczekiwanie zmodyfikować ciągu wątku A.
Pomaga to nie tylko uprościć i tak już dość skomplikowane zadanie programowania wielowątkowego, ale także pomaga w wydajności aplikacji wielowątkowych. Dostęp do zmiennych obiektów musi być w jakiś sposób zsynchronizowany, gdy można uzyskać do nich dostęp z wielu wątków, aby upewnić się, że jeden wątek nie próbuje odczytać wartości twojego obiektu, gdy jest on modyfikowany przez inny wątek. Prawidłowa synchronizacja jest trudna zarówno dla programisty, jak i droga w czasie wykonywania. Niezmienne obiekty nie mogą być modyfikowane i dlatego nie wymagają synchronizacji.
Chociaż wspomniano o internalizacji String, reprezentuje jedynie niewielki wzrost wydajności pamięci programów Java. Tylko literały łańcuchowe są internowane. Oznacza to, że tylko ciągi znaków, które są takie same w kodzie źródłowym, będą miały ten sam obiekt ciągów. Jeśli Twój program dynamicznie tworzy takie same ciągi, będą one reprezentowane w różnych obiektach.
Co ważniejsze, niezmienne ciągi pozwalają im dzielić się swoimi wewnętrznymi danymi. W przypadku wielu operacji na łańcuchach oznacza to, że podstawowa tablica znaków nie musi być kopiowana. Na przykład powiedz, że chcesz wziąć pięć pierwszych znaków ciągu. W Javie wywołałbyś myString.substring (0,5). W tym przypadku metoda substring () polega po prostu na utworzeniu nowego obiektu String, który dzieli bazowy char myString, ale wie, że zaczyna się on od indeksu 0 i kończy na indeksie 5 tego char []. Aby umieścić to w formie graficznej, skończyłbyś z następującymi:
| myString |
v v
"The quick brown fox jumps over the lazy dog" <-- shared char[]
^ ^
| | myString.substring(0,5)
To sprawia, że tego rodzaju operacje są wyjątkowo tanie, a O (1), ponieważ operacja nie zależy ani od długości oryginalnego łańcucha, ani od długości podciągu, który musimy wyodrębnić. Takie zachowanie ma również pewne zalety pamięciowe, ponieważ wiele ciągów może współdzielić swój podstawowy char [].
char[]
jest dość wątpliwą decyzją projektową. Jeśli wczytasz cały plik do jednego ciągu i zachowasz odniesienie do tylko 1-znakowego podłańcucha, cały plik będzie musiał być przechowywany w pamięci.
String.substring()
wykonuje pełną kopię, aby zapobiec problemom wymienionym w komentarzach powyżej. W Javie 8 dwa pola umożliwiające char[]
współużytkowanie, a mianowicie count
i offset
, są usunięte, zmniejszając w ten sposób ślad pamięciowy instancji String.
Bezpieczeństwo i wydajność wątku. Jeśli ciąg nie może zostać zmodyfikowany, można bezpiecznie i szybko przekazać referencję między wieloma wątkami. Jeśli łańcuchy można modyfikować, zawsze trzeba skopiować wszystkie bajty łańcucha do nowej instancji lub zapewnić synchronizację. Typowa aplikacja odczyta ciąg 100 razy za każdym razem, gdy należy go zmodyfikować. Zobacz wikipedia na temat niezmienności .
Naprawdę należy zapytać: „dlaczego X miałby być zmienny?” Lepiej jest przejść do niezmienności, ze względu na korzyści wspomniane już przez księżniczkę Fluff . Wyjątkiem powinno być to, że coś można modyfikować.
Niestety większość obecnych języków programowania jest domyślnie zmienna, ale miejmy nadzieję, że w przyszłości domyślna będzie bardziej niezmienność (patrz Lista życzeń dla następnego głównego języka programowania ).
Łał! Nie mogę uwierzyć w dezinformację tutaj. String
niezmienne nie mają nic z bezpieczeństwem. Jeśli ktoś ma już dostęp do obiektów w uruchomionej aplikacji (co należałoby założyć, jeśli próbujesz uchronić się przed „hakowaniem” a String
w Twojej aplikacji), z pewnością byłoby wiele innych możliwości hakowania.
Jest to dość nowatorski pomysł, że niezmienność String
dotyczy problemów z wątkami. Hmmm ... Mam obiekt, który jest zmieniany przez dwa różne wątki. Jak to rozwiązać? zsynchronizować dostęp do obiektu? Naawww ... nie pozwólmy nikomu zmieniać obiektu - to naprawi wszystkie nasze problemy z niechlujną współbieżnością! W rzeczywistości sprawmy, aby wszystkie obiekty były niezmienne, a następnie możemy usunąć zsynchronizowany konstrukt z języka Java.
Prawdziwym powodem (wskazanym przez innych powyżej) jest optymalizacja pamięci. Powszechnie stosuje się w każdym zastosowaniu wielokrotne używanie tego samego literału łańcuchowego. W rzeczywistości jest tak powszechne, że dekady temu wiele kompilatorów dokonało optymalizacji przechowywania tylko jednego wystąpienia String
literału. Wadą tej optymalizacji jest to, że kod środowiska wykonawczego, który modyfikuje String
literał, wprowadza problem, ponieważ modyfikuje instancję dla wszystkich innych kodów, które go współużytkują. Na przykład nie byłoby dobrze, aby funkcja gdzieś w aplikacji zmieniła String
literał "dog"
na "cat"
. Doprowadziłoby printf("dog")
to do literałów (tzn. Uczyniłoby je niezmiennymi). Niektóre kompilatory (z obsługą systemu operacyjnego) osiągnęłyby to poprzez umieszczenie"cat"
zapisywane na standardowe wyjście. Z tego powodu musiał istnieć sposób ochrony przed kodem, który próbuje się zmienićString
String
literału w specjalnym tylko do odczytu segmencie pamięci, który spowodowałby błąd pamięci, gdyby podjęto próbę zapisu.
W Javie jest to znane jako internowanie. Kompilator Java tutaj postępuje zgodnie ze standardową optymalizacją pamięci wykonywaną przez kompilatory od dziesięcioleci. Aby rozwiązać ten sam problem String
modyfikacji literałów w czasie wykonywania, Java po prostu sprawia, że String
klasa jest niezmienna (tj. Nie daje żadnych ustawień, które pozwalałyby na zmianę String
zawartości). String
nie musiałyby być niezmienne, gdyby internowanie String
literałów nie nastąpiło.
String
i StringBuffer
, ale niestety niewiele innych typów podąża za tym modelem.
String
nie jest prymitywnym typem, ale zwykle chcesz go używać z semantyką wartości, tj. jak wartością.
Wartość to coś, czemu możesz zaufać i nie zmieni się za twoimi plecami. Jeśli napiszesz:String str = someExpr();
nie chcesz, żeby to się zmieniło, chyba że TY coś zrobisz str
.
String
ponieważ Object
ma naturalnie semantykę wskaźnika, aby uzyskać również semantykę wartości, musi być niezmienna.
Jednym z czynników jest to, że gdyby String
były zmienne, obiekty przechowujące je String
musiałyby zachować ostrożność przy przechowywaniu kopii, aby ich wewnętrzne dane nie zmieniły się bez powiadomienia. Biorąc pod uwagę, że String
s są dość prymitywnym typem, takim jak liczby, dobrze jest traktować je tak, jakby były przekazywane przez wartość, nawet jeśli są przekazywane przez referencję (co również pomaga zaoszczędzić na pamięci).
Wiem, że to guz, ale ... Czy naprawdę są niezmienne? Rozważ następujące.
public static unsafe void MutableReplaceIndex(string s, char c, int i)
{
fixed (char* ptr = s)
{
*((char*)(ptr + i)) = c;
}
}
...
string s = "abc";
MutableReplaceIndex(s, '1', 0);
MutableReplaceIndex(s, '2', 1);
MutableReplaceIndex(s, '3', 2);
Console.WriteLine(s); // Prints 1 2 3
Możesz nawet uczynić to metodą rozszerzenia.
public static class Extensions
{
public static unsafe void MutableReplaceIndex(this string s, char c, int i)
{
fixed (char* ptr = s)
{
*((char*)(ptr + i)) = c;
}
}
}
Co sprawia, że następująca praca
s.MutableReplaceIndex('1', 0);
s.MutableReplaceIndex('2', 1);
s.MutableReplaceIndex('3', 2);
Wniosek: są w niezmiennym stanie, który jest znany kompilatorowi. Oczywiście powyższe dotyczy tylko ciągów .NET, ponieważ Java nie ma wskaźników. Jednak ciąg może być całkowicie zmienny przy użyciu wskaźników w języku C #. To nie jest sposób, w jaki wskaźniki mają być używane, mają praktyczne zastosowanie lub są bezpiecznie używane; jest to jednak możliwe, w ten sposób zaginając całą zasadę „zmienności”. Zwykle nie można modyfikować indeksu bezpośrednio ciągu i jest to jedyny sposób. Istnieje sposób, aby temu zapobiec, uniemożliwiając wystąpienie wskaźnika ciągów lub tworzenie kopii, gdy ciąg jest wskazywany, ale nie jest to zrobione, co sprawia, że ciągi w języku C # nie są całkowicie niezmienne.
Dla większości celów „ciąg” jest (używany / traktowany jako / uważany za / uważany za) znaczącą jednostką atomową, podobnie jak liczba .
Powinieneś wiedzieć dlaczego. Po prostu o tym pomyśl.
Nienawidzę tego mówić, ale niestety debatujemy nad tym, ponieważ nasz język jest do bani, i staramy się użyć jednego słowa, ciągu , aby opisać złożoną, kontekstowo usytuowaną koncepcję lub klasę obiektu.
Wykonujemy obliczenia i porównania z „ciągami znaków” podobnie jak w przypadku liczb. Jeśli łańcuchy (lub liczby całkowite) byłyby zmienne, musielibyśmy napisać specjalny kod, aby zablokować ich wartości w niezmiennych formach lokalnych w celu wiarygodnego wykonania dowolnego rodzaju obliczeń. Dlatego najlepiej jest traktować ciąg znaków jak identyfikator numeryczny, ale zamiast 16, 32 lub 64 bitów, może mieć setki bitów.
Kiedy ktoś mówi „sznurek”, wszyscy myślimy o różnych rzeczach. Ci, którzy myślą o tym po prostu jako zestaw znaków, bez szczególnego celu, będą oczywiście przerażeni, że ktoś po prostu zdecydował , że nie będzie w stanie manipulować tymi postaciami. Ale klasa „string” to nie tylko tablica znaków. To STRING
nie jest char[]
. Istnieje kilka podstawowych założeń dotyczących pojęcia, które nazywamy „łańcuchem”, i ogólnie można je opisać jako znaczącą, atomową jednostkę zakodowanych danych, jak liczba. Kiedy ludzie mówią o „manipulowaniu ciągami”, być może naprawdę mówią o manipulowaniu znakami w celu budowania ciągów , a StringBuilder jest do tego świetny.
Zastanów się przez chwilę, jak by to było, gdyby łańcuchy były zmienne. Poniższa funkcja API mogą nabrać do zwrotu informacji dla różnych użytkowników, jeżeli zmienne nazwa ciąg jest celowo lub przypadkowo zmodyfikowane przez inny wątek, podczas gdy ta funkcja używa:
string GetPersonalInfo( string username, string password )
{
string stored_password = DBQuery.GetPasswordFor( username );
if (password == stored_password)
{
//another thread modifies the mutable 'username' string
return DBQuery.GetPersonalInfoFor( username );
}
}
Bezpieczeństwo to nie tylko „kontrola dostępu”, ale także „bezpieczeństwo” i „gwarancja poprawności”. Jeśli metody nie da się łatwo napisać i od tego zależy wiarygodne wykonanie prostych obliczeń lub porównań, wywołanie jej nie jest bezpieczne, ale można bezpiecznie zakwestionować sam język programowania.
unsafe
) lub po prostu poprzez odbicie (można łatwo uzyskać pole leżące poniżej). Powoduje to, że nie ma sensu bezpieczeństwa, ponieważ każdy, kto celowo chce zmienić ciąg, może to zrobić dość łatwo. Zapewnia to jednak programistom bezpieczeństwo: jeśli nie zrobisz czegoś specjalnego, łańcuch jest niezmienny (ale nie jest bezpieczny dla wątków!).
Niezmienność nie jest tak ściśle związana z bezpieczeństwem. Do tego, przynajmniej w .NET, dostajesz SecureString
klasę.
Późniejsza edycja: w Javie znajdziesz GuardedString
podobną implementację.
Decyzja o zmiennym łańcuchu znaków w C ++ powoduje wiele problemów, zobacz doskonały artykuł Kelvina Henneya na temat choroby Mad COW .
COW = Kopiuj przy zapisie.
To jest kompromis. String
s idą do String
puli, a kiedy tworzysz wiele identycznych String
s, współużytkują tę samą pamięć. Projektanci doszli do wniosku, że ta technika oszczędzania pamięci będzie dobrze działać w zwykłym przypadku, ponieważ programy często grindują te same łańcuchy.
Minusem jest to, że konkatenacje tworzą wiele dodatkowych, String
które są tylko przejściowe i po prostu stają się śmieciami, co w rzeczywistości szkodzi wydajności pamięci. Masz StringBuffer
i StringBuilder
(w Javie, StringBuilder
również w .NET), aby użyć do zachowania pamięci w takich przypadkach.
String
w Javie nie są niezmienne, możesz zmienić ich wartość za pomocą refleksji i / lub ładowania klas. Bezpieczeństwo nie powinno zależeć od tej właściwości. Przykłady patrz: Magic Trick In Java
Niezmienność jest dobra. Zobacz Efektywna Java. Gdybyś musiał kopiować Ciąg za każdym razem, gdy go przekazywałeś, byłby to dużo podatnego na błędy kodu. Masz również wątpliwości, które modyfikacje wpływają na które odniesienia. W ten sam sposób, w jaki liczba całkowita musi być niezmienna, aby zachowywać się jak int, ciągi muszą zachowywać się jako niezmienne, aby zachowywać się jak prymitywy. W C ++ przekazywanie ciągów przez wartość robi to bez wyraźnej wzmianki w kodzie źródłowym.
Istnieje prawie wyjątek dla prawie każdej reguły:
using System;
using System.Runtime.InteropServices;
namespace Guess
{
class Program
{
static void Main(string[] args)
{
const string str = "ABC";
Console.WriteLine(str);
Console.WriteLine(str.GetHashCode());
var handle = GCHandle.Alloc(str, GCHandleType.Pinned);
try
{
Marshal.WriteInt16(handle.AddrOfPinnedObject(), 4, 'Z');
Console.WriteLine(str);
Console.WriteLine(str.GetHashCode());
}
finally
{
handle.Free();
}
}
}
}
Jest to głównie ze względów bezpieczeństwa. O wiele trudniej jest zabezpieczyć system, jeśli nie możesz ufać, że twoje String
są odporne na manipulacje.