C# 好还是坏的做法?在 getter 中初始化对象
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/14774008/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Good or bad practice? Initializing objects in getter
提问by John Willemse
I have a strange habit it seems... according to my co-worker at least. We've been working on a small project together. The way I wrote the classes is (simplified example):
我似乎有一个奇怪的习惯……至少据我的同事说。我们一直在共同致力于一个小项目。我编写类的方式是(简化示例):
[Serializable()]
public class Foo
{
public Foo()
{ }
private Bar _bar;
public Bar Bar
{
get
{
if (_bar == null)
_bar = new Bar();
return _bar;
}
set { _bar = value; }
}
}
So, basically, I only initialize any field when a getter is called and the field is still null. I figured this would reduce overload by not initializing any properties that aren't used anywhere.
所以,基本上,我只在调用 getter 并且该字段仍然为空时初始化任何字段。我认为这可以通过不初始化任何不在任何地方使用的属性来减少过载。
ETA: The reason I did this is that my class has several properties that return an instance of another class, which in turn also have properties with yet more classes, and so on. Calling the constructor for the top class would subsequently call all constructors for all these classes, when they are not alwaysall needed.
ETA:我这样做的原因是我的类有几个属性返回另一个类的实例,而另一个类又具有包含更多类的属性,依此类推。调用顶级类的构造函数随后会调用所有这些类的所有构造函数,但并不总是需要它们。
Are there any objections against this practice, other than personal preference?
除了个人喜好外,是否有任何反对这种做法的人?
UPDATE: I have considered the many differing opinions in regards to this question and I will stand by my accepted answer. However, I have now come to a much better understanding of the concept and I'm able to decide when to use it and when not.
更新:我已经考虑了关于这个问题的许多不同意见,我将支持我接受的答案。然而,我现在对这个概念有了更好的理解,我能够决定何时使用它,何时不使用它。
Cons:
缺点:
- Thread safety issues
- Not obeying a "setter" request when the value passed is null
- Micro-optimizations
- Exception handling should take place in a constructor
- Need to check for null in class' code
- 线程安全问题
- 当传递的值为空时不遵守“setter”请求
- 微观优化
- 异常处理应该在构造函数中进行
- 需要在类的代码中检查 null
Pros:
优点:
- Micro-optimizations
- Properties never return null
- Delay or avoid loading "heavy" objects
- 微观优化
- 属性从不返回 null
- 延迟或避免加载“重”对象
Most of the cons are not applicable to my current library, however I would have to test to see if the "micro-optimizations" are actually optimizing anything at all.
大多数缺点不适用于我当前的库,但是我必须测试以查看“微优化”是否真的在优化任何东西。
LAST UPDATE:
最后更新:
Okay, I changed my answer. My original question was whether or not this is a good habit. And I'm now convinced that it's not. Maybe I will still use it in some parts of my current code, but not unconditionally and definitely not all the time. So I'm going to lose my habit and think about it before using it. Thanks everyone!
好的,我改变了我的答案。我最初的问题是这是否是一个好习惯。而我现在确信事实并非如此。也许我仍然会在我当前代码的某些部分使用它,但不是无条件的,也绝对不是一直使用它。所以我要丢掉我的习惯,在使用之前考虑一下。谢谢大家!
采纳答案by Daniel Hilgarth
What you have here is a - naive - implementation of "lazy initialization".
你在这里拥有的是一个 - 天真的 - “延迟初始化”的实现。
Short answer:
简短的回答:
Using lazy initialization unconditionallyis not a good idea. It has its places but one has to take into consideration the impacts this solution has.
无条件地使用延迟初始化不是一个好主意。它有它的位置,但必须考虑到该解决方案的影响。
Background and explanation:
背景及说明:
Concrete implementation:
Let's first look at your concrete sample and why I consider its implementation naive:
具体实现:
让我们先看看你的具体示例,以及为什么我认为它的实现是幼稚的:
It violates the Principle of Least Surprise (POLS). When a value is assigned to a property, it is expected that this value is returned. In your implementation this is not the case for
null
:foo.Bar = null; Assert.Null(foo.Bar); // This will fail
- It introduces quite some threading issues: Two callers of
foo.Bar
on different threads can potentially get two different instances ofBar
and one of them will be without a connection to theFoo
instance. Any changes made to thatBar
instance are silently lost.
This is another case of a violation of POLS. When only the stored value of a property is accessed it is expected to be thread-safe. While you could argue that the class simply isn't thread-safe - including the getter of your property - you would have to document this properly as that's not the normal case. Furthermore the introduction of this issue is unnecessary as we will see shortly.
它违反了最小惊喜原则 (POLS)。将值分配给属性时,预计会返回此值。在您的实现中,情况并非如此
null
:foo.Bar = null; Assert.Null(foo.Bar); // This will fail
- 它引入了相当多的线程问题:
foo.Bar
不同线程上的两个调用者可能会获得两个不同的实例,Bar
其中一个将没有与Foo
实例的连接。对该Bar
实例所做的任何更改都会悄悄丢失。
这是另一个违反 POLS 的案例。当仅访问属性的存储值时,它应该是线程安全的。虽然您可能会争辩说该类根本不是线程安全的 - 包括您的财产的 getter - 您必须正确记录这一点,因为这不是正常情况。此外,我们很快就会看到这个问题的引入是不必要的。
In general:
It's now time to look at lazy initialization in general:
Lazy initialization is usually used to delay the construction of objects that take a long time to be constructed or that take a lot of memoryonce fully constructed.
That is a very valid reason for using lazy initialization.
一般情况:
现在是时候看看一般的延迟初始化了:
延迟初始化通常用于延迟构建需要很长时间或一旦完全构建就需要大量内存的对象的构建。
这是使用延迟初始化的一个非常有效的理由。
However, such properties normally don't have setters, which gets rid of the first issue pointed out above.
Furthermore, a thread-safe implementation would be used - like Lazy<T>
- to avoid the second issue.
然而,这些属性通常没有设置器,这就摆脱了上面指出的第一个问题。
此外,将使用线程安全实现 - 类似Lazy<T>
- 以避免第二个问题。
Even when considering these two points in the implementation of a lazy property, the following points are general problems of this pattern:
即使在惰性属性的实现中考虑了这两点,以下几点也是这种模式的普遍问题:
Construction of the object could be unsuccessful, resulting in an exception from a property getter. This is yet another violation of POLS and therefore should be avoided. Even the section on propertiesin the "Design Guidelines for Developing Class Libraries" explicitly states that property getters shouldn't throw exceptions:
Avoid throwing exceptions from property getters.
Property getters should be simple operations without any preconditions. If a getter might throw an exception, consider redesigning the property to be a method.
Automatic optimizations by the compiler are hurt, namely inlining and branch prediction. Please see Bill K's answerfor a detailed explanation.
对象的构造可能不成功,从而导致属性 getter 出现异常。这是对 POLS 的又一次违反,因此应该避免。甚至“开发类库的设计指南”中关于属性的部分也明确指出属性 getter 不应抛出异常:
避免从属性 getter 中抛出异常。
属性 getter 应该是没有任何先决条件的简单操作。如果 getter 可能抛出异常,请考虑将属性重新设计为方法。
编译器的自动优化受到损害,即内联和分支预测。有关详细说明,请参阅Bill K 的回答。
The conclusion of these points is the following:
For each single property that is implemented lazily, you should have considered these points.
That means, that it is a per-case decision and can't be taken as a general best practice.
这些要点的结论如下:
对于每个懒惰实现的单个属性,您应该考虑这些要点。
这意味着,这是每个案例的决定,不能作为一般的最佳实践。
This pattern has its place, but it is not a general best practice when implementing classes. It should not be used unconditionally, because of the reasons stated above.
这种模式有其一席之地,但在实现类时它不是一般的最佳实践。由于上述原因,不应无条件使用它。
In this section I want to discuss some of the points others have brought forward as arguments for using lazy initialization unconditionally:
在本节中,我想讨论其他人作为无条件使用延迟初始化的论据提出的一些观点:
Serialization:
EricJ states in one comment:An object that may be serialized will not have it's contructor invoked when it is deserialized (depends on the serializer, but many common ones behave like this). Putting initialization code in the constructor means that you have to provide additional support for deserialization. This pattern avoids that special coding.
There are several problems with this argument:
- Most objects never will be serialized. Adding some sort of support for it when it is not needed violates YAGNI.
- When a class needs to support serialization there exist ways to enable it without a workaround that doesn't have anything to do with serialization at first glance.
Micro-optimization: Your main argument is that you want to construct the objects only when someone actually accesses them. So you are actually talking about optimizing the memory usage.
I don't agree with this argument for the following reasons:- In most cases, a few more objects in memory have no impact whatsoever on anything. Modern computers have way enough memory. Without a case of actual problems confirmed by a profiler, this is pre-mature optimizationand there are good reasons against it.
I acknowledge the fact that sometimes this kind of optimization is justified. But even in these cases lazy initialization doesn't seem to be the correct solution. There are two reasons speaking against it:
- Lazy initialization potentially hurts performance. Maybe only marginally, but as Bill's answer showed, the impact is greater than one might think at first glance. So this approach basically trades performance versus memory.
- If you have a design where it is a common use case to use only parts of the class, this hints at a problem with the design itself: The class in question most likely has more than one responsibility. The solution would be to split the class into several more focused classes.
序列化:
EricJ 在一条评论中指出:一个可能被序列化的对象在反序列化时不会调用它的构造函数(取决于序列化器,但许多常见的行为都是这样)。将初始化代码放在构造函数中意味着您必须为反序列化提供额外的支持。这种模式避免了这种特殊的编码。
这个论点有几个问题:
- 大多数对象永远不会被序列化。在不需要时为其添加某种支持违反了YAGNI。
- 当一个类需要支持序列化时,有一些方法可以在没有乍一看与序列化无关的解决方法的情况下启用它。
微优化:您的主要论点是您只想在有人实际访问对象时才构造对象。所以你实际上是在谈论优化内存使用。
我不同意这个论点,原因如下:- 在大多数情况下,内存中的更多对象对任何事情都没有任何影响。现代计算机有足够的内存。如果没有分析器确认实际问题的案例,这是过早的优化,并且有充分的理由反对它。
我承认有时这种优化是合理的。但即使在这些情况下,延迟初始化似乎也不是正确的解决方案。反对它的理由有两个:
- 延迟初始化可能会损害性能。也许只是微不足道,但正如比尔的回答所表明的那样,影响比乍一看想象的要大。所以这种方法基本上是在性能与内存之间进行权衡。
- 如果您的设计通常只使用类的一部分,这暗示了设计本身存在问题:所讨论的类很可能有多个职责。解决方案是将班级分成几个更集中的班级。
回答by Luis Tellez
The downside that I can see is that if you want to ask if Bars is null, it would never be, and you would be creating the list there.
我可以看到的缺点是,如果你想询问 Bars 是否为空,它永远不会是,而且你会在那里创建列表。
回答by Colin Mackay
I think it depends on what you are initialising. I probably wouldn't do it for a list as the construction cost is quite small, so it can go in the constructor. But if it was a pre-populated list then I probably wouldn't until it was needed for the first time.
我认为这取决于您要初始化的内容。我可能不会为列表做这件事,因为建设成本非常小,所以它可以放在构造函数中。但是如果它是一个预先填充的列表,那么我可能不会直到第一次需要它。
Basically, if the cost of construction outweighs the cost of doing an conditional check on each access then lazy create it. If not, do it in the constructor.
基本上,如果构建成本超过对每次访问进行条件检查的成本,那么惰性创建它。如果没有,请在构造函数中执行。
回答by Matías Fidemraizer
Do you consider implementing such pattern using Lazy<T>
?
您是否考虑使用Lazy<T>
?
In addition to easy creation of lazy-loaded objects, you get thread safety while the object is initialized:
除了可以轻松创建延迟加载的对象之外,您还可以在初始化对象时获得线程安全性:
As others said, you lazily-load objects if they're really resource-heavy or it takes some time to load them during object construction-time.
正如其他人所说,如果对象确实占用大量资源,或者在对象构建期间加载它们需要一些时间,则您可以延迟加载它们。
回答by AMissico
It is a good design choice. Strongly recommended for library code or core classes.
这是一个很好的设计选择。强烈推荐用于库代码或核心类。
It is called by some "lazy initialization" or "delayed initialization" and it is generally considered by all to be a good design choice.
它被一些“延迟初始化”或“延迟初始化”调用,并且通常被所有人认为是一个很好的设计选择。
First, if you initialize in the declaration of class level variables or constructor, then when your object is constructed, you have the overhead of creating a resource that may never be used.
首先,如果您在类级变量或构造函数的声明中进行初始化,那么当您的对象被构造时,您就有了创建可能永远不会使用的资源的开销。
Second, the resource only gets created if needed.
其次,只有在需要时才会创建资源。
Third, you avoid garbage collecting an object that was not used.
第三,避免垃圾收集未使用的对象。
Lastly, it is easier to handle initialization exceptions that may occur in the property then exceptions that occur during initialization of class level variables or the constructor.
最后,处理可能发生在属性中的初始化异常比处理类级变量或构造函数初始化期间发生的异常更容易。
There are exceptions to this rule.
这条规则也有例外。
Regarding the performance argument of the additional check for initialization in the "get" property, it is insignificant. Initializing and disposing an object is a more significant performance hit than a simple null pointer check with a jump.
关于在“get”属性中额外检查初始化的性能参数,它无关紧要。初始化和处置一个对象比简单的空指针检查和跳转对性能的影响更大。
Design Guidelines for Developing Class Librariesat http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx
设计准则类库开发的http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx
Regarding Lazy<T>
关于 Lazy<T>
The generic Lazy<T>
class was created exactly for what the poster wants, see Lazy Initializationat http://msdn.microsoft.com/en-us/library/dd997286(v=vs.100).aspx. If you have older versions of .NET, you have to use the code pattern illustrated in the question. This code pattern has become so common that Microsoft saw fit to include a class in the latest .NET libraries to make it easier to implement the pattern. In addition, if your implementation needs thread safety, then you have to add it.
通用Lazy<T>
类是完全相同的海报想要什么创建,请参阅延迟初始化的http://msdn.microsoft.com/en-us/library/dd997286(v=vs.100).aspx。如果您有旧版本的 .NET,则必须使用问题中说明的代码模式。这种代码模式已经变得如此普遍,以至于 Microsoft 认为在最新的 .NET 库中包含一个类是合适的,以便更容易地实现该模式。此外,如果您的实现需要线程安全,那么您必须添加它。
Primitive Data Types and Simple Classes
原始数据类型和简单类
Obvioulsy, you are not going to use lazy-initialization for primitive data type or simple class use like List<string>
.
显然,您不会对原始数据类型或简单的类使用(如List<string>
.
Before Commenting about Lazy
在评论懒惰之前
Lazy<T>
was introduced in .NET 4.0, so please don't add yet another comment regarding this class.
Lazy<T>
是在 .NET 4.0 中引入的,所以请不要再添加关于这个类的评论。
Before Commenting about Micro-Optimizations
在评论微优化之前
When you are building libraries, you must consider all optimizations. For instance, in the .NET classes you will see bit arrays used for Boolean class variables throughout the code to reduce memory consumption and memory fragmentation, just to name two "micro-optimizations".
在构建库时,必须考虑所有优化。例如,在 .NET 类中,您将在整个代码中看到用于布尔类变量的位数组,以减少内存消耗和内存碎片,仅举两个“微优化”。
Regarding User-Interfaces
关于用户界面
You are not going to use lazy initialization for classes that are directly used by the user-interface. Last week I spent the better part of a day removing lazy loading of eight collections used in a view-model for combo-boxes. I have a LookupManager
that handles lazy loading and caching of collections needed by any user-interface element.
您不会对用户界面直接使用的类使用延迟初始化。上周我花了一天的大部分时间来删除组合框视图模型中使用的八个集合的延迟加载。我有一个LookupManager
处理延迟加载和缓存任何用户界面元素所需的集合。
"Setters"
“二传手”
I have never used a set-property ("setters") for any lazy loaded property. Therefore, you would never allow foo.Bar = null;
. If you need to set Bar
then I would create a method called SetBar(Bar value)
and not use lazy-initialization
我从未对任何延迟加载的属性使用 set-property(“setter”)。因此,您永远不会允许foo.Bar = null;
. 如果您需要设置,Bar
那么我将创建一个调用的方法SetBar(Bar value)
而不使用延迟初始化
Collections
收藏
Class collection properties are always initialized when declared because they should never be null.
类集合属性在声明时总是被初始化,因为它们永远不应该为空。
Complex Classes
复杂类
Let me repeat this differently, you use lazy-initialization for complex classes. Which are usually, poorly designed classes.
让我以不同的方式重复这一点,您对复杂的类使用延迟初始化。这通常是设计糟糕的类。
Lastly
最后
I never said to do this for all classes or in all cases. It is a bad habit.
我从未说过要对所有课程或所有情况都这样做。这是一个坏习惯。
回答by Tormod
Lazy instantiation/initialization is a perfectly viable pattern. Keep in mind, though, that as a general rule consumers of your API do not expect getters and setters to take discernable time from the end user POV (or to fail).
延迟实例化/初始化是一种完全可行的模式。但请记住,作为一般规则,API 的使用者不希望 getter 和 setter 从最终用户 POV 中花费可辨别的时间(或失败)。
回答by Bill K
I was just going to put a comment on Daniel's answer but I honestly don't think it goes far enough.
我只是想对丹尼尔的回答发表评论,但老实说,我认为这还不够。
Although this is a very good pattern to use in certain situations (for instance, when the object is initialized from the database), it's a HORRIBLE habit to get into.
尽管这是在某些情况下使用的非常好的模式(例如,当对象从数据库初始化时),但这是一个可怕的习惯。
One of the best things about an object is that it offeres a secure, trusted environment. The very best case is if you make as many fields as possible "Final", filling them all in with the constructor. This makes your class quite bulletproof. Allowing fields to be changed through setters is a little less so, but not terrible. For instance:
关于对象的最好的事情之一是它提供了一个安全、可信的环境。最好的情况是,如果您制作尽可能多的字段“最终”,并使用构造函数将它们全部填充。这使您的课程非常防弹。允许通过 setter 更改字段有点少,但并不可怕。例如:
class SafeClass { String name=""; Integer age=0; public void setName(String newName) { assert(newName != null) name=newName; }// follow this pattern for age ... public String toString() { String s="Safe Class has name:"+name+" and age:"+age } }
With your pattern, the toString method would look like this:
使用您的模式, toString 方法将如下所示:
if(name == null) throw new IllegalStateException("SafeClass got into an illegal state! name is null") if(age == null) throw new IllegalStateException("SafeClass got into an illegal state! age is null") public String toString() { String s="Safe Class has name:"+name+" and age:"+age }
Not only this, but you need null checks everywhere you might possibly use that object in your class (Outside your class is safe because of the null check in the getter, but you should be mostly using your classes members inside the class)
不仅如此,您还需要在任何可能在类中使用该对象的地方进行空检查(在您的类之外是安全的,因为 getter 中的空检查,但您应该主要在类内使用您的类成员)
Also your class is perpetually in an uncertain state--for instance if you decided to make that class a hibernate class by adding a few annotations, how would you do it?
此外,您的类永远处于不确定状态——例如,如果您决定通过添加一些注释使该类成为休眠类,您会怎么做?
If you make any decision based on some micro-optomization without requirements and testing, it's almost certainly the wrong decision. In fact, there is a really really good chance that your pattern is actually slowing down the system even under the most ideal of circumstances because the if statement can cause a branch prediction failure on the CPU which will slow things down many many many more times than just assigning a value in the constructor unless the object you are creating is fairly complex or coming from a remote data source.
如果您在没有要求和测试的情况下基于某种微优化做出任何决定,那几乎肯定是错误的决定。事实上,即使在最理想的情况下,你的模式也很有可能会拖慢系统的速度,因为 if 语句会导致 CPU 上的分支预测失败,这会比除非您创建的对象相当复杂或来自远程数据源,否则只需在构造函数中分配一个值。
For an example of the brance prediction problem (which you are incurring repeatedly, nost just once), see the first answer to this awesome question: Why is it faster to process a sorted array than an unsorted array?
有关 brance 预测问题的示例(您反复出现,而不是一次),请参阅这个很棒的问题的第一个答案:为什么处理已排序数组比处理未排序数组更快?
回答by KaptajnKold
Are you sure Foo should be instantiating anything at all?
你确定 Foo 应该实例化任何东西吗?
To me it seems smelly (though not necessarily wrong) to let Foo instantiate anything at all. Unless it is Foo's express purpose to be a factory, it should not instantiate it's own collaborators, but instead get them injected in its constructor.
对我来说,让 Foo 实例化任何东西似乎很糟糕(虽然不一定是错的)。除非 Foo 的明确目的是成为工厂,否则它不应该实例化自己的合作者,而是将它们注入到其构造函数中。
If however Foo's purpose of being is to create instances of type Bar, then I don't see anything wrong with doing it lazily.
然而,如果 Foo 的目的是创建 Bar 类型的实例,那么我认为懒惰地这样做没有任何问题。
回答by Branko Dimitrijevic
Let me just add one more point to many good points made by others...
让我在其他人提出的许多优点上再增加一点......
The debugger will (by default) evaluate the properties when stepping through the code, which could potentially instantiate the Bar
sooner than would normally happen by just executing the code. In other words, the mere act of debugging is changing the execution of the program.
调试器将(默认情况下)在单步执行代码时评估属性,这可能Bar
比仅执行代码通常会更快地实例化。换句话说,仅仅是调试行为就是改变程序的执行。
This may or may not be a problem (depending on side-effects), but is something to be aware of.
这可能是也可能不是问题(取决于副作用),但需要注意。