2010年10月22日星期五

实用的原型继承

现实生活中的原型继承

时间匆忙,此帖就略,来介绍一下原型继承是什么东西。


* Self

Self语言,是一种基于原型的面向对象程序设计语言,以原型简化了类的概念,让类在视觉上直观。
虽然其研究已经停滞,但是在动态语言的实现优化上有应用。
相关信息在一份名为self: the power of simplicity的paper有明了的说明。

此文详述了原型继承的优点(简化实现,增加对象灵活性),以及原型继承和类继承的比较,这里不再转述。
其中关键的两个概念是slot和parent:
在self中所有对对象的使用都是通过消息来进行的,对象的数据成员的读写通过命名slots。
例如对象有属性x,则x消息用于读取,x:消息用于写入,只读消息则只有读槽没有写槽。
槽是一个方法,也是Lisp中的一个Closure。
当对象消息所指向的槽不存在时,则指向其parent对象,对象间的数据共享也通过parent的solts进行。
从简化实现的角度说,对象方法的局部环境也可以用一个子类来实现,用parent指向对象的环境。


* 类对象系统

C++中的Class是对Struct的扩展,数据和虚函数表属于对象,而方法函数属于类。
如果一个方法不是虚函数的话,它将在编译的时候转化为一个函数的调用。
smalltalk的对象机制则有些差别,它对方法的查找是在运行时动态的,详情我不清楚。
c++所选择的机制是从运行时性能的角度来考虑的。


* 设计模式

在《设计模式》一书中,PROTOTYPE是作为一种对象创建模式来讲解的。
可以用于以原型的克隆来减少类的数目,且提供在运行时控制对象原型的能力。
在C++这样的类对象系统中使用原型继承的方法并不唯一,在实现的时候需要考虑的是对象的管理方式(是否需要存放在一个关联数组中),如何去定义对象的clone操作(就是C++的复制构造,有复制深浅和如何初始化的问题),以及指向原型对象的指针可以用来托管操作(this->proto->clone())。
因为实现方式上有很大的自由度,这里不做代码举例,重点在对象的名为proto的指针和名为clone的方法,前者类型为指向自身类型的指针,后者返回和对象自己同样的类型。
当然,这里也不排除我对文本的内容的某些片面理解。

class A{
private:
    A* proto;
public:
    A clone(){
        return A(*this)
    }
}
这里的代码只是用来体现代码结构,并没有完善实现细节,例如完善默认构造和复制构造。
对象成员中可以包含函数指针,且方法调用中有一个隐含的this指针,这为自行对机制的扩种提供的方便,只是缺少一些语法的方便(用操作符作DSL?)。


* JavaScript

依照ECMA-262的1.3和1.5文档第4.2.1节的描述,js中提供原型机制实现了对对象继承和数据共享的支持。
在Javascript中函数也是对象的一种,所以对象中不会像类继承中和其他成员不区分对待,函数作为方法时可以通过this访问使用的对象。
构造器constructor是一种特殊的对象Object,它是一个函数(有[[Call]]方法)且拥有"prototype"(原型)属性(默认是每个函数对象都会自动创建该属性,以便我们往其中添加添加属性,当然也可以指向已有对象)。
因为构造器是一个函数,它可以按照一个函数来定义它的功能(例如Date(2009,11)返回一个字符串)。
而如果通过new表达式使用constructor,则会创建新的对象(例如new Date(2009,11)返回一个Date对象),并以该对象为变量this去执行构造函数(可选的是如果构造函数有返回对象则new返回这个对象)。
其创建的对象的属性prototype指向其构造其的prototype,在查询对象成员的时候如果缺少则系统就会转向查询其prototype所指向的对象。
函数对象在创建时在原型属性中默认会有指向自身的constructor属性,可方便于执行clone操作。
操作符instanceof调用对象的[[HasInstance]]的方法,其实现是依据对象的prototype实行来判断。
Type和对象将自动在需要的时候被执行,类似与Java的Box/unBox机制,例如从StringType到StringObject的转换。
对象的prototype属性可以在运行时改变,这是原型机制在动态性的一个体现。

实例代码(没有测试):
Point=function(x,y){
    this.x=x;
    this.y=y
}
Point.prototype.move=function(x,y){
this.x+=x;
this.y+=y;
}
使用:
var p=new Point(4,5)
p.move(3,6)
以上是目前较常见的使用方式,在第三方库往往会再做包装。
值得注意的是,这里的构造器在来的创建中不是必须的,它只是js语言本身提供的一种方便的机制,并和其他一些识别对象的函数保持供用的信息。


* Lua

Lua是一种和Javascript很相似的语言。
它对象是Table,并提供了metatable机制,对象实现的方式交给了使用者指定。

例如,可以实现为(未测试):
Point = {
    new = function(self, x, y)
        local object = { x=x, y=y }
        setmetatable(object, self)
        self.__index = self
        return object
    end,
    move = function(self,x,y)
        self.x, self.y = self.x+x, self.y+y
    end,
}
然后使用
local p=Point:new(4,5)
p:move(5,-2)
这里虽然用了类似于原型的机制,但实现上Point是起的类的作用,用来创建实例和负责类的方法。


* Python

虽然Python是使用的Class机制,不过由于其动态的,在实现上和原型机制有类似的地方。
类和实例都处于对象:类有属性__dict__和__bases__,前者是方法和类属性所在的空间,后者是基类的列表;实例是有调用class对象创建,有属性__dict__和__class__,前者是用于存储类的属性,后者指向创建它的类。
如果没有方法截获属性查询的语法,实例成员的查找会现在实例中查找然后再到它的类中,也就是说只有类可以做原型使用(Python在它的class机制中会保护一点),而类的原型则是其继承机制的实现方式。
这样的限制为在Python中导入C++类带来方便,而如果仅有原型机制则为实现的一致性带来的障碍。

试验代码:
class A:
    pass
class B:
    pass
a=A()
a.__class__=B
print a
虽然这段代码看不出明了的价值,但是它是能够工作的,最后的输出为%lt;__main__.B instance$gt;。
class也是对象,它是type对象的实例,也可以用type(name, bases, dict)的语法来创建。
并同时也实现了元类,即类的类这一递归的概念,派生出type的子类来进行类的创建。

不过虽然Python允许在运行时给对象添加属性包括使用lamdba,但是对象方法仍然需要在类中定义。
如果需要self那样的原型机制,一个可行的方式是使用@classmethod,代码示例如下:
class A:
    v=5
    pass
class B(A):
    @classmethod
    def f(cls):
        print cls.v
即把class直接作为一个对象来使用,用继承来表示原型。
或者,用模块和import *,效果类似。


* 单实例类

原型机制的一个好处是用来实现单实例类,例如用来实现对一个虚拟世界的抽象。
代码请自行补脑。


* Scheme

Scheme给使用者相当大的灵活性,这让我们可以实现自己的对象与消息机制,一个成熟的参考是CLOS。
不过简单的一个对象可以写成:
(define (num value)
(lambda (method)
    (cond
     ((eq? method 'print) (display value) (newline))
     ((eq? method 'next) (num (+ value 1)))
     (else (error "method missing"))
     )
    )
)
对原型的查找实在else语句的那一步执行。


* 原型继承的使用

用来读取富含逻辑的数据


* 多分派

这是题外话,关于C++的Oberride机制。
在C++中,方法可以是virtual的,也就是说调用对象方法所执行的代码是运行时动态判断的。
而目前的overload机制却完全是在编译时静态决定的,也就是说当参数是派生类时,C++依然只按照传入指针变量的类型而不是指向对象的类型来判断所调用的函数。
用if语句判断类型是一种办法,目前一个较通常的解决双分派办法是visitor模式。
再一次virtual,以arg->m(*this)来使用。


* 链接

Self - the power of simplicity:
http://selflanguage.org/
Standard ECMA-262
http://www.ecma-international.org/publications/standards/Ecma-262.htm
一个javascript的仿self环境
http://adamspitz.com/Lively-Outliners/example.xhtml
A Minimal Javascript Object Environment
http://minijoe.com/


* 附以前一段试验代码

虽然这段代码没用用到原型继承,不过有值得这方面的尝试。
此外,在lua用可以用自定函数来构造table,而不一定只用默认的{}语法。
--[[
演示
ee.zsy
2010.1
]]

--BASE
function table.pos(t,sth)
    for i,v in ipairs(t) do
        if v==sth then
            --say("i="..i)
            return i
        end
    end
    return 0
end
function table.has(t,sth)
    return (table.pos(t,sth)>0)
end
function table.del(t,sth)

end
function table.add(t,sth)
    table.insert(t,sth)
end
--UI
function put(sth)
    print(sth)
end
function say(sth)
    print(sth)
    io.read()
end
function key()
    return tonumber(io.read())
end
function cls()
    os.execute("cls")
end
function select(sth)
    if sth.quest then
        print(sth.quest) end
    for i,v in ipairs(sth) do
        print(i.."."..v)
    end
    local c=key()
    return c
end

--SAVE
save={

place="",
bag={}

}
--DATA
map={

home={
    name="家",
    goto={"tree"},
    has={"she"},
    run=function(self)

    end
    },
tree={
    name="森林",
    goto={"home"},
    has={},
    run=function(self)
        local tree={}
        self.goto={"home","shop"}
        self.has={}
        if not table.has(save.bag,"card") then
            self.has={"card"}
        end
        --table.insert(self.goto,"shop")
    end
    },
shop={
    name="商店",
    goto={"home","tree"},
    has={},
    run=function(self)

    --return k==1 and "home" or k==2 and "tree" or nil
    end
    }
    
}
obj={

she={
    name="女友",
    type="human",
    run=function(self)
        say("你好啊")
        --select{"df","df","df"}
    end,
    catch=function(self,o)
        say("什么东西?")
        if o==obj.card then
            say("好啦,我早就认识你了")
            return true
        end
        return false
    end
    },
card={
    name="身份证",
    type="thing",
    run=function(self)    
        return false
    end,
    use=function(self,obj)
        say("你出示了"..self.name)
        return true
    end,
    take=function(self)
        table.add(save.bag,"card")
        return true
    end
    
    }
    
}



--VIEW
function move(plc)
    put("这里通往:")
    for i,v in ipairs(plc.goto) do
        put(i.."."..map[v].name)
    end
    local c=key()
    local o=map[plc.goto[c]]
    if o~=nil then
        save.place=o
        return true
    end
    return false
end
function search(plc)
    put("这里有:")
    for i,v in ipairs(plc.has) do
        put(i.."."..obj[v].name)
    end
    local c=key()
    local o=obj[plc.has[c]]
    if o~=nil then
        o:run()
        
        if o.take and o:take() then
            say("你得到了"..o.name)
        end
        return true
    end
    return false
end
function bag(plc)
    put("你要使用:")
    for i,v in ipairs(save.bag) do
        put(i.."."..obj[v].name)
    end
    local c=key()
    local o=obj[save.bag[c]]
    if o~=nil then
        if o:run() then
            return true
        end
        if o.use then
            put("对什么使用呢?")
            for i,v in ipairs(plc.has) do
                put(i.."."..obj[v].name)
            end
            local c=key()
            local oo=obj[plc.has[c]]
            if oo then
                o:use(oo)
                if oo.catch then oo:catch(o) end
            return true
            end
            return false
        end
    end
    return false
end
function show(plc)
    cls()
    plc:run()
    while 1 do
        cls()
        put("==你在==\n"..plc.name)
        local s=select{quest="你的操作","移动","调查","背包"}
        if s==1 then
            if move(plc) then
                return
            end
        elseif s==2 then
            if search(plc) then
                return
            end
        elseif s==3 then
            if bag(plc) then
                return
            end
        end
        say("你未操作")
    end
end

function main()
    save.place=map.home
    while 1 do
        show(save.place)
    end
end

main()
另一种写法是用switch语句。

这里,包括整篇文字的目的,是表达一下抽象(v.-%gt;n.)。


继承类型的选择

最后,两种继承机制有其侧重,与应用的场合。
在系统设计上,类继承有比较成熟的方法,且比原型机制更容易抽象。
原型继承则较直观地用来变现数据,用代码体现出数据中可执行的方面。

没有评论: