鍍金池/ 教程/ iOS/ 字符串解析
與四軸無人機的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機工作原理
Build 過程

字符串解析

在幾乎每一種計算機程序語言中,解析字符串都是我們不得不面對的問題。有時這些字符串以一種簡單的格式出現(xiàn),有時它們又變得很復(fù)雜。我們將利用多種方法把字符串轉(zhuǎn)換成我們需要的東西。下面,我們將討論正則表達(dá)式、掃描器、解析器以及在什么時候使用它們。

正則法 vs. 上下文無關(guān)文法(Context-Free Grammars)

首先,介紹一點點背景知識:解析一個字符串,其實就是用特定的語言來描述它。例如:把 @"42" 解析成數(shù)字,我們會采用自然數(shù)來描述這個字符串。語言都是用語法來描述的,語法其實就是一些規(guī)則的集合,這些規(guī)則可以用字符串來描述。比如自然數(shù),僅僅有一條規(guī)則:字符串的描述就是一個數(shù)字序列。這種語言也可以用標(biāo)準(zhǔn) C 函數(shù)或者正則表達(dá)式來描述。如果我們用正則表達(dá)式來描述一種語言,我們就可以說它有正則語法。

假設(shè)我們有一個表達(dá)式:"1 + 2 * 3",解析它就不容易。像這種表達(dá)式,我們可以用歸納語法來描述。換句話說,就是有一種語法,它的規(guī)則就是指的是它們自己,有時候甚至是遞歸的方式。為了識別這種語法,我們有三個規(guī)則:

  1. 任何數(shù)字都是語言的成員。
  2. 如果 x 是語言的成員,同時 y 也是語言的成員,那么 x+y 也是語言的成員。
  3. 如果 x 是語言的成員,同時 y 也是語言的成員,那么 x*y 也是語言的成員。

使用這種語法描述的語言稱之為上下文無關(guān)文法 (context-free grammars),或者簡稱 CFG 1。需要注意的是這種語法不能使用正則表達(dá)式來解析(雖然一些正則表達(dá)式能實現(xiàn),如 PCRE,但這遠(yuǎn)遠(yuǎn)超越了一般的正則語法)。一個經(jīng)典的例子就是括號匹配,它可以用 CFG 來解析,卻不能用正則表達(dá)式 2。

像數(shù)字,字符串和時間這些,就可以用正則語言來解析。意思是說你可以使用正則表達(dá)式(或者相似的技術(shù))去解析它們。

郵箱地址,JSON,XML 以及其它大多數(shù)的編程語言,都不能夠使用正則表達(dá)式來解析 3。我們需要一個真正的解析器來解析它們。大多數(shù)時候,我們需要的解析器就有現(xiàn)成的。蘋果就已經(jīng)為我們提供了 XML 和 JSON 解析器,如果想要解析 XML 和 JSON,用蘋果的就可以了。

正則表達(dá)式

當(dāng)你想要去識別一些簡單的語言時,正則表達(dá)式是一個好工具。但是,它們經(jīng)常被濫用在一些不適合它們的地方,比如 HTML 的解析?,F(xiàn)在假定我們有一個文件, 其中包含一個簡單的定義顏色的變量,設(shè)計者們可以利用該變量來改變你 iPhone app 中的顏色。格式如下:

backgroundColor = #ff0000

想要解析這種格式,我們就可以用正則表達(dá)式。正則表達(dá)式中最重要的是模式(pattern。如果你不知道什么是正則表達(dá)式,我們將很快的重新溫習(xí)一下,但是完全的解釋什么是正則表達(dá)式已經(jīng)超出了這篇文章的范圍。首先,我們來看一下 \\w+, 它的意思是匹配任何一個數(shù)字、字母或者是下劃線至少一次(\\w 代表匹配任意一個數(shù)字、字母或者是下劃線,+ 代表至少匹配一次)。然后,為了確保我們以后可以使用匹配的結(jié)果,需要用括號將它括起來,創(chuàng)建一個捕獲組(capture group)。接下來是一個空格符,一個等號,又一個空格符和一個 # 號。然后,我們需要匹配 6 個十六進制數(shù)字。\\p{Hex_Digit} 意思是匹配一個十六進制數(shù)字(Hex_Digit 是一個 unicode 屬性名)。修飾符 {6} 意味著我們需要匹配 6 個,然后和之前一樣,把這些一起用括號括起來,這樣就創(chuàng)建了第二個捕獲組:

NSError *error = nil;
NSString *pattern = @"(\\w+) = #(\\p{Hex_Digit}{6})";
NSRegularExpression *expression = [NSRegularExpression regularExpressionWithPattern:pattern
                                                                            options:0
                                                                              error:&error];
NSTextCheckingResult *result = [expression firstMatchInString:string 
                                                      options:0
                                                        range:NSMakeRange(0, string.length)];
NSString *key = [string substringWithRange:[result rangeAtIndex:1]];
NSString *value = [string substringWithRange:[result rangeAtIndex:2]];

上面我們創(chuàng)建了一個正則表達(dá)式對象,讓它匹配一個字符串對象 string,通過 rangeAtIndex 方法可以獲取用括號捕獲的兩組數(shù)據(jù)。在匹配的結(jié)果對象中,索引 0 是正則表達(dá)式對象自己,索引 1 是第一個捕獲組,索引 2 是第二個捕獲組,依此類推。最后,我們獲取到的 key 的值是 backgroundColor,value 的值是 ff0000。上面的正則表達(dá)式只解析了一行,下一步我們將要解析多行,并添加一些錯誤檢查。比如,輸入如下:

backgroundColor = #ff0000
textColor = #0000ff

首先,利用換行符將輸入字符串分隔開,然后遍歷返回的數(shù)組,并將解析的結(jié)果添加到我們的字典中,最后我們將生成這樣一個字典:@{@"backgroundColor": @"ff0000", @"textColor": @"0000ff"}。下面是具體的代碼:

NSString *pattern = @"(\\w+) = #([\\da-f]{6})";
NSRegularExpression *expression = [NSRegularExpression regularExpressionWithPattern:pattern
                                                                            options:0 
                                                                              error:NULL];
NSArray *lines = [input componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
NSMutableDictionary *result = [NSMutableDictionary dictionary];
for (NSString *line in lines) {
    NSTextCheckingResult *textCheckingResult = [expression firstMatchInString:line 
                                                                      options:0 
                                                                        range:NSMakeRange(0, line.length)];
    NSString* key = [line substringWithRange:[textCheckingResult rangeAtIndex:1]];
    NSString* value = [line substringWithRange:[textCheckingResult rangeAtIndex:2]];
    result[key] = value;
}
return result;

說句題外話,將字符串分解成數(shù)組,你還可以用 componentsSeparatedByString: 這個方法,或者用 enumerateSubstringsInRange:options:usingBlock: 這個方法來枚舉子串,其中 option 這個參數(shù)應(yīng)該傳 NSStringEnumerationByLines。

假如某一行數(shù)據(jù)沒有匹配上(比如,我們不小心忘記一個十六進制字符),我們可以檢查 textCheckingResult 對象是否為 nil,如果為 nil,就拋出一個錯誤,代碼如下:

 if (!textCheckingResult) {
     NSString* message = [NSString stringWithFormat:@"Couldn't parse line: %@", line]
     NSDictionary *errorDetail = @{NSLocalizedDescriptionKey: message};
     *error = [NSError errorWithDomain:MyErrorDomain code:FormatError userInfo:errorDetail];
     return nil;
 }

掃描器(Scanner)

把一個字符串轉(zhuǎn)化為一個字典,還有一種方式就是使用掃描器。幸運的是,F(xiàn)oundation 框架為我們提供了 NSScanner,一個易于使用的面向?qū)ο蟮腁PI。首先,我們需要創(chuàng)建一個掃描器:

NSScanner *scanner = [NSScanner scannerWithString:string];

默認(rèn)情況下,掃描器會跳過所有空格符和換行符。但這里我們只希望跳過空格符:

scanner.charactersToBeSkipped = [NSCharacterSet whitespaceCharacterSet];

然后,我們定義一個十六進制字符集。系統(tǒng)定義了很多字符集,但卻沒有十六進制字符集:

NSCharacterSet *hexadecimalCharacterSet = 
  [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdefABCDEF"];

我們先寫一個沒有錯誤檢查的版本。掃描器的工作原理是這樣的:它接收一個字符串,并將光標(biāo)設(shè)置在字符串的開始處。然后調(diào)用掃描方法,像這樣:[sanner scanString:@"=" intoString:NULL]。如果掃描成功,該方法會返回 YES,光標(biāo)會自動后移。scanCharactersFromSet:intoString: 方法的工作原理和之前的相似,只不過它掃描的是字符集,并將掃描的結(jié)果放入第二個參數(shù)的字符串指針?biāo)赶虻牡刂分?。我們使?&& 對不同的掃描方法進行 “與” 操作。這種方式的好處是只有與 && 操作符左邊的掃描成功時,&& 右邊的掃描方法才會被調(diào)用。

NSMutableDictionary *result = [NSMutableDictionary dictionary];
while (!scanner.isAtEnd) {
    NSString *key = nil;
    NSString *value = nil;
    NSCharacterSet *letters = [NSCharacterSet letterCharacterSet];
    BOOL didScan = [scanner scanCharactersFromSet:letters intoString:&key] &&
                   [scanner scanString:@"=" intoString:NULL] &&
                   [scanner scanString:@"#" intoString:NULL] &&
                   [scanner scanCharactersFromSet:hexadecimalCharacterSet intoString:&value] &&
                   value.length == 6;
    result[key] = value;
    [scanner scanCharactersFromSet:[NSCharacterSet newlineCharacterSet] 
                        intoString:NULL]; // 繼續(xù)掃描下一行
}
return result;

接下來添加一個有錯誤處理的版本,我們可以在 didScan 該行后面開始寫。如果掃描不成功,我們就返回 nil,并設(shè)置相應(yīng)的 error 參數(shù)。在解析文本時,當(dāng)輸入字符串格式不正確時,這個時候應(yīng)該怎么辦呢?是讓解析器崩潰,將錯誤值呈現(xiàn)給用戶,還是嘗試從錯誤中恢復(fù),這值得我們仔細(xì)地思考清楚:

    if (!didScan) {
        NSString *message = [NSString stringWithFormat:@"Couldn't parse: %u", scanner.scanLocation];
        NSDictionary *errorDetail = @{NSLocalizedDescriptionKey: message};
        *error = [NSError errorWithDomain:MyErrorDomain code:FormatError userInfo:errorDetail];
        return nil;
    }

C 語言也提供了具有掃描器功能的函數(shù),例如 sscanf(可以用 man sscanf 查看怎么使用)。它遵循和 printf 類似的語法,只不過操作是逆序的(它是解析一個字符串, 而不是生成一個)。

解析器

如果設(shè)計者希望像 (100,0,255) 這樣來定義 RGB 顏色,該怎么辦呢?我們必須讓解析顏色的方法更智能一些。事實上,在完成后面的代碼后,我們就已經(jīng)會寫一個基本的解析器了。

首先,我們將添加一些方法到我們類中,并聲明一個屬性,類型為 NSScanner。第一個方法是 scanColor:,其作用是掃描十六進制的顏色值(例如 ff0000)或者 RGB 元組,例如(255,0,0)

- (NSDictionary *)parse:(NSString *)string error:(NSError **)error
{
    self.scanner = [NSScanner scannerWithString:string];
    self.scanner.charactersToBeSkipped = [NSCharacterSet whitespaceCharacterSet];

    NSMutableDictionary *result = [NSMutableDictionary dictionary];
    NSCharacterSet *letters = [NSCharacterSet letterCharacterSet]
    while (!self.scanner.isAtEnd) {
        NSString *key = nil;
        UIColor *value = nil;
        BOOL didScan = [self.scanner scanCharactersFromSet:letters intoString:&key] &&
                       [self.scanner scanString:@"=" intoString:NULL] &&
                       [self scanColor:&value];
        result[key] = value;
        [self.scanner scanCharactersFromSet:[NSCharacterSet newlineCharacterSet]
                                 intoString:NULL]; // 繼續(xù)掃描下一行
    }
}

scanColor: 這個方法非常簡單。首先,它試圖掃描一個十六進制的顏色值,如果失敗,它會嘗試掃描 RGB 元組:

- (BOOL)scanColor:(UIColor **)out
{
    return [self scanHexColorIntoColor:out] || [self scanTupleColorIntoColor:out];
}

掃描一個十六進制顏色和之前是一樣的。唯一的區(qū)別是我們將其封裝在一個方法中, 并且使用的都是 NSScanner 的方法。它會返回一個 BOOL 值表示掃描成功,并將結(jié)果存儲到一個指向 UIColor 對象的指針:

- (BOOL)scanHexColorIntoColor:(UIColor **)out
{
    NSCharacterSet *hexadecimalCharacterSet = 
       [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdefABCDEF"];
    NSString *colorString = NULL;
    if ([self.scanner scanString:@"#" intoString:NULL] &&
        [self.scanner scanCharactersFromSet:hexadecimalCharacterSet 
                                 intoString:&colorString] &&
        colorString.length == 6) {
        *out = [UIColor colorWithHexString:colorString];
        return YES;
    }
    return NO;
}

掃描基于 RGB 元組的顏色值也非常相似。在掃描 @"(" 時,我們進行了與操作。在生產(chǎn)環(huán)境代碼中,我們可能需要更多的錯誤檢查,例如確保整數(shù)的范圍 0-255

- (BOOL)scanTupleColorIntoColor:(UIColor **)out
{
    NSInteger red, green, blue = 0;
    BOOL didScan = [self.scanner scanString:@"(" intoString:NULL] &&
                   [self.scanner scanInteger:&red] &&
                   [self.scanner scanString:@"," intoString:NULL] &&
                   [self.scanner scanInteger:&green] &&
                   [self.scanner scanString:@"," intoString:NULL] &&
                   [self.scanner scanInteger:&blue] &&
                   [self.scanner scanString:@")" intoString:NULL];
    if (didScan) {
        *out = [UIColor colorWithRed:(CGFloat)red/255.
                               green:(CGFloat)green/255.
                                blue:(CGFloat)blue/255. 
                               alpha:1];
        return YES;
    } else {
        return NO;
    }
}

寫一個掃描器,就是在邏輯上將多個可變的掃描值混合起來,并調(diào)用其它的一些方法。解析器不僅是一個非常吸引人的主題,還是一個強大的工具。一旦你知道如何編寫一個解析器,你就可以發(fā)明一些小語言,如定義樣式表、解析約束、查詢數(shù)據(jù)模型、描述業(yè)務(wù)邏輯,等等。關(guān)于這個話題 Fowler 寫了一本非常有趣的書,名為《領(lǐng)域特定語言》。

標(biāo)記化(Tokenization)

我們已經(jīng)有一個非常簡單的解析器,它可以從一個文件中的字符串中提取鍵值對,我們也可以使用這些字符串生成 UIColor 對象。但是還沒有完。要是設(shè)計者想要定義更多的事情,怎么辦?比如,假設(shè)我們有不同的文件,其中包含一些布局的約束,格式如下:

myView.left = otherView.right * 2 + 10
viewController.view.centerX + myConstant <= self.view.centerX

我們該如何解析這個呢?實踐證明正則表達(dá)式并不是最好的方法。

在我們進行解析之前,先把這個字符串進行標(biāo)記化是一個不錯的主意。標(biāo)記化就是將一個字符串轉(zhuǎn)換成一連串標(biāo)記 (token)的過程。 例如,myConstant = 100 被標(biāo)記化的結(jié)果可能會是 @[@"myConstant", @"=", @100]。在大多數(shù)程序語言中, 標(biāo)記化就是刪除空白符并將相關(guān)的字符解析成標(biāo)記。在我們的語言中,標(biāo)記可以是標(biāo)識符(如 myConstantcenterX),操作符(如 .,+=)或數(shù)字(如 100)。在標(biāo)記化之后,標(biāo)記會繼續(xù)被解析。

為了實現(xiàn)標(biāo)記化(有時也稱為詞法分析 lexing 或掃描 scanning),我們可以重用 NSScanner 類。首先,我們可以專注于解析只包含操作符的字符串:

NSScanner *scanner = [NSScanner scannerWithString:contents];
NSMutableArray *tokens = [NSMutableArray array];
while (![scanner isAtEnd]) {
  for (NSString *operator in @[@"=", @"+", @"*", @">=", @"<=", @"."]) {
      if ([scanner scanString:operator intoString:NULL]) {
          [tokens addObject:operator];
      }
  }
}

下一步是識別像 myConstantviewController 這樣的標(biāo)識符。為了簡單起見,標(biāo)識符只包含字母(沒有數(shù)字)。如下:

NSString *result = nil;
if ([scanner scanCharactersFromSet:[NSCharacterSet letterCharacterSet] 
                        intoString:&result]) {
    [tokens addObject:result];
}

如果這些字符被找到,scanCharactersFromSet:intoString: 這個方法會返回 YES,然后我們將這些找到的字符添加到我們的標(biāo)記數(shù)組。我們快要完成了,唯一剩下的事情就是是解析數(shù)字了。幸運的是,NSScanner 也提供了一些方法。我們可以使用 scanDouble: 方法來掃描 double 類型數(shù)據(jù),并將其封裝成 NSNumber 對象然后添加到標(biāo)記數(shù)組:

double doubleResult = 0;
if ([scanner scanDouble:&doubleResult]) {
    [tokens addObject:@(doubleResult)];
}

現(xiàn)在我們的解析器完成了,下面我們來進行測試:

NSString* example = @"myConstant = 100\n"
                    @"\nmyView.left = otherView.right * 2 + 10\n"
                    @"viewController.view.centerX + myConstant <= self.view.centerX";
NSArray *result = [self.scanner tokenize:example];
NSArray *expected = @[@"myConstant", @"=", @100, @"myView", @".", @"left", 
                      @"=", @"otherView", @".", @"right", @"*", @2, @"+", 
                      @10, @"viewController", @".", @"view", @".", 
                      @"centerX", @"+", @"myConstant", @"<=", @"self", 
                      @".", @"view", @".", @"centerX"];
XCTAssertEqualObjects(result, expected);

我們的掃描器可以對操作符,姓名,以及被封裝成 NSNumber 對象的數(shù)字創(chuàng)建獨立的標(biāo)記。完成這些之后,我們準(zhǔn)備進行第二步:把這個標(biāo)記數(shù)組解析成更有意義的一些東西。

語法解析(Parsing)

我們之所以不能用正則表達(dá)式或掃描器來解決上述問題,是因為解析有可能失敗。假定我們現(xiàn)在有一個標(biāo)記:@“myConstant”。在我們的解析函數(shù)中,我們并不知道這是約束表達(dá)式的開始還是一個常數(shù)定義。我們需要兩個都試一下,看看哪一個成功。我們可以手工來寫這個解析代碼,難倒是不難,但是寫出來的代碼就像一坨屎;或者我們可以使用更合適的工具:語法解析庫(parsing library)

首先,我們需要語法分析庫能理解的方式來描述我們的語言。下面的代碼就是專為我們那個布局約束語言寫的解析語法,使用的是擴展的巴科斯范式EBNF)寫法:

constraint = expression comparator expression
comparator = "=" | ">=" | "<="
expression = keyPath "." attribute addMultiplier addConstant
keyPath = identifier | identifier "." keyPath
attribute = "left" | "right" | "top" | "bottom" | "leading" | "trailing" | "width" | "height" | "centerX" | "centerY" | "baseline"
addMultiplier = "*" atom
addConstant = "+" atom
atom = number | identifier

有許多的 Objective-C 庫用于語法解析(參見 CocoaPods)。像 CoreParse 就提供了很多 Objective-C 的 API。然而,我們并不能直接將我們的語法應(yīng)用在它上面。CoreParse 一次僅僅只有一個解析器工作。這意味著每當(dāng)解析器需要在兩個規(guī)則之間做決定(比如 keyPath 規(guī)則)的時候,它會根據(jù)下一個標(biāo)記來做決定。如果事后我們發(fā)現(xiàn)它選錯了,那麻煩就大了。當(dāng)然有的解析器允許更模糊的語法,但性能損失很大。

為了確保能夠兼容語法分析庫,可以對我們的語法做一些重構(gòu)。 我們也可以將它轉(zhuǎn)換成標(biāo)準(zhǔn)的巴科斯范式BNF),下面的代碼就是 CoreParse 支持的格式:

NSString* grammarString = [@[
    @"Atom ::= num@'Number' | ident@'Identifier';",
    @"Constant ::= name@'Identifier' '=' value@<Atom>;",
    @"Relation ::= '=' | '>=' | '<=';",
    @"Attribute ::= 'left' | 'right' | 'top' | 'bottom' | 'leading' | 'trailing' | 'width' | 'height' | 'centerX' | 'centerY' | 'baseline';",
    @"Multiplier ::= '*' num@'Number';",
    @"AddConstant ::= '+' num@'Number';",
    @"KeypathAndAttribute ::= 'Identifier' '.' <AttributeOrRest>;",
    @"AttributeOrRest ::= att@<Attribute> | 'Identifier' '.' <AttributeOrRest>;",
    @"Expression ::= <KeypathAndAttribute> <Multiplier>? <AddConstant>?;",
    @"LayoutConstraint ::= lhs@<Expression> rel@<Relation> rhs@<Expression>;",
    @"Rule ::= <Atom> | <LayoutConstraint>;",
] componentsJoinedByString:@"\n"];

如果一個規(guī)則被匹配了,那么這個解析器就試圖找到具有同樣名稱的類(如 Expression)。如果這個類實現(xiàn)了 initWithSyntaxTree: 方法,那么該方法就會被調(diào)用。另外,解析器還有一個委托,當(dāng)有一個規(guī)則被匹配上或者發(fā)生錯誤時,委托都會被調(diào)用。舉例來說,我們先來看一下 CPSyntaxTree 類,它的第一個子節(jié)點是一個關(guān)鍵字標(biāo)記(調(diào)用 keyword 方法獲?。赡馨?@"=",@">=" 或者 @"<=" 中的任意一個。屬性 layoutAttributes 是一個字典,它的 key 是一個字符串,value 是一個關(guān)于布局的 NSNumber 對象:

- (id)parser:(CPParser *)parser didProduceSyntaxTree:(CPSyntaxTree *)syntaxTree
    NSString *ruleName = syntaxTree.rule.name;
    if ([ruleName isEqualToString:@"Attribute"]) {
        return self.layoutAttributes[[[syntaxTree childAtIndex:0] keyword]];
    }
    ...

解析器的完整代碼在 GitHub,其中有一個類,大約 100 行代碼,我們可以用它解析復(fù)雜的布局約束,如:

viewController.view.centerX + 20 <= self.view.centerX * 0.5

我們會得到下面這樣的結(jié)果,它可以很容易地轉(zhuǎn)換成一個 NSLayoutConstraint 對象:

(<Expression: self.keyPath=(viewController, view), 
              self.attribute=9,
              self.multiplier=1, 
              self.constant=20> 
 -1 
 <Expression: self.keyPath=(self, view), 
              self.attribute=9,
              self.multiplier=0.5,
              self.constant=0>)

其他的工具

除了 Objective-C 的庫,其他的一些工具比如 Bison,YaccRagel,以及 Lemon,都是用 C 語言實現(xiàn)的。

另一件你可以做的事就是在 Build 時使用這些解析器生成一部分自己的代碼。例如,一旦你有了一種語言的解析器,你就可以創(chuàng)建一個簡單的命令行轉(zhuǎn)換工具。添加一個 Xcode 的 Build 規(guī)則,每一次 Build 時,你自己的語言就會被一起編譯。

關(guān)于語法分析的思考

語法分析看起來有一點奇怪,而且創(chuàng)建基于字符串的語言似乎并不是 Objective-C 的風(fēng)格。但事實恰恰相反,蘋果一直廣泛使用著基于字符串的語言。如 NSLog 格式化字符串,NSPredicate 字符串,可視化的布局約束格式語言,甚至是 KVC。所有這些都用了一些小的內(nèi)部解析器來解析字符串,并將其變成對象和方法。通常你不必自己編寫一個解析器,這大大節(jié)省了工作時間:常見的語言如 JSON 和 XML 都有通用的解析器。但是如果你想要編寫一個計算器,一種圖形語言,甚至是一個嵌入式的 Smalltalk,解析器大有幫助。