Czy istnieje powód, dla którego przypisanie tablicy Swift jest niespójne (ani odwołanie, ani głęboka kopia)?

216

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 ai bsą 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

cjest [1, 2, 3, 42]ALE djest [1, 2, 3]. To znaczy, dwidział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

ejest [4, 5, 3], co jest fajne. Fajnie jest mieć zamiennik z wieloma indeksami, ale fSTILL 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!

Cthutu
źródło
95
Dla przypomnienia, nie sądzę, że to pytanie powinno zostać zamknięte. Swift jest nowym językiem, więc przez jakiś czas będą pojawiać się takie pytania. Uważam to pytanie za bardzo interesujące i mam nadzieję, że ktoś będzie miał ważną sprawę w obronie.
Joel Berger
4
@Joel Fine, zapytaj programistów, Przepełnienie stosu dotyczy konkretnych problemów programistycznych.
bjb568,
21
@ bjb568: Ale to nie jest opinia. Na to pytanie należy odpowiedzieć faktami. Jeśli pojawi się jakiś programista Swift i odpowie „Tak zrobiliśmy w przypadku X, Y i Z”, to jest to prosty fakt. Możesz nie zgadzać się z X, Y i Z, ale jeśli decyzja została podjęta dla X, Y i Z, to tylko historyczny fakt projektu języka. Podobnie jak kiedy zapytałem, dlaczego std::shared_ptrnie 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).
Cornstalks
7
@JasonMArcher: Tylko ostatni akapit opiera się na opiniach (które, może, powinny zostać usunięte). Rzeczywisty tytuł pytania (który uważam za samo pytanie) odpowiada faktami. Jest to powód, tablice zostały zaprojektowane do pracy sposób ich pracy.
Cornstalks
7
Tak, jak powiedział API-Beast, zwykle nazywa się to „kopiowaniem na pół assed-Language-Design”.
R. Martinho Fernandes,

Odpowiedzi:

109

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ą :

W przypadku tablic kopiowanie odbywa się tylko wtedy, gdy wykonasz akcję, która może zmodyfikować długość tablicy. Obejmuje to dodawanie, wstawianie lub usuwanie elementów lub używanie indeksu dolnego do zastępowania zakresu elementów w tablicy.

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ęć.

Lukas
źródło
61
Uważam, że zarówno cofasz udostępnianie, jak i kopiujesz WIELKĄ czerwoną flagę w projekcie.
Cthutu
9
To jest poprawne. Inżynier opisał mi, że przy projektowaniu języka nie jest to pożądane i jest to coś, co mają nadzieję „naprawić” w nadchodzących aktualizacjach Swift. Głosuj za pomocą radarów.
Erik Kerber,
2
To po prostu coś takiego jak kopiowanie przy zapisie (COW) w zarządzaniu pamięcią procesu potomnego w systemie Linux, prawda? Być może możemy nazwać to kopiowaniem na zmianę długości (COLA). Widzę to jako pozytywny projekt.
justhalf
3
@ justhalf Mogę przewidzieć grupę zdezorientowanych początkujących przybywających do SO i pytających, dlaczego ich tablice były / nie były udostępniane (po prostu w mniej jasny sposób).
John Dvorak,
11
@justhalf: COW jest i tak pesymizacją we współczesnym świecie, a po drugie, COW jest techniką tylko do implementacji, a te rzeczy COLA prowadzą do całkowicie losowego udostępniania i udostępniania.
Szczeniak
25

Z oficjalnej dokumentacji języka Swift :

Zauważ, że tablica nie jest kopiowana po ustawieniu nowej wartości za pomocą składni indeksu dolnego, ponieważ ustawienie pojedynczej wartości za pomocą składni indeksu dolnego nie ma możliwości zmiany długości tablicy. Jeśli jednak dodasz nowy element do tablicy, zmodyfikujesz długość tablicy . To powoduje, że Swift tworzy nową kopię tablicy w punkcie, w którym dodajesz nową wartość. Odtąd a jest osobną, niezależną kopią tablicy .....

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.

iPatel
źródło
4
Dzięki. Odniosłem się do tego tekstu niejasno w swoim pytaniu. Ale pokazałem przykład, w którym zmiana zakresu indeksu dolnego nie zmieniła długości i nadal jest kopiowana. Więc jeśli nie chcesz kopii, musisz ją zmieniać pojedynczo.
Cthutu,
21

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ę unsharemetodę. Spowoduje to skopiowanie tablicy, chyba że ma ona tylko jedno odwołanie. Oczywiście możesz także wywołać copymetodę, która zawsze utworzy kopię, ale preferowane jest nieudostępnianie, aby upewnić się, że żadna inna zmienna nie trzyma się tej samej tablicy.

var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a               // [1, 42, 3]
b               // [1, 2, 3]
Pascal
źródło
hmm, dla mnie ta unshare()metoda jest niezdefiniowana.
Hlung
1
@Hlung Został on usunięty w wersji beta 3, zaktualizowałem swoją odpowiedź.
Pascal
12

Zachowanie jest bardzo podobne do Array.Resizemetody 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 refparametru do metody. W ten sposób wywołanie Array.Resize(ref someArray, 23);podczas someArrayidentyfikacji tablicy 20 elementów spowoduje someArrayzidentyfikowanie nowej tablicy 23 elementów, bez wpływu na tablicę oryginalną. Użycie parametru refwyjaś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.

supercat
źródło
5

Dla mnie ma to większy sens, jeśli najpierw zastąpisz swoje stałe zmiennymi:

a[i] = 42            // (1)
e[i..j] = [4, 5]     // (2)

Pierwszy wiersz nigdy nie musi zmieniać rozmiaru a. W szczególności nigdy nie musi dokonywać alokacji pamięci. Bez względu na wartość ijest to lekka operacja. Jeśli wyobrażasz sobie, że pod maską aznajduje 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 ii jmoże być konieczne zarządzanie pamięcią. Jeśli wyobrażasz sobie, że ejest 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:

  • letDomyślnie używaj stałych ( ) zawsze wszędzie i nie będzie żadnych większych niespodzianek.
  • Używaj zmiennych ( 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].
Jukka Suomela
źródło
5

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.

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e // 4,5,3
f // 1,2,3


var e1 = [1, 2, 3]
var f1 = e1

e1[0] = 4
e1[1] = 5

e1 //  - 4,5,3
f1 // - 4,5,3
Kumar KL
źródło
8
„traktowane, jak zmieniła się długość” Mogę zrozumieć, że zostanie skopiowane, jeśli długość zostanie zmieniona, ale w połączeniu z powyższym cytatem myślę, że jest to naprawdę niepokojąca „cecha” i myślę, że wiele osób pomyli się
Joel Berger
25
To, że język jest nowy, nie oznacza, że ​​może zawierać rażące wewnętrzne sprzeczności.
Wyścigi lekkości na orbicie
Zostało to „naprawione” w wersji beta 3, vartablice są teraz całkowicie zmienne, a lettablice są całkowicie niezmienne.
Pascal
4

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ę.

Trade-Ideas Philip
źródło
4

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.

Gael
źródło
1
Nowe zachowanie tablicy jest teraz dostępne od zestawu SDK dołączonego do systemu iOS 8 / Xcode 6 Beta 3.
smileyborg
0

Używam do tego .copy ().

    var a = [1, 2, 3]
    var b = a.copy()
     a[1] = 42 
Preetham
źródło
1
Gdy uruchamiam kod,
pojawia
0

Czy coś się zmieniło w zachowaniu tablic w późniejszych wersjach Swift? Właśnie uruchamiam twój przykład:

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b

A moje wyniki to [1, 42, 3] i [1, 2, 3]

jreft56
źródło