为什么我应该避免在C#中使用属性?
在他的优秀着作CLR Via C#中,杰弗里·里希特(Jeffrey Richter)表示,他不喜欢财产,并build议不要使用它们。 他提出了一些理由,但我不明白。 任何人都可以向我解释为什么我应该或不应该使用属性? 在具有自动属性的C#3.0中,这个改变了吗?
作为参考,我添加了Jeffrey Richter的观点:
•财产可能是只读或只写; 字段访问总是可读写的。 如果你定义一个属性,最好同时提供get和set访问方法。
•属性方法可能会抛出exception; 字段访问永远不会引发exception。
•属性不能作为out或refparameter passing给方法; 一个领域可以。 例如,下面的代码将不能编译:
using System; public sealed class SomeType { private static String Name { get { return null; } set {} } static void MethodWithOutParam(out String n) { n = null; } public static void Main() { // For the line of code below, the C# compiler emits the following: // error CS0206: A property or indexer may not // be passed as an out or ref parameter MethodWithOutParam(out Name); } }
•属性方法可能需要很长时间才能执行; 字段访问总是立即完成。 使用属性的一个常见原因是执行线程同步,这可能永远停止线程,因此,如果需要线程同步,则不应使用属性。 在这种情况下,一种方法是优选的。 此外,如果您的类可以远程访问(例如,您的类是从System.MashalByRefObject派生的),调用属性方法将非常缓慢,因此,方法是首选属性。 在我看来,从MarshalByRefObject派生的类不应该使用属性。
•如果连续调用多次,属性方法可能每次都返回一个不同的值; 一个字段每次返回相同的值。 System.DateTime类具有一个只读的Now属性,它返回当前的date和时间。 每次查询该属性时,都会返回一个不同的值。 这是一个错误,微软希望他们能够通过使Now成为一个方法而不是一个属性来修复这个类。
•财产法可能导致可观察到的副作用; 字段访问永远不会。 换句话说,types的用户应该能够按照他或她select的任何顺序来设置由types定义的各种属性,而不会注意到types中的任何不同的行为。
属性方法可能需要额外的内存或者返回一个对象实际上并不是对象状态的一部分的引用,所以修改返回的对象对原始对象没有影响; 查询一个字段总是返回一个对象的引用,该对象被保证是原始对象状态的一部分。 使用返回副本的属性可能会使开发人员感到困惑,而且这种特性通常没有logging。
杰夫之所以不喜欢房地产,是因为他们看起来像田野,所以不了解差异的开发者会认为他们好像是田地,假设他们执行起来便宜。
就我个人而言,我不同意他在这一点上 – 我发现属性使客户端代码比等效的方法调用更容易阅读。 我同意开发人员需要知道,属性基本上是变相的方法 – 但我认为教育开发人员比使代码更难读取使用方法更好。 (尤其是,在同一个语句中看到了多个getter和setter被调用的Java代码,我知道等价的C#代码会更容易阅读,Demeter的法则理论上都非常好,但有时甚至是foo.Name.Length
真的是正确的使用…)
(不,自动实现的属性并不真的改变这一切。)
这有点像反对使用扩展方法的论点 – 我可以理解推理,但是实际的好处(谨慎使用)超过了我认为的缺点。
那么让我们一个接一个地说:
属性可以是只读的或只写的; 字段访问总是可读写的。
这是属性的一个胜利,因为你对访问进行了更细致的控制。
属性方法可能会抛出exception; 字段访问永远不会引发exception。
虽然这大部分是正确的,但您可以在未初始化的对象字段上调用方法,并抛出exception。
•属性不能作为out或refparameter passing给方法; 一个领域可以。
公平。
•属性方法可能需要很长时间才能执行; 字段访问总是立即完成。
它也可能需要很less的时间。
•如果连续调用多次,属性方法可能每次都返回一个不同的值; 一个字段每次返回相同的值。
不对。 你怎么知道字段的值没有改变(可能由另一个线程)?
System.DateTime类具有一个只读的Now属性,它返回当前的date和时间。 每次查询该属性时,都会返回一个不同的值。 这是一个错误,微软希望他们能够通过使Now成为一个方法而不是一个属性来修复这个类。
如果这是一个错误,这是一个小问题。
•财产法可能导致可观察到的副作用; 字段访问永远不会。 换句话说,types的用户应该能够按照他或她select的任何顺序来设置由types定义的各种属性,而不会注意到types中的任何不同的行为。
公平。
属性方法可能需要额外的内存或者返回一个对象实际上并不是对象状态的一部分的引用,所以修改返回的对象对原始对象没有影响; 查询一个字段总是返回一个对象的引用,该对象被保证是原始对象状态的一部分。 使用返回副本的属性可能会使开发人员感到困惑,而且这种特性通常没有logging。
对于Java的getter和setter,大部分的抗议都可以这么说 – 我们在实践中已经有了一段时间没有这样的问题。
我认为大部分问题都可以通过更好的语法突出来解决(即区分属性和字段),这样程序员就知道应该期待什么。
我没有看过这本书,你没有引用你不明白的部分,所以我不得不猜测。
有些人不喜欢财产,因为他们可以让你的代码做出令人惊讶的事情。
如果我inputFoo.Bar
,读取它的人通常会期望这只是访问Foo类的成员字段。 这是一个便宜,几乎免费的操作,而且是确定性的。 我可以一遍又一遍地调用它,每次都得到相同的结果。
相反,对于属性,它实际上可能是一个函数调用。 这可能是一个无限循环。 它可能会打开一个数据库连接。 每次访问它时可能会返回不同的值。
这是Linus讨厌C ++的原因。 您的代码可能会令读者感到意外。 他讨厌操作符重载: a + b
不一定意味着简单的加法。 这可能意味着一些非常复杂的操作,就像C#属性一样。 它可能有副作用。 它可以做任何事情。
老实说,我认为这是一个弱言。 两种语言都是这样的东西。 (我们是否应该避免在C#中的操作符重载?毕竟,在那里可以使用相同的参数)
属性允许抽象。 我们可以假装某个东西是一个普通的领域,把它当作一个东西来使用,而不必担心幕后发生的事情。
这通常被认为是一件好事,但它显然依赖于程序员写有意义的抽象。 你的物业应该像田野一样。 他们不应该有副作用,他们不应该执行昂贵的或不安全的操作。 我们希望能够把它们看作是一个领域。
不过,我还有另一个理由find他们不完美。 它们不能通过引用其他函数来传递。
字段可以作为ref
传递,允许被调用的函数直接访问它。 函数可以作为委托传递,允许被调用的函数直接访问它。
属性…不能。
这很糟糕。
但这并不意味着属性是邪恶的,也不应该被使用。 对于很多目的来说,他们很棒。
早在2009年,这个build议就好像是“ 谁动了我的奶酪” 。 今天,这几乎已经过时了。
一个非常重要的观点,许多答案似乎tip手but脚,但却没有明确地指出,这些声称属性的“危险”是框架devise的有意识的一部分!
是的,物业可以:
-
为getter和setter指定不同的访问修饰符。 这是领域的优势 。 一个常见的模式是拥有一个公共的getter和一个受保护或内部的 setter,这是一个非常有用的inheritance技术,这是单靠领域无法实现的。
-
抛出exception。 迄今为止,这仍然是最有效的validation方法之一,特别是在涉及数据绑定概念的UI框架时。 在使用字段时,确保对象保持有效状态要困难得多。
-
需要很长时间来执行。 这里的有效比较是采用同样长的方法 ,而不是字段 。 除了一个作者的个人喜好之外,没有给出“一种方法是优选的”的说法。
-
在后续执行中从其获取器返回不同的值。 这几乎看起来像一个笑话,在这样一个接近的点赞扬
ref
/out
参数字段的优点,其中一个ref
/out
调用后的字段的价值几乎保证不同于以前的价值,并且不可预测。如果我们谈论的是没有传入耦合的单线程访问的具体(实际上是学术的)情况,那么就可以很好地理解 ,只有坏的属性devise才会产生可见状态变化的副作用,也许我的记忆是褪色,但我似乎无法回想任何使用
DateTime.Now
的人的例子,并期望每次出现相同的价值。 至less在任何情况下,他们都不会把它搞砸,就像假设的DateTime.Now()
。 -
导致可观察到的副作用 – 这当然恰恰是属性首先被发明为语言特征的原因。 微软自己的房产devise指导方针表明,设置顺序应该不重要,否则将意味着时间耦合 。 当然,你不能单独实现与场的时间耦合,但那只是因为你不能单独地引起任何有意义的行为,直到某个方法被执行。
属性访问器实际上可以帮助防止某些types的时间耦合,方法是在采取任何操作之前强制对象进入有效状态 – 例如,如果某个类具有
StartDate
和EndDate
,则在StartDate
可以强制StartDate
之前设置EndDate
以及。 即使在multithreading或asynchronous环境中也是如此,包括事件驱动用户界面的明显例子。
属性可以做的其他事情哪些字段不能包括:
- 延迟加载是防止初始化错误的最有效方法之一。
- 更改通知 ,这几乎是MVVM体系结构的全部基础。
- inheritance ,例如定义一个抽象的
Type
或Name
所以派生类可以提供有趣的,但仍然不变的元数据关于他们自己。 - 拦截 ,感谢以上。
- 索引者 ,所有曾经与COM进行过互操作的人,以及
Item(i)
调用的不可避免的特征,都将被认为是一件美妙的事情。 - 使用PropertyDescriptor对于创builddevise者和一般的XAML框架是非常重要的。
Richter显然是一位多产的作者,对CLR和C#有很多了解,但是我不得不说,他似乎是最初写这个build议的时候(我不确定是否在他最近的版本中 – 我真心希望不会)他只是不想放弃旧习惯,并且无法接受C#(比如C ++)的惯例。
我的意思是,他认为“有害的财产”的论点基本上归结为一个单一的陈述: 属性看起来像田野,但他们可能不像田野。 这个陈述的问题是, 这不是真的 ,或者充其量是误导性的。 属性看起来不像字段 – 至less,它们不应该看起来像字段。
在C#中有两种非常强大的编码约定,其他CLR语言共享相似的约定,如果不遵循这些约定,FXCop会尖叫你:
- 领域应该永远是私人的, 从不公开。
- 字段应该在camelCase中声明。 属性是PascalCase。
因此, Foo.Bar = 42
是一个属性访问器还是一个字段访问器是没有歧义的。 这是一个属性访问器,应该像其他任何方法一样对待 – 它可能很慢,可能会抛出exception等。这就是抽象的本质 – 完全取决于声明类的判断如何作出反应。 阶级devise师应该应用最less惊喜的原则,但是调用者不应该对一个财产承担任何责任,除非它在jar子上做了什么。 这是有目的的。
属性的替代方法是无处不在的getter / setter方法。 这是Java的方法, 从一开始就有争议 。 如果这是你的包,那很好,但是这不是我们在.NET阵营中的方式。 至less在静态types系统的范围内,我们试着避免福勒称之为句法噪声 。 我们不需要额外的括号,额外的get
/ set
疣或额外的方法签名 – 如果我们可以避免它们而不会损失清晰度。
说你喜欢的任何东西,但是foo.Bar.Baz = quux.Answers[42]
总是比foo.getBar().setBaz(quux.getAnswers().getItem(42))
更容易阅读。 而当你每天读数以千计的这一行的时候,这是有所作为的。
(如果你对上述段落的自然反应是这样说的,“确定它很难阅读,但是如果你把它分成多行,这会更容易”,那么我很遗憾地说你完全错过了这一点。)
我没有看到任何你不应该使用属性的原因。
C#3 +中的自动属性只能简化句法(一个合成糖)。
这只是一个人的意见。 我已经阅读了很多C#书籍,但还没有看到其他人说“不要使用属性”。
我个人认为属性是关于C#最好的事情之一。 它们允许你通过你喜欢的任何机制来暴露状态。 你可以懒惰地第一次实例化一些东西,你可以在设置一个值时进行validation。在使用和写入时,我只是将属性看作setter和getter,这是一个更好的语法。
至于有关物业的警告,有一对夫妇。 一个可能是滥用性质,另一个可能是微妙的。
首先,属性是方法的types。 如果将复杂的逻辑放在属性中,这可能会令人惊讶,因为大多数类的用户会期望该属性相当轻量级。
例如
public class WorkerUsingMethod { // Explicitly obvious that calculation is being done here public int CalculateResult() { return ExpensiveLongRunningCalculation(); } } public class WorkerUsingProperty { // Not at all obvious. Looks like it may just be returning a cached result. public int Result { get { return ExpensiveLongRunningCalculation(); } } }
我发现使用这些案例的方法有助于区分。
其次,更重要的是,如果您在debugging过程中对其进行评估,则属性会产生副作用。
假设你有这样的一些财产:
public int Result { get { m_numberQueries++; return m_result; } }
现在假设你有太多的查询发生exception。 猜猜当你开始debugging和翻转debugging器中的属性会发生什么? 坏事。 避免这样做! 看着这个属性改变了程序的状态。
这些是我唯一的警告。 我认为,房地产的好处远远大于问题。
这个理由一定是在一个非常具体的背景下给出的。 这通常是相反的 – build议使用属性,因为它们提供了一个抽象层次,使您可以在不影响客户端的情况下更改类的行为。
我不禁琢磨着Jeffrey Richter的意见细节:
属性可以是只读的或只写的; 字段访问总是可读写的。
错误:字段可以标记为只读,所以只有对象的构造函数可以写入它们。
属性方法可能会抛出exception; 字段访问永远不会引发exception。
错误:类的实现可以将字段的访问修饰符从公共私有变为私有。 尝试在运行时读取专用字段将始终导致exception。
我不同意杰弗里·里希特的看法,但我可以猜测他为什么不喜欢房产(我还没有读过他的书)。
尽pipe属性就像方法(实现方式),作为一个类的用户,我期望它的属性像公共领域一样“或多或less”,例如:
- 在属性getter / setter内部没有耗时的操作
- 属性getter没有副作用(多次调用,不会改变结果)
不幸的是,我已经看到了不以这种方式行事的财产。 但问题不在于属性本身,而在于实施它们的人。 所以只需要一些教育。
有一段时间,我认为不使用属性,这是写在.NET Compact Framework代码。 CF JIT编译器不会像桌面JIT编译器那样执行相同的优化,也不会优化简单的属性访问器,所以在这种情况下,添加一个简单的属性会导致使用公共字段的less量代码膨胀。 通常这不会是一个问题,但是几乎总是在Compact Framework的世界里,你遇到了严格的内存限制,所以即使是这样的小存储也算。
你不应该避免使用它们,但是你应该使用它们,并且要有资格和谨慎,因为其他贡献者给出的理由。
我曾经看到过一个名为Customers的属性,它在内部打开了一个对数据库的进程外调用并读取了客户列表。 客户端代码有一个“for(int i to Customers.Count)”,它在每次迭代中导致对数据库的单独调用以及所选客户的访问。 这是一个令人震惊的例子,certificate了保持财产非常轻的原则 – 很less比内部的领域获取更多。
使用属性的一个参数是它们允许您validation正在设置的值。 另一个是该属性的值可能是一个派生值,而不是单个字段,如TotalValue =金额*数量。
就个人而言,我只在创build简单的get / set方法时使用属性。 当遇到复杂的数据结构时,我会偏离它。
调用方法而不是属性大大降低了调用代码的可读性。 在J#中,例如,使用ADO.NET是一场噩梦,因为Java不支持属性和索引(实质上是带有参数的属性)。 由此产生的代码是非常丑陋的,用空括号方法调用遍地。
对属性和索引器的支持是C#over Java的基本优势之一。