Szybka i mutująca struktura

97

Jest coś, czego nie do końca rozumiem, jeśli chodzi o mutowanie typów wartości w języku Swift.

Jak stwierdza iBook „Swift Programming Language”: Domyślnie właściwości typu wartości nie mogą być modyfikowane z poziomu jego metod instancji.

Aby było to możliwe, możemy zadeklarować metody ze mutatingsłowem kluczowym wewnątrz struktur i wyliczeń.

Rzecz, która nie jest dla mnie do końca jasna, jest taka: możesz zmienić var ​​z zewnątrz struktury, ale nie możesz tego zmienić za pomocą jego własnych metod. Wydaje mi się to sprzeczne z intuicją, ponieważ w językach zorientowanych obiektowo na ogół próbuje się hermetyzować zmienne, aby można je było zmieniać tylko od wewnątrz. W przypadku struktur wydaje się, że jest odwrotnie. Aby to rozwinąć, oto fragment kodu:

struct Point {
    var x = 0, y = 0
    mutating func moveToX(x: Int, andY y:Int) { //Needs to be a mutating method in order to work
        self.x = x
        self.y = y
    }
}

var p = Point(x: 1, y: 2)
p.x = 3 //Works from outside the struct!
p.moveToX(5, andY: 5) 

Czy ktoś zna powód, dla którego struktury nie mogą zmieniać swojej zawartości z własnego kontekstu, podczas gdy zawartość można łatwo zmienić w innym miejscu?

Mike Seghers
źródło

Odpowiedzi:

81

Atrybut zmienności jest oznaczony w magazynie (stałym lub zmiennym), a nie typie. Możesz pomyśleć, że struct ma dwa tryby: zmienny i niezmienny . Jeśli przypiszesz wartość struktury do niezmiennego magazynu (nazywamy go letlub stałą w języku Swift), wartość stanie się niezmiennym trybem i nie możesz zmienić żadnego stanu wartości. (w tym wywołanie dowolnej metody mutującej)

Jeśli wartość jest przypisana do magazynu zmiennego (nazywamy go varlub zmienną w Swift), możesz dowolnie modyfikować ich stan, a wywołanie metody mutującej jest dozwolone.

Ponadto klasy nie mają tego niezmiennego / zmiennego trybu. IMO, dzieje się tak dlatego, że klasy są zwykle używane do reprezentowania jednostki referencyjnej . A jednostka zdolna do odniesienia jest zwykle zmienna, ponieważ bardzo trudno jest tworzyć i zarządzać wykresami referencyjnymi jednostek w niezmienny sposób z odpowiednią wydajnością. Mogą dodać tę funkcję później, ale przynajmniej nie teraz.

Dla programistów Objective-C pojęcia mutowalne / niezmienne są bardzo dobrze znane. W Objective-C mieliśmy dwie oddzielne klasy dla każdej koncepcji, ale w Swift można to zrobić za pomocą jednej struktury. Połowa pracy.

Dla programistów C / C ++ jest to również bardzo znana koncepcja. Dokładnie to constrobi słowo kluczowe w C / C ++.

Niezmienną wartość można również bardzo ładnie zoptymalizować. Teoretycznie kompilator Swift (lub LLVM) może wykonywać elizję kopiowania na przekazywanych wartościach let, tak jak w C ++. Jeśli mądrze użyjesz niezmiennej struktury, osiągnie ona lepsze wyniki niż klasy zliczane.

Aktualizacja

Ponieważ @Joseph twierdził, że to nie wyjaśnia dlaczego , dodaję trochę więcej.

Struktury mają dwa rodzaje metod. proste i mutujące metody. Zwykły sposób zakłada niezmienny (lub braku mutagennym) . Ta separacja istnieje tylko po to, aby wspierać niezmienną semantykę. Obiekt w trybie niezmiennym nie powinien w ogóle zmieniać swojego stanu.

Następnie niezmienne metody muszą gwarantować tę semantyczną niezmienność . Co oznacza, że ​​nie powinien zmieniać żadnej wartości wewnętrznej. Dlatego kompilator nie zezwala na jakiekolwiek zmiany stanu samego siebie w niezmiennej metodzie. W przeciwieństwie do metod mutacji można dowolnie modyfikować stany.

A potem możesz mieć pytanie, dlaczego niezmienność jest wartością domyślną? Dzieje się tak, ponieważ bardzo trudno jest przewidzieć przyszły stan mutowania wartości, co zwykle staje się głównym źródłem bólów głowy i błędów. Wiele osób zgodziło się, że rozwiązaniem jest unikanie elementów podlegających zmianom, a następnie niemodyfikowalność domyślnie znajdowała się na szczycie listy życzeń od dziesięcioleci w językach z rodziny C / C ++ i ich pochodnych.

Zobacz czysto funkcjonalny styl, aby uzyskać więcej informacji. W każdym razie nadal potrzebujemy zmiennych elementów, ponieważ niezmienne elementy mają pewne słabości, a dyskusja o nich wydaje się być poza tematem.

Mam nadzieję, że to pomoże.

eonil
źródło
2
Zasadniczo mogę to zinterpretować w ten sposób: struktura nie wie z góry, czy będzie zmienna, czy niezmienna, więc zakłada niezmienność, chyba że stwierdzono inaczej. Widziane w ten sposób ma to sens. Czy to jest poprawne?
Mike Seghers
1
@MikeSeghers Myślę, że to ma sens.
eonil
3
Chociaż jest to jak dotąd najlepsza z dwóch odpowiedzi, nie sądzę, aby odpowiadała DLACZEGO, domyślnie właściwości typu wartości nie mogą być modyfikowane z poziomu jego metod instancji. sugerujesz, że to dlatego, że struktury domyślnie są niezmienne, ale nie sądzę, że to prawda
Joseph Knight,
4
@JosephKnight Nie chodzi o to, że struktury domyślnie są niezmienne. Chodzi o to, że metody struktur są domyślnie niezmienne. Pomyśl o tym jak C ++ z odwrotnym założeniem. W metodach C ++ domyślnie nie jest to stała wartość, musisz jawnie dodać plikconst do metody, jeśli chcesz, aby miała ona „const this” i była dozwolona w stałych instancjach (normalne metody nie mogą być używane, jeśli instancja jest stała). Swift robi to samo z odwrotnym ustawieniem domyślnym: metoda ma domyślnie „const this” i zaznaczasz ją inaczej, jeśli ma to być zabronione dla stałych instancji.
Plik analogowy
3
@golddove Tak, i nie możesz. Nie powinieneś. Zawsze musisz utworzyć lub wyprowadzić nową wersję wartości, a twój program musi być zaprojektowany tak, aby celowo przyjmował ten sposób. Funkcja istnieje, aby zapobiec takiej przypadkowej mutacji. Jest to jedna z podstawowych koncepcji programowania w stylu funkcjonalnym .
eonil
25

Struktura jest agregacją pól; jeśli konkretna instancja struktury jest zmienna, jej pola będą modyfikowalne; jeśli instancja jest niezmienna, jej pola będą niezmienne. W związku z tym typ struktury musi być przygotowany na możliwość, że pola dowolnej konkretnej instancji mogą być zmienne lub niezmienne.

Aby metoda struktury mogła mutować pola podstawowej struktury, te pola muszą być modyfikowalne. Jeśli metoda, która mutuje pola podstawowej struktury, jest wywoływana na niezmiennej strukturze, spróbuje zmutować niezmienne pola. Ponieważ nie mogło z tego wyniknąć nic dobrego, takie wzywanie musi być zabronione.

Aby to osiągnąć, Swift dzieli metody struktury na dwie kategorie: te, które modyfikują podstawową strukturę, a zatem mogą być wywoływane tylko w przypadku mutowalnych instancji struktury, oraz te, które nie modyfikują podstawowej struktury, a zatem powinny być wywoływalne zarówno w instancjach mutowalnych, jak i niezmiennych . To drugie użycie jest prawdopodobnie częstsze i dlatego jest domyślne.

Dla porównania .NET obecnie (nadal!) Nie oferuje żadnego sposobu na odróżnienie metod strukturalnych, które modyfikują strukturę, od tych, które tego nie robią. Zamiast tego wywołanie metody struktury na niezmiennej instancji struktury spowoduje, że kompilator utworzy modyfikowalną kopię instancji struktury, pozwoli metodzie zrobić z nią wszystko, co chce, i odrzuci kopię po zakończeniu metody. Powoduje to zmuszenie kompilatora do marnowania czasu na kopiowanie struktury bez względu na to, czy metoda modyfikuje ją, czy też nie, nawet jeśli dodanie operacji kopiowania prawie nigdy nie zmieni tego, co byłby kodem niepoprawnym semantycznie, w kod poprawny semantycznie; spowoduje tylko, że kod, który jest semantycznie błędny w jeden sposób (modyfikacja „niezmiennej” wartości) będzie błędny w inny sposób (pozwalając kodowi myśleć, że modyfikuje strukturę, ale odrzucając próby zmian). Zezwolenie metodom struct na wskazanie, czy zmodyfikują podstawową strukturę, może wyeliminować potrzebę bezużytecznej operacji kopiowania, a także zapewnia, że ​​próba błędnego użycia zostanie oflagowana.

supercat
źródło
1
Porównanie z .NET i przykład, w którym nie ma słowa kluczowego mutating, dały mi do zrozumienia. Powód, dla którego słowo kluczowe mutujące przyniosłoby jakiekolwiek korzyści.
Binarian
20

Uwaga: terminy laika przed nami.

To wyjaśnienie nie jest rygorystycznie poprawne na najbardziej skomplikowanym poziomie kodu. Jednak zostało sprawdzone przez gościa, który faktycznie pracuje nad Swiftem i powiedział, że jest wystarczająco dobre jako podstawowe wyjaśnienie.

Więc chcę spróbować prosto i bezpośrednio odpowiedzieć na pytanie „dlaczego”.

A dokładniej: dlaczego musimy oznaczać funkcje struktury jako takie, w mutatingktórych możemy zmieniać parametry struktury bez modyfikowania słów kluczowych?

Tak więc, ogólnie rzecz biorąc, ma to wiele wspólnego z filozofią, która sprawia, że ​​Swift jest szybki.

Można o tym pomyśleć jak o problemie zarządzania rzeczywistymi adresami fizycznymi. Gdy zmieniasz adres, jeśli jest dużo osób, które mają Twój obecny, musisz powiadomić je wszystkie o przeprowadzce. Ale jeśli nikt nie ma Twojego aktualnego adresu, możesz po prostu przenieść się w dowolne miejsce i nikt nie musi o tym wiedzieć.

W tej sytuacji Swift przypomina trochę pocztę. Jeśli wiele osób z wieloma kontaktami dużo się przemieszcza, wiąże się to z naprawdę dużymi kosztami. Za obsługę tych wszystkich powiadomień musi płacić wielu ludzi, a proces ten zajmuje dużo czasu i wysiłku. Dlatego idealnym stanem Swifta jest, aby każdy w jego mieście miał jak najmniej kontaktów. Wtedy nie potrzebuje dużego personelu do obsługi zmian adresów, a wszystko inne może zrobić szybciej i lepiej.

To jest również powód, dla którego wszyscy Swift-ludzie zachwycają się typami wartości w porównaniu do typów referencyjnych. Z natury typy referencyjne gromadzą „kontakty” w każdym miejscu, a typy wartości zwykle nie potrzebują więcej niż kilka. Typy wartości to „Swift” -er.

Wracając do małego obrazu: structs . Struktury to wielka sprawa w Swift, ponieważ mogą robić większość rzeczy, które mogą robić obiekty, ale są typami wartości.

Kontynuujmy analogię adresu fizycznego, wyobrażając sobie, misterStructże żyje w someObjectVille. Tutaj analogia jest nieco skomplikowana, ale myślę, że nadal jest pomocna.

Tak więc, aby modelować zmianę zmiennej na struct, powiedzmy, że misterStructma zielone włosy i dostaje polecenie przejścia na niebieskie włosy. Jak powiedziałem, analogia staje się niewyjaśniona, ale w pewnym sensie dzieje się tak, że zamiast zmieniać misterStructwłosy, stara osoba wyprowadza się i pojawia się nowa osoba z niebieskimi włosami i ta nowa osoba zaczyna nazywać siebie misterStruct. Nikt nie musi otrzymywać powiadomienia o zmianie adresu, ale jeśli ktoś spojrzy na ten adres, zobaczy faceta z niebieskimi włosami.

Teraz zamodelujmy, co się dzieje, gdy wywołujesz funkcję na struct. W tym przypadku to tak, jakby misterStructotrzymać takie zamówienie jak changeYourHairBlue(). Tak więc poczta dostarcza polecenie misterStruct„idź zmienić włosy na niebieskie i powiedz mi, kiedy skończysz”.

Jeśli on zgodnie z tą samą procedurę jak wcześniej, jeśli robi to, co zrobił, gdy zmienna została zmieniona bezpośrednio, co misterStructzrobi to wyjść z własnego domu i rozmowy w nowej osoby z niebieskimi włosami. Ale to jest problem.

Rozkaz brzmiał: „Zmień włosy na niebieskie i powiedz mi, kiedy skończysz”, ale to zielony facet dostał to zamówienie. Po wprowadzeniu się niebieskiego faceta nadal trzeba odesłać powiadomienie o ukończeniu zadania. Ale ten niebieski nic o tym nie wie.

[Aby naprawdę wywnioskować tę analogię, coś okropnego, technicznie rzecz biorąc, zielonowłosemu facetowi przydarzyło się to, że po wyprowadzce natychmiast popełnił samobójstwo. Więc nie może też nikogo powiadomić, że zadanie zostało wykonane ! ]

Aby uniknąć tego problemu, w takich przypadkach tylko , Swift musi przejść bezpośrednio do domu pod tym adresem i rzeczywiście zmienić bieżący mieszkańców regionu, włosy . To zupełnie inny proces niż zwykłe wysłanie nowego faceta.

I dlatego Swift chce, abyśmy używali mutatingsłowa kluczowego!

Efekt końcowy wygląda tak samo w przypadku wszystkiego, co musi odnosić się do struktury: mieszkaniec domu ma teraz niebieskie włosy. Ale procesy służące do osiągnięcia tego są w rzeczywistości zupełnie inne. Wygląda na to, że robi to samo, ale robi zupełnie inną rzecz. Robi coś, czego generalnie struktury Swift nigdy nie robią.

Tak więc, aby dać biednemu kompilatorowi trochę pomocy i nie zmuszać go do zastanawiania się, czy funkcja sama mutuje, structczy nie, dla każdej pojedynczej funkcji struktury kiedykolwiek, jesteśmy proszeni o litość i użycie mutatingsłowa kluczowego.

Zasadniczo, aby pomóc Szybkiemu zachować szybkość, wszyscy musimy wykonać swoją część. :)

EDYTOWAĆ:

Hej koleś / koleś, który mnie zlekceważył, właśnie całkowicie przepisałem swoją odpowiedź. Jeśli z tobą będzie lepiej, czy usuniesz głos przeciw?

Le Mot Juiced
źródło
1
To jest tak <f-word-here> niesamowicie napisane! Tak więc łatwo to zrozumieć. Przeszukałem internet w poszukiwaniu tego i oto odpowiedź (cóż, i tak bez konieczności przechodzenia przez kod źródłowy). Wielkie dzięki za poświęcenie czasu na napisanie tego.
Vaibhav
1
Chcę tylko potwierdzić, że rozumiem to poprawnie: czy możemy powiedzieć, że gdy właściwość instancji Struct jest bezpośrednio zmieniana w Swift, ta instancja Struct jest „przerywana” i „tworzona” jest nowa instancja ze zmienioną właściwością za kulisami? A kiedy używamy metody mutowania w instancji, aby zmienić właściwość tej instancji, ten proces kończenia i ponownej inicjalizacji nie ma miejsca?
Ted
@Teo, chciałbym móc odpowiedzieć definitywnie, ale najlepsze, co mogę powiedzieć, to to, że przeprowadziłem tę analogię obok rzeczywistego programisty Swift, a oni powiedzieli „to jest w zasadzie dokładne”, sugerując, że chodzi o coś więcej w sensie technicznym. Ale z tym zrozumieniem - że jest w tym coś więcej niż tylko to - w szerokim sensie, tak, to właśnie mówi ta analogia.
Le Mot Juiced
8

Swift structs można tworzyć jako instancje stałe (via let) lub zmienne (via var)

Rozważmy Arraystrukturę Swifta (tak, to jest struktura).

var petNames: [String] = ["Ruff", "Garfield", "Nemo"]
petNames.append("Harvey") // ["Ruff", "Garfield", "Nemo", "Harvey"]

let planetNames: [String] = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
planetNames.append("Pluto") //Error, sorry Pluto. No can do

Dlaczego dodatek nie działał z nazwami planet? Ponieważ dołączanie jest oznaczone mutatingsłowem kluczowym. A ponieważ planetNameszadeklarowano używanie let, wszystkie metody oznaczone w ten sposób są niedostępne.

W Twoim przykładzie kompilator może powiedzieć, że modyfikujesz strukturę, przypisując ją do jednej lub kilku jej właściwości poza plikiem init. Jeśli zmienisz nieco swój kod, zobaczysz to xi ynie zawsze będzie dostępne poza strukturą. Zwróć uwagę letna pierwszy wiersz.

let p = Point(x: 1, y: 2)
p.x = 3 //error
p.moveToX(5, andY: 5) //error
Lanca
źródło
5

Rozważmy analogię z C ++. Metoda struct w języku Swift będąca mutating/ nie- mutatingjest analogiczna do metody w C ++ nie będącej const/ const. Metoda oznaczona constw C ++ podobnie nie może mutować struktury.

Możesz zmienić var ​​spoza struktury, ale nie możesz zmienić jego własnych metod.

W C ++ możesz także „zmienić zmienną spoza struktury” - ale tylko wtedy, gdy masz constzmienną niestrukturalną . Jeśli masz constzmienną strukturalną, nie możesz przypisać jej do zmiennej ani też wywołać constmetody niebędącej metodą. Podobnie w języku Swift można zmienić właściwość struktury tylko wtedy, gdy zmienna struct nie jest stałą. Jeśli masz stałą strukturalną, nie możesz przypisać jej do właściwości, a także nie możesz wywołać mutatingmetody.

newacct
źródło
1

Zastanawiałem się nad tym samym, kiedy zacząłem uczyć się języka Swift, a każda z tych odpowiedzi, być może dodając pewne spostrzeżenia, jest dość rozwlekła i myląca. Myślę, że odpowiedź na Twoje pytanie jest całkiem prosta…

Metoda mutacji zdefiniowana w twojej strukturze wymaga pozwolenia na modyfikowanie każdej instancji siebie, która kiedykolwiek zostanie utworzona w przyszłości. Co się stanie, jeśli jedna z tych instancji zostanie przypisana do niezmiennej stałej z let? O o. Aby chronić Cię przed sobą (i pozwolić edytorowi i kompilatorowi wiedzieć, czego próbujesz zrobić), jesteś zmuszony do wyraźnego wyrażenia się, gdy chcesz nadać metodzie instancji tego rodzaju moc.

Natomiast ustawienie właściwości spoza twojej struktury działa na znanym wystąpieniu tej struktury. Jeśli jest przypisany do stałej, Xcode poinformuje Cię o tym w momencie wpisania wywołania metody.

Jest to jedna z rzeczy, które uwielbiam w Swift, ponieważ zaczynam go częściej używać - ostrzeganie o błędach podczas ich wpisywania. Z pewnością bije na głowę rozwiązywanie niejasnych błędów JavaScript!

Kal
źródło
1

SWIFT: Użycie funkcji mutacji w Structs

Szybcy programiści opracowali Structs w taki sposób, że jego właściwości nie mogą być modyfikowane w ramach metod Struct. Na przykład sprawdź kod podany poniżej

struct City
{
  var population : Int 
  func changePopulation(newpopulation : Int)
  {
      population = newpopulation //error: cannot modify property "popultion"
  }
}
  var mycity = City(population : 1000)
  mycity.changePopulation(newpopulation : 2000)

Podczas wykonywania powyższego kodu otrzymujemy błąd, ponieważ próbujemy przypisać nową wartość populacji nieruchomości Miasta Struct. Domyślnie właściwości Structs nie mogą być modyfikowane wewnątrz ich własnych metod. W ten sposób stworzyli go deweloperzy Apple, dzięki czemu Structs będą domyślnie miały statyczny charakter.

Jak to rozwiązujemy? Jaka jest alternatywa?

Mutujące słowo kluczowe:

Zadeklarowanie funkcji jako mutującej wewnątrz Struct pozwala nam zmieniać właściwości w Structs. Linia nr: 5 powyższego kodu zmienia się w ten sposób,

mutating changePopulation(newpopulation : Int)

Teraz możemy przypisać wartość nowej populacji do populacji majątku w zakresie metody.

Uwaga :

let mycity = City(1000)     
mycity.changePopulation(newpopulation : 2000)   //error: cannot modify property "popultion"

Jeśli użyjemy let zamiast var dla obiektów Struct, nie możemy zmienić wartości żadnych właściwości. Jest to również powód, dla którego otrzymujemy błąd, gdy próbujemy wywołać funkcję mutującą za pomocą instancji let. Dlatego lepiej jest używać var ​​za każdym razem, gdy zmieniasz wartość właściwości.

Chciałbym usłyszeć Twoje komentarze i przemyślenia…

sDev
źródło