Common Lisp Package 白痴指南
Common Lisp Package 白痴指南
1.介绍
当多个程序员编写大型项目时,两个不同的程序员通常可能会为两个不同的目的使用相同的名字。可以使用命名约定来解决这个问题,例如 Bob 在他所有的名字前加上“BOB-”,而 Jane 在她所有的名字前加上“JANE-”。这实际上就是 Scheme 解决这个问题的办法。(R6RS之后的 Scheme 标准也已经有了“模块”的标准了,所以这个问题已经不存在了。)
Common Lisp 提供了一个语言机制来隔离不同的命名空间。下面是包如何工作的示例:
? (make-package :bob :use '(common-lisp)) #<Package "BOB"> ? (make-package :jane :use '(common-lisp)) #<Package "JANE"> ? (in-package :bob) #<Package "BOB"> ? (defun foo () "This is Bob's foo") FOO ? (in-package :jane) #<Package "JANE"> ? (defun foo () "This is Jane's foo") FOO ? (foo) "This is Jane's foo" ? (in-package :bob) #<Package "BOB"> ? (foo) "This is Bob's foo"
(注意:代码示例是从 Macintosh Common Lisp (MCL) 中剪切和粘贴的。MCL 的命令提示符是一个问号。)
(注意:make-package
中的参数:use '(common-lisp)
是译者加上去的,sbcl 在定义新的包时不会自动:use :common-lisp
这个包,导致进入新的包以后,连defun
都无法执行,也无法使用in-package
切换到别的包。所以,原来的代码在sbcl上是无法运行的。考虑到目前sbcl基本上是使用人数最多的实现,所以译者作了一点修改,使得代码可以在sbcl上跑起来.)
如果 Bob 想要使用 Jane 编写的函数怎么办呢?有几种方法可以做到。一种是使用特殊语法来指定要使用不同的包:
? (in-package :bob) #<Package "BOB"> ? (jane::foo) "This is Jane's foo"
另一个办法是将他想要使用的东西导入到自己的包中。当然,Bob 可能不会想要导入 Jane 的 foo
函数,因为会和他自己的 foo
函数发生冲突。如果 Bob 想要导入 Jane 的 baz函数,他希望简单地通过 (baz)
去调用该函数,而不是输入(jane::baz)
,他可以像这样做:
? (in-package :jane) #<Package "JANE"> ? (defun baz () "This is Jane's baz") BAZ ? (in-package :bob) #<Package "BOB"> ? (import 'jane::baz) T ? (baz) "This is Jane's baz"
可惜,事情并不总是那么顺利:
? (in-package :jane) #<Package "JANE"> ? (defun bar () "This is Jane's bar") BAR ? (in-package :bob) #<Package "BOB"> ? (bar) > Error: Undefined function BAR called with arguments () . ;; 哦,忘了 import ? (import 'jane::bar) > Error: Importing JANE::BAR to #<Package "BOB"> would conflict with symbol BAR . ;; 咦, 又出错了?怎么回事?
要了解为什么会发生这种情况,如何处理,以及包的许多其他微妙之处以及惊喜,重要的是要了解包的实际作用以及它们是如何工作的。例如,重要的是要理解,当您键入 (import ‘jane::foo)
时,您导入的是符号JANE::FOO
,而不是与该符号关联的函数。理解这个差异很重要,因此我们必须从回顾一些基本的 Lisp 概念开始。
2.符号,值,以及 REPL
Lisp 在 READ-EVAL-PRINT 循环中运行。大多数有趣的事情发生在 EVAL 阶段,但是当谈到包时,有趣的事情发生在所有三个阶段,重要的是要了解什么时候发生。特别是,一些与包相关的处理会在 READ 时改变 Lisp 系统的状态,这反过来又会导致一些令人惊讶(常常是令人讨厌的)的行为,就像上一节中的最后一个例子。
包是 Lisp 符号的集合,因此要了解包,您首先必须了解符号。符号是一个非常普通的 Lisp 数据结构,就像列表、数字、字符串等一样。有用于创建和操作符号的内置 Lisp 函数。例如,有一个名为 gentemp
的函数可以创建新符号:
? (gentemp) T1 ? (setq x (gentemp)) T2 ? (set x 123) ;; 注意,这里使用的是 set, 不是 setq 或 setf 123 ? x T2 ? t2 123
(如果您不熟悉 SET
函数,现在就是查文档的好时机,因为如果您不搞清楚,您很快就会迷失方向。)
GENTEMP
创建的符号在所有方面的行为与你通过直接输入而产生的符号相当类似。
您只能有限地控制由 GENTEMP
创建的符号的名字。您可以向它传递一个可选的前缀字符串,但系统会自动 添加一个后缀,您必须接受它给您的任何内容。如果要使用特定的名字创建符号,则必须使用另外的函数 MAKE-SYMBOL:
(make-symbol "MY-SYMBOL") #:MY-SYMBOL
嗯,这很奇怪。那个看起来很有趣的前缀#:
在做什么?
要理解这一点,我们必须更深入地挖掘符号的内涵。
? (setq symbol1 (make-symbol "MY-SYMBOL")) #:MY-SYMBOL ? (setq symbol2 (make-symbol "MY-SYMBOL")) #:MY-SYMBOL ? (setq symbol3 'my-symbol) MY-SYMBOL ? (setq symbol4 'my-symbol) MY-SYMBOL ? (eq symbol1 symbol2) NIL ? (eq symbol3 symbol4) T
如您所见,MAKE-SYMBOL
可以生成多个具有相同名字的不同的符号,而直接输入相同的('my-symbol
)两次而得到的符号是相同的符号。
符号的这种特性非常重要。它确保了您在一个地方输入的FOO
与其它地方输入的FOO
是相同的符号,否则,就会得到一些非常奇怪的结果:
? (set symbol1 123) 123 ? (set symbol2 456) 456 ? (setq code-fragment-1 (list 'print symbol1)) (PRINT #:MY-SYMBOL) ? (setq code-fragment-2 (list 'print symbol2)) (PRINT #:MY-SYMBOL) ? (eval code-fragment-1) 123 123 ? (eval code-fragment-2) 456 456
对比一下:
? (set symbol3 123) 123 ? (set symbol4 456) 456 ? (setq code-fragment-3 (list 'print symbol3)) (PRINT MY-SYMBOL) ? (setq code-fragment-4 (list 'print symbol4)) (PRINT MY-SYMBOL) ? (eval code-fragment-3) 456 456 ? (eval code-fragment-4) 456 456
symbol1-symbol4 都具有名字“MY-SYMBOL”,但符号 1 和 2 是不同的符号,而符号 3 和 4 是相同的符号。这怎么发生的?嗯,一个明显的区别是我们调用函数 MAKE-SYMBOL 来生成符号 1 和 2,而符号 3 和 4 是由 Lisp reader(读取器)为我们生成的。也许 Lisp 读取器生成符号的方式与调用 MAKE-SYMBOL 生成符号并不相同.我们可以检验这个假设:
? (trace make-symbol) NIL ? 'foobaz Calling (MAKE-SYMBOL "FOOBAZ") MAKE-SYMBOL returned #:FOOBAZ FOOBAZ
[注:reader 读取到一个新符号时会调用 make-symbol 来产生新符号这种行为仅仅在 MCL & Clozure CL 测试中发生,其它实现可能采取不一样的实现方式。]
显然 reader 通过在内部调用 MAKE-SYMBOL
以与我们直接调用MAKE-SYMBOL
相同的方式产生符号。但是等等,MAKE-SYMBOL
返回的符号前面有那个有趣的#:
前缀,但是最后,那个前缀消失了。这是如何发生的?
我们可以在对MAKE-SYMBOL
保持跟踪的情况下再次尝试相同的实验来找出答案:
? 'foobaz FOOBAZ
啊哈!我们第二次输入 'FOOBAZ
时,reader 不再调用 MAKE-SYMBOL
。因此,reader显然保留了它所创建的所有符号的集合,并且在创建新符号之前,它会检查该集合以查看是否已经存在同名的符号。如果有,那么它只是返回相同的符号而不是创建一个新的。作为此类集合成员的符号会丢失其神秘的 #:
前缀。
这个符号的集合就是包(package)。
包是 Lisp 符号的集合,其特性是集合中不可能有两个名字相同的符号。
不幸的是,这或多或少是包简单明了的最后一个方面。从这里开始,事情变得更加棘手。
3.Interning
将符号放入包中的行为称为 interning (内部化)符号。作为包成员的符号被称为 interned (被内置)在该包中。不属于任何包成员的符号被称为 uninterned (未内部化)。当一个没有宿主包的符号被打印出来时,它就被附上前缀 #:
,以便于将它与内部的符号相区分。并且提示,它与你之前见过的符号仅仅是相似,它们实际上可能不是同一个符号。
[译注] intern 实际上是 internal 的简写
现在,事情开始变得有点混乱。
有一个名为INTERN
的 Lisp 函数,凭直觉您可能会认为它能够将不属于任何包的符号添加到某个包中,但事实并非如此。这个功能由名为import
的函数提供的:
? symbol1 #:MY-SYMBOL ? (import symbol1) T ? symbol1 MY-SYMBOL ? (eq symbol1 'my-symbol) T
如您所见,symbol1
已经从一个未内部化的符号变成了一个内部化的符号。它已经失去了#:
前缀,现在它等同于由 Lisp reader产生的符号 MY-SYMBOL
。
现在,你可能想调用UNIMPORT
来撤销掉IMPORT
的效果,但是并没有这样的函数。(我警告过你,事情不会那么简单),要从包中删除一个符号,你可以调用UNINTERN
:
? (unintern symbol1) T ? symbol1 #:MY-SYMBOL ? (eq symbol1 'my-symbol) NIL
现在,事情又回到原来的样子。symbol1
又变成未内部化的状态,它和 reader 返回给你的符号也不再相同。让我们再一次把它 import 到包里来:
? (import symbol1) > Error: Importing #:MY-SYMBOL to #<Package "COMMON-LISP-USER"> would conflict with symbol MY-SYMBOL .
又发生了什么?(在进一步阅读之前尝试弄清楚这里发生了什么是一个很好的练习。你已经有足够的信息来弄清楚事实。提示:你可以尝试使用 trace make-symbol 的实验)
这就是所发生的事情:
? (unintern 'my-symbol) T ? (eq symbol1 'my-symbol Calling (MAKE-SYMBOL "MY-SYMBOL") MAKE-SYMBOL returned #:MY-SYMBOL ) NIL
当我们执行测试 (eq symbol1 'my-symbol)
的时候,reader 已经悄悄地调用MAKE-SYMBOL
产生并且内部化了一个符号,当我们再次 import
的时候,包里已经有一个同名的符号了。这种情况称为符号冲突,唉,这种情况相当普遍。
请注意,顺便说一下,上面 MAKE-SYMBOL
跟踪的输出出现在 S 表达式(eq symbol1 ‘my-symbol)
中。这是因为 MCL 重新排列了屏幕上的文本以反映事件的真实顺序。因为 MAKE-SYMBOL
在处理字符串“my-symbol”时被读取器调用,但在处理关闭括号之前,输出出现在那个点。如果我们输入: (list 'x1 'x2 'x3 'x4)
结果输出如下所示:
? (list 'x1 Calling (MAKE-SYMBOL "X1") MAKE-SYMBOL returned #:X1 'x2 Calling (MAKE-SYMBOL "X2") MAKE-SYMBOL returned #:X2 'x3 Calling (MAKE-SYMBOL "X3") MAKE-SYMBOL returned #:X3 'x4 Calling (MAKE-SYMBOL "X4") MAKE-SYMBOL returned #:X4 )
在这一点上,我们可以继续并提及一些更有用的函数。 SYMBOL-NAME
以字符串形式返回符号的名字。 FIND-SYMBOL
接受一个字符串并告诉您是否已经存在具有该名字的符号。最后是 INTERN
,其实它可以像这样定义:
(defun intern (name) (or (find-symbol name) (let ((s (make-symbol name))) (import s) s)))
换句话说,INTERN
与 SYMBOL-NAME
相反。 SYMBOL-NAME
接受一个符号并返回该符号名字的字符串。 INTERN
接受字符串并返回名字为该字符串的符号。INTERN
是 READ
用来制作符号的函数。
intern string &optional package
=> symbol, status
参数和返回值:
string - 一个字符串。
package - 包描述符,默认为当前包。
symbol - 一个符号.
status - 下列之一 :inherited
, :external
, :internal
, or nil
.
可以把 intern
理解为UNIX命令 touch
,当文件不存在时,touch
只修改文件的时间戳,当文件不存在时,它会自创建一个新文件。而intern
在指定的符号不存在时会自动创建该符号,当符号存在时,则什么也不做,只是返回该符号。
4.哪一个包
对包进行操作的 FIND-SYMBOL
、IMPORT
和 INTERN
等函数默认的操作对象是全局变量*PACKAGE*
的值所标识的包,也称为当前包。和符号一样,包也有名字,系统中不可能同时存在两个相同名字的包。包和符号一样,也是普通的 Lisp 数据结构。有一些函数 PACKAGE-NAME
和 FIND-PACKAGE
的操作类似于 SYMBOL-NAME
和 FIND-SYMBOL
,当然,除了FIND-PACKAGE
不接受 PACKAGE 参数。 (就好像有一个全局的元包一样。)
正如我们已经看到的,我们可以通过调用 MAKE-PACKAGE
来创建新包,并通过调用 IN-PACKAGE
设置当前包。 (请注意, IN-PACKAGE
不是函数,而是不其参数求值的宏。)
您可以使用特殊语法访问不在当前包中的符号:包名后跟两个冒号,后跟符号名。如果您只想使用符号而不导入它,这样做很方便。例如,如果 Bob 想使用 Jane 的 FOO
函数,他可以输入 JANE::FOO
。
4.1 home 包
Common Lisp 试图维护的不变量之一是称为"打印-读取一致性"的属性。此属性表示,如果您打印一个符号,稍后再读取该符号的打印表示,结果应该得到相同的符号,但有两个警告:1)这不适用于非内部化的符号,2)仅当您避免某些“危险”的行为的时候。我们稍后会介绍有哪些操作会影响到“打印-读取一致性”。
为了保持打印读取的一致性,一些符号需要使用它们的包限定符来打印。例如:
? (in-package jane) #<Package "JANE"> ? 'foo FOO ? 'jane::foo FOO ? (in-package :bob) #<Package "BOB"> ? 'foo FOO ? 'jane::foo JANE::FOO ? 'bob::foo FOO
显然,会影响到"打印读取一致性"的“危险操作”之一是调用 IN-PACKAGE
。
现在,思考以下情况:
? (in-package 'bob) #<Package "BOB"> ? (unintern 'foo) T ? (import 'jane::foo) T ? (make-package :charlie) #<Package "CHARLIE"> ? (in-package :charlie) #<Package "CHARLIE"> ? 'bob::foo JANE::FOO
这里我们有一个符号 FOO
,它同时存在于 JANE
和 BOB
包中。因此,该符号可以同时作为 JANE::FOO
和 BOB::FOO
访问。当从 CHARLIE
包中打印符号时,系统如何决定使用哪种打印表示来打印非内置的符号?
事实证明,每个符号都跟踪一个称为其 home package 的包,它通常是该符号所在的第一个包(但也有例外)。当一个符号需要用包限定符打印时,它使用来自它的 home 包的限定符。您可以使用函数 SYMBOL-PACKAGE
查询其 home 包的符号。
? (symbol-package 'bob::foo) #<Package "JANE"> ?
请注意,可以在没有 home 的情况下制作符号。例如,未内部化的包。还可以在没有 home 包的情况下制作已内部化的包,比如:
? (in-package :jane) #<Package "JANE"> ? 'weird-symbol WEIRD-SYMBOL ? (in-package :bob) #<Package "BOB"> ? (import 'jane::weird-symbol) T ? (in-package :jane) #<Package "JANE"> ? (unintern 'weird-symbol) T ? (in-package 'bob) #<Package "BOB"> ? 'weird-symbol WEIRD-SYMBOL ? (symbol-package 'weird-symbol) NIL ? (in-package :jane) #<Package "JANE"> ? 'bob::weird-symbol #:WEIRD-SYMBOL
这其实是需要避免发生的事情。
5. export 和 use
假设 Jane 和 Bob 正在合作进行一个软件开发项目,每个人都在自己的包中工作以避免冲突。简写道:
? (in-package 'jane) #<Package "JANE"> ? (defclass jane-class () (slot1 slot2 slot3)) #<STANDARD-CLASS JANE-CLASS>
现在,假设 Bob 想要使用 JANE-CLASS
,他写道:
? (in-package :bob) #<Package "BOB"> ? (import 'jane::jane-class) T ? (make-instance 'jane-class) #<JANE-CLASS #x130565E>
到目前为止没什么问题。现在他尝试:
? (setq jc1 (make-instance 'jane-class)) #<JANE-CLASS #x130BA96> ? (slot-value jc1 'slot1) > Error: #<JANE-CLASS #x130BA96> has no slot named SLOT1.
发生了什么?好吧,JANE-CLASS
是在 JANE
包中定义的,所以,槽的名字是该包中的符号,但是 Bob 尝试使用 BOB
包中的符号访问该类的实例。换句话说,JANE-CLASS
有一个名为 JANE::SLOT1
的槽,而 Bob 尝试访问一个名为 BOB::SLOT1
的槽,然而并不存在这样的槽。
Bob 真正想做的是导入与 JANE-CLASS
有“关联”的所有符号,即所有的槽名、方法名等。他怎么知道所有这些符号是什么?他可以检查 Jane
的代码并尝试自己弄清楚,但这会导致许多问题,尤其是他可能决定导入一个 Jane 不想让 Bob 乱搞的符号(记住,将 Jane 的符号与 Bob 的瞎搞可能造成的影响隔离开就是包存在的全部意义)。
更好的解决方案是让 Jane 收集起一个 Bob 应该导入的符号列表,以方便 Bob 使用她的软件。然后 Jane 可以执行以下操作:
? (defvar *published-symbols* '(jane-class slot1 slot2 slot3)) *PUBLISHED-SYMBOLS*
然后 Bob 可以 (import jane::*published-symbols*)
.
Common Lisp 提供了一个标准的机制来做到这一点。每个包都维护着一个旨在供其他包使用的符号列表。此列表称为该包的导出符号列表,要向该包添加符号,请使用 EXPORT
函数。要从包的已导出符号列表中删除符号,请使用(正如您的直觉所期望的那样)UNEXPORT
。要导入包中的所有导出符号,请调用 USE-PACKAGE
。 反导入它们你可以使用UNUSE-PACKAGE
。
关于导出符号有两点需要注意。
首先,一个符号可以从内部化了该符号的任意包中导出,而不仅仅是它的主包。一个符号也不必非要从它的主包导出,就可以从该符号所在的其他包中导出。[注:绕来绕去,说的是:p2 导入了 p1 中的符号,甭管是 use 还是 import 导入的,p2 都可以将它 export, 然后,p3 可以 use p2, 间接地导入 p1 中的符号]
其次,从其主包导出的符号在使用其包限定符打印时仅使用一个冒号而不是两个。这是为了提醒您符号是从其主包导出的事实(因此您可能希望use-package
导入符号所属的包而不是用import
导入符号本身),并通过强制您键入额外的冒号来阻止您使用未导出的符号以便访问它们。 (不,我没有开玩笑。)
6.阴影
最后还有一个问题:使用 USE-PACKAGE
与 IMPORT
包中的所有导出符号略有不同。当您IMPORT
一个符号时,您可以使用 UNINTERN
取消导入的效果。您不能使用 UNINTERN
来(部分)撤销 USE-PACKAGE
的效果。例如:
? (in-package :jane) #<Package "JANE"> ? (export '(slot1 slot2 slot3)) T ? (in-package :bob) #<Package "BOB"> ? (use-package 'jane) T ? (symbol-package 'slot1) #<Package "JANE"> ? (unintern 'slot1) NIL ? (symbol-package 'slot1) #<Package "JANE">
这是一个问题,它似乎让使用两个不同的包导出具有相同的符号变得不可能。例如,假设您想在名为 MYPACKAGE
的第三个包中USE
了两个包 P1
和 P2
,并且 P1
和 P2
都导出了一个名为 X
的符号。如果你尝试同时 use-package
p1
和p2
,你就会遇到名字冲突,因为 Lisp 搞不清楚 MYPACKAGE
中的 X
现在应该是 P1:X
还是 P2:X
。
为了解决这种名字冲突,每个包都维护了一个所谓的 影子符号列表 。影子符号列表是一个符号列表,它会隐藏或覆盖掉由 USE-PACKAGE
导入的外部符号。
有两种方法可以将符号添加到包的影子符号列表中,SHADOW
和 SHADOWING-IMPORT
。 SHADOW
用于将包中的符号添加到影子符号列表中。 SHADOWING-IMPORT
用于添加其他包中的符号。
[说人话: shadow 屏蔽掉从外部引入的同名符号,shadowing-import 则屏蔽掉与指定的外部符号冲突的其它符号]
例如:
? (in-package :p1) #<Package "P1"> ? (export '(x y z)) T ? (in-package :p2) #<Package "P2"> ? (export '(x y z)) T ? (in-package :bob) T ? (use-package :p1) T ? (use-package :p2) > Error: Using #<Package "P2"> in #<Package "BOB"> > would cause name conflicts with symbols inherited > by that package: > Z P2:Z > Y P2:Y > X P2:x ... 1 > Aborted ? (unuse-package :p1) T ? (shadow 'x) T ? (shadowing-import 'p1:y) T ? (shadowing-import 'p2:z) T ? (use-package :p1) T ? (use-package :p2) T ? (symbol-package 'x) #<Package "BOB"> ? (symbol-package 'y) #<Package "P1"> ? (symbol-package 'z) #<Package "P2">
要撤销 SHADOW
或 SHADOWING-IMPORT
的效果,请使用 UNINTERN
。
请注意,UNINTERN
(以及许多其他令人惊讶的事情)可能会导致意外地出现名字冲突。意外的名字冲突的最常见原因通常是通过在 reader 中键入它而无意中将符号插入包中,而没有密切注意您当时处在哪个包中。
7.DEFPACKAGE
现在您已经了解了无数用于操作包的函数和宏,但是您不应该真正直接使用它们中的任何一个。相反,IMPORT
、EXPORT
、SHADOW
等的所有功能都集中在一个名为 DEFPACKAGE
的宏中,您应该将其用于实际(非原型)代码。
我不打算在这里解释 DEFPACKAGE
,因为现在您了解了包的基本概念,您应该能够阅读 hyperspec 中的文档并理解它。 (在 hyperspec 中还有很多其他好东西,例如 DO-SYMBOLS
和 WITH-PACKAGE-ITERATOR
,您现在应该能够自己理解了。)
不过,关于使用 DEFPACKAGE
的一个警告:请注意,DEFPACKAGE
的大多数参数是字符串,而不是符号。这意味着虽然它们可以是符号,但是如果您选择使用符号,那么在读取 DEFPACKAGE
表单时,无论它是什么,这些符号都将被内部化在当前包中。这通常会导致不良后果,因此养成在 DEFPACKAGE
表单中使用关键字参数或字符串的习惯是个好主意。
8.最后的想法
了解包最重要的一点是,它们从根本上是 Lisp 读取器的一部分,而不是求值器的一部分。一旦你的大脑围绕这个想法,其他一切都会水到渠成。包控制读取器如何将字符串映射到符号(以及 PRINT
如何将符号映射到字符串),没有别的。特别是,包与可能与或可能不与任何特定符号相关联的函数、值、属性列表等无关。
特别要注意(这与包无关,但无论如何新手经常对此感到困惑)符号和函数对象都可以用作函数,但它们的行为略有不同。例如:
? (defun foo () "Original foo") FOO ? (setf f1 'foo) FOO ? (setf f2 #'foo) #<Compiled-function FOO #xEB3446> ? (defun demo () (list (funcall f1) (funcall f2) (funcall #'foo) (funcall #.#'foo))) ;Compiler warnings : ; Undeclared free variable F1, in DEMO. ; Undeclared free variable F2, in DEMO. DEMO ? (demo) ("Original foo" "Original foo" "Original foo" "Original foo") ? (defun foo () "New foo") FOObaz ? (demo) ("New foo" "Original foo" "New foo" "Original foo")
在这个例子中,我们有两个变量,F1
和 F2
。F1
的值是符号FOO
。 F2
的值是在分配 F2
时位于符号 FOO
的符号函数槽中的函数对象。
请注意,当重新定义 FOO
时,调用符号 FOO
的效果是获得新行为,而调用函数对象会产生旧行为。
解释列表中的第二个和第三个结果留给读者练习(没有双关语)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2017-01-06 Mersenne twister 随机数算法实现 in Scheme