Swift 宏:深入探索獨立宏的潛力


摘要

本文章深入探討 Swift 宏及其在現代開發中的重要性,揭示了這項技術如何改變我們編寫程式碼的方式。 歸納要點:

  • Swift 宏的演進從早期概念到獨立宏的成熟應用,展現了其設計理念與實現方式的變化。
  • SwiftSyntax 在獨立宏開發中扮演關鍵角色,提供解析程式碼結構及操作語法樹的工具和技巧。
  • 獨立宏在程式碼生成、驗證、最佳化等場景中的實務應用,不僅提升程式碼品質,也增強開發效率。
透過分析 Swift 宏的歷史、應用和未來展望,我們可以看到它對於提升開發流程的重要影響。


Swift Macros:革新程式碼開發的強大工具

在2023年,隨著 Swift 5.9 的發布,Swift Macros 被引入並迅速成為開發者們喜愛的功能。Swift Macros 提供了一種強大的方式來減少重複程式碼,提高程式碼可讀性,以及實現更具表達力的程式碼,因為它允許在編譯時生成程式碼。在這篇文章中,我們將探討獨立宏(Freestanding Macros)及其如何在您的專案中發揮作用。

Swift Macros 是一種編譯時功能,允許開發者在構建過程中自動生成程式碼。與傳統的執行時程式碼生成不同,Swift Macros 在編譯期間操作,確保生成的程式碼是型別安全的並且無縫整合到您的程式碼庫中。這使得維護和除錯變得更加容易,因為生成的程式碼是可見的,可以直接在 Xcode 中檢查。

要充分理解和利用 Swift Macros,對 SwiftSyntax 有一定了解是至關重要的。SwiftSyntax 是一個 Swift 庫,提供了用於解析、分析和生成 Swift 程式碼的工具。它形成了宏如何與您程式碼在語法層面互動的基礎,使開發者能夠以結構化和型別安全的方式操作和生成 Swift 程式碼。

**專案1:最新趨勢:Macros 在 Swift 框架開發中的應用**

Swift Macros 的出現不僅僅是減少重複程式碼的一個工具,更為框架開發帶來了革命性的改變。開發者可以利用 macros 來構建更複雜的框架,例如:

* **動態生成 API 和資料模型:** 使用 macros 可以根據配置檔案或其他資訊自動生成 API 端點和資料結構,簡化框架擴充套件性,同時減少硬編碼的程式碼量。

* **實現框架級驗證和安全機制:** macros 能夠在編譯階段對程式碼進行驗證,例如檢查函式引數型別、約束訪問許可權,有效提升框架安全性。

* **構建可定製化框架:** macros 可以根據使用者設定動態生成不同程式碼,使得框架更具靈活性,以滿足各種使用場景。

**專案2:深入要點:Macros 與超程式設計之間的關係**

透過有效地利用 macros,可以進一步探索超程式設計(metaprogramming)的概念,它讓程式設計師能夠寫出能夠產生或操控其他程式自身結構或行為的程式。在 Swift 中,引入 macros 不僅提高了生產力,也讓語言本身更具彈性與適應性。因此,在當前軟體工程領域,上述技術將成為未來開發的重要基石。

總之,Swift Macros 為開發者提供了一個創新的解決方案,不但能簡化繁瑣任務,更加速了從設計到實作之間的重要流程,是未來軟體工程不可忽視的一環。

SwiftSyntax 與獨立宏:提升程式碼品質與可維護性的利器

SwiftSyntax 提供了一個原生的 Swift 介面,讓開發者能夠操作 Swift 程式碼的抽象語法樹(AST)。這使得開發者可以解析原始碼、對其進行修改,以及以程式化方式生成新的程式碼。這對於 Swift 宏來說至關重要,因為宏依賴於在編譯過程中轉換和生成程式碼。透過 SwiftSyntax,您可以建立自定義宏,分析現有程式碼並產生新程式碼,同時確保所生成的程式碼遵循 Swift 的語法規則。在 Swift 中,有兩種型別的宏:獨立宏(Freestanding Macros)與附加宏(Attached Macros)。獨立宏是指不依附於任何特定宣告而獨立使用的宏;而附加宏則會修改其所附加的宣告。

在這篇部落格文章中,我們將深入探討獨立宏。當談及靜態分析與程式碼生成時,SwiftSyntax 與獨立宏之間展現出強大的協同效應。透過利用這些工具,開發者能夠有效地提升程式碼質量與可維護性,同時借助最佳實踐來最佳化效能表現。例如,在處理大型專案或複雜邏輯時,自訂獨立宏可幫助自動化繁瑣任務,加快開發流程,使得團隊能更專注於核心功能的實現。因此,不僅是技術上的選擇,更是一種促進效率和增強軟體品質的重要策略。


要新增一個宏,我們需要建立一個新的套件。這個套件應該依賴於 swift-syntax 套件。

選擇檔案 > 新增 > 套件,然後選擇 Swift Macro。


輸入宏的名稱並點選建立。(避免在命名中使用 ′Macro′ 字尾。)建立套件後,Xcode 將為您生成一個 Package.swift 檔案。

// swift-tools-version: 5.10 // The swift-tools-version declares the minimum version of Swift required to build this package.  import PackageDescription import CompilerPluginSupport  let package = Package(     name: "Stringfy",     platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],     products: [         // Products define the executables and libraries a package produces, making them visible to other packages.         .library(             name: "Stringfy",             targets: ["Stringfy"]         ),         .executable(             name: "StringfyClient",             targets: ["StringfyClient"]         ),     ],     dependencies: [         .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),     ],     targets: [         // Targets are the basic building blocks of a package, defining a module or a test suite.         // Targets can depend on other targets in this package and products from dependencies.         // Macro implementation that performs the source transformation of a macro.         .macro(             name: "StringfyMacros",             dependencies: [                 .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),                 .product(name: "SwiftCompilerPlugin", package: "swift-syntax")             ]         ),          // Library that exposes a macro as part of its API, which is used in client programs.         .target(name: "Stringfy", dependencies: ["StringfyMacros"]),          // A client of the library, which is able to use the macro in its own code.         .executableTarget(name: "StringfyClient", dependencies: ["Stringfy"]),          // A test target used to develop the macro implementation.         .testTarget(             name: "StringfyTests",             dependencies: [                 "StringfyMacros",                 .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),             ]         ),     ] )

Xcode 將會建立一個名為 stringify 的範例宏。

/// A macro that produces both a value and a string containing the /// source code that generated the value. For example, /// ///     #stringify(x + y) /// /// produces a tuple `(x + y, "x + y")`. @freestanding(expression) public macro stringify(_ value: T) -> (T, String) = #externalMacro(module: "StringfyMacros", type: "StringifyMacro")

/// Implementation of the `stringify` macro, which takes an expression /// of any type and produces a tuple containing the value of that expression /// and the source code that produced the value. For example /// ///     #stringify(x + y) /// ///  will expand to /// ///     (x + y, "x + y") public struct StringifyMacro: ExpressionMacro {     public static func expansion(         of node: some FreestandingMacroExpansionSyntax,         in context: some MacroExpansionContext     ) -> ExprSyntax {         guard let argument = node.argumentList.first?.expression else {             fatalError("compiler bug: the macro does not have any arguments")         }          return "(\(argument), \(literal: argument.description))"     } }  @main struct StringfyPlugin: CompilerPlugin {     let providingMacros: [Macro.Type] = [         StringifyMacro.self,     ] }

這裡是一個如何使用 stringify 宏的示例:

import Stringfy  let a = 17 let b = 25  let (result, code) = #stringify(a + b)  print("The value \(result) was produced by the code \"\(code)\"")

獨立巨集是 Swift 中一個獨特且強大的特性,以符號井號(#)來識別。這些巨集是自包含的,意味著它們所產生的程式碼會精確地放置在呼叫該巨集的位置。獨立巨集可以分為兩種主要型別:表示式巨集和宣告巨集。

表示式巨集具有透過操作引數的語法來轉換和擴充套件原始碼的能力。當你希望強制執行特定的編碼實踐或自動化重複性程式碼模式時,它們特別有用。例如,考慮一個情境,你想要避免強制解包 URL(這在許多專案中是一個常見的陷阱)。你可以建立一個表示式巨集,自動將任何強制解包的 URL 替換為更安全且經過良好測試的替代方案。這樣可以確保你的程式碼庫遵循最佳實踐,而無需人工幹預。

這裡有一個簡單的範例,展示瞭如何使用表示式宏:

import Foundation  /// The `URL` macro is a Swift macro designed to facilitate the creation of `URL` objects from string literals. /// This macro ensures that the provided string is a valid URL at compile time, reducing runtime errors /// related to invalid URLs. /// /// - Parameters: ///   - string: A `String` representing the URL. This string is validated at compile time to ensure it ///   is well-formed and can be safely used as a `URL`. /// /// - Returns: ///   A `URL` object initialized with the provided string. /// /// - Example: /// ///     ```swift ///     let website = #URL("https://sisal.com.tr") /// ///     // If the string is not a valid URL, the compiler will raise an error. If it is valid, it will return ///     // a non-optional `URL` object. ///     ```  @freestanding(expression) public macro URL(_ string: StaticString) -> URL = #externalMacro(module: "SisalMacros", type: "URLMacro")

import Foundation  public struct URLMacro: ExpressionMacro {          static let invalidCharacters = CharacterSet.urlQueryAllowed         .union(CharacterSet(charactersIn: "%+?#[]"))         .inverted          static let urlSchemeAllowedCharacters = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: ".-"))          public static func expansion(of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> SwiftSyntax.ExprSyntax {                  guard node.argumentList.count == 1 else {             throw MacroError.message("#URL should have one String literal argument")         }                  guard let literal = node.argumentList.first?.expression.as(StringLiteralExprSyntax.self),               literal.segments.count == 1,               let string = literal.segments.first?.as(StringSegmentSyntax.self)?.content else {             throw MacroError.message("#URL should be a String literal")         }                  if let idx = string.text.rangeOfCharacter(from: Self.invalidCharacters)?.lowerBound {             throw MacroError.message("\"\(string.text)\" has invalid character at index \(string.text.distance(from: string.text.startIndex, to: idx)) (\(string.text[idx]))")         }                  guard let scheme = string.text.range(of: ":").map({ string.text[..<$0.lowerBound] }),               scheme.rangeOfCharacter(from: Self.urlSchemeAllowedCharacters.inverted) == nil else {             throw MacroError.message("URL must contain a scheme")         }                  guard let url = URL(string: string.text) else {             throw MacroError.message("\"\(string.text)\" is not a valid URL")         }                  guard url.scheme == String(scheme) else {             throw MacroError.message("URL must contain a scheme")         }                  guard url.absoluteString == string.text else {             throw MacroError.message("Resulting URL \"\(url.absoluteString)\" is not equal to \"\(string.text)\"")         }                  return "URL(string: \"\(raw: string.text)\")!"     } }  enum MacroError: Error, CustomStringConvertible {          case message(String)          var description: String {         switch self {         case .message(let text):             return text         }     } }

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

import Sisal import Foundation // WARNING: Don't forget to add Foundation to see results for this example.  // A function to send a GET request to an API func fetchData(from url: URL) {     let task = URLSession.shared.dataTask(with: url) { data, response, error in         if let error = error {             print("An error occurred: \(error)")             return         }                  guard let data = data else {             print("No data received.")             return         }                  // Process the received data (e.g., JSON parsing)         if let jsonString = String(data: data, encoding: .utf8) {             print("Received data: \(jsonString)")         }     }     task.resume() }  // Valid URL using the #URL macro let validAPIEndpoint = #URL("https://api.sisal.com.tr/v1/data")  // Invalid URLs to demonstrate error handling by our URL macro // let missingScheme = #URL("api.sisal.com.tr/v1/data") // Error: URL must contain a scheme // let invalidCharacter = #URL("https://api.sisal.com.tr/v1/data?query=hello world!") // Error: "https://api.sisal.com.tr/v1/data?query=hello world!" has invalid character at index 44 ( ) // let multipleArguments = #URL("https://sisal.com.tr", "https://example.com") // Error: Extra argument in macro expansion // let invalidURLFormat = #URL("https://:invalid-url") // Error: "https://:invalid-url" is not a valid URL  // Start fetching data from the valid API endpoint fetchData(from: validAPIEndpoint)

編譯階段輸出:


宣告宏的運作方式與表達宏相似,但有一個關鍵的區別:它們生成的是宣告,例如函式、變數或型別。一個實用的宣告宏示例可能涉及在類別中生成唯一的函式名稱,以避免名稱衝突或確保功能的獨特性。例如,@Unique 宏可以用來自動生成類別內具有唯一名稱的函式。這樣可以確保該函式名稱不會與類別中的任何現有名稱發生衝突,從而幫助避免潛在的錯誤並改善程式碼的可維護性。

/// The `@Unique` macro is a custom Swift macro designed to automatically generate /// a unique function name within a class. This can be useful in scenarios where /// you need to avoid name collisions or want to ensure that a function has a distinct name /// within a class. /// /// The macro is declared as a freestanding declaration macro and can be applied to any class /// to inject a function with a unique name. /// /// - Example: /// /// ```swift /// @Unique /// public class MyClass { ///     // The class will have a uniquely named function automatically generated ///     // by the macro. The function name is guaranteed to be unique within the scope of the class. /// } /// ```  @freestanding(declaration, names: named(MyClass)) public macro Unique() = #externalMacro(module: "SisalMacros", type: "UniqueMacro")

/// The `UniqueMacro` is the implementation of the `@Unique` macro. It conforms to the `DeclarationMacro` /// protocol, which defines the behavior of macros that generate declarations, such as classes, /// functions, or variables. /// /// This macro generates a unique function name by appending a unique identifier to the function name. /// The generated function is inserted into the class where the macro is applied.  public enum UniqueMacro: DeclarationMacro {          /// Expands the `@Unique` macro into a class declaration with a uniquely named function.     ///     /// - Parameters:     ///   - node: The syntax node where the macro is applied. This represents the location in the source code     ///   where the macro is invoked.     ///   - context: The context in which the macro is expanded. This provides additional information and tools     ///   needed to generate the code, such as generating unique names.     ///     /// - Returns: An array of `DeclSyntax` representing the generated declarations. In this case, it returns     ///   a class declaration with a uniquely named function.     ///     /// - Throws: If the macro encounters an error during expansion, it throws an error that will be reported     ///   to the developer.      public static func expansion(         of node: some FreestandingMacroExpansionSyntax,         in context: some MacroExpansionContext     ) throws -> [DeclSyntax] {                  let name = context.makeUniqueName("unique")            return [       """       class MyClass {         func \(name)() {}       }       """         ]     } }

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

獨立宏:提升 Swift 程式碼品質與效率的利器

獨立宏(Freestanding macros)具有多樣化的應用性,可以在多種場景中運用,以提升程式碼品質、執行最佳實踐並減少重複程式碼。一些實際的使用案例包括:

自訂日誌:利用表示式宏自動生成詳細的日誌訊息,這些訊息可以包含當前函式的名稱、行號或其他上下文資訊。

編譯時檢查:使用獨立宏引入編譯時警告或錯誤,以針對特定情況進行檢查,例如過時的 API 使用或潛在的程式碼異味。

程式碼生成:利用宣告宏來生成重複性的程式碼,例如從 JSON 資料生成模型物件或初始化器,確保一致性並減少手動工作量。

透過將獨立宏整合進您的開發流程中,您能夠創造出更高效、可維護且富有表現力的 Swift 程式碼。

Freestanding Macros 的能力遠不止於生成重複程式碼。其強大的超程式設計能力可以更進一步地利用編譯時資訊進行更複雜的程式碼生成。例如,結合 Swift 的反射機制,可以設計出自動生成 API 檔案、測試用例,甚至根據資料模型自動生成 UI 介面等功能。這將大幅提升開發效率,並降低維護成本。

另外,Freestanding Macros 可被視為一種定製化 Swift 語言特性的方法。透過定義新的語法或語法糖,可以更方便地表達特定領域的邏輯或概念。例如,可以定義專屬於資料科學領域的資料處理宏,以簡化常用的資料分析操作,使 Swift 更加適合特定領域的開發。

總之,開發者希望了解 Freestanding Macros 如何幫助他們更有效地編寫 Swift 程式碼並提升程式碼質量。透過其超程式設計能力,不僅能實現更複雜的程式碼生成功能,也能與其他 Swift 特性(如反射)結合,用以滿足各種定製化需求。因此,在探索如何最佳化和擴充套件您的 Swift 開發流程時,不妨考慮充分運用 Freestanding Macros 的潛力。

LinkedIn:Sisal 的 LinkedIn 頁面:https://www.linkedin.com/company/sisal-digital-hub-t%C3%BCrkiye
Sisal 的 Medium 平台頁面:https://medium.com/sisaldigitalhubturkiye

Swift AST Explorer
SwiftSyntax 文件
SwiftSyntax 範例於 GitHub:https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/


MZ

專家

相關討論

❖ 相關專欄