Łańcuch jest typem odniesienia, mimo że ma większość cech typu wartości, takich jak niezmienność i przeciążenie == w celu porównania tekstu zamiast upewnienia się, że odnoszą się do tego samego obiektu.
Dlaczego zatem łańcuch nie jest tylko typem wartości?
c#
string
clr
value-type
reference-type
Davy8
źródło
źródło
is
testy), odpowiedź jest prawdopodobnie „z przyczyn historycznych”. Wydajność kopiowania nie może być przyczyną, ponieważ nie ma potrzeby fizycznego kopiowania niezmiennych obiektów. Teraz nie można zmienić bez zerwania kodu, który faktycznie używais
czeków (lub podobnych ograniczeń).std::string
zachowaniu się jak kolekcja jest starym błędem, którego nie można teraz naprawić.Odpowiedzi:
Ciągi nie są typami wartości, ponieważ mogą być ogromne i muszą być przechowywane na stercie. Typy wartości są (we wszystkich implementacjach CLR jak dotąd) przechowywane na stosie. Ciągi alokujące stos psują różne rzeczy: stos ma tylko 1 MB dla wersji 32-bitowej i 4 MB dla wersji 64-bitowej, będziesz musiał umieścić każdy łańcuch, ponosząc karę za kopiowanie, nie możesz internować łańcuchów i użycia pamięci balonem itp.
(Edycja: Dodano wyjaśnienie na temat przechowywania typów wartości będących szczegółami implementacji, co prowadzi do sytuacji, w której mamy typ z wartościami matematycznymi nie dziedziczącymi po System.ValueType. Dzięki Ben.)
źródło
String
nie ma zmiennej wielkości. Po dodaniu do niego w rzeczywistości tworzysz innyString
obiekt, przydzielając mu nową pamięć.Int32
ma zawsze 4 bajty, dlatego kompilator przydziela 4 bajty za każdym razem, gdy definiujesz zmienną łańcuchową. Ile pamięci powinien przydzielić kompilator, gdy napotkaint
zmienną (jeśli byłaby to typ wartości)? Zrozum, że wartość nie została jeszcze przypisana w tym czasie.Int32
ma zawsze 4 bajty, dlatego kompilator przydziela 4 bajty za każdym razem, gdy definiujeszint
zmienną. Ile pamięci powinien przydzielić kompilator, gdy napotkastring
zmienną (gdyby był to typ wartości)? Zrozum, że wartość nie została jeszcze przypisana w tym czasie.Nie jest to typ wartości, ponieważ wydajność (przestrzeń i czas!) Byłaby straszna, gdyby był typem wartości, a jego wartość musiałaby być kopiowana za każdym razem, gdy był przekazywany i zwracany z metod itp.
Ma semantykę wartości, aby utrzymać świat przy zdrowych zmysłach. Czy możesz sobie wyobrazić, jak trudno byłoby kodować, jeśli
ustawić
b
sięfalse
? Wyobraź sobie, jak trudne byłoby kodowanie w prawie każdej aplikacji.źródło
new String("foo");
i inninew String("foo")
mogą oceniać w tym samym referencji, jakiego rodzaju nie jest to, czego oczekuje się odnew
operatora. (Czy możesz mi powiedzieć przypadek, w którym chciałbym porównać odniesienia?)ReferenceEquals(x, y)
jest szybkim testem i możesz natychmiast zwrócić 0, a po zmieszaniu z testem zerowym nie dodaje on nawet więcej pracy.string
mogłaby zachowywać się jak pusty ciąg znaków (jak w systemach wcześniejszych niż.net), a nie jako odwołanie zerowe. Właściwie, wolę mieć typ wartości,String
który zawiera typ referencyjnyNullableString
, przy czym ten pierwszy ma wartość domyślną równoważną,String.Empty
a drugi domyślnąnull
, i ze specjalnymi regułami boksu / rozpakowania (takimi jak boksowanie domyślnego- wycenionyNullableString
dałby odniesienie doString.Empty
).Rozróżnienie między typami referencyjnymi a typami wartości jest w zasadzie kompromisem wydajnościowym w projekcie języka. Typy referencyjne mają pewne koszty związane z budową i zniszczeniem oraz wyrzucaniem elementów bezużytecznych, ponieważ są tworzone na stercie. Z drugiej strony typy wartości mają narzut na wywołania metod (jeśli rozmiar danych jest większy niż wskaźnik), ponieważ cały obiekt jest kopiowany, a nie tylko wskaźnik. Ponieważ łańcuchy mogą być (i zwykle są) znacznie większe niż rozmiar wskaźnika, są one zaprojektowane jako typy referencyjne. Ponadto, jak wskazał Servy, rozmiar typu wartości musi być znany w czasie kompilacji, co nie zawsze ma miejsce w przypadku łańcuchów.
Kwestia zmienności jest osobną kwestią. Zarówno typy referencyjne, jak i typy wartości mogą być zmienne lub niezmienne. Typy wartości są jednak zwykle niezmienne, ponieważ semantyka zmiennych typów może być myląca.
Typy referencyjne są generalnie zmienne, ale można je zaprojektować jako niezmienne, jeśli ma to sens. Ciągi są zdefiniowane jako niezmienne, ponieważ umożliwia pewne optymalizacje. Na przykład, jeśli ten sam literał ciągu występuje wiele razy w tym samym programie (co jest dość powszechne), kompilator może ponownie użyć tego samego obiektu.
Dlaczego więc „==” jest przeciążone, aby porównać ciągi tekstowe? Ponieważ jest to najbardziej przydatna semantyka. Jeśli dwa ciągi tekstowe są równe, mogą, ale nie muszą być tym samym odwołaniem do obiektu ze względu na optymalizacje. Porównywanie referencji jest więc bezużyteczne, a porównywanie tekstu prawie zawsze jest tym, czego chcesz.
Mówiąc bardziej ogólnie, ciągi mają tak zwaną semantykę wartości . Jest to koncepcja bardziej ogólna niż typy wartości, która jest szczegółem implementacji specyficznym dla języka C #. Typy wartości mają semantykę wartości, ale typy referencyjne mogą również mieć semantykę wartości. Gdy typ ma semantykę wartości, nie można tak naprawdę stwierdzić, czy podstawowa implementacja jest typem referencyjnym czy typem wartości, więc można uznać, że szczegół implementacji.
źródło
string
typ musiałby mieć bufor char o pewnym stałym rozmiarze, który byłby zarówno restrykcyjny, jak i wysoce nieefektywny.To późna odpowiedź na stare pytanie, ale wszystkie inne odpowiedzi nie mają sensu, to znaczy, że .NET nie miał generycznych aż do .NET 2.0 w 2005 roku.
String
jest typem referencyjnym zamiast typu wartości, ponieważ dla Microsoftu kluczowe znaczenie miało zapewnienie, aby ciągi mogły być przechowywane w najbardziej efektywny sposób w kolekcjach innych niż ogólne , takich jakSystem.Collections.ArrayList
.Przechowywanie typu wartości w kolekcji innej niż ogólna wymaga specjalnej konwersji na typ
object
nazywany boksem. Gdy CLR zawiera typ wartości, otacza wartość wewnątrz aSystem.Object
i przechowuje ją na zarządzanej stercie.Odczyt wartości z kolekcji wymaga operacji odwrotnej, która nazywa się rozpakowaniem.
Zarówno boks, jak i rozpakowanie mają niemały wpływ: boks wymaga dodatkowego przydziału, rozpakowanie wymaga sprawdzenia typu.
Niektóre odpowiedzi twierdzą, że niepoprawnie
string
nie mogły zostać zaimplementowane jako typ wartości, ponieważ jej rozmiar jest zmienny. W rzeczywistości łatwo jest zaimplementować ciąg jako strukturę danych o stałej długości przy użyciu strategii optymalizacji małych ciągów: ciągi byłyby przechowywane w pamięci bezpośrednio jako sekwencja znaków Unicode, z wyjątkiem dużych ciągów, które byłyby przechowywane jako wskaźnik do bufora zewnętrznego. Obie reprezentacje można zaprojektować tak, aby miały tę samą stałą długość, tj. Rozmiar wskaźnika.Gdyby istniały generyczne od samego początku, prawdopodobnie ciąg znaków jako typ wartości byłby prawdopodobnie lepszym rozwiązaniem, z prostszą semantyką, lepszym wykorzystaniem pamięci i lepszą lokalizacją pamięci podręcznej. A
List<string>
zawierające tylko małe struny mogło być jeden ciągły blok pamięci.źródło
string
zawiera tylko swój rozmiar i wskaźnik dochar
tablicy, więc nie byłby to „ogromny typ wartości”. Ale jest to prosty, istotny powód tej decyzji projektowej. Dzięki!Nie tylko łańcuchy są niezmiennymi typami referencyjnymi. Delegaci z wielu obsad także. Dlatego można bezpiecznie pisać
Przypuszczam, że ciągi są niezmienne, ponieważ jest to najbezpieczniejsza metoda pracy z nimi i przydzielania pamięci. Dlaczego nie są to typy wartości? Poprzedni autorzy mają rację co do wielkości stosu itp. Dodałbym również, że tworzenie ciągów jako typów referencyjnych pozwala zaoszczędzić na rozmiarze zestawu, gdy używasz tego samego stałego ciągu w programie. Jeśli zdefiniujesz
Możliwe, że oba wystąpienia stałej „mój ciąg” zostaną przydzielone w twoim zestawie tylko raz.
Jeśli chcesz zarządzać ciągami jak zwykle typem odwołania, umieść ciąg w nowym StringBuilder (ciągach). Lub użyj MemoryStreams.
Jeśli chcesz utworzyć bibliotekę, w której oczekujesz, że w twoich funkcjach będą przekazywane ogromne ciągi, zdefiniuj parametr jako StringBuilder lub jako Stream.
źródło
Ponadto sposób implementacji ciągów znaków (inny dla każdej platformy) i rozpoczęcie ich łączenia. Jak za pomocą
StringBuilder
. Przydziela bufor do skopiowania, gdy dotrzesz do końca, przydziela ci jeszcze więcej pamięci, mając nadzieję, że jeśli wykonasz dużą konkatenację, wydajność nie będzie utrudniona.Może Jon Skeet może tu pomóc?
źródło
Jest to głównie problem z wydajnością.
Posługiwanie się łańcuchami typu LIKE pomaga w pisaniu kodu, ale posiadanie go BE typu wartości spowodowałoby ogromny spadek wydajności.
Aby uzyskać dogłębne spojrzenie, rzuć okiem na fajny artykuł na temat ciągów znaków w środowisku .net.
źródło
W bardzo prostych słowach każdą wartość, która ma określony rozmiar, można traktować jako typ wartości.
źródło
Jak
string
rozpoznać typ odniesienia? Nie jestem pewien, czy ma to znaczenie, jak to jest realizowane. Ciągi w C # są niezmienne, więc nie musisz się martwić tym problemem.źródło
W rzeczywistości łańcuchy mają bardzo niewiele podobieństw do typów wartości. Na początek, nie wszystkie typy wartości są niezmienne, możesz zmienić wartość Int32, jak chcesz, i nadal będzie to ten sam adres na stosie.
Ciągi są niezmienne z bardzo dobrego powodu, nie ma to nic wspólnego z tym, że jest typem referencyjnym, ale ma wiele wspólnego z zarządzaniem pamięcią. Po prostu bardziej efektywne jest tworzenie nowego obiektu, gdy zmienia się rozmiar łańcucha, niż przenoszenie rzeczy na zarządzanej stercie. Myślę, że łączysz ze sobą typy wartości / referencji i niezmienne obiekty.
O ile „==”: tak jak powiedziałeś „==” to przeciążenie operatora, i ponownie zostało zaimplementowane z bardzo dobrego powodu, aby uczynić strukturę bardziej przydatną podczas pracy z łańcuchami.
źródło
To nie jest tak proste, jak Ciągi składają się z tablic znaków. Patrzę na ciągi jako tablice znaków []. Dlatego znajdują się na stercie, ponieważ referencyjna lokalizacja pamięci jest przechowywana na stosie i wskazuje początek lokalizacji pamięci tablicy na stercie. Rozmiar łańcucha nie jest znany przed przydzieleniem ... idealny dla sterty.
Właśnie dlatego ciąg znaków jest niezmienny, ponieważ kiedy go zmienisz, nawet jeśli ma ten sam rozmiar, kompilator nie wie o tym i musi przydzielić nową tablicę i przypisać znaki do pozycji w tablicy. Ma to sens, jeśli myślisz o ciągach jako sposobie, w jaki języki chronią cię przed koniecznością alokacji pamięci w locie (czytaj C jak programowanie)
źródło
Ryzykuje kolejne tajemnicze głosowanie w dół ... fakt, że wielu wspomina o stosie i pamięci w odniesieniu do typów wartości i typów pierwotnych, ponieważ muszą zmieścić się w rejestrze mikroprocesora. Nie możesz pchać ani wrzucać czegoś do / ze stosu, jeśli zajmuje więcej bitów niż rejestr ma .... instrukcje to na przykład „pop eax” - ponieważ eax ma 32 bity szerokości w systemie 32-bitowym.
Typy pierwotne zmiennoprzecinkowe są obsługiwane przez FPU o szerokości 80 bitów.
To wszystko zostało postanowione na długo przed pojawieniem się języka OOP, który zaciemniałby definicję typu pierwotnego i zakładam, że typ wartości jest terminem, który został stworzony specjalnie dla języków OOP.
źródło