使用脚本编写 Vim 编辑器,第 4 部分: 字典
Vimscript 中的字典?在本質上和 AWK 關聯數組、Perl 哈希表,或者 Python 字典都是一樣。也就是說,這是一個無序容器,按字符串而不是整數來進行索引。
Vimscript?系列?的第四篇將會介紹這一重要的數據結構,并解釋其復制、過濾、擴展和整理的多項功能。這些例子重點說明列表和字典之間的差別,以及一些例子。在這些例子中,與講述內置列表的?使用腳本編寫 Vim 編輯器,第 3 部分:內置列表?中開發出的基于列表的解決方案相比,使用字典是一個更好的替代方案。
Vimscript 中的字典
您可以通過在鍵/值對列表上加花括號來創建一個字典。在每一對中,鍵和值用冒號分隔。例如:
清單 1. 創建一個字典
let seen = {} " Haven't seen anything yetlet daytonum = { 'Sun':0, 'Mon':1, 'Tue':2, 'Wed':3, 'Thu':4, 'Fri':5, 'Sat':6 } let diagnosis = {\ 'Perl' : 'Tourettes',\ 'Python' : 'OCD',\ 'Lisp' : 'Megalomania',\ 'PHP' : 'Idiot-Savant',\ 'C++' : 'Savant-Idiot',\ 'C#' : 'Sociopathy',\ 'Java' : 'Delusional',\}一旦完成了創建字典,您就可以使用標準方括號索引符號來訪問它的值,但是要使用字符串作為索引而不是一個數字:
let lang = input("Patient's name? ")let Dx = diagnosis[lang]如果在字典中不存在該鍵,就拋出一個異常:
let Dx = diagnosis['Ruby'] **E716: Key not present in Dictionary: Ruby**不過,您可以使用?get()?函數,安全地訪問一個可能不存在的條目。get()?使用兩個參數:一個是字典本身,另一個是在字典中查找的鍵。如果字典中存在該鍵,就會返回相應的值;如果鍵不存在,get()?就返回零。或者,您可以指定第三個參數,如果沒有找到鍵,get()?返回這個值:
let Dx = get(diagnosis, 'Ruby') " Returns: 0let Dx = get(diagnosis, 'Ruby', 'Schizophrenia') " Returns: 'Schizophrenia'訪問一個特殊的字典條目還有第三個方法。如果這個條目的鍵只由標識符字符(字母數字和下劃線)組成,您可以使用 “點符號” 訪問相應的值,就像:
let Dx = diagnosis.Lisp " Same as: diagnosis['Lisp']diagnosis.Perl = 'Multiple Personality' " Same as: diagnosis['Perl']這種特殊限制符號使得字典就像記錄或者結構體一樣易于使用:
let user = {}let user.name = 'Bram' let user.acct = 123007 let user.pin_num = '1337'回頁首
字典的批量處理
Vimscript 提供一些功能,允許您獲取字典中所有鍵的列表、所有值的列表,或者所有鍵/值對的列表:
let keylist = keys(dict) let valuelist = values(dict) let pairlist = items(dict)這個?items()?函數事實上返回一個列表的清單,其中,每個 “內部” 清單正好有兩個元素:一個鍵及其對應值。因此,items()?在您想要迭代字典中所有條目的時候尤為方便:
for [next_key, next_val] in items(dict)let result = process(next_val)echo "Result for " next_key " is " result endfor回頁首
賦值和身份
在字典中賦值就和在 Vimscript 列表中一樣。字典由引用(即指針)來表示,所以將字典賦給另一個變量就將兩個變量設為相同的底層數據結構,前者相當于后者的別名。您可以首先通過復制或者深復制(deep-coping)原始內容來解決這個問題:
let dict2 = dict1 " dict2 just another name for dict1let dict3 = copy(dict1) " dict3 has a copy of dict1's top-level elementslet dict4 = deepcopy(dict1) " dict4 has a copy of dict1 (all the way down)和列表一樣,您可以用?is?操作符來比較身份,用?==?操作符來比較值:
if dictA is dictB" They alias the same container, so must have the same keys and values elseif dictA == dictB" Same keys and values, but maybe in different containers else" Different keys and/or values, so must be different containers endif回頁首
添加和刪除條目
將一個條目添加到字典中,只需要對新的鍵賦一個值:
let diagnosis['COBOL'] = 'Dementia'要合并來自其它字典的條目,可以使用?extend()?函數。第一參數(正在進行擴展的)和第二參數(包含額外的條目)都必須是字典:
call extend(diagnosis, new_diagnoses)當您想要顯式添加多個條目時,使用?extend()?也是非常方便的:
call extend(diagnosis, {'COBOL':'Dementia', 'Forth':'Dyslexia'})將一個獨立條目從字典中刪除有兩種方法:使用內置的?remove()?函數,或者使用?unlet?命令:
let removed_value = remove(dict, "key")unlet dict["key"]從一個字典刪除多個條目時,使用?filter()?會更簡潔,更有效。filter()?函數的工作方法和列表中的相同,除了用?v:val?來檢測各條目的值,您還可以用?v:key?來檢測它的鍵。例如:
清單 2. 檢測值和鍵
" Remove any entry whose key starts with C... call filter(diagnosis, 'v:key[0] != "C"')" Remove any entry whose value doesn't contain 'Savant'... call filter(diagnosis, 'v:val =~ "Savant"')" Remove any entry whose value is the same as its key... call filter(diagnosis, 'v:key != v:val')回頁首
其它字典相關函數
除了?filter()?以外,字典可以使用其它和列表相同的內置函數和方法。幾乎在所有的情況(最顯著的例外是?string())下,一個應用到字典的列表函數的行為就像該函數收到了該字典的一個值列表。清單 3 顯示了最常用的函數。
清單 3. 其它適用于字典的列表函數
let is_empty = empty(dict) " True if no entries at alllet entry_count = len(dict) " How many entries?let occurrences = count(dict, str) " How many values are equal to str?let greatest = max(dict) " Find largest value of any entry let least = min(dict) " Find smallest value of any entrycall map(dict, value_transform_str) " Transform values by eval'ing stringecho string(dict) " Print dictionary as key/value pairs內置的?filter()?在字典內規范化數據時非常方便。例如,給定一個包含首選用戶名(或許按用戶 ID 索引)的字典,您可以保證每個名稱的大小寫都正確,就像這樣:
call map( names, 'toupper(v:val[0]) . tolower(v:val[1:])' )調用?map()?可以遍歷各個值,將其作為別名賦給?v:val,在字符串中計算表達式,并用表達式結果替換值。在這個例子中,它將名稱的首字母大寫,其它的字母保持小寫,然后用修改過的字符串作為新的名稱值。
回頁首
部署字典獲得更簡潔的代碼
本系列的?使用腳本編寫 Vim 編輯器,第 3 部分:內置列表?用一個在指定文本周圍生成評論框的小例子,解釋了 Vimscript 的?variadic?函數參數。可選參數可以添加在文本字符串之后,用來指定評論人,用作 “框” 的字符,以及評論的寬度。清單 4 復制了原始的函數。
清單 4. 將可選參數傳遞為可變參數
function! CommentBlock(comment, ...)" If 1 or more optional args, first optional arg is introducer...let introducer = a:0 >= 1 ? a:1 : "//"" If 2 or more optional args, second optional arg is boxing character...let box_char = a:0 >= 2 ? a:2 : "*"" If 3 or more optional args, third optional arg is comment width...let width = a:0 >= 3 ? a:3 : strlen(a:comment) + 2 " Build the comment box and put the comment inside it...return introducer . repeat(box_char,width) . "\<CR>"\ . introducer . " " . a:comment . "\<CR>"\ . introducer . repeat(box_char,width) . "\<CR>" endfunction可變參數(variadic arguments)用于指定函數選項是很方便的,但是有兩個主要的缺點:它們對函數的參數強制進行明確排序,但在函數調用時沒有明確排序。
重溫自動評論
正如?清單 4?所說明的,當任何參數可選時,通常需要事前決定其指定的排序。然而,這一需要凸顯了一個設計問題:為了指定一個稍后的選項,用戶必須在此之前明確地指定所有選項。理想情況下,第一選項應該是最常用的,第二選項是第二常用的,以此類推。事實上,在函數廣泛部署以前決定這些排序是很困難的;您如何能知道哪個選項對大多數人是最重要的?
例如,在清單 4 中的?CommentBlock()?函數,假設評論人可能是最需要的可選參數,所以將其放置在參數列表的第一位。但是如果這個函數用戶只用 C 和 C++ 編程,所以從沒有改變過默認評論人要怎么辦?更糟糕的是,如果評論塊的寬度因各個新項目而不同又會怎樣?這將是讓人惱火的,因為開發人員現在不得不每次指定所有三個可選參數,即便如此,頭兩個還是常常會給出其默認值:
" Comment of required width, with standard delimiter and box character... let new_comment = CommentBlock(comment_text, '//', '*', comment_width)這個問題也直接導致了第二個問題,即當任何選項需要被明確地指定時,它們中的幾個都不得不被指定。但是,因為默認是通常最需要的值,用戶可能會不熟悉如何指定選項,因此也不熟悉所需的排序。這就會導致如下的實現錯誤:
" Box comment using ==== to standard line width... let new_comment = CommentBlock(comment_text, '=', 72)……令人不安的是,這會產生像這樣的(非)評論:
=727272727272727272727272727272 = A bad comment =727272727272727272727272727272
這個問題就是,可選參數沒有明確指出它們打算設置哪個選項。它們的含義由它們在參數列表里的位置隱式確定,所以其排序中的任何錯誤都會悄悄改變其含義。
這是一個使用錯誤工具工作的經典案例。只有在順序很重要,且位置最好地暗示身份時,列表才是完美的。但是,在這個例子中,可選參數的排序與其說是一個有利條件,倒不是說是個麻煩,它們的選項很容易被搞混,這會導致微妙的身份識別錯誤。
從某種意義上說,您所想要的和列表上給出的是完全相反的:一個順序不相關,但身份明確的數據結構。換句話說,就是字典。清單 5 顯示了相同的函數,但是使用的是通過字典,而不是可變參數指定的選項。
清單 5. 在字典中傳遞可選參數
點擊查看代碼清單
這個版本的函數,只傳遞了兩個參數:最重要的評論文本,以及一個選項字典。如果選項沒有指定,內置的?get()?函數可以用來檢索各個選項,或者其默認值。然后調用該函數,用已命名的選項/值對來配置其行為。在函數內實施參數解析就變得較為簡潔,函數調用也變得更具可讀性,不易出錯。例如:
" Comment of required width, with standard delimiter and box character... let new_comment = CommentBlock(comment_text, {'width':comment_width})" Box comment using ==== to standard line width... let new_comment = CommentBlock(comment_text, {'box':'=', 'width':72})回頁首
重構自動對齊
在本系列?使用腳本編寫 Vim 編輯器,第 3 部分:內置列表?中,我們更新了較早的名為?AlignAssignments()?的示例函數,對其進行轉換使用列表來存儲正在修改的文本內容。清單 6 再現了這個函數的升級版。
清單 6. 更新后的 AlignAssignments() 函數
function! AlignAssignments ()" Patterns needed to locate assignment operators...let ASSIGN_OP = '[-+*/%|&]\?=\@<!=[=~]\@!'let ASSIGN_LINE = '^\(.\{-}\)\s*\(' . ASSIGN_OP . '\)\(.*\)$'" Locate block of code to be considered (same indentation, no blanks)...let indent_pat = '^' . matchstr(getline('.'), '^\s*') . '\S'let firstline = search('^\%('. indent_pat . '\)\@!','bnW') + 1let lastline = search('^\%('. indent_pat . '\)\@!', 'nW') - 1if lastline < 0let lastline = line('$')endif" Decompose lines at assignment operators...let lines = []for linetext in getline(firstline, lastline)let fields = matchlist(linetext, ASSIGN_LINE)call add(lines, fields[1:3])endfor" Determine maximal lengths of lvalue and operator...let op_lines = filter(copy(lines),'!empty(v:val)')let max_lval = max( map(copy(op_lines), 'strlen(v:val[0])') ) + 1let max_op = max( map(copy(op_lines), 'strlen(v:val[1])' ) )" Recompose lines with operators at the maximum length...let linenum = firstlinefor line in linesif !empty(line)let newline\ = printf("%-*s%*s%s", max_lval, line[0], max_op, line[1], line[2])call setline(linenum, newline)endiflet linenum += 1endfor endfunction這個版本對數據進行緩存而不是重新加載,很大程度上改善了函數的有效性,但這會需要更多的維護費用。具體來說,因為它將各行的各種組件存儲在小的三元素數組中,代碼被 “神奇索引”(例如?v:val[0]?和?line[1])打亂,這些索引的名字對它們的用途沒有任何提示。
字典就是專門解決這一問題的,因為,像列表一樣,它將數據整理在一個單獨的結構中,但和列表不一樣的是,它們用一個字符串對各個數據進行標識,而不是用數字。如果這些字符串經過精心挑選,它們就可以使結果代碼更簡潔。不使用神奇索引,我們將獲得有意義的名稱(例如,v:val.lval?用于各行的?lvalue,line.op?用于各行的操作符)。
使用字典來重寫函數是非常簡單的,如清單 7 所示。
清單 7. 進一步改良的 AlignAssignments() 函數
function! AlignAssignments ()" Patterns needed to locate assignment operators...let ASSIGN_OP = '[-+*/%|&]\?=\@<!=[=~]\@!'let ASSIGN_LINE = '^\(.\{-}\)\s*\(' . ASSIGN_OP . '\)\(.*\)$'" Locate block of code to be considered (same indentation, no blanks)...let indent_pat = '^' . matchstr(getline('.'), '^\s*') . '\S'let firstline = search('^\%('. indent_pat . '\)\@!','bnW') + 1let lastline = search('^\%('. indent_pat . '\)\@!', 'nW') - 1if lastline < 0let lastline = line('$')endif" Decompose lines at assignment operators...let lines = []for linetext in getline(firstline, lastline)let fields = matchlist(linetext, ASSIGN_LINE)if len(fields) call add(lines, {'lval':fields[1], 'op':fields[2], 'rval':fields[3]})elsecall add(lines, {'text':linetext, 'op':'' })endifendfor" Determine maximal lengths of lvalue and operator...let op_lines = filter(copy(lines),'!empty(v:val.op)')let max_lval = max( map(copy(op_lines), 'strlen(v:val.lval)') ) + 1let max_op = max( map(copy(op_lines), 'strlen(v:val.op)' ) )" Recompose lines with operators at the maximum length...let linenum = firstlinefor line in lineslet newline = empty(line.op)\ ? line.text\ : printf("%-*s%*s%s", max_lval, line.lval, max_op, line.op, line.rval)call setline(linenum, newline)let linenum += 1endfor endfunction新版本中的差異用黑體字標出。只有兩處:各行的記錄現在是一個字典,而不是一個哈希表,每個記錄的元素的后續訪問使用已命名的查詢,而不是數值索引。總的結果是代碼的可讀性更強,更不容易發生數組索引常出現的離一(off-by-one)誤差。
回頁首
作為數據結構的字典
Vim 提供一個內置的命令,允許您從一個文件中刪除重復的行:
:%sort uu?選項會使內置的?sort?命令刪除重復的行(如果它們已經被存儲),前導的?%?會應用那個特殊的?sort?來應用到整個文件。如果您不在意保存文件中唯一一行的原始排序,這是非常方便的。如果這些行是獲獎者的名單,一個有限資源的登記表,一個待辦事項清單,或者其他注重順序的序列,這就可能會是一個問題。
無排序唯一性
字典的鍵天生是唯一的,所以可以用字典來刪除文件的重復行,為了完成這項工作可以采用保存這些行的原始排序的方法。清單 8 展示了一個用于完成該任務的簡單函數。
清單 8. 一個保存排序唯一性的函數
function! Uniq () range" Nothing unique seen yet...let have_already_seen = {}let unique_lines = []" Walk through the lines, remembering only the hitherto-unseen ones...for original_line in getline(a:firstline, a:lastline)let normalized_line = '>' . original_line if !has_key(have_already_seen, normalized_line)call add(unique_lines, original_line)let have_already_seen[normalized_line] = 1endifendfor" Replace the range of original lines with just the unique lines...exec a:firstline . ',' . a:lastline . 'delete'call append(a:firstline-1, unique_lines) endfunctionUniq()?函數被聲明為接受一個范圍,因此只能調用一次,即使在緩沖區內的一個行范圍上調用時。
調用時,它首先設置一個空的字典(have_already_seen),這個字典用于跟蹤在指定范圍內已經遇到了哪些行。之前沒有見過的行就會被添加到存儲在?unique_lines?的清單中。
然后函數提供一個循環,準確地完成這一工作。它通過?getline()?從緩沖區獲取代碼的指定范圍,并對各項進行迭代。它首先在每一行添加一個前導?'>'?,確保它不是空的。Vimscript 字典不能存儲一個鍵為空字符串的條目,所以緩沖區中為空的代碼不會被正確地添加到have_already_seen。
一旦這些行被規范化,那么函數就能檢查該行是否已經作為鍵在?have_already_seen?字典中被使用過。如果是的話,被確定的這行肯定已經被查看過,所以被添加到?unique_lines,這樣重復的部分就可以忽略。相反地,如果該行是第一次遇到,那么原始(未規范化的)一行必須被添加到?unique_lines,規范化的那一版必須作為鍵被添加到?have_already_seen。
當所有的代碼已經按這種方法過濾了一遍之后,unique_lines?將會只會包含它們中唯一的子集,按照遇見的先后順序排列。所有留下的這些行將會刪除其原始的行組,用這些積累下來的唯一行來替換它(通過一個?append())。
有了這樣一個可用的函數,您可以設置一個正常模式的鍵映射來調用全部文件的命令,就像這樣:
nmap ;u :%call Uniq()<CR>或者您可以將其應用到一個代碼的特殊集中(例如,一個在 Visual 模式中選定的范圍),就像這樣:
vmap u :call Uniq()<CR>回頁首
展望未來
目前我們討論的 Vimscript 基本特性(語句和函數,數組和哈希表)已經足夠為 Vim 的核心特性集創建幾乎任何類型的添加項。但是我們所看到的擴展,都需要用戶通過發布一個正常模式命令,或者在插入模式中輸入一個特殊的序列,明確地請求行為。
在本系列的下一篇文章中,我們會介紹 Vim 的內置事件模型,并探索如何建立在用戶編輯時自動觸發的用戶自定義函數。
參考資料
學習
- 從本系列第一篇文章:“使用腳本編寫 Vim 編輯器,第 1 部分:變量、值和表達式”(developerWorks,2009 年 5 月)開始學習 Vimscript 和擴展 Vim 編輯器的嵌入式語言。
- “使用腳本編寫 Vim 編輯器,第 2 部分:用戶定義函數”(developerWorks,2009 年 7 月)了解 Vimscript 的標量數據類型:字符串、數字和布爾值。
- “使用腳本編寫 Vim 編輯器,第 3 部分:內置列表”(developerWorks,2010 年 1 月)介紹列表數據結構并學習介紹其用法的幾個例子。
- 查看以下資源,繼續學習 Vim 編輯器及其許多命令:
- Vim 主頁
- 在線圖書?A Byte of Vim
- 關于 Vim 的各種圖書
- Vim 手冊
- Steve Oualline 的?Vim Cookbook
- 要獲得 Vimscript 腳本的大量例子,請查看:
- Vim Tips wiki
- Vimscript 歸檔
- from:?http://www.ibm.com/developerworks/cn/linux/l-vim-script-4/index.html
總結
以上是生活随笔為你收集整理的使用脚本编写 Vim 编辑器,第 4 部分: 字典的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用脚本编写 Vim 编辑器,第 2 部
- 下一篇: 使用脚本编写 Vim 编辑器,第 5 部