是否可以使用 haxe 宏来检测对象何时变脏(任何 属性 已更改)

Could haxe macro be used to detect when object is dirty (any property has been changed)

假设我们有一个对象:

@:checkDirty
class Test {
   var a:Int;
   var b(default, default):String;
   var c(get, set):Array<Int>;

   public function new() {
     ...
   }
   public function get_c() {
      ...
   }
   public function set_c(n) {
     ... 
   }


}

我们可以编写一个宏 checkDirty,以便对 field/properties 的任何更改都会将 属性 dirty 设置为 true。宏将生成 dirty 字段作为 BoolclearDirty 函数将其设置为 false.

var test = new Test();
trace(test.dirty); // false
test.a = 12;
trace(test.dirty); // true
test.clearDirty();
trace(test.dirty); //false
test.b = "test"
trace(test.dirty); //true

test.clearDirty();
test.c = [1,2,3];
trace(test.dirty); //true

请注意 - 每当您考虑代理访问对象时,根据我的经验,总会有隐藏的成本/增加的复杂性。 :)

也就是说,您有几种方法:

首先,如果你想让它成为纯粹的Haxe,那么无论是宏还是抽象都可以完成工作。无论哪种方式,您都有效地将每个 属性 访问转换为设置值并设置 dirty.

的函数调用

例如,可以在 NME source code 中找到使用 @:resolve getter 和 setter 的摘要,为方便起见,在此处复制:

@:forward(decode,toString)
abstract URLVariables(URLVariablesBase)
{
   public function new(?inEncoded:String)
   {
      this = new URLVariablesBase(inEncoded);
   }
   @:resolve
   public function set(name:String, value:String) : String
   {
      return this.set(name,value);
   }

   @:resolve
   public function get(name:String):String
   {
      return this.get(name);
   }
}

这可能是一个较旧的语法,我不确定...另请参阅 Haxe 手册中的 operator overloading examples

  @:op(a.b) public function fieldRead(name:String)
    return this.indexOf(name);

  @:op(a.b) public function fieldWrite(name:String, value:String)
    return this.split(name).join(value);

其次,我只想指出,如果底层语言/运行时支持某种代理对象(例如 JavaScript Proxy),并且宏/抽象没有按预期工作,那么您可以构建最重要的是你的功能。

我写过 a post (archive) 关于做这种事情(除了发出事件)之前 - 你可以使用 @:build 宏来修改 class 成员,无论是附加一个额外分配到 setter 或用 属性.

替换该字段

因此修改后的版本可能如下所示:

class Macro {
    public static macro function build():Array<Field> {
        var fields = Context.getBuildFields();
        for (field in fields.copy()) { // (copy fields so that we don't go over freshly added ones)
            switch (field.kind) {
                case FVar(fieldType, fieldExpr), FProp("default", "default", fieldType, fieldExpr):
                    var fieldName = field.name;
                    if (fieldName == "dirty") continue;
                    var setterName = "set_" + fieldName;
                    var tmp_class = macro class {
                        public var $fieldName(default, set):$fieldType = $fieldExpr;
                        public function $setterName(v:$fieldType):$fieldType {
                            $i{fieldName} = v;
                            this.dirty = true;
                            return v;
                        }
                    };
                    for (mcf in tmp_class.fields) fields.push(mcf);
                    fields.remove(field);
                case FProp(_, "set", t, e):
                    var setter = Lambda.find(fields, (f) -> f.name == "set_" + field.name);
                    if (setter == null) continue;
                    switch (setter.kind) {
                        case FFun(f):
                            f.expr = macro { dirty = true; ${f.expr}; };
                        default:
                    }
                default:
            }
        }
        if (Lambda.find(fields, (f) -> f.name == "dirty") == null) fields.push((macro class {
            public var dirty:Bool = false;
        }).fields[0]);
        return fields;
    }
}

如果用作

@:build(Macro.build())
@:keep class Some {
    public function new() {}
    public var one:Int;
    public var two(default, set):String;
    function set_two(v:String):String {
        two = v;
        return v;
    }
}

会发出以下 JS:

var Some = function() {
    this.dirty = false;
};
Some.prototype = {
    set_two: function(v) {
        this.dirty = true;
        this.two = v;
        return v;
    }
    ,set_one: function(v) {
        this.one = v;
        this.dirty = true;
        return v;
    }
};