一:类的定义
DEFCLASS
(defclass name (direct-superclass-name) (slot-specifier*))
二:对象初始化与槽描述符:
Make-instance的参数是想要实例化的类的名字,而返回值就是新的对象。
Slot-specifier 槽描述符也就是我们平常说的成员变量。
槽描述符中的一些选项:
:initarg 即initial argument,指定随后make-instance用作形参时的名字
:initform 给你提供一个默认值。
Slot-value 读取槽的值,然后跟setf合用来设置槽的值。
CL-USER> (defclass bank-account()
(customer-name
balance))
#<STANDARD-CLASS BANK-ACCOUNT>
CL-USER> (make-instance 'bank-account)
#<BANK-ACCOUNT {24F862F9}>
CL-USER> (defparameter *account* (make-instance 'bank-account))
*ACCOUNT*
CL-USER> (setf (slot-value *account* 'customer-name) "Joe")
"Joe"
CL-USER> (slot-value *account* 'customer-name)
"Joe"
三:对象初始化:
当有参数描述的时候,整个槽就会被括号包含起来。否则不用如account-type
CL-USER> (defclass bank-account()
((customer-name
:initarg :customer-name
:initform (error "must supply a customer name"))
(balance
:initarg :balance
:initform 0)
account-type))
现在你想定义account-type为gold,silver,bronze,这个时候你就要考虑到底什么时候给他初始化这些值了,因为我们只能给一个具体的对象赋予具体什么值,但是我们无法访问一个正在初始化的对象,因此不能基于一个槽的值来初始化另一个槽的值。
基于:initarg选项,standard-object上特化的initial-instance主方法负责槽的初始化,所以你最好添加一个:after方法。
四:访问槽
Slot-value我们是用来访问对象中的位置。
CL-USER> (setf (slot-value *account* 'customer-name) "Joe")
"Joe"
CL-USER> (slot-value *account* 'customer-name)
"Joe"
但是这就有个问题了,假设一个程序中多处用到'customer-name,你不能说每次都是像上面这样把slot-value每一次都输入。
于是就有了下面的扩展setf的方式。你也许回想的是下面这种函数,用defun来定义。如果是这样的话,你最好能够定义一个广义函数的形式,这样就为子类提供了不同的方法或附加方法。
CL-USER> (defun (setf customer-name)(name account)
(setf (slot-value account 'customer-name) name))
(SETF CUSTOMER-NAME)
CL-USER> (setf (customer-name *account*) "sall")
"sall"
CL-USER> (defun customer-name(account)
(slot-value account 'customer-name))
CL-USER> (customer-name *account*)
"sall"
广义函数版:
CL-USER> (defgeneric (setf customer-name)(value account))
CL-USER> (defmethod (setf customer)(value (account bank-account))
(setf (slot-value account 'customer-name)value))
CL-USER> (defgeneric customer-name(account))
CL-USER> (defmethod customer-name((account bank-account))
(slot-value account 'customer-name))
疑问:为啥扩展setf可以写成(setf (customer-name accout) value)?
Setf 扩展定义了一种新的位置类型使其知道如何设置它,setf函数的名字是一个两元素列表,其第一个元素是符号setf而第二个元素是一个符号,通常是一个用来访问该setf函数将要设置的位置的函数名。Setf函数可以接受任何数量的参数,但第一个参数总是复制到位置上的值。
为了省去你每次都需要重新定义这几个函数,程序又提供了三个槽选项:reader/writer/accessor它与寻你为一个特定的槽自动创建读取和写入函数。
具体应用如下。
(defclass bank-account5()
((customer-name
:initarg :customer-name
:initform 0
:reader customer-name
:writer (setf customer-name))
(balance
:initarg :balance
:initform 0)
account-type))
#<STANDARD-CLASS BANK-ACCOUNT5>
CL-USER> (defparameter *a* (make-instance 'bank-account5)) *A*
CL-USER> (setf (customer-name *A*) "fd") "fd"
CL-USER> (customer-name *A*) "fd"
Compare :reader/:writer with :accessor,we could get just the first half of this behavior, or just the second.
With-slots和with-accessors
现在我们就有两种形式来访问槽了。
(defun assess-low-balance-penalty (account)
(when (< (balance account) 99992000)
(decf (slot-value account 'balance)(* (balance account) .01))))
(defun assess-low-balance-penalty (account)
(when (< (slot-value account 'balance) 99992000)
(decf (slot-value account 'balance)(* (balance account) .01))))
你会发现decf用的形式不变,应该记得以前说过decf是setf的修改宏,我感觉因为我们已经扩展了setf函数,那么同样适用于decf吧,但是最后你会发现他是不适用的,所以decf都是最古老的方式来访问的。
WITH-SLOTS的基本形式如下:
(With-slots (slot*) instance-form
Body-form*)
每一个slot元素可以使一个槽的名字,也可以是一个变量名。
或者一个两元素列表,第一个元素一个用作变量的名字,它的作用如下为了区分当对不同对象的同一个槽进行的处理,第二个元素则是对应槽的名字。
CL-USER> (defmethod assess-low-balance-penalty ((account bank-account))
(with-slots (balance) account
(when (< balance *minimum-balance*)
(decf balance (* balance .01)))))
CL-USER>(defmethod merge-account((account1bank-account)(account2 bank-account))
(with-accessors ((balance1 balance)) account1 两元素列表
with-accessors (customer-name balance-type) account2 slot槽集
(incf balance 1)))
五:分配在类上的槽。
槽:allocation,它的值可以使:instance或:class,字面上理解的话,一个是实例,另一个是类,也就是说当是:instance的时候,在每一次创建实例的时候你都需要创建一个它,但是如果是:class的话,说明这个槽是类的,只有单一值存储在类中并且被所有实例所共享。
可以通过slot-value或函数来访问该槽的值,尽管他不保存在任何一个实例中。:initform和
:initarg仍可以使用,只是initform在类的定义的时候就需要创建。并且你可以通过创建实例make-instance调用:initarg来改变这个值,从而影响了所有实例。
we would want to use shared slots tocontain properties that all the instances would have in common.We do this by declaring the slot to have :allocation :class.When we change the value of such a slot in one instance, that slot will get the same value in every other instance.
注意点:
我本来以为initarg 就相当于一个槽的别名呢,可以再任何位置对于这个别名的访问就相当于直接对槽的访问,但实际上他就只是在make-instance 的时候可以等价于它所指的槽,其他地方如果你用这个initarg在访问的话,他会报下面的问题。
现在我感觉有个问题就是既然initarg不能够在除make-instance 的地方访问,那么他到底有什么用,难道仅仅是为了在初始化的时候,把你想要设置为一样值的参数设置为同一个initarg关键字,实现初始化方便吗?
CL-USER> (defclass initarg-test()
((a :initarg :a-1 :accessor a-a)))
#<STANDARD-CLASS INITARG-TEST>
CL-USER> (defparameter *hh* (make-instance 'initarg-test :a-1 "bb")) *HH*
CL-USER> (a-a *hh*) "bb"
CL-USER> (slot-value *hh* 'a) "bb"
2::accessor customer-name,形式要注意,他后面不是关键字,也不是引用名,同时更不是一个string。
其实你完全可以用关键字来替换:the-b,因为下面的plist形式你应该清楚,后面是对前面的赋值。而本身:accessor后面跟的是一个能够接受函数对象的参数。你可以让:the-b来接收,但是一般plist的话,他后面接受的是一个自求值对象,因为他后面后面还可以指向另一个对象。而如果你用一个关键字指向一个函数相当于这个plist就此结束了(第二种结构不推荐),实际上无可非议,但是你得这个功能完全就是可以让the-b这个变量来实现的。所以最好还是用一个变量而不是关键字。
CL-USER> (defclass par()
((b :initarg :b :initform "b" :accessor :the-b)
(a :initarg :b :initform "a" :accessor the-b)))
#<STANDARD-CLASS PAR>
CL-USER> (defparameter *jj* (make-instance 'par)) *JJ*
CL-USER> (:the-b *jj*) "b"
然后再说为啥:initarg的值还为关键字:b,现在你应该明确一点除了全局变量跟自求值以外,能够被赋值的也就是变量跟关键字了,现在咱们说如果不是:b 而是b,会有什么问题。执行表达式:(defparameter *hh* (make-instance kk a "me"))会报A is unbound.,你应该明白的就是lisp的每一步都是必须得有值的,现在说为啥在defclass的时候他没有报b is unbound.我感觉他应该对定义的时候不检查他,但同样accessor后也是一个变量,为啥(the-b *jj*)不报错呢?我感觉究其原因应该是在定义对象的时候,他没有检查关键字后面的对象的是否bound,而像b就是落网之鱼,而the-b在defclass的时候已经bound好了,他指向一个函数,所以可以用,而现在b出门就被卡,所以我说这个应该是个漏洞。因为你即使defclass可以,但是不能继续接着操作,或者干脆是像the-b一样,表面上是unbound,实际上内部一些逻辑已经实现让他绑定一个对象了。
总结一下就是b在defclass的时候之所以可以过,关键是看在了前面是关键字的面子,并且它是在定义的时候处理的。比方说你执行(defparameter *hh* (make-instance 'kk :a b))他通用报B is unbound。所以单纯说因为关键字后面的东西不查值是不对的。
我怀疑这个为啥是:initarg而不是initarg,主要也是因为如果是initarg他会说unbound.
CL-USER> (defclass kk()
((b :initarg b :initform "b" :accessor :the-b)
(a :initarg a :initform "a" :accessor the-b)))
#<STANDARD-CLASS KK>
CL-USER> (defparameter *hh* (make-instance kk :a "me"))