Swift Macros: ví dụ thực tế về metaprogramming

Hướng dẫn đầy đủ về Swift Macros: tạo macro freestanding và attached, thao tác AST với swift-syntax, kèm ví dụ thực tế giúp loại bỏ mã lặp.

Metaprogramming với Swift Macros và ví dụ mã thực tế

Swift Macros, được giới thiệu cùng Swift 5.9 và Xcode 15, mang đến cuộc cách mạng trong cách viết mã Swift. Tính năng này cho phép sinh mã ngay tại thời điểm biên dịch, loại bỏ boilerplate mà vẫn giữ được an toàn kiểu tĩnh. Khác với macro của bộ tiền xử lý C, Swift Macros an toàn về kiểu, tích hợp trong trình biên dịch và được công cụ phát triển hỗ trợ đầy đủ.

Phạm vi hướng dẫn này

Hướng dẫn này tìm hiểu việc tạo Swift Macros từ đầu đến cuối: từ các khái niệm nền tảng đến các triển khai nâng cao, kèm những ví dụ mã đang chạy sẵn sàng áp dụng cho mọi dự án iOS.

Hiểu các loại Swift Macros

Swift cung cấp hai nhóm macro chính, mỗi nhóm phù hợp với những tình huống sử dụng khác nhau. Macro freestanding hoạt động độc lập như biểu thức hoặc khai báo, trong khi macro attached gắn với một khai báo có sẵn để chỉnh sửa hoặc bổ sung.

Macro freestanding: biểu thức và khai báo

Macro freestanding bắt đầu bằng ký hiệu # và có thể trả về một giá trị (biểu thức) hoặc tạo các khai báo mới. Dưới đây là ví dụ cụ thể về macro biểu thức:

MacroUsage.swiftswift
// Freestanding expression macro - generates a value
let buildInfo = #buildDate
// Expansion → "2026-03-11 10:30:45"

// Freestanding macro with arguments
let message = #stringify(1 + 2)
// Expansion → "1 + 2 = 3"

// Freestanding declaration macro - creates declarations
#makeCase("success", "failure", "pending")
// Expansion →
// case success
// case failure
// case pending

Khác biệt cốt lõi giữa biểu thức và khai báo nằm ở kết quả: biểu thức tạo ra một giá trị, còn khai báo tạo ra mã mang tính cấu trúc (kiểu, hàm, biến).

Macro attached: năm vai trò

Macro attached bắt đầu bằng @ và đặt trước một khai báo. Swift quy định năm vai trò khác nhau cho các macro này:

AttachedMacroRoles.swiftswift
// @attached(peer) - adds declarations at the same level
@AddAsync
func fetchUser(id: Int) -> User { ... }
// Expansion → adds func fetchUserAsync(id: Int) async -> User

// @attached(accessor) - adds getters/setters
@UserDefault("theme")
var currentTheme: String
// Expansion → adds get { UserDefaults.standard.string(...) }

// @attached(member) - adds members to a type
@AutoEquatable
struct Point {
    var x: Int
    var y: Int
}
// Expansion → adds static func == (lhs: Point, rhs: Point) -> Bool

// @attached(memberAttribute) - applies attributes to members
@CodableKeys
struct Config {
    var apiUrl: String
    var timeout: Int
}
// Expansion → adds @CodingKey("api_url") before apiUrl

// @attached(conformance) / @attached(extension) - adds conformances
@Hashable
struct User {
    var id: Int
    var name: String
}
// Expansion → adds extension User: Hashable { ... }

Các vai trò này có thể được kết hợp để tạo ra macro mạnh mẽ, biến đổi mã trên nhiều khía cạnh cùng lúc.

Thiết lập dự án để tạo macro

Việc tạo Swift Macros yêu cầu một Swift Package có cấu trúc cụ thể. Gói phụ thuộc vào swift-syntax, thư viện chính thức để thao tác mã Swift dưới dạng cây cú pháp trừu tượng (AST).

Cấu trúc Package.swift

Package.swiftswift
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "MyMacros",
    platforms: [.macOS(.v10_15), .iOS(.v13)],
    products: [
        // Library exposing macros to the main project
        .library(
            name: "MyMacros",
            targets: ["MyMacros"]
        ),
        // Executable for testing macros
        .executable(
            name: "MyMacrosClient",
            targets: ["MyMacrosClient"]
        )
    ],
    dependencies: [
        // Required dependency for macros
        .package(
            url: "https://github.com/apple/swift-syntax.git",
            from: "509.0.0"
        )
    ],
    targets: [
        // Compiler plugin containing implementation
        .macro(
            name: "MyMacrosPlugin",
            dependencies: [
                .product(name: "SwiftSyntax", package: "swift-syntax"),
                .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
                .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
            ]
        ),
        // Target exposing macro declarations
        .target(
            name: "MyMacros",
            dependencies: ["MyMacrosPlugin"]
        ),
        // Test client
        .executableTarget(
            name: "MyMacrosClient",
            dependencies: ["MyMacros"]
        ),
        // Unit tests
        .testTarget(
            name: "MyMacrosTests",
            dependencies: [
                "MyMacrosPlugin",
                .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax")
            ]
        )
    ]
)

Cấu hình này tách bạch rõ ràng phần khai báo macro (mà mã phía client thấy) khỏi phần triển khai (chạy lúc biên dịch).

Tổ chức được khuyến nghị

Cần ít nhất ba tệp: MyMacros.swift cho khai báo, MyMacrosPlugin.swift cho triển khai, và MyMacrosTests.swift cho kiểm thử. Cách tách này giúp bảo trì dễ dàng hơn.

Tạo macro biểu thức

Macro biểu thức tạo ra một giá trị có thể dùng ngay trong mã. Sau đây là cách tạo macro #unwrap để mở giá trị optional kèm thông báo lỗi tùy biến chứa tên biến.

Khai báo macro

MyMacros.swiftswift
import Foundation

/// Macro that unwraps an optional with an explicit error message
/// Usage: let value = #unwrap(optionalValue)
/// Expansion: guard let optionalValue else { fatalError("...") }; optionalValue
@freestanding(expression)
public macro unwrap<T>(_ value: T?) -> T = #externalMacro(
    module: "MyMacrosPlugin",
    type: "UnwrapMacro"
)

Chữ ký khai báo cho biết macro nhận một optional và trả về giá trị non-optional. #externalMacro trỏ tới phần triển khai trong plugin.

Triển khai bằng swift-syntax

UnwrapMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftCompilerPlugin

public struct UnwrapMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        // Get the first argument passed to the macro
        guard let argument = node.argumentList.first?.expression else {
            throw MacroError.missingArgument
        }

        // Extract the variable name for the error message
        let variableName = argument.description.trimmingCharacters(
            in: .whitespacesAndNewlines
        )

        // Generate the expansion code
        // Uses an immediately-invoked closure to encapsulate the guard
        return """
            {
                guard let value = \(argument) else {
                    fatalError("Failed to unwrap '\\(\(literal: variableName))' - value was nil")
                }
                return value
            }()
            """
    }
}

// Custom errors for macros
enum MacroError: Error, CustomStringConvertible {
    case missingArgument
    case invalidSyntax(String)

    var description: String {
        switch self {
        case .missingArgument:
            return "The macro requires an argument"
        case .invalidSyntax(let message):
            return "Invalid syntax: \(message)"
        }
    }
}

Phương thức expansion nhận node AST đại diện cho lời gọi macro cùng ngữ cảnh biên dịch, sau đó trả về ExprSyntax chứa mã được sinh.

Đăng ký plugin

MyMacrosPlugin.swiftswift
import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct MyMacrosPlugin: CompilerPlugin {
    // List all macros provided by this plugin
    let providingMacros: [Macro.Type] = [
        UnwrapMacro.self,
        // Add other macros here
    ]
}

Điểm vào này thông báo cho trình biên dịch về các macro mà plugin cung cấp.

Sẵn sàng chinh phục phỏng vấn iOS?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Tạo macro attached kiểu member

Macro member thêm thành viên (thuộc tính, phương thức, kiểu lồng) vào một kiểu hiện có. Sau đây là macro @AutoInit tự động tạo bộ khởi tạo bao gồm mọi stored property.

Khai báo và triển khai đầy đủ

MyMacros.swiftswift
/// Automatically generates an initializer with all stored properties
@attached(member, names: named(init))
public macro AutoInit() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "AutoInitMacro"
)
AutoInitMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros

public struct AutoInitMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // Verify the macro is applied to a struct or class
        guard declaration.is(StructDeclSyntax.self) ||
              declaration.is(ClassDeclSyntax.self) else {
            throw MacroError.invalidSyntax(
                "@AutoInit can only be applied to structs and classes"
            )
        }

        // Collect stored properties
        let properties = declaration.memberBlock.members
            .compactMap { $0.decl.as(VariableDeclSyntax.self) }
            .filter { isStoredProperty($0) }

        // Generate initializer parameters
        let parameters = properties.compactMap { property -> String? in
            guard let binding = property.bindings.first,
                  let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
                  let type = binding.typeAnnotation?.type else {
                return nil
            }

            let name = identifier.identifier.text
            let typeName = type.description.trimmingCharacters(in: .whitespaces)

            // Check if the property has a default value
            if binding.initializer != nil {
                return "\(name): \(typeName) = \(binding.initializer!.value)"
            }
            return "\(name): \(typeName)"
        }

        // Generate assignments in the init body
        let assignments = properties.compactMap { property -> String? in
            guard let binding = property.bindings.first,
                  let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else {
                return nil
            }
            let name = identifier.identifier.text
            return "self.\(name) = \(name)"
        }

        // Build the complete initializer
        let initDecl: DeclSyntax = """
            public init(\(raw: parameters.joined(separator: ", "))) {
                \(raw: assignments.joined(separator: "\n        "))
            }
            """

        return [initDecl]
    }

    // Check if a variable is a stored property (not computed)
    private static func isStoredProperty(_ variable: VariableDeclSyntax) -> Bool {
        guard let binding = variable.bindings.first else { return false }

        // A computed property has an accessor block with get/set
        if let accessor = binding.accessorBlock {
            // If it's a block with explicit accessors, it's computed
            if accessor.accessors.is(AccessorDeclListSyntax.self) {
                return false
            }
        }

        // let or var without accessor = stored property
        return true
    }
}

Sử dụng macro AutoInit

UserModel.swiftswift
@AutoInit
struct User {
    let id: UUID
    var name: String
    var email: String
    var isActive: Bool = true
}

// Automatically generated code:
// public init(id: UUID, name: String, email: String, isActive: Bool = true) {
//     self.id = id
//     self.name = name
//     self.email = email
//     self.isActive = isActive
// }

// Usage
let user = User(id: UUID(), name: "Alice", email: "alice@example.com")
// isActive uses the default value

Macro này loại bỏ boilerplate cho bộ khởi tạo, đặc biệt hữu ích với mô hình dữ liệu có nhiều thuộc tính.

Macro attached peer cho việc sinh phiên bản async

Macro peer thêm các khai báo cùng cấp với khai báo được chú thích. Dưới đây là macro @AddAsync sinh ra phiên bản async của một hàm dùng completion handler.

MyMacros.swiftswift
/// Automatically generates an async version of a function with completion handler
@attached(peer, names: suffixed(Async))
public macro AddAsync() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "AddAsyncMacro"
)
AddAsyncMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros

public struct AddAsyncMacro: PeerMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingPeersOf declaration: some DeclSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // Verify it's a function
        guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
            throw MacroError.invalidSyntax(
                "@AddAsync requires a function"
            )
        }

        let functionName = funcDecl.name.text
        let asyncFunctionName = "\(functionName)Async"

        // Analyze parameters to find the completion handler
        let parameters = funcDecl.signature.parameterClause.parameters

        // Filter parameters (exclude completion handler)
        var regularParams: [String] = []
        var completionType: String? = nil

        for param in parameters {
            let paramType = param.type.description

            // Detect a completion handler (closure with Result or simple value)
            if paramType.contains("->") && paramType.contains("Void") {
                // Extract the return type from completion
                completionType = extractCompletionReturnType(from: paramType)
            } else {
                let paramName = param.firstName.text
                let paramSecondName = param.secondName?.text
                let label = paramSecondName ?? paramName
                regularParams.append("\(paramName): \(paramType)")
            }
        }

        guard let returnType = completionType else {
            throw MacroError.invalidSyntax(
                "No completion handler found"
            )
        }

        // Generate arguments for internal call
        let callArgs = parameters.dropLast().map { param in
            let name = param.firstName.text
            return "\(name): \(name)"
        }.joined(separator: ", ")

        // Generate the async function
        let asyncFunc: DeclSyntax = """
            func \(raw: asyncFunctionName)(\(raw: regularParams.joined(separator: ", "))) async throws -> \(raw: returnType) {
                try await withCheckedThrowingContinuation { continuation in
                    \(raw: functionName)(\(raw: callArgs.isEmpty ? "" : callArgs + ", ")completion: { result in
                        switch result {
                        case .success(let value):
                            continuation.resume(returning: value)
                        case .failure(let error):
                            continuation.resume(throwing: error)
                        }
                    })
                }
            }
            """

        return [asyncFunc]
    }

    // Extract return type from a Result type
    private static func extractCompletionReturnType(from type: String) -> String {
        // Simplified pattern - in production, use the AST
        if let match = type.range(of: #"Result<([^,]+)"#, options: .regularExpression) {
            var result = String(type[match])
            result = result.replacingOccurrences(of: "Result<", with: "")
            return result.trimmingCharacters(in: .whitespaces)
        }
        return "Void"
    }
}

Minh họa macro AddAsync

NetworkService.swiftswift
class NetworkService {
    @AddAsync
    func fetchUser(
        id: Int,
        completion: @escaping (Result<User, Error>) -> Void
    ) {
        // Implementation with callback
        URLSession.shared.dataTask(with: URL(string: "/users/\(id)")!) { data, _, error in
            if let error = error {
                completion(.failure(error))
            } else if let data = data {
                let user = try? JSONDecoder().decode(User.self, from: data)
                completion(.success(user!))
            }
        }.resume()
    }

    // Automatically generates:
    // func fetchUserAsync(id: Int) async throws -> User {
    //     try await withCheckedThrowingContinuation { continuation in
    //         fetchUser(id: id, completion: { result in
    //             switch result {
    //             case .success(let value):
    //                 continuation.resume(returning: value)
    //             case .failure(let error):
    //                 continuation.resume(throwing: error)
    //             }
    //         })
    //     }
    // }
}

// Modern usage with async/await
let user = try await networkService.fetchUserAsync(id: 42)
Hạn chế của macro peer

Tên hàm được sinh phải được khai báo trong names: của thuộc tính @attached. Ở đây, suffixed(Async) cho biết hàm sinh ra sẽ thêm hậu tố "Async" vào tên gốc.

Kiểm thử đơn vị cho macro

Kiểm thử macro là bắt buộc bởi macro tạo ra mã sẽ được biên dịch sau đó. Swift cung cấp SwiftSyntaxMacrosTestSupport để hỗ trợ những bài kiểm thử như vậy.

MyMacrosTests.swiftswift
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import MyMacrosPlugin

final class MyMacrosTests: XCTestCase {

    // Dictionary of macros to test
    let testMacros: [String: Macro.Type] = [
        "unwrap": UnwrapMacro.self,
        "AutoInit": AutoInitMacro.self,
        "AddAsync": AddAsyncMacro.self
    ]

    func testUnwrapMacroExpansion() throws {
        assertMacroExpansion(
            """
            let value = #unwrap(optionalString)
            """,
            expandedSource: """
            let value = {
                guard let value = optionalString else {
                    fatalError("Failed to unwrap 'optionalString' - value was nil")
                }
                return value
            }()
            """,
            macros: testMacros
        )
    }

    func testAutoInitMacroWithStruct() throws {
        assertMacroExpansion(
            """
            @AutoInit
            struct Point {
                let x: Int
                var y: Int
            }
            """,
            expandedSource: """
            struct Point {
                let x: Int
                var y: Int

                public init(x: Int, y: Int) {
                    self.x = x
                    self.y = y
                }
            }
            """,
            macros: testMacros
        )
    }

    func testAutoInitWithDefaultValues() throws {
        assertMacroExpansion(
            """
            @AutoInit
            struct Config {
                var timeout: Int = 30
                var retryCount: Int
            }
            """,
            expandedSource: """
            struct Config {
                var timeout: Int = 30
                var retryCount: Int

                public init(timeout: Int = 30, retryCount: Int) {
                    self.timeout = timeout
                    self.retryCount = retryCount
                }
            }
            """,
            macros: testMacros
        )
    }

    func testAutoInitFailsOnEnum() throws {
        assertMacroExpansion(
            """
            @AutoInit
            enum Status {
                case active
            }
            """,
            expandedSource: """
            enum Status {
                case active
            }
            """,
            diagnostics: [
                DiagnosticSpec(
                    message: "@AutoInit can only be applied to structs and classes",
                    line: 1,
                    column: 1
                )
            ],
            macros: testMacros
        )
    }
}

Các bài kiểm thử kiểm tra việc mở rộng mã đúng đắn cũng như những thông báo lỗi thích hợp khi macro bị dùng sai.

Sẵn sàng chinh phục phỏng vấn iOS?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Macro nâng cao: property wrapper observable

Macro này kết hợp nhiều vai trò để xây dựng hệ thống quan sát thuộc tính kèm thông báo tự động.

MyMacros.swiftswift
/// Adds automatic property change observation
@attached(accessor)
@attached(peer, names: prefixed(_))
public macro Observable() = #externalMacro(
    module: "MyMacrosPlugin",
    type: "ObservableMacro"
)
ObservableMacro.swiftswift
import SwiftSyntax
import SwiftSyntaxMacros

// Implements both roles: accessor and peer
public enum ObservableMacro {}

extension ObservableMacro: AccessorMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingAccessorsOf declaration: some DeclSyntax,
        in context: some MacroExpansionContext
    ) throws -> [AccessorDeclSyntax] {
        guard let varDecl = declaration.as(VariableDeclSyntax.self),
              let binding = varDecl.bindings.first,
              let identifier = binding.pattern.as(IdentifierPatternSyntax.self) else {
            return []
        }

        let name = identifier.identifier.text
        let storageName = "_\(name)"

        // Generate get and set accessors
        let getter: AccessorDeclSyntax = """
            get {
                access(keyPath: \\.\(raw: name))
                return \(raw: storageName)
            }
            """

        let setter: AccessorDeclSyntax = """
            set {
                withMutation(keyPath: \\.\(raw: name)) {
                    \(raw: storageName) = newValue
                }
            }
            """

        return [getter, setter]
    }
}

extension ObservableMacro: PeerMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingPeersOf declaration: some DeclSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        guard let varDecl = declaration.as(VariableDeclSyntax.self),
              let binding = varDecl.bindings.first,
              let identifier = binding.pattern.as(IdentifierPatternSyntax.self),
              let type = binding.typeAnnotation?.type else {
            return []
        }

        let name = identifier.identifier.text
        let storageName = "_\(name)"
        let typeName = type.description

        // Generate private storage property
        let initializer = binding.initializer.map { " \($0)" } ?? ""
        let storageDecl: DeclSyntax = """
            private var \(raw: storageName): \(raw: typeName)\(raw: initializer)
            """

        return [storageDecl]
    }
}

Sử dụng pattern Observable

ViewModel.swiftswift
@Observable
class UserViewModel {
    @Observable var name: String = ""
    @Observable var age: Int = 0
    @Observable var isActive: Bool = true

    // Generated code for each property:
    // private var _name: String = ""
    // var name: String {
    //     get {
    //         access(keyPath: \.name)
    //         return _name
    //     }
    //     set {
    //         withMutation(keyPath: \.name) {
    //             _name = newValue
    //         }
    //     }
    // }
}

Apple áp dụng đúng pattern này trong framework Observation mới ra mắt từ Swift 5.9.

Gỡ lỗi và kiểm tra macro

Xcode cung cấp nhiều công cụ để gỡ lỗi macro và hiểu rõ mã được tạo ra.

Mở rộng trong Xcode

DebuggingMacros.swiftswift
// Right-click on macro call → "Expand Macro"
// Displays generated code inline

@AutoInit
struct Product {
    let id: UUID
    var name: String
    var price: Decimal
}

// To see the expansion:
// 1. Right-click on @AutoInit
// 2. Select "Expand Macro"
// 3. Generated code displays inline for inspection and debugging

Ghi nhật ký trong quá trình phát triển

DebugMacro.swiftswift
public struct DebugMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        // Print the node's AST to understand the structure
        print("=== DEBUG MACRO ===")
        print("Node: \(node)")
        print("Arguments: \(node.argumentList)")

        // Complete dump of the syntax tree
        dump(node)

        // Continue with normal expansion
        return "42"
    }
}

Khám phá AST với swift-ast-explorer

Công cụ trực tuyến swift-ast-explorer.com hiển thị cây cú pháp cho bất kỳ đoạn mã Swift nào. Đây là tài nguyên thiết yếu để hiểu cách duyệt qua các node AST khi triển khai macro.

Thực hành tốt với Swift Macros

Việc tạo macro dễ bảo trì đòi hỏi tuân thủ một số quy ước và tránh các bẫy phổ biến.

Xác thực và thông báo lỗi

ValidationBestPractices.swiftswift
public struct ValidatedMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        // ✅ Validate usage context
        guard declaration.is(StructDeclSyntax.self) else {
            // ✅ Clear error messages with possible localization
            context.diagnose(
                Diagnostic(
                    node: node,
                    message: MacroDiagnosticMessage(
                        id: "invalid-target",
                        message: "This macro can only be applied to structs",
                        severity: .error
                    )
                )
            )
            return []
        }

        // ✅ Check required arguments
        guard let arguments = node.arguments else {
            context.diagnose(
                Diagnostic(
                    node: node,
                    message: MacroDiagnosticMessage(
                        id: "missing-args",
                        message: "Required arguments missing",
                        severity: .error
                    )
                )
            )
            return []
        }

        // Implementation...
        return []
    }
}

// Structure for diagnostic messages
struct MacroDiagnosticMessage: DiagnosticMessage {
    let id: String
    let message: String
    let severity: DiagnosticSeverity

    var diagnosticID: MessageID {
        MessageID(domain: "MyMacros", id: id)
    }
}

Sinh mã dễ đọc

ReadableCodeGeneration.swiftswift
// ❌ Hard-to-read generated code
let badCode: DeclSyntax = "public init(a:Int,b:String,c:Bool){self.a=a;self.b=b;self.c=c}"

// ✅ Properly formatted generated code
let goodCode: DeclSyntax = """
    public init(
        a: Int,
        b: String,
        c: Bool
    ) {
        self.a = a
        self.b = b
        self.c = c
    }
    """

Mã được sinh nên dễ đọc như mã viết tay vì lập trình viên sẽ kiểm tra qua "Expand Macro".

Kết luận

Swift Macros là công cụ mạnh để loại bỏ boilerplate mà vẫn giữ được an toàn kiểu tĩnh. Công nghệ này cho phép:

Điểm mấu chốt:

  • ✅ Hai nhóm: freestanding (#) và attached (@)
  • ✅ Năm vai trò attached: peer, accessor, member, memberAttribute, conformance
  • ✅ Triển khai bằng swift-syntax và thao tác AST
  • ✅ Bắt buộc kiểm thử với SwiftSyntaxMacrosTestSupport
  • ✅ Yêu cầu gói riêng cho phần triển khai
  • ✅ Gỡ lỗi qua "Expand Macro" trong Xcode
  • ✅ Thông báo lỗi rõ ràng là yếu tố thiết yếu cho trải nghiệm lập trình viên

Swift Macros đặc biệt hữu ích để sinh các conformance (Equatable, Codable), tạo property wrapper nâng cao và hiện đại hóa các API dựa trên callback sang async/await.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

#swift
#ios
#macros
#metaprogramming
#swift-syntax

Chia sẻ

Bài viết liên quan