Swift-makrot hallintaan: Opas omien makrojen luomiseen käytännön esimerkein

Swift-makrot poistavat toistuvan boilerplate-koodin ja siirtävät virheitä käännösaikaan. Opi luomaan omia makroja SwiftSyntax-kirjastolla — URL-validoinnista CodingKeys-generointiin. Mukana Swift 6.2:n tuomat parannukset.

Johdanto: Miksi Swift-makrot ovat pelin muuttaja

Jos olet lukenut aiemmat artikkelimme Swift 6.2:n rinnakkaisuudesta ja SwiftData-oppaasta, olet jo törmännyt makroihin — todennäköisesti huomaamattasi. @Model, @Observable, #Predicate, @Query — nämä kaikki ovat makroja. Ja rehellisesti sanottuna, ne ovat Swiftin yksi tehokkaimmista ominaisuuksista.

Niiden ymmärtäminen avaa aivan uuden tason ohjelmointikokemuksessasi.

Swift-makrot esiteltiin Swift 5.9:ssä WWDC23:ssa, ja ne ovat sittemmin kasvaneet yhdeksi kielen tärkeimmistä rakennuspalikoista. Toisin kuin C-kielen makrot, jotka olivat yksinkertaisia tekstikorvauksia, Swiftin makrot ovat tyyppiturvallisuudeltaan vahvoja, kääntäjän tarkistamia ja hiekkalaatikossa ajettavia metaohjelmointityökaluja. Ne generoivat koodia käännösaikana — eivätkä koskaan poista tai muokkaa olemassa olevaa koodiasi.

Tässä oppaassa sukellamme syvälle Swift-makrojen maailmaan. Käymme läpi makrotyypit, opimme luomaan omia makroja alusta alkaen, tutustumme SwiftSyntax-kirjastoon ja käymme läpi käytännön esimerkkejä, jotka voit soveltaa suoraan omissa projekteissasi. Lopuksi vilkaisemme Swift 6.2:n tuomia makroparannuksia ja parhaita käytäntöjä.

Makrojen perusteet: Mitä ne ovat ja miksi niitä tarvitaan

Makrojen ydintehtävä on loppujen lopuksi yksinkertainen: poistaa toistuva boilerplate-koodi. Jokainen iOS-kehittäjä tuntee sen tunteen, kun kirjoittaa samantyyppistä koodia kymmeniä kertoja — Codable-protokollan CodingKeys-enumeraatioita, Equatable-vertailufunktioita tai ominaisuuksien getter- ja setter-rakenteita. Se on puuduttavaa ja virhealtista.

Makrot ratkaisevat tämän generoimalla tarvittavan koodin puolestasi käännösaikana.

Tärkeää on ymmärtää, miten makrot eroavat muista koodingeneroinnin menetelmistä:

  • Protokollalaajennukset tarjoavat oletustoteutuksia, mutta eivät voi generoida uusia ominaisuuksia tai mukautettua logiikkaa kunkin tyypin rakenteen perusteella.
  • Koodigenerointi (esim. Sourcery) toimii erillisenä työkaluna, joka generoi tiedostoja ulkopuolelta. Makrot sen sijaan ovat kielen sisäinen ominaisuus ja integroituvat suoraan kääntäjään.
  • Property wrapperit käärivät ominaisuuksia, mutta eivät voi lisätä uusia jäseniä tai protokollanmukaisuuksia tyyppiin.

Makrot ovat additiivisia — ne voivat ainoastaan lisätä uutta koodia. Tämä on tärkeä turvallisuusominaisuus: makro ei voi koskaan vahingossa poistaa tai muokata olemassa olevaa koodiasi. Lisäksi ne ajetaan hiekkalaatikossa (sandbox), joten makro ei pääse lukemaan tiedostojärjestelmää, tekemään verkkopyyntöjä tai suorittamaan muita sivuvaikutuksellisia operaatioita. Turvallista siis.

Kaksi makroperhettä: Vapaat ja liitetyt makrot

Swift jakaa makrot kahteen pääperheeseen niiden käyttötavan mukaan: vapaisiin makroihin (freestanding macros) ja liitettyihin makroihin (attached macros). Tämä jako on ensimmäinen asia, joka kannattaa sisäistää ennen kuin sukeltaa syvemmälle.

Vapaat makrot (#-merkki)

Vapaat makrot tunnistaa risuaitamerkistä (#) ja ne seisovat itsenäisinä lausekkeina koodissa. Olet todennäköisesti käyttänyt jo useita vapaita makroja ihan tietämättäsi:

// Esimerkkejä vapaista makroista, joita käytät jo
let ennuste = #Predicate<Projekti> { $0.onValmis }
let kuva = #imageLiteral(resourceName: "logo")
let sarake = #column

Vapailla makroilla on kaksi roolia:

  • @freestanding(expression) — Generoi lausekkeen, joka palauttaa arvon. Esimerkiksi #stringify tai #URL.
  • @freestanding(declaration) — Generoi yhden tai useamman uuden julistuksen. Voi luoda vaikkapa kokonaisia rakenteita tai funktioita.

Liitetyt makrot (@-merkki)

Liitetyt makrot kiinnittyvät olemassa olevaan julistukseen attribuuttina ja käyttävät @-merkkiä. Tunnetuimpia esimerkkejä ovat nämä tutut kasvot:

@Model          // SwiftData-malli
class Projekti { ... }

@Observable      // Observoitava luokka
class ViewModel { ... }

@Test           // Swift Testing -testitapaus
func testaaLaskuri() { ... }

Liitetyillä makroilla on viisi roolia, ja — tämä on hienoa — yksi makro voi toteuttaa useita näistä samanaikaisesti:

  • @attached(peer) — Lisää uusia julistuksia olemassa olevan julistuksen rinnalle.
  • @attached(accessor) — Lisää getter- ja setter-aksessorit ominaisuuteen.
  • @attached(memberAttribute) — Lisää attribuutteja tyypin jäseniin.
  • @attached(member) — Lisää uusia jäseniä (ominaisuuksia, funktioita jne.) tyyppiin.
  • @attached(conformance) — Lisää protokollanmukaisuuksia tyypille.

Esimerkiksi @Model-makro hyödyntää useita näistä rooleista yhtäaikaisesti: se lisää PersistentModel-protokollanmukaisuuden (conformance), generoi tallennuslogiikan jäseninä (member) ja muokkaa ominaisuuksien aksessoreita (accessor). Tämä on syy, miksi yksi pieni @Model-merkintä tekee niin valtavasti työtä konepellin alla.

SwiftSyntax: Makrojen moottori

Ennen kuin luomme ensimmäisen oman makron, meidän on ymmärrettävä SwiftSyntax-kirjasto. Se on Applen kirjasto, joka jäsentää Swift-lähdekoodin abstraktiksi syntaksipuuksi (Abstract Syntax Tree, AST). Jokainen makro toimii tämän syntaksipuun kanssa — lukee olemassa olevia solmuja ja tuottaa uusia.

Ajattele AST:tä koodisi rakenteellisena esityksenä. Kun kirjoitat:

let nimi: String = "Swift"

SwiftSyntax näkee tämän puurakenteena: VariableDeclSyntaxPatternBindingSyntaxIdentifierPatternSyntax ("nimi") + TypeAnnotationSyntax ("String") + InitializerClauseSyntax ("Swift"). Jokainen koodin osa on solmu puurakenteessa, jota voit tarkastella ja käsitellä makrossasi.

Ei ehkä kuulosta kovin intuitiiviselta aluksi, mutta käytännössä siihen tottuu nopeasti.

SwiftSyntax-kirjasto koostuu useista moduuleista:

  • SwiftSyntax — Perustason syntaksipuun tyypit ja jäsentäjä.
  • SwiftSyntaxMacros — Protokollat ja tyypit makrojen kirjoittamiseen (ExpressionMacro, MemberMacro jne.).
  • SwiftSyntaxBuilder — Kätevät API:t syntaksipuun rakentamiseen uutta koodia generoitaessa.
  • SwiftCompilerPlugin — Infrastruktuuri makropluginin rekisteröimiseen kääntäjälle.

Tärkeä yksityiskohta: Swift 6.2:sta lähtien SwiftSyntax-kirjasto tulee esikäännettynä Xcoden ja Swift Package Managerin mukana. Tämä poistaa sen aiemmin turhauttavan ongelman, jossa jokaisessa puhtaassa käännöksessä jouduttiin kääntämään koko SwiftSyntax — prosessi, joka saattoi kestää minuutteja. Nyt puhtaat käännösajat ovat huomattavasti nopeampia, mikä tekee makrokehityksestä paljon miellyttävämpää.

Ensimmäinen oma makro: URL-validointi käännösaikana

No niin, nyt päästään asiaan. Aloitetaan klassisella ja hyödyllisellä esimerkillä: makro, joka validoi URL-osoitteet käännösaikana. Tämä tarkoittaa, että jos kirjoitat virheellisen URL:n, saat virheilmoituksen jo ennen sovelluksen ajamista. Ei enää suoritusaikaisia kaatumisia.

Vaihe 1: Makropaketin luominen

Makrot tarvitsevat oman Swift Package -paketin. Xcodessa valitse File → New → Package ja valitse Swift Macro -pohja. Paketin rakenne näyttää tältä:

URLMakro/
├── Package.swift
├── Sources/
│   ├── URLMakro/           // Julkinen makromäärittely
│   │   └── URLMakro.swift
│   └── URLMakroPlugin/     // Makron toteutus (käännösaika)
│       └── URLMakroPlugin.swift
└── Tests/
    └── URLMakroTests/
        └── URLMakroTests.swift

Vaihe 2: Package.swift-konfiguraatio

// swift-tools-version: 6.0
import PackageDescription
import CompilerPluginSupport

let package = Package(
    name: "URLMakro",
    platforms: [.macOS(.v10_15), .iOS(.v13)],
    products: [
        .library(name: "URLMakro", targets: ["URLMakro"])
    ],
    dependencies: [
        .package(
            url: "https://github.com/swiftlang/swift-syntax.git",
            from: "602.0.0"
        )
    ],
    targets: [
        .macro(
            name: "URLMakroPlugin",
            dependencies: [
                .product(name: "SwiftSyntax", package: "swift-syntax"),
                .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
                .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
            ]
        ),
        .target(
            name: "URLMakro",
            dependencies: ["URLMakroPlugin"]
        ),
        .testTarget(
            name: "URLMakroTests",
            dependencies: [
                "URLMakroPlugin",
                .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax")
            ]
        )
    ]
)

Vaihe 3: Makron julkinen rajapinta

Ensin määritellään, miltä makro näyttää käyttäjälle. Tämä menee URLMakro.swift-tiedostoon:

import Foundation

@freestanding(expression)
public macro URL(_ stringLiteral: String) -> URL =
    #externalMacro(module: "URLMakroPlugin", type: "URLMacro")

Tämä kertoo kääntäjälle: "Kun kohtaat #URL("...")-lausekkeen, käytä URLMacro-tyyppiä URLMakroPlugin-moduulista sen laajentamiseen." Huomaa #externalMacro — se on erikoismakro, joka yhdistää julkisen rajapinnan varsinaiseen toteutukseen.

Vaihe 4: Makron toteutus

Nyt tulee se mielenkiintoisin osa — varsinainen makrologiikka. Tämä menee URLMakroPlugin.swift-tiedostoon:

import SwiftSyntax
import SwiftSyntaxMacros
import SwiftCompilerPlugin
import Foundation

public struct URLMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        // 1. Haetaan ensimmäinen argumentti
        guard let argumentti = node.arguments.first?.expression,
              let merkkijonoLiteraali = argumentti.as(
                  StringLiteralExprSyntax.self
              ),
              let segmentti = merkkijonoLiteraali.segments.first,
              case .stringSegment(let stringSegmentti) = segmentti
        else {
            throw MacroError.viestiVirhe(
                "Käytä: #URL(\"https://esimerkki.fi\")"
            )
        }

        let urlMerkkijono = stringSegmentti.content.text

        // 2. Validoidaan URL käännösaikana
        guard URL(string: urlMerkkijono) != nil else {
            throw MacroError.viestiVirhe(
                "Virheellinen URL: \"\(urlMerkkijono)\""
            )
        }

        // 3. Palautetaan turvallinen URL-lauseke
        return "URL(string: \(argumentti))!"
    }
}

enum MacroError: Error, CustomStringConvertible {
    case viestiVirhe(String)

    var description: String {
        switch self {
        case .viestiVirhe(let viesti):
            return viesti
        }
    }
}

@main
struct URLMakroPluginEntry: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        URLMacro.self
    ]
}

Käydään läpi mitä tässä oikeastaan tapahtuu:

  1. Argumentin jäsentäminen: SwiftSyntax antaa meille koko syntaksipuun solmun. Kaivamme sieltä merkkijonoliteraalin — sen URL-osoitteen, jonka kehittäjä kirjoitti koodiin.
  2. Käännösaikainen validointi: Yritämme luoda URL-objektin. Jos se epäonnistuu, heitetään virhe, joka näkyy Xcodessa kääntäjävirheenä — ihan kuten mikä tahansa muu syntaksivirhe.
  3. Koodin generointi: Palautamme ExprSyntax-solmun, joka sisältää lopullisen koodin. Tämä korvaa #URL("...")-kutsun käännöksessä.

Vaihe 5: Makron käyttäminen

import URLMakro

// Tämä kääntyy onnistuneesti
let appleUrl = #URL("https://developer.apple.com")

// Tämä aiheuttaa kääntäjävirheen!
// let virheellinenUrl = #URL("ei ole url")
// Virhe: Virheellinen URL: "ei ole url"

Ja siinä se! Nyt saat kääntäjävirheen joka kerta, kun kirjoitat virheellisen URL:n — suoritusaikainen kaatuminen on käytännössä mahdoton. Tämä on konkreettinen esimerkki siitä, miten makrot siirtävät virheitä ajonajasta käännösaikaan, jolloin ne on huomattavasti halvempaa (ja vähemmän stressaavaa) korjata.

Liitetty makro käytännössä: Automaattinen CodingKeys-generointi

Yksi yleisimmistä boilerplate-koodin lähteistä on CodingKeys-enumeraation kirjoittaminen JSON-muunnosten yhteydessä. Luodaan liitetty makro, joka generoi CodingKeys-enumeraation automaattisesti muuntaen Swiftin camelCase-nimistön API:n snake_case-muotoon.

// Makromäärittely
@attached(member, names: named(CodingKeys))
public macro SnakeCaseCodable() =
    #externalMacro(module: "MakroPlugin", type: "SnakeCaseCodableMacro")

// Makron toteutus
public struct SnakeCaseCodableMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // Haetaan kaikki tallennetut ominaisuudet
        let ominaisuudet = declaration.memberBlock.members
            .compactMap { $0.decl.as(VariableDeclSyntax.self) }
            .filter { $0.bindingSpecifier.tokenKind == .keyword(.var) ||
                      $0.bindingSpecifier.tokenKind == .keyword(.let) }
            .compactMap { $0.bindings.first?.pattern
                .as(IdentifierPatternSyntax.self)?.identifier.text }

        // Generoidaan CodingKeys-enumeraatio
        let tapaukset = ominaisuudet.map { nimi in
            let snakeCase = nimi.snakeCased()
            if nimi == snakeCase {
                return "    case \(nimi)"
            } else {
                return "    case \(nimi) = \"\(snakeCase)\""
            }
        }.joined(separator: "\n")

        return [
            """
            enum CodingKeys: String, CodingKey {
            \(raw: tapaukset)
            }
            """
        ]
    }
}

Kun käytät tätä makroa, lopputulos näyttää tältä:

@SnakeCaseCodable
struct Kayttaja: Codable {
    let etuNimi: String
    let sukuNimi: String
    let sahkopostiOsoite: String
    let luontiPvm: Date
}

// Makro generoi automaattisesti:
// enum CodingKeys: String, CodingKey {
//     case etuNimi = "etu_nimi"
//     case sukuNimi = "suku_nimi"
//     case sahkopostiOsoite = "sahkoposti_osoite"
//     case luontiPvm = "luonti_pvm"
// }

Tämä poistaa kokonaan manuaalisen CodingKeys-kirjoittamisen ja siihen liittyvät kirjoitusvirheet. Kun lisäät uuden ominaisuuden rakenteeseen, CodingKeys päivittyy automaattisesti. Inhimillisen virheen mahdollisuus? Käytännössä nolla.

Peer-makro: Asynkronisen funktion luominen automaattisesti

Peer-makrot lisäävät uusia julistuksia olemassa olevan julistuksen rinnalle. Klassinen käyttötapaus on completion handler -funktion muuntaminen async/await-muotoon — erityisen hyödyllistä, kun modernisoit vanhempia koodikantoja.

@attached(peer, names: overloaded)
public macro LisaaAsync() =
    #externalMacro(module: "MakroPlugin", type: "LisaaAsyncMacro")

public struct LisaaAsyncMacro: PeerMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingPeersOf declaration: some DeclSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        guard let funktioSyntaksi = declaration.as(FunctionDeclSyntax.self)
        else {
            throw MacroError.viestiVirhe(
                "@LisaaAsync voidaan liittää vain funktioihin"
            )
        }

        let funktion_nimi = funktioSyntaksi.name.text

        // Generoidaan async-versio
        return [
            """
            func \(raw: funktion_nimi)() async throws -> Data {
                try await withCheckedThrowingContinuation { jatko in
                    \(raw: funktion_nimi) { tulos in
                        switch tulos {
                        case .success(let data):
                            jatko.resume(returning: data)
                        case .failure(let virhe):
                            jatko.resume(throwing: virhe)
                        }
                    }
                }
            }
            """
        ]
    }
}

Käytettäessä:

@LisaaAsync
func haeTiedot(completion: @escaping (Result<Data, Error>) -> Void) {
    // Alkuperäinen callback-pohjainen toteutus
    URLSession.shared.dataTask(with: url) { data, _, virhe in
        if let virhe = virhe {
            completion(.failure(virhe))
        } else if let data = data {
            completion(.success(data))
        }
    }.resume()
}

// Makro generoi automaattisesti async-version:
// func haeTiedot() async throws -> Data { ... }

Tämä on erityisen kätevää vanhojen koodipohjien modernisoinnissa. Haluat tarjota async/await-rajapintoja, mutta et voi heittää vanhoja callback-funktioita menemään yhteensopivuuden takia. Makro hoitaa molemmat puolestasi.

Makrojen testaaminen: Luotettavuus ennen kaikkea

Makrojen testaaminen on vähintään yhtä tärkeää kuin minkä tahansa muun koodin testaaminen — ehkä jopa tärkeämpää. Miksi? Koska makrovirhe voi monistua jokaiseen paikkaan, jossa makroa käytetään. Yksi bugi, kymmeniä ongelmia.

SwiftSyntax tarjoaa erinomaisen testaustyökalun: assertMacroExpansion-funktion.

import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import URLMakroPlugin

final class URLMakroTests: XCTestCase {
    let testMakrot: [String: Macro.Type] = [
        "URL": URLMacro.self
    ]

    func testKelvollinenURL() throws {
        assertMacroExpansion(
            """
            #URL("https://developer.apple.com")
            """,
            expandedSource: """
            URL(string: "https://developer.apple.com")!
            """,
            macros: testMakrot
        )
    }

    func testVirheellinenURL() throws {
        assertMacroExpansion(
            """
            #URL("ei ole kelvollinen url")
            """,
            diagnostics: [
                DiagnosticSpec(
                    message: "Virheellinen URL: \"ei ole kelvollinen url\"",
                    line: 1,
                    column: 1
                )
            ],
            macros: testMakrot
        )
    }

    func testTyhjaURL() throws {
        assertMacroExpansion(
            """
            #URL("")
            """,
            diagnostics: [
                DiagnosticSpec(
                    message: "Virheellinen URL: \"\"",
                    line: 1,
                    column: 1
                )
            ],
            macros: testMakrot
        )
    }
}

assertMacroExpansion ottaa syötteenä makrokutsun, odottaa joko laajennettua lähdekoodia tai diagnostiikkaviestejä ja vertaa niitä todellisuuteen. Yksinkertaista ja tehokasta.

Toinen mainitsemisen arvoinen työkalu on Swift Macro Testing -kirjasto, joka tarjoaa assertMacro-funktion. Se generoi automaattisesti snapshot-testejä makrolaajennuksista — kirjoitat vain makrokutsun, ja kirjasto tallentaa odotetun tuloksen automaattisesti ensimmäisellä ajokerralla. Säästää paljon aikaa testien kirjoittamisessa.

Makrojen debuggaus ja virheenkorjaus

Täytyy myöntää, makrojen debuggaus voi olla välillä turhauttavaa. Ne ajetaan käännösaikana erillisessä prosessissa, joten perinteiset debuggausmenetelmät eivät suoraan toimi. Tässä muutamia tekniikoita, jotka ovat auttaneet minua käytännössä:

Swift AST Explorer

Ennen kuin kirjoitat makroa, tutki kohdekoodisi syntaksipuuta Swift AST Explorer -työkalulla. Se näyttää visuaalisesti, miltä koodisi näyttää SwiftSyntax-puuna. Tämä auttaa valtavasti ymmärtämään, mitä solmuja sinun täytyy käsitellä makrossasi.

Makron laajennuksen tarkistaminen Xcodessa

Xcode tarjoaa sisäänrakennetun tavan tarkistaa, mitä makro generoi. Napsauta hiiren kakkospainikkeella makrokutsua koodissa ja valitse Expand Macro. Tämä näyttää generoidun koodin suoraan editorissa — korvaamaton työkalu ongelmien paikantamisessa.

Diagnostiikkaviestit

Sen sijaan, että heittäisit aina virheen, voit käyttää MacroExpansionContext-kontekstia lähettämään diagnostiikkaviestejä kääntäjälle:

context.diagnose(Diagnostic(
    node: node,
    message: OmaDiagnostiikka.varoitus(
        "Tämä ominaisuus on vanhentunut"
    ),
    severity: .warning
))

Tämä mahdollistaa varoitusten ja huomautusten näyttämisen ilman, että käännös keskeytyy. Voit esimerkiksi varoittaa kehittäjää vanhentuneesta käyttötavasta samalla kun koodi edelleen kääntyy normaalisti.

MacroExpansionContext: Hyödylliset työkalut

MacroExpansionContext on arvokas kumppani jokaisessa makrototeutuksessa. Se tarjoaa muutamia todella hyödyllisiä ominaisuuksia:

public static func expansion(
    of node: some FreestandingMacroExpansionSyntax,
    in context: some MacroExpansionContext
) throws -> ExprSyntax {
    // Luo uniikki muuttujanimi, joka ei törmää olemassa oleviin nimiin
    let uniikkiNimi = context.makeUniqueName("valiaikainen")

    // Hae makron sijainti lähdekoodissa
    let sijainti = context.location(of: node)

    return """
    {
        let \(uniikkiNimi) = suoritaOperaatio()
        return \(uniikkiNimi)
    }()
    """
}

makeUniqueName()-metodi ansaitsee erityismaininnan: se generoi muuttujanimiä, jotka eivät koskaan törmää käyttäjän koodissa oleviin nimiin. Tämä on kriittistä, koska makro ei voi tietää, mitä nimiä kehittäjä on jo käyttänyt ympäröivässä koodissa. Ilman tätä olisit nimikonfliktihelvetissä.

Swift 6.2:n makroparannukset

Swift 6.2 toi mukanaan merkittäviä parannuksia makrojärjestelmään. Jos olet lukenut aiemman artikkelimme Swift 6.2:n rinnakkaisuudesta, tiedät, että tämä versio keskittyi kehittäjäkokemuksen parantamiseen — ja sama filosofia ulottuu makroihin.

Ajonaikaisesti tutkittavat makrot

Yksi suurimmista uutuuksista on ajonaikaisesti tutkittavat makrot (runtime-introspectable macros). Aiemmin makrojen generoima koodi oli täysin näkymätöntä ajonaikana — et voinut millään tarkistaa, mitä makro oli generoinut. Swift 6.2:ssa makrot voivat tuottaa ajonaikaista metadataa, jota voit tarkastella suorituksen aikana.

Tämä avaa ovia uusille mahdollisuuksille:

  • Dynaamiset DSL-kielet (Domain-Specific Languages), jotka voivat tarkistaa oman rakenteensa ajonaikana.
  • Kehitystyökalut, jotka voivat tutkia makrojen generoimaa koodia sovelluksen ajon aikana.
  • Tehokkaammat debuggaustyökalut ja inspektorit.

Esikäännetty SwiftSyntax

Kuten mainitsimme aiemmin, Swift 6.2 toimittaa SwiftSyntax-kirjaston esikäännettynä. Käytännössä tämä tarkoittaa:

  • Ensimmäinen puhdas käännös on merkittävästi nopeampi projekteissa, jotka käyttävät makroja.
  • CI/CD-putkistojen käännösajat lyhenevät huomattavasti.
  • Makropakettien integrointi projektiin on helpompaa ja nopeampaa.

Tämä on rehellisesti sanottuna yksi niistä muutoksista, joka vaikuttaa arkipäivän kehitystyöhön eniten. Kukaan ei kaipaa niitä minuuttien odotuksia puhtaissa käännöksissä.

Parannetut diagnostiikkaviestit

Swift 6.2 parantaa myös makrojen tuottamien virheilmoitusten laatua. Kääntäjä osaa nyt näyttää tarkemmin, missä kohtaa makron laajennusta ongelma ilmeni, ja ehdottaa korjauksia (fix-its) useammissa tilanteissa. Pieni mutta merkittävä parannus.

Applen omat makrot: Mitä konepellin alla tapahtuu

Katsotaan tarkemmin, mitä Applen omat makrot oikeastaan tekevät konepellin alla. Tämän ymmärtäminen auttaa suunnittelemaan omia makroja paremmin.

@Observable-makro

@Observable on liitetty makro, joka muuttaa tavallisen luokan observoitavaksi. Se tekee konepellin alla yllättävän paljon:

  1. Lisää Observable-protokollanmukaisuuden (conformance-rooli).
  2. Generoi sisäisen _$observationRegistrar-ominaisuuden (member-rooli).
  3. Muuttaa jokaisen tallennetun ominaisuuden computed property -ominaisuudeksi, joka raportoi muutokset (memberAttribute- ja accessor-roolit).
// Kirjoitat tämän:
@Observable
class LaskuriMalli {
    var lukumaara = 0
    var nimi = "Laskuri"
}

// Makro generoi käytännössä tämän:
class LaskuriMalli: Observable {
    private let _$observationRegistrar = ObservationRegistrar()

    var lukumaara: Int {
        get {
            _$observationRegistrar.access(self, keyPath: \.lukumaara)
            return _lukumaara
        }
        set {
            _$observationRegistrar.withMutation(self, keyPath: \.lukumaara) {
                _lukumaara = newValue
            }
        }
    }
    private var _lukumaara = 0

    // Sama rakenne nimelle...
}

Huomaat varmaan, kuinka paljon toistuvan koodin makro generoi puolestasi. Ilman @Observable-makroa joutuisit kirjoittamaan tämän kaiken käsin jokaiselle ominaisuudelle. Ei kiitos.

@Model-makro (SwiftData)

Jos olet lukenut SwiftData-oppaamme, tiedät jo, että @Model tekee valtavasti työtä. Se yhdistää useita rooleja:

  • Lisää PersistentModel-protokollanmukaisuuden.
  • Generoi skeematiedot jokaiselle ominaisuudelle.
  • Muuttaa ominaisuudet pysyviksi aksessorien kautta.
  • Lisää automaattisen muutosten seurannan.

Tämä on myös syy, miksi @Model toimii vain luokkien kanssa — strukturit eivät tue samaa aksessori-mekanismia, jota makro tarvitsee toimiakseen.

Parhaat käytännöt ja sudenkuopat

Kokemuksen ja yhteisön oppien pohjalta tässä ovat tärkeimmät ohjeet Swift-makrojen kanssa työskentelyyn. Olen oppinut näistä osittain kantapään kautta.

Milloin käyttää makroja

  • Käytä, kun sama boilerplate-koodi toistuu useissa paikoissa ja manuaalinen kirjoittaminen on virhealtista.
  • Käytä, kun haluat siirtää virheentarkistuksen ajonajasta käännösaikaan (kuten URL-validointi).
  • Käytä, kun tarvitset koodia, joka mukautuu kontekstin mukaan — esimerkiksi ominaisuuksien nimien perusteella generoitava koodi.

Milloin välttää makroja

  • Älä käytä, kun protokollalaajennus tai generics ratkaisee ongelman yhtä hyvin. Makrot ovat järeämpi työkalu ja lisäävät monimutkaisuutta.
  • Älä käytä, kun boilerplate-koodia on vähän. Makropaketin luominen ja ylläpitäminen on merkittävä investointi — ei kannata ampua tykillä kärpästä.
  • Älä käytä yleiskäyttöisenä korvikkeena kaikelle metaohjelmoinnille. Ylisuunnittelu makroilla voi tehdä koodista vaikeaselkoista myös tiimikavereille.

Yleisimmät sudenkuopat

  1. Puutteellinen virheiden käsittely: Makrosi saa syötteenä minkä tahansa syntaksipuun. Varmista, että käsittelet kaikki reunatapaukset — tyhjät rakenteet, puuttuvat ominaisuudet, virheelliset tyypit. Jos jokin oletus ei pidä paikkaansa, anna selkeä virheilmoitus.
  2. Nimien törmäykset: Jos makrosi generoi uusia muuttujia, käytä aina context.makeUniqueName()-metodia. Älä koskaan oleta, että jokin nimi on vapaana.
  3. Liian monimutkaiset makrot: Jos makro kasvaa liian suureksi, jaa se pienempiin osiin. Yksittäinen makro, joka yrittää tehdä kaiken, on painajainen ylläpitää ja debugata.
  4. Testien puuttuminen: Testaa jokainen makro perusteellisesti — onnistuneet laajennukset, virhetilanteet ja reunatapaukset. Muista: makrovirhe voi monistua jokaiseen käyttöpaikkaan.
  5. SwiftSyntax-version yhteensopivuus: SwiftSyntax päivittyy Swiftin mukana, ja API:ssa voi tulla rikkovia muutoksia. Lukitse versio Package.swift-tiedostossa ja testaa päivitykset huolellisesti.

Käytännön vinkki: Makrojen hyödyntäminen olemassa olevissa projekteissa

Makrojen integroiminen olemassa olevaan projektiin ei vaadi kaiken uudelleenkirjoittamista. Tässä vaiheittainen lähestymistapa, joka toimii hyvin:

  1. Tunnista toistuva koodi: Etsi projektistasi paikkoja, joissa sama rakenne toistuu — CodingKeys, Equatable-toteutukset, lokituskoodi, tehdastyyppien generointi.
  2. Aloita yksinkertaisesta: Ensimmäisen makrosi ei tarvitse olla monimutkainen MemberMacro. Aloita vapaalla lausekemakrolla, kuten #URL tai #buildDate.
  3. Luo erillinen paketti: Sijoita makrosi omaan Swift Package -pakettiinsa. Tämä pitää makrologiikan erillään pääsovelluksesta ja mahdollistaa uudelleenkäytön muissa projekteissa.
  4. Testaa ensin, toteuta sitten: Kirjoita testit ennen makron toteutusta (TDD). assertMacroExpansion tekee tästä helppoa — määrittele, mitä haluat generoitavan, ja toteuta sitten makro vastaamaan testiä.
  5. Dokumentoi selkeästi: Koska makron generoima koodi ei näy suoraan lähdekoodissa, on erityisen tärkeää dokumentoida, mitä makro tekee ja miksi. Tulevaisuuden sinä kiittää.

Tulevaisuuden näkymät: Mihin makrot ovat menossa

Swift-makrojen kehitys jatkuu aktiivisesti. Tulevien versioiden odotetaan tuovan mukanaan:

  • Laajennettu makrojärjestelmä, joka vähentää boilerplate-koodia entisestään.
  • Seuraavan sukupolven reflektiojärjestelmä, joka integroituu tiiviimmin makrojen kanssa.
  • Apple Intelligence -integraatio, jossa makrot voivat hyödyntää laitteella ajettavaa tekoälyä koodin generoimiseen.
  • Parempi IDE-tuki, jossa Xcode tarjoaa automaattisia ehdotuksia makrojen luomiseen tunnistetun toistuvan koodin perusteella.

Yhteisö on myös aktiivisesti luonut avoimen lähdekoodin makroja. Githubista löytyvät kuratoidut Swift-Macros-listat ovat erinomainen resurssi — ne sisältävät satoja valmiita makroja erilaisiin käyttötarkoituksiin, riippuvuusinjektion automatisoinnista lokituksen standardointiin.

Yhteenveto

Swift-makrot ovat yksi kielen merkittävimmistä lisäyksistä viime vuosina. Ne tarjoavat tyyppitur­vallisen, kääntäjän tarkistaman ja hiekkalaatikossa ajettavan tavan generoida koodia — jotain, mitä Swift-kehittäjät ovat kaivanneet pitkään.

Tässä oppaassa olemme käyneet läpi:

  • Makrojen kaksi pääperhettä: vapaat ja liitetyt makrot.
  • SwiftSyntax-kirjaston roolin makrojen moottorina.
  • Käytännön esimerkit: URL-validointimakro, automaattinen CodingKeys ja peer-makro asynkronisille funktioille.
  • Makrojen testaamisen assertMacroExpansion-funktiolla.
  • Swift 6.2:n tuomat parannukset: ajonaikaisesti tutkittavat makrot ja esikäännetty SwiftSyntax.
  • Parhaat käytännöt ja yleisimmät sudenkuopat.

Seuraava askel on kokeilla itse. Luo uusi Swift Macro -paketti Xcodessa, aloita yksinkertaisella lausekemakrolla ja laajenna siitä. Yhdistettynä aiemmin käsittelemiimme aiheisiin — Swift 6.2:n rinnakkaisuuteen, Liquid Glass -suunnitteluun ja SwiftDatan tietojen pysyvyyteen — sinulla on nyt kokonaisvaltainen käsitys modernin Swift-kehityksen avainteknologioista.

Makrot eivät ole vain edistyneiden kehittäjien työkalu. Ne ovat tulevaisuus, johon koko Swiftin ekosysteemi on investoinut — ja mitä aiemmin aloitat niiden kanssa, sitä paremmin olet valmistautunut tulevaan.

Tietoa Kirjoittajasta Editorial Team

Our team of expert writers and editors.