属性的惰性初始化方法

8

我目前正在修改一个广泛使用的类,将尽可能多的昂贵初始化从类构造函数移至惰性初始化属性中。下面是一个示例(使用c#语言):

修改前:

public class ClassA
{
    public readonly ClassB B;

    public void ClassA()
    {
        B = new ClassB();
    }
}

之后:

public class ClassA
{
    private ClassB _b;

    public ClassB B
    {
        get
        {
            if (_b == null)
            {
                _b = new ClassB();
            }

            return _b;
        }
    }
}

在我正在修改的类中有更多这样的属性,某些属性在特定情况下不会被使用(因此需要懒加载),但如果它们被使用,则很可能会被重复调用。
不幸的是,这些属性通常也在类内部使用。这意味着私有变量(_b)有可能被方法直接使用而没有初始化。
是否有一种方法只使公共属性(B)在类内部可用,或者甚至具有相同的按需初始化的替代方法?
这是从程序员(显然不够主观)重新发布的: https://softwareengineering.stackexchange.com/questions/34270/best-methods-for-lazy-initialization-with-properties
3个回答

16

我的建议是告诉你的同事使用属性而不是字段。但你可以通过以下方式使其更加易用:

public class ClassA
{
    private Lazy<ClassB> _b = new Lazy<ClassB>(() => new ClassB());

    public ClassB B
    {
        get
        {
            return _b.Value;
        }
    }
}

现在很难搞砸。


1
既然他的第一个示例不是线程安全的,我认为他并不关心线程安全。而且我不知道性能影响是什么,除了它们必须非常小,以至于我只会在某些极其重要的内部循环中担心它。 - mqp
1
@Stuart:编写一个Lazy<>类非常简单,如果您经常使用此模式,您可能希望考虑自己编写它。 - mqp
1
@mquander 更进一步,你可以通过将懒加载实例化推到基类并让属性延迟到基本属性来完全防呆。然后同事就不需要被告知不要使用该字段,他别无选择,只能使用该属性,并且这样做还有双重效果,可以从ClassA实例中删除多余的字段。 - BenAlabaster
1
@BenAlabaster:当然,你可以这样做,但我的观点是,它很难被误用。尝试从字段中获取ClassB的唯一方法是调用.Value,如果同事这样做了(因为他不知道属性),那么一切都没问题。就个人而言,我认为除此之外再去费力和增加代码行数是不值得的。 - mqp
1
@Ryan:默认情况下不是线程安全的部分是:当两个线程同时调用它进行“第一次”初始化并竞争时会发生什么?请查看Hans提到的MSDN中的LazyThreadSafetyMode枚举。它解决了这个问题。 - mqp
显示剩余13条评论

6

您可以考虑将惰性属性推入基类中,以避免直接访问备份变量。虽然不是理想的解决方案,但我一直认为这是C#所缺少的支持惰性属性的直接特性。


@BenAlabaster 当我第一次看到自动属性时,我以为它们添加了延迟功能-不幸的是并没有。如果能够定义带有初始化函数的自动属性将会很棒。 - Tim Lloyd
@BenAlabaster 他们肯定需要引入一些新的语法和编译器后端。我认为难点在于实现易读且易于理解的东西。除非你默认启用线程安全,否则还需要引入线程配置。获得一些漂亮清晰的语法将是棘手的。public Bling B { lazyget: new Bling(); } :) - Tim Lloyd
@chibacity 新的Bling()能否通过返回类型和Bling具有无参数构造函数这一事实来推断?因此,您只需要public Bling B { lazyget; }。当然,如果您没有无参数构造函数,则需要类似于您的语法。 - BenAlabaster
@BenAlabaster 对于默认构造函数,可以很好地推断出来,尽管我担心如果删除默认构造函数,用户可能会如何解释编译错误。你需要一个好的地方来放置红色波浪线 :) 我非常喜欢具有功能的想法,因为这将很好地适应 DI。 - Tim Lloyd
@mquander, @BenAlabaster:你们的回答非常有趣,但我选择了这个答案,因为使用一个简单明了的基类是我最容易采用的解决方案。 - Stu Pegg
显示剩余2条评论

3

@chibacity发布了一个使用抽象基类的替代选项,后来又删除了它(然后再次取消删除:P)。虽然在代码分发方面可能不是理想的选择,但它确实提供了一个很好的封装,消除了大量的代码混乱,使ClassA更加简洁和清晰。例如,您可以考虑结合这些技术以实现两个目标:

public class ClassB { /* Class to be lazily instantiated */ }

public abstract class BaseA
{
    private Lazy<ClassB> _b = new Lazy<ClassB>(() => new ClassB());
    public virtual ClassB B { get { return _b.Value; } }
}

public class ClassA : BaseA
{
    public override ClassB B { get { return base.B; } }
}

乍一看,这似乎更加冗长,但是当你考虑到ClassA是你要操作和使用的类时,这意味着现在所有的引用都通过同一个属性进行 - 没有不必要的字段导致潜在的混淆,没有绕过该属性直接引用_b,也没有必要告诉你的同事应该使用哪个...只有一个。
我并不是说这是正确的做法或者这是一个应该或不应该遵循的模式,我只是指出了@chibacity建议的优点,这些优点可能会被忽视。
如果你能够隐式延迟加载属性而不必引用B.Value就好了...例如:
[Lazy]
public ClassB B { get; }

或者对于没有无参构造函数的对象

[Lazy(() => new ClassB("Hello", "World"))]
public ClassB B { get; }

或者像@chibacity在评论中建议的那样。
public ClassB B { lazyget; }

或者

public ClassB B { lazyget : new ClassB(); }

唉,我认为目前这些都不是可用的解决方案...

涉及到任何形式的it技术。请注意,保留HTML标记。


我很喜欢使用func和attribute的语法,但不幸的是,attribute参数必须是编译时常量,所以这个方法行不通 - 不过看起来非常好。 :) - Tim Lloyd
@chibacity 这是我在编写API时始终注意的一件事情 - 当我调用此功能或使用此功能时,我希望语法看起来如何?通常,我会先设计我的调用外观,然后再设计我的API,以便一切都恰到好处。 - BenAlabaster

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