使用脚本配置vim编辑器(三)、字典

回复 收藏
学习何时使用字典使代码更简洁,更迅速
字典是一个从列表提供不同优化和权衡的容器数据结构。特别说明的是,字典中元素存储的顺序是无关紧要的,每个元素的身份是明确的。在介绍 Vimscript 系列 文章的第四篇中,Damian Conway 将向您介绍字典,包括它们的基础语法和许多函数的概述。他最后还举出了一些例子,来说明如何使用字典获取更有效的数据处理和更简洁的代码。
vimscript 中的字典 在本质上和 AWK 关联数组、Perl 哈希表,或者 Python 字典都是一样。也就是说,这是一个无序容器,按字符串而不是整数来进行索引。vimscript 系列 的第四篇将会介绍这一重要的数据结构,并解释其复制、过滤、扩展和整理的多项功能。这些例子重点说明列表和字典之间的差别,以及一些例子。在这些例子中,与讲述内置列表的 使用脚本编写 Vim 编辑器,第 3 部分:内置列表 中开发出的基于列表的解决方案相比,使用字典是一个更好的替代方案。
vimscript 中的字典
您可以通过在键/值对列表上加花括号来创建一个字典。在每一对中,键和值用冒号分隔。例如:
清单 1. 创建一个字典
let seen = {}   " Haven't seen anything yet
let 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: 0

let 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 dict1
let dict3 = copy(dict1)       " dict3 has a copy of dict1's top-level elements
let 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 all
let 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 entry
call map(dict, value_transform_str)    " Transform values by eval'ing string
echo 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) . "\"
    \    . introducer . " " . a:comment        . "\"
    \    . introducer . repeat(box_char,width) . "\"
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   = '[-+*/%|&]\?=\@' . original_line
        if !has_key(have_already_seen, normalized_line)
            call add(unique_lines, original_line)
            let have_already_seen[normalized_line] = 1
        endif
    endfor
    " Replace the range of original lines with just the unique lines...
    exec a:firstline . ',' . a:lastline . 'delete'
    call append(a:firstline-1, unique_lines)
endfunction
uniq() 函数被声明为接受一个范围,因此只能调用一次,即使在缓冲区内的一个行范围上调用时。
调用时,它首先设置一个空的字典(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()
或者您可以将其应用到一个代码的特殊集中(例如,一个在 Visual 模式中选定的范围),就像这样:
vmap  u :call Uniq()
2015-07-29 23:52 举报
已邀请:

回复帖子,请先登录注册

退出全屏模式 全屏模式 回复
评分
可选评分理由: