在Groovy中覆盖metaClass属性

3

我希望覆盖metaClass上的一个动态属性,但它并没有按照我的预期工作。以下是一个简单的示例,演示了这个问题。你可以在Groovy web console上在线运行它。

class Bike {}
class Bell { def ring() { println("calling " + this) } }

def createNamespaces = {
    return [ "bell": new Bell() ]
}

def resetNamespaces = { bike ->
    // setting to null means 'set it to the default'
    bike.metaClass = null

    createNamespaces().each { name, namespace ->
        println("setting " + namespace)
        bike.metaClass."$name" = namespace
    }
}

def bike= new Bike()

resetNamespaces(bike)
bike.bell.ring()
resetNamespaces(bike)
bike.bell.ring()

结果如下:
setting Bell@14e9bd2b
calling Bell@14e9bd2b
setting Bell@948a7ad
calling Bell@14e9bd2b

所以,尽管我已更改了metaClass上的属性,但调用它始终返回设置的第一个对象。是否存在某种缓存机制?

当我将示例的最后一部分更改为以下内容时:

resetNamespaces(bike)
resetNamespaces(bike)
bike.bell.ring()

那么,结果如预期一样。也就是说,调用该属性会返回在metaClass上最后设置的对象:
setting Bell@5b47e0c7
setting Bell@19f373a4
calling Bell@19f373a4

我甚至尝试手动设置metaClass,如下所示。
def resetNamespaces = { bike ->
    def newMetaClass = new ExpandoMetaClass(Bike.class, true, true)
    newMetaClass.initialize()

    bike.metaClass = newMetaClass

    ...
}

但结果仍然是一样的。因此可能涉及到某种缓存机制。我在文档中找不到任何关于这种行为的说明。

1个回答

0
混淆的原因在于你试图改变元类的属性,而不是方法。如Groovy元编程的官方文档所述,属性是通过元类的setAttribute/getAttribute方法进行访问的。
// throws MissingFieldException!
def resetNamespaces = { bike ->
    createNamespaces().each { name, namespace ->
        bike.metaClass.setAttribute(bike, name, namespace)
    }
}

不幸的是,这只在属性在原始类定义中才起作用,否则会抛出一个 MissingFieldException: No such field: bell for class: Bike 异常。

另一方面,重写方法非常有效,并且通过添加动态 getter 方法提供所需的语法:

def resetNamespaces(bike) {
    createNamespaces().each { name, namespace ->
        bike.metaClass."get${name.capitalize()}" = { namespace }
    }
}

def bike = new Bike()
resetNamespaces(bike)
bike.bell.ring()
resetNamespaces(bike)
bike.bell.ring()

实际上,expando也可以很好地工作。
class Bell {
    def ring() { println this }
}

def bike = new Expando()
bike.createNamespaces = {
    return [ bell : new Bell() ]
}

bike.resetNamespaces = {
    bike.createNamespaces().each { name, namespace ->
        bike."$name" = namespace
    }
}

bike.resetNamespaces bike
bike.bell.ring()
bike.resetNamespaces bike
bike.bell.ring() 

那么你的意思是我不能更改对象实例的元类的动态属性吗?这个信息在文档或源代码中有吗,以便我可以找到它?我认为Groovy对象上的每个方法/属性调用都会委托给它的元类。请参见我问题中添加的最后一部分。 - Jan Krakora
抱歉,你是完全正确的,当然可以覆盖元类...请看我的编辑答案。无法提供更深入的Groovy内部见解,但希望能有所帮助。 - chriopp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接