在幾乎每一種計(jì)算機(jī)程序語(yǔ)言中,解析字符串都是我們不得不面對(duì)的問(wèn)題。有時(shí)這些字符串以一種簡(jiǎn)單的格式出現(xiàn),有時(shí)它們又變得很復(fù)雜。我們將利用多種方法把字符串轉(zhuǎn)換成我們需要的東西。下面,我們將討論正則表達(dá)式、掃描器、解析器以及在什么時(shí)候使用它們。
首先,介紹一點(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ī)則:
x 是語(yǔ)言的成員,同時(shí) y 也是語(yǔ)言的成員,那么 x+y 也是語(yǔ)言的成員。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ā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;
}
把一個(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ǔ)言》。
我們已經(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í)符(如 myConstant 或 centerX),操作符(如 .,+ 或 =)或數(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í)別像 myConstant 和 viewController 這樣的標(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ù)組解析成更有意義的一些東西。
我們之所以不能用正則表達(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ù),其他的一些工具比如 Bison,Yacc,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ì)被一起編譯。
語(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,解析器大有幫助。