Przejdź do treści

DevStyle - Strona Główna
Swift okiem programisty C#

Swift okiem programisty C#

Maciej Aniserowicz

15 marca 2018

Mobile

Stało się. Planowałem to od dawna: cały dzień w kodzie po baaardzo długiej przerwie. Bałem się, że się po prostu porzygam, potnę żyły, popłaczę, roztrzaskam komputer i tyle tego będzie. A tymczasem…

Background

Swoje programistyczne zęby zjadłem na platformie Microsoft .NET. Startowałem, gdy .NET się rodził i przez kilkanaście lat byliśmy swoimi najlepszymi przyjaciółmi. I najbardziej zagorzałymi wrogami.

Z Visual Studio – czyli dedykowanym dla .NET IDE od Microsoftu – podobnie. Raz czule kochałem, raz zajadle nienawidziłem. Z przewagą tego drugiego.

Po długiej przerwie od zarobkowego programowania – do którego zresztą wracać nie zamierzam – zaczęło mi się wreszcie… chcieć. Zatęskniłem. Wiedziałem jednak, że jeśli wskoczę do tej samej rzeki, to wrócą te same frustracje i wszystkie najgorsze wspomnienia. Musiało to być coś zupełnie innego, nowego.

Stanęło na IOS. Rok temu kupiłem iphone, kilka miesięcy później: maca. Czemu by nie pokodować na to? Nigdy nie robiłem nic mobilnego, nigdy nawet nie tknąłem Objective-C ani Swift. No to… siup, w nieznane!

Killing me SWIFTly with Xcode 😎

I tak oto teraz, po jednym dniu zabawy, chcę zaprezentować swoje dotychczasowe wrażenia. Niby jeden dzień to za mało, żeby mieć cokolwiek interesującego do powiedzenia. Może tak, może nie, może pprrff a kto to wie. Jedziemy.

Mój setup:

  • Swift 4
  • Xcode 9.2
  • MacOS High Sierra 10.13.3
  • MacBook Pro (Retina, 13-inch, Early 2015)
  • 2,9 GHz Intel Core i5

Uwaga: poniższe obserwacje mogą być niedokładne, a opinie: zdecydowanie subiektywne (jak to opinie). Sprostowania w komentarzach mile widziane. Punktem wyjścia był dla mnie oficjalny dokument na stronie Apple: Start Developing iOS Apps (Swift).

To jest pierwszy z – jeśli dobrze pójdzie – wieeelu postów. Dzisiaj skupię się tylko na pierwszych wrażeniach z języka. O narzędziach już nie dałem rady się rozpisać. Zapraszam do zapisania się na newsletter, żeby nie przegapić kolejnych odcinków!

Język Swift

Na pierwszy rzut oka – wygląda czytelnie, wygląda znajomo.

Ciekawi koncept braku, znanej wszak z wielu innych języków, funkcji Main(). Po prostu to, co napisane w global scope, zostaje wykonane. W sumie: fajny pomysł!

Zmienne

Mamy dwa rodzaje deklaracji zmiennych: var i let. Z tym, że let to nie zmienna, a… nie, nie stała. To “immutable value“, odpowiednik “readonly” z C#, czyli nie musimy nadawać wartości w momencie deklaracji. Możemy to zrobić w konstruktorze. Raz.

Dziwi nieco brak niejawnej konwersji (implicit conversion), ale z tego co czytałem, ma to być język “maksymalnie bezpieczny”. Więc: okej, approved, będziemy rzutować ręcznie. Może to i dobrze? W C# możliwość niejawnej konwersji, a szczególnie nadpisania jej operatorów, dawała pole do popisu programistycznej fantazji, ale… nieraz ugryzła w tyłek.

Jednak w przypadku braku niejawnej konwersji od razu pojawia się pytanie: a co z komponowaniem tekstu? Wszystko trzeba będzie z palca rzutować na stringi? Na szczęście: nie.

"tekst \(zmienna lub wyrażenie) dalszy tekst"

… i gra muzyka. Uff.

Tablice i słowniki

Standardowo można zadeklarować tablicę: [1,2,3].

Słownik: tak samo: [“key1”: “value1”, “key2”: “value2”]. Swift sam sobie wykona “type inference“.

Jeśli typ tablicy lub słownika można wydedukować z kontekstu, to wystarczy napisać [] albo [:], nie trzeba powtarzać nazw typów.

A pusta tablica? Proszę: [String](). Słownik? Proszę: [String:Int](). Easy peasy.

NULL vs NIL!

Chyba w .NET 2.0 pojawiło się Nullable. Od tej pory można było wstawić nulla nawet pod value type. W Swift dostajemy do dyspozycji “optional variable” – podobny koncept z taką samą składnią: znakiem zapytania po nazwie typu. Tyle że w Swift tyczy się to zarówno klas, jak i struktur.

Coś podobnego – czyli rozszerzenie koncepcji “ochrony przed nullem“, by walczyć z “billion-dollar mistake” – szykuje się w kolejnych wersjach C#. Więcej o “nullable referencje types” w C# można poczytać chociażby tutaj czy tutaj.

Swift daje możliwość zastosowania dodatkowo jednego ciekawego myku: wykrzyknika zamiast znaku zapytania. Wykrzyknik po nazwie typu oznacza: “implicitly unwrapped optional variable“. Ktoś z doświadczeniem w programowaniu od razu chwyci, o co chodzi, a jak to szkoły programowania tłumaczą totalnym świeżakom to… sam jestem bardzo ciekaw.

Podobnie jak w C#, tak i tutaj mamy do dyspozycji “coalesce operator” (składnia ta sama: dwa znaki zapytania).

Funkcje

Standard:

func name (arg_name1: arg_type1, arg_name2: arg_type2) -> return_type

i po krzyku.

Możemy mieć także zagnieżdżone funkcje. Możemy i anonimowe funkcje. Może funkcje tworzyć, zwracać, robić cuda wianki. I to wszystko bardzo ładnie, zwięźle wygląda.

func create_adder() -> ((Int, Int) -> Int){
    return { $0+$1 }
}
create_adder()(2,13)

W tym przypadku nie musimy nawet nazywać parametrów, odwołujemy się do nich po indeksach. Dla kilkulinijkowców: werynajs!

ALE!

Tutaj pojawia się moja pierwsza wątpliwość: tzw. argument labels. Gdy definiujemy funkcję, to nadajemy nazwy parametrom – naturalna sprawa:

func add(x: Int, y: Int) -> Int

Jednak domyślnie musimy powtórzyć tę nazwę podczas wywołania funkcji:

add(x: 1, y: 2)

To autor funkcji – a nie jej “klient” – decyduje o tym, czy nazwa parametru ma zostać powtórzona przy argumencie. Można tę konieczność wyłączyć właśnie w deklaracji funkcji, dodając znak _ przed parametrem:

func add(_ x: Int, _ y: Int) -> Int

Wtedy mógłbym wywołać funkcję tak:

add(1, 2)

Nie podoba mi się to. Ale to nie wszystko, bo parametrowi można nadać jeszcze jedną nazwę. Tę, która będzie użyta przy wywołaniu. Zobaczcie:

func add(arg1 x: Int, arg2 y: Int) -> Int

Wywołując funkcję, muszę posłużyć się tymi dodatkowymi nazwami:

func add(arg1: 1, arg2: 2)

Wątpliwości co do tego ficzera towarzyszyły mi przez cały dzień. I nie zostały rozwiane. Nawet po lekturze tego dedykowanego mu kawałka dokumentacji. Moim zdaniem taki lukier składniowy dobrze jest “móc włączyć”, a nie “musieć wyłączyć”.

I jeszcze o funkcjach, good news! W Swift funkcje domyślnie są wirtualne, jak w Javie! Więc można je nadpisywać! Nie trzeba ich dodatkowo oznaczać jako “virtual” jak w C#. A to jedna z moich najbardziej znielubianych cech C#. (Uwaga: nie wnikałem jeszcze w mechanizm method dispatch w Swift, więc określenie “wirtualne” może być na wyrost; chodzi o koncepcję, nie o konkretną implementację)

Ctor

Co się rzuciło w oczy: konstruktor nie nazywa się tak jak typ. Kontruktor nazywa się “init”. Ma to sens, bo niezależnie od nazwy typu, wszędzie wygląda tak samo.

Co mnie zdziwiło to destruktor nazwany “deinit“. Jakoś głupio to wygląda. Chociaż nie wiem, jak to nazwać inaczej.

“OnPropertyChanged” – pseudo

Property jak property – getter i setter, normalna sprawa. Ale jest coś naprawdę ciekawego: można w osobnych blokach dodać kod, który wykona się PRZED i PO zmianie wartości. Obadajcie:

class WillDidSet {
    var prop: Int {
        willSet {
            print ("WILL - before changing \(prop) to \(newValue)")
        }
        didSet{
            print ("DID - after changing \(oldValue) to \(prop)")
        }
    }
    
    init(){
        prop = 44
    }
}

Można ten sam efekt osiągnąć w C#, pisząc po prostu ciało settera, ale… cóż, tutaj z jakiegoś powodu zrobiono to w ten, a nie inny, sposób. Nie mam jeszcze opinii czy to fajne czy nie, ale na pewno interesujące.

Enumy

Wyglądają na bardziej rozbudowane, niż w C#. Choćby przez to, że mogą mieć metody! To ograniczenie w C# obchodziło się używając extension methods.

Dodatkowo w Swift enum ma property “rawValue”. W C# dobieraliśmy się do tego poprzez rzutowanie. I jeszcze jedna ciekawostka: enumy w Swift mogą… nie mieć typu! Wówczas nie będą też miały rawValue.

Nie ogarniam jeszcze konsekwencji takich możliwości, ale wygląda to bardzo interesująco.

W ogóle enumy w Swift to mechanizm “o wielu dnach” ;) i z chęcią będę kolejne tajemnice odkrywał.

Interfejsy

W Swift nazywają się Protocols – protokołami. Makes sense!

Są to takie same kontrakty jak w C#, tyle że… nie do końca takie same.

Pierwsza różnica: na dowolny parametr można nałożyć ograniczenie “musi implementować wiele interfejsów“. W C# w takiej sytuacji stworzyłoby się dodatkowy interfejs, implementujący wszystkie wymagane. A tutaj proszę bardzo:

func gimmeParamWith2Protocols(arg: FirstProtocol & AnotherProtocol)

Czy się przydaje? Ciężko powiedzieć. Ale zapada w pamięć.

A druga różnica, nawet ważniejsza: w swiftowym protokole można zadeklarować metody… opcjonalne! Nie wniknąłem jeszcze jak to działa, ale już w najprostszej aplikacji na ajfona natknąłem się na ten mechanizm. Czy gdzieś w protokole siedzi domyślna implementacja, czy jest to rozwiązywane przez extension methods, czy ma to jakieś korzenie w Objective-C… ciężko powiedzieć. Nie rozumiem też jeszcze sensu takiego mechanizmu – bo co to za kontrakt, który nie musi być do końca spełniony? – ale pewnie jakiś jest.

Extensions

Już kilka razy pojawiło się określenie “extension methods“. Good news: tak, tutaj też są. I implementuje się je podobnie jak w C#.

Bardzo dobrze, bo ten mechanizm przydaje się często i otwiera dużo ciekawych możliwości.

Wyjątki

Wyjątkiem jest wszystko, co implementuje protokół Error. Może to być klasa, struktura, albo… ten tajemniczy enum!

Jest to dla mnie temat do dalszego researchu: dlaczego rekomenduje się użycie enumów do impelementacji wyjątków, zamiast dla każdego nowego wyjątku, jak Bozia w Redmond nakazała, zrobić klaskę?

Powiew świeżości można poczuć patrząc na realizację koncepcji “checked exceptions” (jak w Javie). A raczej jej braku (jak w C#). A raczej… jeszcze inaczej!

Tutaj funkcja nie musi deklarować typów rzucanych wyjątków. Ale jeśli COŚ rzuca, to trzeba ją udekorować dodatkowym słowem kluczowym “throws“.

Interesująca jest też zmiana klasycznego zestawu try/catch/finally. Tutaj mam blok do{}, a po nim catch{}. I dopiero w bloku do, przy każdej instrukcji rzucającej potencjalnie wyjątek, dopisujemy try. Nie wiem jeszcze co to daje i dlaczego ma być lepsze od standardowego try/catch, ale rzuca się w oczy.

Defer

Po raz pierwszy zobaczyłem taki koncept w GO. Na czym polega? O, najlepiej będzie pokazać na przykładzie:

enum FileError: Error{
    case NotExists
}
 
func writeFile(_ fileName: String) throws {
    print("opening file \(fileName)")
    defer {
        print("closing file \(fileName)")
    }
    print ("writing to file \(fileName)")
    throw FileError.NotExists
}

Sekcja zamykająca plik wykona się na samym końcu funkcji… nawet jeśli poleci wyjątek. Możemy zatem ładnie, obok siebie, umieścić utworzenie i zwolnienie zasobów bez pamiętania, żeby je zwolnić na sam koniec i nawet w przypadku wyjątku.

Pomocne.

I jestem pewny, że – tak jak using i IDisposable w C# – można to jakoś fajnie NADuzyć i wykorzystać do ciekawych, niekoniecznie zgodnych w pierwotnym zamierzeniem, celów ;).

Generics

Są – a jakże! I wydaje się, że mają się dobrze. Bo i jak mają się mieć?

Poruszam ich temat tylko dlatego, że… enumy mogą być generyczne!! Jak wspominałem, nie do końca te enumy jeszcze ogarniam

C.D.N.

Planowałem rozpisać się dzisiaj jeszcze o wrażeniach z pracy z Xcode, wdrożeniem (udanym lub nie) swojej pierwszej aplikacji itede, ale… ale już nie dam rady.

Więc do przeczytania wkrótce!

Póki co – jestem bardzo zadowolony. Nie zniechęciłem się, wręcz przeciwnie. Po raz pierwszy od dawna cały dzień spędzony w kodzie dał mi naprawdę dużo radości. I chcę więcej.

Dev4life, piona!

Zobacz również