在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

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

字符串解析

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

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

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

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

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

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

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

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

正則表達(dá)式

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

backgroundColor = #ff0000

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

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

backgroundColor = #ff0000
textColor = #0000ff

首先,利用換行符將輸入字符串分隔開,然后遍歷返回的數(shù)組,并將解析的結(jié)果添加到我們的字典中,最后我們將生成這樣一個(gè)字典:@{@"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;

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

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

 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)

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

NSScanner *scanner = [NSScanner scannerWithString:string];

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

scanner.charactersToBeSkipped = [NSCharacterSet whitespaceCharacterSet];

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

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

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

接下來(lái)添加一個(gè)有錯(cuò)誤處理的版本,我們可以在 didScan 該行后面開始寫。如果掃描不成功,我們就返回 nil,并設(shè)置相應(yīng)的 error 參數(shù)。在解析文本時(shí),當(dāng)輸入字符串格式不正確時(shí),這個(gè)時(shí)候應(yīng)該怎么辦呢?是讓解析器崩潰,將錯(cuò)誤值呈現(xiàn)給用戶,還是嘗試從錯(cuò)誤中恢復(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 語(yǔ)言也提供了具有掃描器功能的函數(shù),例如 sscanf(可以用 man sscanf 查看怎么使用)。它遵循和 printf 類似的語(yǔ)法,只不過(guò)操作是逆序的(它是解析一個(gè)字符串, 而不是生成一個(gè))。

解析器

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

首先,我們將添加一些方法到我們類中,并聲明一個(gè)屬性,類型為 NSScanner。第一個(gè)方法是 scanColor:,其作用是掃描十六進(jìn)制的顏色值(例如 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: 這個(gè)方法非常簡(jiǎn)單。首先,它試圖掃描一個(gè)十六進(jìn)制的顏色值,如果失敗,它會(huì)嘗試掃描 RGB 元組:

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

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

- (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 元組的顏色值也非常相似。在掃描 @"(" 時(shí),我們進(jìn)行了與操作。在生產(chǎn)環(huán)境代碼中,我們可能需要更多的錯(cuò)誤檢查,例如確保整數(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;
    }
}

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

標(biāo)記化(Tokenization)

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

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

我們?cè)撊绾谓馕鲞@個(gè)呢?實(shí)踐證明正則表達(dá)式并不是最好的方法。

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

為了實(shí)現(xiàn)標(biāo)記化(有時(shí)也稱為詞法分析 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];
      }
  }
}

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

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

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

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

現(xiàn)在我們的解析器完成了,下面我們來(lái)進(jìn)行測(cè)試:

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);

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

語(yǔ)法解析(Parsing)

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

首先,我們需要語(yǔ)法分析庫(kù)能理解的方式來(lái)描述我們的語(yǔ)言。下面的代碼就是專為我們那個(gè)布局約束語(yǔ)言寫的解析語(yǔ)法,使用的是擴(kuò)展的巴科斯范式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 庫(kù)用于語(yǔ)法解析(參見 CocoaPods)。像 CoreParse 就提供了很多 Objective-C 的 API。然而,我們并不能直接將我們的語(yǔ)法應(yīng)用在它上面。CoreParse 一次僅僅只有一個(gè)解析器工作。這意味著每當(dāng)解析器需要在兩個(gè)規(guī)則之間做決定(比如 keyPath 規(guī)則)的時(shí)候,它會(huì)根據(jù)下一個(gè)標(biāo)記來(lái)做決定。如果事后我們發(fā)現(xiàn)它選錯(cuò)了,那麻煩就大了。當(dāng)然有的解析器允許更模糊的語(yǔ)法,但性能損失很大。

為了確保能夠兼容語(yǔ)法分析庫(kù),可以對(duì)我們的語(yǔ)法做一些重構(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"];

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

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

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

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

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

(<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 的庫(kù),其他的一些工具比如 BisonYacc,Ragel,以及 Lemon,都是用 C 語(yǔ)言實(shí)現(xiàn)的。

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

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

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