Articles

Literały łańcuchowe w języku Swift

możliwość wyrażania podstawowych wartości, takich jak ciągi i liczby całkowite, przy użyciu literałów wbudowanych jest istotną cechą w większości języków programowania. Jednak podczas gdy wiele innych języków ma wsparcie dla konkretnych literałów umieszczonych w ich kompilatorze, Swift przyjmuje znacznie bardziej dynamiczne podejście-używając własnego systemu typów do definiowania, w jaki sposób różne literały powinny być obsługiwane, poprzez protokoły.

w tym tygodniu skupmy się w szczególności na literałach łańcuchowych, przyjrzyjmy się wielu różnym sposobom ich użycia i jak — dzięki wysoce zorientowanemu na protokół projektowi Swifta-jesteśmy w stanie dostosować sposób interpretacji literałów, co pozwala nam zrobić naprawdę interesujące rzeczy.

Essential Developer

Essential Developer: Dołącz do bezpłatnego kursu online dla programistów iOS, którzy chcą zostać starszym programistą-czyli: Osiągnij poziom praktycznych umiejętności eksperckich i stań się częścią najlepiej opłacanych programistów na świecie.

podstawy

podobnie jak w wielu innych językach, Łańcuchy Swift są wyrażane za pomocą literałów otoczonych cudzysłowami-i mogą zawierać zarówno sekwencje specjalne (takie jak znaki nowej linii), znaki ucieczki i wartości interpolowane:

let string = "\(user.name) says \"Hi!\"\nWould you like to reply?"// John says "Hi!"// Would you like to reply?

chociaż funkcje użyte powyżej już zapewniają nam dużą elastyczność i są najprawdopodobniej wystarczające dla zdecydowanej większości przypadków użycia, istnieją sytuacje, w których przydają się potężniejsze sposoby wyrażania literałów. Przyjrzyjmy się niektórym z nich, zaczynając od tego, kiedy musimy zdefiniować łańcuch zawierający wiele linii tekstu.

literały Wielowierszowe

chociaż każdy standardowy literał Łańcuchowy można podzielić na wiele linii za pomocą\n, nie zawsze jest to praktyczne — zwłaszcza jeśli chcemy zdefiniować większy fragment tekstu jako literał wbudowany.

na szczęście, od Swift 4, jesteśmy również w stanie zdefiniować Wielowierszowe literały ciągów za pomocą trzech cudzysłowów zamiast tylko jednego. Na przykład, używamy tej możliwości, aby wypisać tekst pomocy dla skryptu Swift, na wypadek, gdyby użytkownik nie przekazał żadnych argumentów podczas wywoływania go w wierszu poleceń:

// We're comparing against 1 here, since the first argument passed// to any command line tool is the current path of execution.guard CommandLine.arguments.count > 1 else { print(""" To use this script, pass the following: - A string to process - The maximum length of the returned string """) // Exit the program with a non-zero code to indicate failure. exit(1)}

powyżej wykorzystujemy fakt, że Wielowierszowe literały łańcuchowe zachowują wcięcie tekstu w stosunku do kończącego zestawu cudzysłowów na dole. Pozwalają też na znacznie swobodniejsze używanie w nich nieusuniętych cudzysłowów, ponieważ są one zdefiniowane przez zestaw trzech cudzysłowów, co znacznie zmniejsza prawdopodobieństwo, że granice dosłowności staną się niejednoznaczne.

Obie powyższe cechy sprawiają, że literały Wielowierszowe są doskonałym narzędziem do definiowania inline HTML — na przykład w jakiejś formie narzędzia do generowania stron internetowych lub podczas renderowania części zawartości aplikacji za pomocą widoków internetowych-jak to:

extension Article { var html: String { // If we want to break a multiline literal into separate // lines without causing an *actual* line break, then we // can add a trailing '\' to one of our lines. let twitterLink = """ <a href="https://twitter.com/\(author.twitterHandle)">\ @\(author.twitterHandle)</a> """ return """ <article> <h1>\(title)</h1> <div class="author"> <p>\(author.name)</p> \(twitterLink) </div> <div class="body">\(body)</div> </article> """ }}

powyższa technika może być również bardzo przydatna podczas definiowania danych testowych opartych na łańcuchach. Załóżmy na przykład, że ustawienia naszej aplikacji muszą być eksportowane jako XML i że chcemy napisać test, który weryfikuje tę funkcjonalność. Zamiast definiować XML, który chcemy zweryfikować w osobnym pliku, możemy użyć wielowierszowego literału, aby wprowadzić go do naszego testu:

class SettingsTests: XCTestCase { func testXMLConversion() { let settings = Settings( messageLimit: 7, enableSync: true, signature: "Sent from my Swift app" ) XCTAssertEqual(settings.xml, """ <?xml version="1.0" encoding="UTF-8"?> <settings> <messagelimit>7</messagelimit> <enablesync>1</enablesync> <signature>Sent from my Swift app</signature> </settings> """) }}

zaletą definiowania danych testowych w wierszu, jak to robimy powyżej, jest to, że znacznie łatwiej jest szybko wykryć błędy popełnione podczas pisania testu — ponieważ kod testowy i oczekiwane wyniki są umieszczone tuż obok siebie. Jeśli jednak niektóre dane testowe przekraczają kilka linii lub te same dane muszą być użyte w wielu miejscach, nadal warto przenieść je do własnego pliku.

surowe ciągi znaków

nowość w Swift 5, surowe ciągi znaków umożliwiają wyłączenie wszystkich dynamicznych funkcji literalnych ciągów znaków (takich jak interpolacja i interpretacja znaków specjalnych, takich jak\n), na rzecz po prostu traktowania dosłownego jako surowego ciągu znaków. Surowe ciągi są definiowane przez otaczanie ciągu literalnymi znakami funta (lub „hashtagami”, jak je nazywają dzieci):

let rawString = #"Press "Continue" to close this dialog."#

podobnie jak powyżej użyliśmy wielowierszowego literału do definiowania danych testowych, surowe literały łańcuchów są szczególnie przydatne, gdy chcemy wstawiać ciągi, które muszą zawierać znaki specjalne — takie jak cudzysłowy lub ukośniki. Poniżej znajduje się kolejny przykład związany z testami, w którym używamy surowego dosłownego ciągu znaków, aby zdefiniować łańcuch JSON do kodowania User instancja z:

class UserTests: XCTestCase { func testDecoding() throws { let json = #"{"id": 37, "name": "John"}"# let data = Data(json.utf8) let user = try data.decoded() as User XCTAssertEqual(user.id, 37) XCTAssertEqual(user.name, "John") }}

powyżej używamy API dekodowania opartego na wnioskowaniu typów z „serializacji opartej na wnioskowaniu typów w języku Swift”.

podczas gdy surowe łańcuchy znaków domyślnie wyłączają funkcje takie jak interpolacja łańcuchów znaków, istnieje sposób na nadpisanie tego przez dodanie kolejnego znaku funta zaraz po początkowym ukośniku wstecznym interpolacji — jak to:

extension URL { func html(withTitle title: String) -> String { return #"<a href="\#(absoluteString)">\#(title)</a>"# }}

wreszcie, surowe łańcuchy znaków są szczególnie przydatne podczas interpretacji ciągu znaków przy użyciu określonej składni, zwłaszcza jeśli składnia ta opiera się w dużej mierze na znakach, które normalnie musiałyby zostać uniknięte w ciągu literalnym — takich jak wyrażenia regularne. Definiując wyrażenia regularne za pomocą nieprzetworzonych łańcuchów znaków, nie są potrzebne żadne ucieczki, dając nam wyrażenia, które są tak czytelne, jak je uzyskują:

// This expression matches all words that begin with either an// uppercase letter within the A-Z range, or with a number.let regex = try NSRegularExpression( pattern: #"(()|(\d))\w+"#)

nawet przy powyższych ulepszeniach, wątpliwe jest, jak łatwe są odczytywane (i debugowane) wyrażenia regularne — zwłaszcza gdy są używane w kontekście języka bezpiecznego dla typów, takiego jak Swift. Najprawdopodobniej sprowadzi się to do wcześniejszych doświadczeń programistów z wyrażeniami regularnymi, niezależnie od tego, czy wolą je od implementacji bardziej niestandardowych algorytmów parsowania ciągów, bezpośrednio w języku Swift.

wyrażanie wartości za pomocą literałów łańcuchowych

podczas gdy wszystkie literały łańcuchowe są zamieniane naString wartości domyślnie, możemy również użyć ich do wyrażenia własnych wartości. Jak przyjrzeliśmy się w „type-safe identifiers in Swift”, dodanie obsługi dosłownych łańcuchów znaków do jednego z naszych własnych typów może pozwolić nam osiągnąć zwiększone bezpieczeństwo typów, bez poświęcania wygody korzystania z literałów.

na przykład, załóżmy, że zdefiniowaliśmySearchable protokół, który działa jako API do wyszukiwania dowolnego rodzaju bazy danych lub bazowej pamięci masowej, z której korzysta nasza aplikacja-i że używamy Query enum, aby modelować różne sposoby wykonywania takiego wyszukiwania:

protocol Searchable { associatedtype Element func search(for query: Query) -> }enum Query { case matching(String) case notMatching(String) case matchingAny()}

powyższe podejście daje nam dużą siłę i elastyczność co do sposobu, w jaki będziemy wykonywać każde wyszukiwanie, ale najczęstszym przypadkiem użycia jest prawdopodobnie najprostszy — Wyszukiwanie elementów pasujących do danego ciągu znaków — i byłoby naprawdę miło, gdybyśmy byli w stanie to zrobić używając dosłownego ciągu znaków.

dobrą wiadomością jest to, że możemy to zrobić, zachowując jednocześnie całkowicie nienaruszone powyższe API, dostosowującQuery do ExpressibleByStringLiteral:

extension Query: ExpressibleByStringLiteral { init(stringLiteral value: String) { self = .matching(value) }}

w ten sposób możemy teraz wykonywać pasujące wyszukiwania bez konieczności ręcznego tworzenia wartościQuery — wszystko, co musimy zrobić, to przekazać dosłownie ciąg znaków tak, jakby API, które nazywamy, faktycznie zaakceptowałoString bezpośrednio. Tutaj używamy tej możliwości, aby zaimplementować test, który sprawdza, czy typ UserStorage poprawnie implementuje swoją funkcjonalność wyszukiwania:

class UserStorageTests: XCTestCase { func testSearch() { let storage = UserStorage.inMemory let user = User(id: 3, name: "Amanda") storage.insert(user) let matches = storage.search(for: "anda") XCTAssertEqual(matches, ) }}

niestandardowe wyrażenia dosłowne mogą w wielu sytuacjach pozwolić nam uniknąć konieczności wybierania między bezpieczeństwem a wygodą podczas pracy z typami opartymi na łańcuchach, takimi jak zapytania i identyfikatory. Może to być świetne narzędzie w celu uzyskania projektu API, który dobrze skaluje się od najprostszych przypadków użycia, aż po pokrywanie przypadków brzegowych i oferowanie większej mocy i możliwości dostosowywania w razie potrzeby.

Niestandardowa interpolacja

jedną rzeczą, którą wszystkie „smaki” literałów Swift string mają wspólną, jest ich wsparcie dla interpolacji wartości. Chociaż zawsze byliśmy w stanie dostosować sposób interpolacji danego typu, dostosowując się do CustomStringConvertible — Swift 5 wprowadza nowe sposoby implementacji niestandardowych interfejsów API bezpośrednio na silniku interpolacji łańcuchów.

jako przykład, powiedzmy, że chcemy zapisać dany ciąg poprzez opcjonalne zastosowanie do niego prefiksu i sufiksu. Idealnie chcielibyśmy po prostu interpolować te wartości, aby utworzyć końcowy ciąg, w ten sposób:

func save(_ text: String, prefix: String?, suffix: String?) { let text = "\(prefix)\(text)\(suffix)" textStorage.store(text)}

jednak, ponieważ obaprefix Isuffix są opcjami, po prostu użycie ich opisu nie da wyniku, którego szukamy — a kompilator nawet da nam Ostrzeżenie:

String interpolation produces a debug description for an optional value

chociaż zawsze mamy możliwość rozpakowania każdej z tych dwóch opcji przed ich interpolacją, przyjrzyjmy się, jak moglibyśmy zrobić obie te rzeczy za jednym razem, używając niestandardowej interpolacji. Rozpoczniemy od rozszerzenia String.StringInterpolation z nowym appendInterpolation przeciążeniem, które akceptuje dowolną opcjonalną wartość:

extension String.StringInterpolation { mutating func appendInterpolation<T>(unwrapping optional: T?) { let string = optional.map { "\($0)" } ?? "" appendLiteral(string) }}

powyższy unwrapping: Etykieta parametru jest ważna, ponieważ jest to, co użyjemy, aby powiedzieć kompilator do użycia tej specyficznej metody interpolacji — w ten sposób:

func save(_ text: String, prefix: String?, suffix: String?) { let text = "\(unwrapping: prefix)\(text)\(unwrapping: suffix)" textStorage.store(text)}

chociaż jest to tylko cukier składniowy, powyższe wygląda naprawdę schludnie! Jednak to ledwo zarysowuje powierzchnię tego, co mogą zrobić niestandardowe metody interpolacji ciągów. Mogą być zarówno ogólne, jak i nie-ogólne, akceptować dowolną liczbę argumentów, używać wartości domyślnych i prawie wszystko, co „normalne” metody mogą zrobić.

oto kolejny przykład, w którym umożliwiamy, aby nasza metoda konwersji adresu URL do linku HTML z poprzedniego, była również używana w kontekście interpolacji łańcuchów:

extension String.StringInterpolation { mutating func appendInterpolation(linkTo url: URL, _ title: String) { let string = url.html(withTitle: title) appendLiteral(string) }}

mając powyższe na miejscu, możemy teraz łatwo wygenerować linki HTML z takiego adresu URL:

webView.loadHTMLString( "If you're not redirected, \(linkTo: url, "tap here").", baseURL: nil)

fajną rzeczą w niestandardowej interpolacji łańcuchów jest to, jak kompilator pobiera każdą z naszych metod appendInterpolation I przekłada je na odpowiednie API interpolacji — dając nam pełną kontrolę nad tym, jak będzie wyglądać strona wywołania, na przykład poprzez usunięcie zewnętrznych etykiet parametrów, tak jak zrobiliśmy to dla title div > powyżej.

w nadchodzących artykułach będziemy analizować więcej sposobów użycia niestandardowej interpolacji ciągów, na przykład z przypisanymi ciągami i innymi rodzajami metadanych tekstowych.

Wesprzyj Swift by Sundell, sprawdzając tego sponsora:

Essential Developer

Essential Developer: Dołącz do bezpłatnego crash course online dla programistów iOS, którzy chcą zostać specjalistami z czarnego pasa-czyli: osiągnąć poziom praktycznych umiejętności ekspertów i stać się częścią najlepiej opłacanych programistów na świecie.

wnioski

chociaż niektóre z bardziej zaawansowanych możliwości dosłownego napisu Swifta są naprawdę przydatne tylko w bardzo konkretnych sytuacjach, takich jak te w tym artykule, miło jest mieć je dostępne w razie potrzeby — zwłaszcza, że można je całkowicie uniknąć i używać tylko ciągów"the old-fashioned way".

literały łańcuchowe to kolejny obszar, w którym konstrukcja Swifta zorientowana na protokół naprawdę świeci. Poprzez delegowanie większości sposobu interpretacji i obsługi liter do implementatorów protokołów, zamiast twardego kodowania tych zachowań w samym kompilatorze, my jako zewnętrzni Programiści jesteśmy w stanie mocno dostosować sposób obsługi liter — zachowując jednocześnie domyślne ustawienia tak proste, jak to tylko możliwe.