I koden nedan skapar vi en instans av typen Logger och registrerar tre logghanterare. Den första är inställd på .info, den andra på .warning, och den tredje på .error. Varje hanterare skriver ut ett meddelande till konsolen. Om vi behövde mer komplexa logghanterare, kan vi skapa dem och lägga till dem i Logger-klassen på följande sätt:

swift
logger.registerHandler(for: .warning) { message in
print("WARNING: \(message)") } logger.registerHandler(for: .error) { message in print("ERROR: \(message)") }

Här definieras en mer komplex logghanterare utanför funktionen registerHandler(). Detta gör det möjligt att skapa mer flexibla och återanvändbara logghanterare i andra delar av koden. Vi kan också använda closures för att definiera logik som kan återanvändas i olika kontexter, vilket gör att vi kan hålla koden modulär och lätt att underhålla. Här är ett exempel där vi lägger till en funktionalitet för att skicka ett e-postmeddelande vid ett fel:

swift
let emailError = { (message: String) -> Void in // kod för att skicka e-post print("Emailed Error: \(message)") } logger.registerHandler(for: .error, handler: emailError)

När logghanterare har lagts till kan vi börja logga våra meddelanden på följande sätt:

swift
logger.log("Informational Message", level: .info)
logger.log("Warning message", level: .warning) logger.log("We have an error", level: .error)

Den här koden kommer att visa loggmeddelanden på konsolen och, om e-postfunktionen är implementerad, även skicka ett felmeddelande via e-post.

Closures är ett kraftfullt och flexibelt verktyg som gör det möjligt att kapsla in funktionalitet i ett självständigt kodblock och sedan vidarebefordra det inom applikationen. Genom att förstå och använda closures på ett effektivt sätt kan vi förbättra vår förmåga att skapa modulär, återanvändbar och underhållbar kod. Oavsett om vi använder closures som callback-funktioner, eventhantering eller inom funktionella programmeringsmönster, är det avgörande att bemästra closures för varje Swift-utvecklare som vill utnyttja språkets fulla potential.

Nu, låt oss titta på result builders, en funktion som lades till i Swift 5.4. Result builders tillåter utvecklare att skapa anpassade domänspecifika språk (DSL) för att konstruera komplexa datatyper som JSON, SwiftUI-vyer eller HTML-element för server-side Swift-ramverk. Result builders hjälper oss att skriva kod som är mer uttrycksfull och kortfattad, vilket gör den lättare att förstå och underhålla.

För att få en bättre förståelse av vad result builders kan göra, titta på detta enkla exempel som kombinerar flera strängar:

swift
@resultBuilder struct StringBuilder {
static func buildBlock(_ components: String...) -> String { return components.joined() } } func buildString(@StringBuilder _ components: () -> String) -> String { return components() }

I detta exempel använder vi @resultBuilder-attributet för att definiera en anpassad result builder. Attributet appliceras på strukturen StringBuilder, vilket innebär att denna struktur är avsedd att användas som en result builder. Inom strukturen StringBuilder definieras den statiska metoden buildBlock(), som är en specialmetod som result builders känner igen. Metoden tar ett variabelt antal strängar som parametrar och returnerar en sammansatt sträng.

Denna result builder används sedan i en funktion som tar ett closures som parameter, markerat med @StringBuilder. Detta betyder att closures bearbetas av result buildern och returnerar en sammansatt sträng:

swift
let result = buildString { "Hello, " "Mastering " "Swift!" } print("\(result)")

Resultatet av denna kod blir:

Hello, Mastering Swift!

Nu när vi har sett ett enkelt exempel på en result builder, låt oss titta på ett mer komplext exempel som hanterar JSON-data och skapar ett dictionary. Vi börjar med följande kod som definierar vår result builder:

swift
struct DictionaryComponent { let dictionary: [String: Any] func addToJSON(_ json: inout [String: Any]) { for (key, value) in dictionary { json[key] = value } } } @resultBuilder struct JSONBuilder { static func buildBlock(_ components: DictionaryComponent...) -> [String: Any] { var result: [String: Any] = [:] for component in components { component.addToJSON(&result) } return result } static func buildExpression(_ expression: [String: Any]) -> DictionaryComponent { return DictionaryComponent(dictionary: expression) } }

I den här koden skapar vi först en struktur, DictionaryComponent, som används för att lägga till nyckel-värde-par i ett JSON-dictionary. Metoden addToJSON() används för att lägga till dessa par till ett JSON-dictionary som skickas som ett inout-parameter.

Strukturen JSONBuilder definierar två metoder som hanterar uttryck i en result builder: buildBlock() och buildExpression(). Den första hanterar blockuttryck, där flera DictionaryComponent-instanser skickas som variabla parametrar, medan den andra hanterar individuella uttryck, som enstaka dictionary-element.

När man arbetar med result builders är det viktigt att förstå skillnaden mellan blockuttryck och individuella uttryck. Blockuttryck är flera komponenter som hanteras inom ett block, medan individuella uttryck hanteras separat. Genom att använda buildExpression() kan vi hantera enstaka uttryck och skapa en flexibel och återanvändbar struktur för att bygga komplexa data, till exempel JSON.

För att hantera nästlade dictionaries använder vi samma metod, där buildExpression() kan hantera varje inbäddad dictionary, vilket gör det möjligt att skapa komplexa och hierarkiska datastrukturer.

När vi arbetar med result builders på detta sätt får vi inte bara en mer uttrycksfull syntax, utan också en kodstruktur som är lättare att följa och utöka, vilket är en viktig fördel när vi bygger mer komplexa applikationer.

Hur man hanterar och kastar fel i Swift vid tilldelning av spelarnummer

I programmering är korrekt felhantering avgörande för att bygga robusta applikationer som kan hantera oväntade situationer och ge användaren relevant feedback. I Swift kan vi definiera specifika feltyper och sedan kasta dessa fel när något går fel i programmet. Ett bra exempel på detta kan vara i hanteringen av unika spelarnummer för ett basebollteam. Här kommer vi att visa hur man definierar och hanterar sådana fel i en Swift-struktur.

Först definierar vi en egen feltyp som beskriver de olika felen som kan uppstå när man tilldelar ett nummer till en spelare. Denna typ, PlayerNumberError, är en enum (uppräkning) som specificerar tre fel:

swift
enum PlayerNumberError: Error { case numberTooHigh(description: String) case numberTooLow(description: String) case numberAlreadyAssigned case numberDoesNotExist }

Dessa feltyper är användbara när vi försöker tilldela eller hämta ett nummer till en spelare. Exempelvis kan ett nummer vara för högt, för lågt eller redan tilldelat. Vi kan också försöka hämta en spelare med ett nummer som inte finns.

Att kasta fel

När ett fel inträffar i en funktion måste vi informera den kod som anropar denna funktion om felet. Detta gör vi genom att "kasta" felet. För att kasta ett fel i Swift använder vi nyckelordet throws. Detta innebär att den funktion som anropar den potentiellt kan få ett fel, och måste vara beredd på att hantera detta.

För att visa hur man hanterar fel med throws kan vi skapa en struktur som representerar ett basebollteam, med spelare som har unika nummer. I detta exempel lagrar vi spelarna i en ordbok (Dictionary), där varje nummer är nyckeln och varje spelare är värdet. Här definierar vi en BaseballPlayer som en tuple med spelarens förnamn, efternamn och nummer:

swift
typealias BaseballPlayer = (firstName: String, lastName: String, number: Int)

En grundläggande BaseballTeam-struktur ser ut så här:

swift
struct BaseballTeam { private let maxNumber = 99 private let minNumber = 0
private var players = [Int: BaseballPlayer]()
}

Vi har definierat de högsta och lägsta numren en spelare kan ha, samt en ordbok som lagrar spelarna.

För att lägga till en spelare till laget, definierar vi en metod, addPlayer, som tar en spelare och försöker lägga till denne i ordboken. Om spelarens nummer är för högt eller för lågt, eller om numret redan är tilldelat, kastar vi ett fel:

swift
mutating func addPlayer(player: BaseballPlayer) throws { guard player.number < maxNumber else {
throw PlayerNumberError.numberTooHigh(description: "Max number is \(maxNumber)")
}
guard player.number > minNumber else { throw PlayerNumberError.numberTooLow(description: "Min number is \(minNumber)") }
guard players[player.number] == nil else {
throw PlayerNumberError.numberAlreadyAssigned } players[player.number] = player }

Metoden kontrollerar om spelarens nummer är inom det tillåtna intervallet och om det är unikt. Om alla villkor är uppfyllda, läggs spelaren till i lagets ordbok.

Att hämta en spelare

Vi kan också ha en metod för att hämta en spelare baserat på deras nummer. Om inget nummer är tilldelat, kastar vi ett numberDoesNotExist-fel:

swift
func getPlayerByNumber(number: Int) throws -> BaseballPlayer { if let player = players[number] { return player } else { throw PlayerNumberError.numberDoesNotExist } }

Här försöker vi hämta spelaren med det angivna numret. Om spelaren inte finns, kastas ett fel.

Att fånga fel med do-catch

När vi kallar en funktion som kan kasta ett fel måste vi vara beredda på att fånga det. Detta görs med hjälp av en do-catch-block. Här är ett exempel på hur man använder do-catch för att fånga ett numberDoesNotExist-fel:

swift
do {
let player = try myTeam.getPlayerByNumber(number: 34) print("Player is \(player.firstName) \(player.lastName)") } catch PlayerNumberError.numberDoesNotExist { print("No player has that number") }

I det här fallet försöker vi hämta spelaren med nummer 34. Om ingen spelare finns med det numret, fångas felet och vi skriver ut ett meddelande om att ingen spelare har detta nummer.

Att använda flera catch-satser

Om vi vill hantera olika fel på olika sätt, kan vi använda flera catch-satser. Här är ett exempel där vi hanterar tre olika fel från addPlayer-metoden:

swift
do {
try myTeam.addPlayer(player:("David", "Ortiz", 34)) print("Player Added") } catch PlayerNumberError.numberTooHigh(let description) { print("Error: \(description)") } catch PlayerNumberError.numberTooLow(let description) { print("Error: \(description)") } catch PlayerNumberError.numberAlreadyAssigned { print("Error: Number already assigned") }

Varje catch-block kan hantera ett specifikt fel beroende på den feltyp som kastades. Om ingen av de specifika felen matchar kan vi använda en generell catch-satts för att hantera alla andra fel.

Det är viktigt att förstå att felhantering i Swift med throws och do-catch gör koden mer förutsägbar och robust. Genom att definiera specifika feltyper kan vi exakt hantera olika problem i vårt program och ge användaren korrekt feedback när något går fel. Att använda do-catch-block effektivt gör att vi kan fånga fel och hantera dem på ett strukturerat sätt.

Hur man använder bitwise och anpassade operatorer i Swift

För att hantera programmeringsutmaningar som kräver noggrann kontroll och manipulering av data är det viktigt att förstå och bemästra avancerade operatorer. Dessa operatorer gör det möjligt att utföra operationer som är både snabbare och mer effektiva än traditionella aritmetiska operationer. I denna kapitel undersöker vi några av de avancerade operatorerna, inklusive bitwise operatorer, samt hur man skapar egna anpassade operatorer för specifika användningsfall.

För att förstå bitwise operatorer, är det först nödvändigt att känna till begreppen bitar och byte. En dator arbetar med binära siffror, eller bitar, som kan anta ett av två värden: 0 eller 1. Dessa bitar används för att representera olika tillstånd i elektriska kretsar, där 0 kan stå för "av" och 1 för "på". I sig själva är bitarna ganska små och har inte mycket användning förutom att indikera sanningsvärden (som i boolean-flaggor). Deras verkliga användbarhet framträder när flera bitar grupperas för att bilda större enheter som bytes.

En byte består av 8 bitar. Till exempel, om vi ser på en byte där vissa bitar är på (1) och andra är av (0), kan vi representera detta som ett heltal. Om vi tar ett exempel där byte-värdet är 42, skulle bitarna se ut så här:

makefile
Bitvärde: 00101010

I det här fallet representeras 42 genom att kombinera värdena från de bitar som är på: 32, 8 och 2 (32 + 8 + 2 = 42). För att enklare förstå dessa operationer kommer vi i detta kapitel att fokusera på typen UInt8, som representerar en osignerad integer på 8 bitar. Detta underlättar exempel och demonstrationer.

Det är också viktigt att känna till begreppet endianness när det gäller hur bytes lagras i minnet. I vissa system är bitarna lagrade med den minst signifikanta byten först (little-endian), medan andra använder den mest signifikanta byten först (big-endian). För de flesta program i Swift behöver du inte oroa dig för detta, men när du arbetar med lågnivåbibliotek, till exempel C-bibliotek, kan det bli avgörande att förstå denna skillnad.

I Swift finns det inbyggda egenskaper för att hantera dessa skillnader, såsom littleEndian och bigEndian. När vi använder dessa egenskaper kan vi enkelt konvertera mellan olika endian-format. Här är ett exempel på hur det kan göras:

swift
let en = 42 print("Little-endian representation", en.littleEndian) print("Big-endian representation", en.bigEndian)

När det gäller bitwise operatorer, erbjuder Swift ett kraftfullt sätt att manipulera individuella bitar i en variabel. Detta kan göras snabbare än vanliga aritmetiska operationer som multiplikation eller division. Låt oss nu titta på de mest grundläggande bitwise operatorerna.

Bitwise Operatorer

Bitwise operatorer låter oss manipulera varje individuell bit i en variabel. Dessa operatorer stöds direkt av processorn och kan därför vara mycket snabbare än vanliga aritmetiska operationer. De används för att jämföra, sätta och rensa specifika bitar i ett heltal, vilket ger en högre nivå av kontroll över de värden vi arbetar med.

En av de viktigaste operationerna är bitvis OCH (&), vilket utför en jämförelse mellan motsvarande bitar i två värden och returnerar 1 om båda bitarna är 1. På samma sätt finns det bitvis ELLER (|), som returnerar 1 om någon av bitarna är 1. Det finns även bitvis XOR (^), som returnerar 1 om bitarna är olika, och bitvis INTE (~), som inverterar alla bitarna i ett värde.

För att kunna se effekten av dessa operationer är det viktigt att kunna visualisera de binära representationerna av våra variabler. Swift erbjuder en enkel metod för detta genom att använda en initializer för String, som kan ta ett heltal och returnera en binär strängrepresentation:

swift
let en = 42 print(String(en, radix: 2)) // 101010

Här skriver vi ut det binära värdet av 42, som blir 101010. För att göra det enklare att läsa och förstå kan vi skapa en förlängning av BinaryInteger som formaterar binära tal på ett tydligare sätt, med mellanrum mellan varje grupp om 4 bitar. Detta gör det lättare att visualisera och jämföra binära värden, särskilt när vi arbetar med större tal.

swift
extension BinaryInteger {
func binaryFormat(_ nibbles: Int) -> String {
var number = self var binaryString = "" var counter = 0 let totalBits = nibbles * 4 for _ in (1...totalBits).reversed() { binaryString.insert(contentsOf: "\(number & 1)", at: binaryString.startIndex) number >>= 1 counter += 1 if counter % 4 == 0 && counter < totalBits { binaryString.insert(contentsOf: " ", at: binaryString.startIndex) } } return binaryString } }

Anpassade Operatorer

Förutom de inbyggda operatorerna kan vi även skapa våra egna operatorer. Anpassade operatorer tillåter oss att definiera egna symboler och beteenden för operationer som inte stöds av de vanliga operatorerna. Detta kan vara användbart när vi arbetar med specifika domäner eller problem där standardoperatorer inte räcker till.

För att skapa en anpassad operator i Swift behöver vi först definiera en ny operator, sedan implementera metoder för att hantera den. På så sätt kan vi skapa mycket kraftfulla och skräddarsydda lösningar för våra program.

Det är viktigt att förstå att även om anpassade operatorer ger oss stor frihet att utforma vårt kodflöde, bör de användas med försiktighet. För att säkerställa att koden förblir läsbar och underhållbar är det viktigt att vara tydlig med vilka operatorer som används och varför.