【问题标题】:Declaring a field as lazy将字段声明为惰性
【发布时间】:2017-08-08 07:17:57
【问题描述】:

在 TypeScript 中,是否有将字段声明为延迟初始化的语法?

就像在 Scala 中一样,例如:

lazy val f1 = new Family("Stevens")

意味着字段初始化器只会在第一次访问该字段时运行。

【问题讨论】:

  • 你可能需要编写一个装饰器来支持它。like:@lazy val f1 = new Family("Stevens").
  • 很有趣,谢谢。赞成。有什么现成的东西吗?例如。在 Angular 2 中?
  • @MaciejBukowski 受到您的启发,我正在更改可以正确继承的装饰器。我会更新我的答案,请帮我看看每个测试都在测试部分正确吗?
  • @holi-java 我删除了我的评论,因为我发现自己错了;)我用你的第一个版本测试了 TS 中的装饰器,并且 TS 正在用装饰的方法替换原型中的原始方法,一旦程序启动,因此没有任何时间或内存运行时问题。但我会运行你的测试来确定:)

标签: typescript lazy-initialization


【解决方案1】:

我发现它不能自己在 typescript 中使用 @lazyInitialize。所以你必须重写它。这是我的装饰器,你只是复制和使用它。在 getter 上使用 @lazy 而不是属性。

@懒惰

const {defineProperty, getPrototypeOf}=Object;
export default function lazy(target, name, {get:initializer, enumerable, configurable, set:setter}: PropertyDescriptor={}): any {
    const {constructor}=target;
    if (initializer === undefined) {
        throw `@lazy can't be set as a property \`${name}\` on ${constructor.name} class, using a getter instead!`;
    }
    if (setter) {
        throw `@lazy can't be annotated with get ${name}() existing a setter on ${constructor.name} class!`;
    }

    function set(that, value) {
        if (value === undefined) {
            value = that;
            that = this;
        }
        defineProperty(that, name, {
            enumerable: enumerable,
            configurable: configurable,
            value: value
        });
        return value;
    }

    return {
        get(){
            if (this === target) {
                return initializer;
            }
            //note:subclass.prototype.foo when foo exists in superclass nor subclass,this will be called
            if (this.constructor !== constructor && getPrototypeOf(this).constructor === constructor) {
                return initializer;
            }
            return set(this, initializer.call(this));
        },
        set
    };
}

测试

describe("@lazy", () => {
    class Foo {
        @lazy get value() {
            return new String("bar");
        }

        @lazy
        get fail(): string {
            throw new Error("never be initialized!");
        }

        @lazy get ref() {
            return this;
        }
    }


    it("initializing once", () => {
        let foo = new Foo();

        expect(foo.value).toEqual("bar");
        expect(foo.value).toBe(foo.value);
    });

    it("could be set @lazy fields", () => {
        //you must to set object to any
        //because typescript will infer it by static ways
        let foo: any = new Foo();
        foo.value = "foo";

        expect(foo.value).toEqual("foo");
    });

    it("can't annotated with fields", () => {
        const lazyOnProperty = () => {
            class Bar {
                @lazy bar: string = "bar";
            }
        };

        expect(lazyOnProperty).toThrowError(/@lazy can't be set as a property `bar` on Bar class/);
    });

    it("get initializer via prototype", () => {
        expect(typeof Foo.prototype.value).toBe("function");
    });

    it("calling initializer will be create an instance at a time", () => {
        let initializer: any = Foo.prototype.value;

        expect(initializer.call(this)).toEqual("bar");
        expect(initializer.call(this)).not.toBe(initializer.call(this));
    });

    it("ref this correctly", () => {
        let foo = new Foo();
        let ref: any = Foo.prototype.ref;

        expect(this).not.toBe(foo);
        expect(foo.ref).toBe(foo);
        expect(ref.call(this)).toBe(this);
    });

    it("discard the initializer if set fields with other value", () => {
        let foo: any = new Foo();
        foo.fail = "failed";

        expect(foo.fail).toBe("failed");
    });

    it("inherit @lazy field correctly", () => {
        class Bar extends Foo {
        }

        const assertInitializerTo = it => {
            let initializer: any = Bar.prototype.ref;
            let initializer2: any = Foo.prototype.ref;
            expect(typeof initializer).toBe("function");
            expect(initializer.call(it)).toBe(it);
            expect(initializer2.call(it)).toBe(it);
        };

        assertInitializerTo(this);
        let bar = new Bar();
        assertInitializerTo({});
        expect(bar.value).toEqual("bar");
        expect(bar.value).toBe(bar.value);
        expect(bar.ref).toBe(bar);
        assertInitializerTo(this);
    });


    it("overriding @lazy field to discard super.initializer", () => {
        class Bar extends Foo {
            get fail() {
                return "error";
            };
        }

        let bar = new Bar();

        expect(bar.fail).toBe("error");
    });

    it("calling super @lazy fields", () => {
        let calls = 0;
        class Bar extends Foo {
            get ref(): any {
                calls++;
                //todo:a typescript bug:should be call `super.ref` getter  instead of super.ref() correctly in typescript,but it can't
                return (<any>super["ref"]).call(this);
            };
        }

        let bar = new Bar();

        expect(bar.ref).toBe(bar);
        expect(calls).toBe(1);
    });

    it("throws errors if @lazy a property with setter", () => {
        const lazyPropertyWithinSetter = () => {
            class Bar{
                @lazy
                get bar(){return "bar";}
                set bar(value){}
            }
        };


        expect(lazyPropertyWithinSetter).toThrow(/@lazy can't be annotated with get bar\(\) existing a setter on Bar class/);

    });
});

【讨论】:

  • 嗯。我正在使用 TS@2.2.1,只有 return super.ref 对我有用,这里是正确的。
  • 因为你将compilerOptions target设置为es6,我已经测试过,在es5中它不能被称为超级getter。
  • 是的。你说得对。但是如果我选择 es5 目标,它们都不适合我:D 我得到 [ts] Only public and protected methods of the base class are accessible via the 'super' keyword.[ts] Cannot invoke an expression whose type lacks a call signature. Type 'Bar' has no compatible call signatures.
  • 是的,我发现了一些新情况,我会再次更新我的答案。
  • 可能我认为这是错误的,我认为 @lazy 在 getter 上存在的 setter 会导致 setter 的行为无效。@lazy 可以通过 set:setter||set 做到这一点,但我只是禁用了这个以抛出异常为特征
【解决方案2】:

我会使用吸气剂:

class Lazy {
  private _f1;

  get f1() {
    return this._f1 || (this._f1 = expensiveInitializationForF1());
  }

}

是的,您可以使用装饰器来解决这个问题,但对于简单的情况,这可能有点过头了。

【讨论】:

  • 有一个小错字 - 应该是 return this._f1。但我喜欢你的简单解决方案。
  • 你必须把赋值放在括号中,否则打字稿会抱怨第二个this._f1后面缺少分号
  • 今天,您可能应该选择?? 而不是||return this._f1 ?? this._f1 = expensiveInitializationForF1();。如果 _f1 已初始化但具有虚假值,这将避免运行初始化。
  • ECMAScript 中最近发布了新的private class fields,您可以使用#f1 而不是private _f1 来获得真正的隐私。
【解决方案3】:

现代版本,类:

class Lazy<T> {
  private #f: T | undefined;
  constructor(#init: () => T) {}
  public get f(): T {
    return this.#f1 ??= this.#init();
  }
}

现代版本,内联:

let value;
// …
use_by_ref(value ??= lazy_init());

带有代理的潜在未来版本目前无法使用,因为从构造函数返回不同的值通常被视为未定义行为。

class Lazy<T> {
  constructor(private init: { [K in keyof T]: () => T[K] }) {
    let obj = Object.fromEntries(Object.keys(init).map(k => [k, undefined])) as unknown as { [K in keyof T]: undefined | T[K] };
    Object.seal(obj);
    return new Proxy(obj, this);
  }
  get<K extends keyof T>(t: T, k: K): T[K] {
    return t[k] ??= this.init[k];
  }
}

或许可以简化更多。

【讨论】:

    【解决方案4】:

    我正在使用这样的东西:

    export interface ILazyInitializer<T> {(): T}
    
    export class Lazy<T> {
    private instance: T | null = null;
    private initializer: ILazyInitializer<T>;
    
     constructor(initializer: ILazyInitializer<T>) {
         this.initializer = initializer;
     }
    
     public get value(): T {
         if (this.instance == null) {
             this.instance = this.initializer();
         }
    
         return this.instance;
     }
    }
    
    
    
    let myObject: Lazy<MyObject>;
    myObject = new Lazy(() => <MyObject>new MyObject("value1", "value2"));
    const someString = myObject.value.getProp;
    

    【讨论】:

      猜你喜欢
      • 2018-10-23
      • 2012-10-19
      • 2018-03-16
      • 2016-08-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多