Czytam dokumentację i ciągle kręcę głową przy niektórych decyzjach projektowych języka. Ale to, co naprawdę mnie zaskoczyło, to sposób obsługi tablic.
Pospieszyłem na plac zabaw i wypróbowałem je. Możesz też spróbować. Tak więc pierwszy przykład:
var a = [1, 2, 3]
var b = a
a[1] = 42
a
b
Tutaj a
i b
są oba [1, 42, 3]
, które mogę zaakceptować. Tablice są przywoływane - OK!
Teraz zobacz ten przykład:
var c = [1, 2, 3]
var d = c
c.append(42)
c
d
c
jest [1, 2, 3, 42]
ALE d
jest [1, 2, 3]
. To znaczy, d
widziałem zmianę w ostatnim przykładzie, ale nie widzi tego w tym przykładzie. Dokumentacja mówi, że to dlatego, że długość się zmieniła.
A co powiesz na ten:
var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e
f
e
jest [4, 5, 3]
, co jest fajne. Fajnie jest mieć zamiennik z wieloma indeksami, ale f
STILL nie widzi zmiany, nawet jeśli długość się nie zmieniła.
Podsumowując, wspólne odwołania do tablicy widzą zmiany, jeśli zmienisz 1 element, ale jeśli zmienisz wiele elementów lub dodasz elementy, tworzona jest kopia.
To wydaje mi się bardzo kiepskim projektem. Czy mam rację, myśląc o tym? Czy istnieje powód, dla którego nie rozumiem, dlaczego tablice powinny tak działać?
EDYCJA : Tablice uległy zmianie i mają teraz semantykę wartości. O wiele bardziej rozsądny!
std::shared_ptr
nie ma wersji nieatomowej, odpowiedź była oparta na faktach, a nie na opinii (faktem jest, że komitet rozważał to, ale nie chciał go z różnych powodów).Odpowiedzi:
Zauważ, że semantyka i składnia tablicy została zmieniona w wersji Xcode beta 3 ( post na blogu ), więc pytanie nie ma już zastosowania. Poniższa odpowiedź dotyczy wersji beta 2:
To ze względu na wydajność. Zasadniczo starają się unikać kopiowania tablic tak długo, jak to możliwe (i twierdzą, że są „podobne do C”). Aby zacytować książkę językową :
Zgadzam się, że jest to trochę mylące, ale przynajmniej istnieje jasny i prosty opis tego, jak to działa.
Ta sekcja zawiera także informacje o tym, jak upewnić się, że tablica jest unikatowo przywoływana, jak wymusić kopiowanie tablic i jak sprawdzić, czy dwie tablice współużytkują pamięć.
źródło
Z oficjalnej dokumentacji języka Swift :
Przeczytaj całą sekcję Przypisanie i zachowanie kopiowania dla tablic w tej dokumentacji. Przekonasz się, że po wymianie zakresu elementów w tablicy, tablica pobiera kopię siebie dla wszystkich elementów.
źródło
Zachowanie zmieniło się w Xcode 6 beta 3. Tablice nie są już typami referencyjnymi i mają mechanizm kopiowania przy zapisie , co oznacza, że jak tylko zmienisz zawartość tablicy z jednej lub drugiej zmiennej, tablica zostanie skopiowana i tylko jedna kopia zostanie zmieniona.
Stara odpowiedź:
Jak zauważyli inni, Swift stara się unikać kopiowania tablic, jeśli to możliwe, w tym przy zmianie wartości pojedynczych indeksów na raz.
Jeśli chcesz mieć pewność, że zmienna tablicowa (!) Jest unikalna, tzn. Że nie jest współużytkowana z inną zmienną, możesz wywołać tę
unshare
metodę. Spowoduje to skopiowanie tablicy, chyba że ma ona tylko jedno odwołanie. Oczywiście możesz także wywołaćcopy
metodę, która zawsze utworzy kopię, ale preferowane jest nieudostępnianie, aby upewnić się, że żadna inna zmienna nie trzyma się tej samej tablicy.źródło
unshare()
metoda jest niezdefiniowana.Zachowanie jest bardzo podobne do
Array.Resize
metody w .NET. Aby zrozumieć, co się dzieje, pomocne może być przejrzenie historii.
tokena w C, C ++, Java, C # i Swift.W C struktura jest niczym więcej niż agregacją zmiennych. Zastosowanie
.
do zmiennej typu struktury spowoduje dostęp do zmiennej przechowywanej w strukturze. Wskaźniki do obiektów nie przechowują agregacji zmiennych, ale je identyfikują . Jeśli ktoś ma wskaźnik, który identyfikuje strukturę,->
operator może być wykorzystany do uzyskania dostępu do zmiennej przechowywanej w strukturze identyfikowanej przez wskaźnik.W C ++ struktury i klasy nie tylko agregują zmienne, ale także mogą do nich dołączać kod. Użycie
.
do wywołania metody spowoduje, że zmienna poprosi tę metodę o działanie na zawartość samej zmiennej ; użycie->
zmiennej identyfikującej obiekt spowoduje, że ta metoda będzie działać na obiekt zidentyfikowany przez zmienną.W Javie wszystkie niestandardowe typy zmiennych po prostu identyfikują obiekty, a wywołanie metody na zmiennej powie metodzie, który obiekt jest identyfikowany przez zmienną. Zmienne nie mogą bezpośrednio przechowywać żadnego rodzaju złożonego typu danych, ani nie ma żadnych środków, za pomocą których metoda może uzyskać dostęp do zmiennej, na której jest wywoływana. Ograniczenia te, chociaż semantycznie ograniczające, znacznie upraszczają środowisko wykonawcze i ułatwiają sprawdzanie poprawności kodu bajtowego; takie uproszczenia zmniejszyły nakłady związane z Javą w czasie, gdy rynek był wrażliwy na takie problemy, a tym samym pomogły jej zdobyć przyczepność na rynku. Oznaczały także, że nie było potrzeby używania tokena równoważnego z
.
używanym w C lub C ++. Chociaż Java mogła być używana->
w taki sam sposób jak C i C ++, twórcy zdecydowali się na użycie jednego znaku.
ponieważ nie był potrzebny do żadnego innego celu.W języku C # i innych językach .NET zmienne mogą albo identyfikować obiekty, albo bezpośrednio przechowywać złożone typy danych. W przypadku użycia zmiennej o złożonym typie danych
.
działa na zawartość zmiennej; zastosowany do zmiennej typu odniesienia.
działa na zidentyfikowany obiektprzez to. W przypadku niektórych rodzajów operacji rozróżnienie semantyczne nie jest szczególnie ważne, ale w przypadku innych jest tak. Najbardziej problematyczne są sytuacje, w których metoda złożonego typu danych, która zmodyfikowałaby zmienną, na której jest wywoływana, jest wywoływana na zmiennej tylko do odczytu. Jeśli zostanie podjęta próba wywołania metody na wartości lub zmiennej tylko do odczytu, kompilatory zazwyczaj skopiują zmienną, pozwól metodzie na nią zareagować i odrzuć zmienną. Jest to ogólnie bezpieczne w przypadku metod, które tylko odczytują zmienną, ale nie jest bezpieczne w przypadku metod, które do niej piszą. Niestety, .does nie ma jak dotąd żadnych środków wskazujących, które metody można bezpiecznie zastosować z takim zastąpieniem, a które nie.W Swift metody agregujące mogą wyraźnie wskazać, czy zmodyfikują zmienną, na której są wywoływane, a kompilator zabrania używania metod mutacji zmiennych tylko do odczytu (zamiast zmutować tymczasowe kopie zmiennej, które następnie wyrzucić). Z powodu tego rozróżnienia używanie
.
tokena do wywoływania metod modyfikujących zmienne, na które są wywoływane, jest znacznie bezpieczniejsze w Swift niż w .NET. Niestety fakt, że.
do tego celu wykorzystywany jest ten sam token, który działa na obiekt zewnętrzny zidentyfikowany przez zmienną, oznacza, że istnieje możliwość pomyłki.Gdyby dysponował maszyną czasu i wrócił do tworzenia C # i / lub Swift, można by z mocą wsteczną uniknąć wielu nieporozumień związanych z takimi problemami, ponieważ języki używają tokenów
.
i->
w sposób znacznie zbliżony do użycia C ++. Metody zarówno agregatów, jak i typów referencyjnych mogłyby posłużyć.
do działania na zmienną, na której zostały wywołane, i->
na wartość (dla kompozytów) lub na rzecz zidentyfikowanej w ten sposób (dla typów referencyjnych). Żaden język nie jest jednak tak zaprojektowany.W języku C # normalną praktyką dla metody modyfikacji zmiennej, na której jest ona wywoływana, jest przekazywanie zmiennej jako
ref
parametru do metody. W ten sposób wywołanieArray.Resize(ref someArray, 23);
podczassomeArray
identyfikacji tablicy 20 elementów spowodujesomeArray
zidentyfikowanie nowej tablicy 23 elementów, bez wpływu na tablicę oryginalną. Użycie parametruref
wyjaśnia, że należy oczekiwać, że metoda zmodyfikuje zmienną, na której jest wywoływana. W wielu przypadkach korzystna jest możliwość modyfikowania zmiennych bez konieczności używania metod statycznych; Szybkie adresy, co oznacza przy użyciu.
składni. Wadą jest to, że traci wyjaśnienie, jakie metody działają na zmienne, a które na wartości.źródło
Dla mnie ma to większy sens, jeśli najpierw zastąpisz swoje stałe zmiennymi:
Pierwszy wiersz nigdy nie musi zmieniać rozmiaru
a
. W szczególności nigdy nie musi dokonywać alokacji pamięci. Bez względu na wartośći
jest to lekka operacja. Jeśli wyobrażasz sobie, że pod maskąa
znajduje się wskaźnik, może on być wskaźnikiem stałym.Druga linia może być znacznie bardziej skomplikowana. W zależności od wartości
i
ij
może być konieczne zarządzanie pamięcią. Jeśli wyobrażasz sobie, żee
jest to wskaźnik wskazujący na zawartość tablicy, nie możesz dłużej zakładać, że jest to wskaźnik stały; może być konieczne przydzielenie nowego bloku pamięci, skopiowanie danych ze starego bloku pamięci do nowego bloku pamięci i zmiana wskaźnika.Wygląda na to, że projektanci języków starali się zachować możliwie niską wagę (1). Ponieważ (2) może i tak wymagać kopiowania, skorzystali z rozwiązania, które zawsze działa tak, jakbyś zrobił kopię.
Jest to skomplikowane, ale cieszę się, że nie sprawiły, że stało się to jeszcze bardziej skomplikowane, np. Ze specjalnymi przypadkami, takimi jak „jeśli w (2) i oraz j są stałymi czasami kompilacji, a kompilator może wywnioskować, że rozmiar e nie będzie się zmieniał zmienić, wtedy nie kopiujemy ” .
Wreszcie, bazując na moim zrozumieniu zasad projektowania języka Swift, myślę, że ogólne zasady są następujące:
let
Domyślnie używaj stałych ( ) zawsze wszędzie i nie będzie żadnych większych niespodzianek.var
) tylko wtedy, gdy jest to absolutnie konieczne, i zachowaj ostrożność w tych przypadkach, ponieważ będą niespodzianki [tutaj: dziwne niejawne kopie tablic w niektórych, ale nie we wszystkich sytuacjach].źródło
To, co znalazłem, to: tablica będzie modyfikowalną kopią tej, do której istnieje odwołanie , tylko wtedy, gdy operacja może potencjalnie zmienić długość tablicy . W ostatnim przykładzie
f[0..2]
indeksowania wieloma operacja może potencjalnie zmienić jej długość (być może duplikaty nie są dozwolone), więc jest kopiowana.źródło
var
tablice są teraz całkowicie zmienne, alet
tablice są całkowicie niezmienne.Ciągi i tablice Delphi miały dokładnie tę samą „cechę”. Kiedy spojrzałeś na implementację, miało to sens.
Każda zmienna jest wskaźnikiem do pamięci dynamicznej. Ta pamięć zawiera liczbę referencji, po której następują dane w tablicy. Możesz więc łatwo zmienić wartość w tablicy bez kopiowania całej tablicy lub zmiany jakichkolwiek wskaźników. Jeśli chcesz zmienić rozmiar tablicy, musisz przydzielić więcej pamięci. W takim przypadku bieżąca zmienna będzie wskazywać na nowo przydzieloną pamięć. Ale nie możesz łatwo wyśledzić wszystkich innych zmiennych, które wskazywały na pierwotną tablicę, więc zostaw je w spokoju.
Oczywiście nie byłoby trudno dokonać bardziej spójnej implementacji. Jeśli chcesz, aby wszystkie zmienne miały rozmiar, wykonaj następujące czynności: Każda zmienna jest wskaźnikiem do kontenera przechowywanego w pamięci dynamicznej. Kontener zawiera dokładnie dwie rzeczy: liczbę referencji i wskaźnik do rzeczywistych danych tablicy. Dane tablicy są przechowywane w osobnym bloku pamięci dynamicznej. Teraz jest tylko jeden wskaźnik do danych tablicy, więc możesz łatwo zmienić jego rozmiar, a wszystkie zmienne zobaczą zmianę.
źródło
Wielu wczesnych użytkowników Swift narzekało na tę podatną na błędy semantykę tablicy, a Chris Lattner napisał, że semantyka tablicy została zmieniona, aby zapewnić semantykę pełnej wartości ( łącze Apple Developer dla tych, którzy mają konto ). Będziemy musieli przynajmniej poczekać na następną wersję beta, aby zobaczyć, co to dokładnie oznacza.
źródło
Używam do tego .copy ().
źródło
Czy coś się zmieniło w zachowaniu tablic w późniejszych wersjach Swift? Właśnie uruchamiam twój przykład:
A moje wyniki to [1, 42, 3] i [1, 2, 3]
źródło