目录
0 引言
命名空间是一种编程语言中常用的机制,命名空间机制在不同的编程语言中的实现方式和语法可能不同,但是核心思想是一致的,即:通过将相关的变量、函数和类封装在封装在一个独立的命名空间当中,避免命名冲突和提高程序的可读性和可维护性。例如,Ruby 中的命名空间是由模块(module)来实现的,模块可以封装变量、方法和类;Python 中的模块(module)和包(package)也可以看作命名空间,可以用来组织和管理变量、函数和类。
在 Tcl 语言中,全局变量和过程可以在任何地方被修改和访问,这就会导致命名冲突的问题。通过引入命名空间机制,可以将一组相关的变量和过程封装在一个独立的命名空间中,从而避免与其它命名空间中的对象发生冲突。
故命名空间有几大优点:
- 使程序的结构更加清晰,不同部分的代码更加容易理解和修改
- 提供一种隔离机制,使不同模块之间可以独立开发和测试,从而减少了程序的错误和 bug
1 创建命名空间
语法结构:
# 通过namespace eval来创建命名空间
namespace eval spaceName {
# 命名空间内的代码
}
namespace eval 用于在全局命名空间中创建一个新的命名空间,然后在新的命名空间内部执行一段代码,可以定义变量和过程。其中,spaceName 是命名空间的名称,可以是任意字符串。
# 在当前即全局命名空间创建proc
proc put {} {
puts "Hello from global namespace !"
}
# 创建命名空间ns1并在内部创建proc
namespace eval ns1 {
proc put {} {
puts "Hello from ns1 !"
}
}
# 创建命名空间ns2并在内部创建proc
namespace eval ns2 {
proc put {} {
puts "Hello from ns2 !"
}
}
# 命名空间嵌套,在命名空间内部在创建子命名空间
namespace eval ns3 {
namespace eval ns3_1 {
proc put {} {
puts "Hello from ns3_1 !"
}
}
namespace eval ns3_2 {
proc put {} {
puts "Hello from ns3_2 !"
}
}
}
虽然上述过程名均为 put ,但是将其定义在不同的命名空间中,不会导致命名冲突,只需要调用对应的命名空间中的过程即可。我们知道对于全局命名空间定义的过程,可以直接用过程名进行访问。那么如何调用访问其它子命名空间内部的过程呢?
命名空间分隔符 "双冒号" ::
# 调用全局命名空间
put
::put ;#直接以::开头表示访问全局命名空间
# 调用ns1命名空间的proc
ns1::put
# 调用ns3_2命名空间的proc
ns3::ns3_2::put
从上述创建和访问命名空间的过程来看,命名空间呈树状结构。类似于我们访问某个目录下的文件,全局命名空间就相当于根目录。
2 检查命名空间
命令名称 | 解释 |
namespace eval | 创建命名空间 |
namespace current | 获取当前命名空间 |
namespace children | 返回指定命名空间的子空间 |
namespace parent | 返回指定命名空间的父空间 |
namespace exists | 查看指定命名空间是否存在 |
namespace delete | 删除指定命名空间 |
# namespace current 获取当前命名空间
puts [namespace current]
# namespace eval 创建命名空间
namespace eval ns {
proc myproc {} {
puts "This string from ns."
}
namespace eval ns_1 {
proc myproc {} {
puts "This string from ns_1."
}
}
namespace eval ns_2 {
proc myproc {} {
puts "This string from ns_2."
}
}
}
# namespace children 返回指定命名空间的子空间列表
puts [namespace children ::]
puts [namespace children];# 与上条代码等效
puts [namespace children ns]
puts [namespace children ns::ns_1]
# namespace parent 返回指定命名空间的父空间列表
puts [namespace parent ns]
puts [namespace parent ::]
puts [namespace parent];# 与上条代码等效
puts [namespace parent ns::ns_1]
# namespace exists 查看命名空间是否存在
puts [namespace exists ::];# 返回值是1
puts [namespace exists ns::ns_1];# 返回值是1
puts [namespace exists ns::ns_3];# 返回值是0
# namespace delete 删除指定命名空间及其中的变量和命令
namespace delete ns::ns_1
puts [namespace exists ns::ns_1];# 返回值是0
ns::ns_2::myproc ;# 输出 This string from ns_2.
ns::ns_1::myproc ;# 报错 invalid command name "ns::ns_1::myproc"
3 命名空间导出和导入命令
导出 export 和导入 import 命名空间的命令通常成对使用,例如,将某一个子命名空间中的过程导出,然后在导入到全局命名空间中,进而可以在全局命名空间中直接调用该过程,无需加命名空间分隔符的方式调用。
# 创建test命名空间
namespace eval test {
# 定义过程
proc add {a b} {
set sum [expr {$a + $b}]
puts "$a+$b=$sum"
}
proc print {} {
puts "Hello,Tcl!"
}
# 将当前空间的add过程导出;所以要在当前空间执行这条命令
# namespace export add
# namespace export print
# 也可以使用通配符
namespace export *
}
# 在test空间中创建一个test_1的子空间
namespace eval test::test_1 {
proc diff {a b} {
set difference [expr {$a - $b}]
puts "$a-$b=$difference"
}
namespace export diff
}
# 将add过程导入当前空间;所以要在全局空间执行这条命令
# namespace import test::add
# namespace import test::print
namespace import test::*
namespace import test::test_1::diff
add 2 3
diff 2 1
print
4 命名空间中的变量
- 在 Tcl 中要在命名空间中创建一个变量,可以使用如下语法:
# 在命名空间中创建变量
namespace eval spaceName {
variable varName value
}
其中 spaceName 是命名空间名称,命令 variable 创建变量,varName 为变量名称,value 为变量的初始值。
- 访问该变量,可以使用 $spaceName::varName 的语法。
namespace eval ns {
variable var "Hello,Tcl!"
}
puts $ns::var
- 在命名空间中访问全局变量(global关键字)、命名空间变量(variable关键字)及局部变量
# 下面代码中,定义了三个名称都为num的变量,但是作用域和数值不同
set num 1000 ;# 全局命名空间中的变量
namespace eval ns {
# ns子命名空间中的变量
variable num 100
proc print_ns {} {
variable num
puts "ns_var: $num"
}
proc print_global {} {
global num
puts "global_var: $num"
}
proc print_local {} {
# 过程中的变量也叫局部变量
set num 0
puts "local_var: $num"
}
}
# 在ns命名空间中调用过程
ns::print_global ;# 打印全局变量
ns::print_ns ;# 打印命名空间变量
ns::print_local ;# 打印局部变量
思考:我们在命名空间内部定义变量是通过 variable 命令来定义,而在全局命名空间中用 set 命令来定义。但是这两者本质上来讲没有区别,只不过全局命名空间是父空间。所以按道理在全局命名空间中来创建变量也可以用 variable 命令来实现。下面是做的实验:
# 在当前也即全局命名空间定义变量
variable x -100
proc print {} {
# 在全局空间过程内部定义变量
set x 100
puts "$x"
}
print ;# 调用全局命名空间的过程
::print ;# 与上一条代码等价
puts $x
# 在全局过程内部访问外部变量
proc print1 {} {
# 需要用global关键字申明
global x
puts "$x"
}
print1 ;# 调用全局命名空间的过程
从输出结果可以看出,思考是正确的,不过实际使用时,为了区分全局空间还是子空间,还是不能乱用。
总结:实际上,我们可以将不同作用域的变量分为两类:一类是外部变量,是指定义在过程外部的变量,如果外部变量定义在全局命名空间中,则为全局变量,在过程内部需要用 global 关键字申明来实现访问;如果外部变量定义在全局命名以外的子空间,则为命名空间变量,在过程内部则需要用 variable 关键字申明来实现访问。另一类是内部变量,是指定义在过程 proc 内部的变量,不管是定义在全局空间还是子空间中,都只能在过程内部进行访问,且无需关键字申明。
5 访问命名空间变量的几种方式
5.1 过程 proc 外部的变量(外部变量)
# 过程外部变量
# 全局空间中访问全局变量
set x 0
puts $x
# 子空间中访问子空间变量
namespace eval ns {
variable y 1
puts $y
}
# 全局空间中访问子空间变量
puts $ns::y
# 子空间中访问全局变量
namespace eval ns {
global x
puts $x
}
5.2 过程 proc 内部的变量(内部变量)
- 过程内部变量,在各自空间内部访问时无需关键字申明。
# 过程内部变量
# 全局空间过程内部访问
proc value {} {
set a 1
puts "$a"
}
value ;#调用全局空间过程
# 子空间过程内部访问
namespace eval NS {
proc value {} {
set b 9
puts "$b"
}
}
NS::value ;# 调用子空间过程
不同过程内部的变量是无法相互访问的:
# 过程1
proc proc1 {} {
set a 1
puts $a
}
# 过程2
proc proc2 {} {
set b 9
set sum [expr {$a+$b}]
puts $sum
}
proc2
5.3 在过程内部访问外部变量(同一个命名空间)
# 在过程内部访问全局空间变量
set a 100
proc incr {b} {
# 需要用global关键字申明
global a
set sum [expr {$a + $b}]
puts $sum
}
incr 1
# 在过程内部访问子空间变量
namespace eval ns {
variable a 100
proc incr {b} {
# 需要用variable关键字申明
variable a
set sum [expr {$a + $b}]
puts $sum
}
}
ns::incr 1
5.4 在过程内部访问外部变量(不同命名空间)
#子空间1
namespace eval ns1 {
variable a 1
}
#子空间2
namespace eval ns2 {
variable b 9
proc add {} {
variable b
# 通过namespace upvar命令应用命名空间ns1中的变量,要注意分隔符的使用::ns1
namespace upvar ::ns1 a A
set sum [expr {$A + $b}]
puts $sum
}
}
ns2::add
总结:这里有个经常分不清的就是在调用过程和变量的时候,老是不知道要不要加美元符号。后面才发现,spaceName procName 和 spaceName varName这两个命令都是访问的过程名和变量名。因为调用过程的时候,返回过程名,过程中有时候定义了puts命令,直接输出了结果;调用变量时,返回变量名时程序不会直接输出结果,所以需要使用 puts ${spaceName varName}命令才能直接输出结果。
当然,命名空间还有其它的内容,因为笔者目前知识初学阶段,故不在继续深究!如果这篇文章对您有帮助,请给个关注哦,谢谢!