为什么我应该避免在C中使用属性?

Why should I avoid using Properties in C#?

杰弗里·里克特在他出色的书《通过C的clr》中说,他不喜欢财产,并建议不要使用它们。他说了一些原因,但我不太明白。有人能告诉我为什么我应该或不应该使用属性吗?在C 3.0中,自动属性会改变吗?

作为参考,我补充了杰弗里·里克特的观点:

?属性可以是只读的或只写的;字段访问总是可读和可写的。如果定义属性,最好同时提供get和set访问器方法。

?属性方法可能引发异常;字段访问从不引发异常。

?属性不能作为out或ref参数传递给方法;字段可以。为了例如,以下代码将不编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.MasharByRefObject派生的),调用属性方法将非常缓慢,因此,方法比财产。在我看来,从MarshalByRefObject派生的类不应该使用性质。

?如果在一行中多次调用,则属性方法可能会分别返回不同的值时间;字段每次返回相同的值。System.DateTime类具有只读返回当前日期和时间的Now属性。每次你查询这个属性,它将返回不同的值。这是一个错误,微软希望他们无法通过使现在成为方法而不是属性来修复类。

?属性方法可能会导致可观察到的副作用;字段访问永远不会。在其他换句话说,一个类型的用户应该能够在任何命令他或她选择而不注意任何不同的行为类型。

?属性方法可能需要额外的内存或返回对某个对象的引用这实际上不是对象状态的一部分,因此修改返回的对象没有对原始对象的影响;查询字段始终返回对保证是原始对象状态的一部分。使用的属性返回一个副本可能会让开发人员非常困惑,而且这个特性经常未记录。


Jeff不喜欢属性的原因是它们看起来像字段,所以不理解差异的开发人员会将它们视为字段,假设它们的执行成本很低等等。

就我个人而言,我不同意他在这一点上的看法——我发现属性使客户端代码的读取比等效的方法调用简单得多。我同意开发人员需要知道,属性基本上是伪装的方法,但我认为教育开发人员这一点比使用方法使代码更难阅读要好。(特别是,在同一语句中调用了几个具有GET和Stter的Java代码时,我知道等价的C代码会读起来简单多了。德米特定律在理论上是很好的,但有时foo.Name.Length确实是正确的使用方法……)

(不,自动实现的属性不会真正改变其中的任何一个。)

这有点像反对使用扩展方法的论点——我可以理解这种推理,但是实际的好处(如果使用得少)超过了我认为的缺点。


好吧,让我们逐一讨论他的论点:

A property may be read-only or
write-only; field access is always
readable and writable.

这是一个对属性的胜利,因为您对访问有更细粒度的控制。

A property method may throw an
exception; field access never throws
an exception.

虽然这基本上是正确的,但您可以对未初始化的对象字段调用方法,并引发异常。

? A property cannot be passed as an
out or ref parameter to a method; a
field can.

公平的。

? A property method can take a long
time to execute; field access always
completes immediately.

这也需要很少的时间。

? If called multiple times in a row, a
property method may return a different
value each time; a field returns the
same value each time.

不是真的。您如何知道字段的值没有更改(可能由另一个线程更改)?

The System.DateTime class has a
readonly Now property that returns the
current date and time. Each time you
query this property, it will return a
different value. This is a mistake,
and Microsoft wishes that they could
fix the class by making Now a method
instead of a property.

如果是一个错误,那就是一个小错误。

? A property method may cause
observable side effects; field access
never does. In other words, a user of
a type should be able to set various
properties defined by a type in any
order he or she chooses without
noticing any different behavior in the
type.

公平的。

? A property method may require
additional memory or return a
reference to something that is not
actually part of the object's state,
so modifying the returned object has
no effect on the original object;
querying a field always returns a
reference to an object that is
guaranteed to be part of the original
object's state. Working with a
property that returns a copy can be
very confusing to developers, and this
characteristic is frequently not
documented.

对于Java的吸气剂和定位器来说,大部分的描述都是可以说的,我们在实践中已经有相当长一段时间没有出现这样的问题。

我认为大多数问题都可以通过更好的语法突出显示(即区分属性和字段)来解决,这样程序员就知道应该期待什么了。


早在2009年,这条建议就好像是在抱怨谁动了我的奶酪品种。今天,它几乎是可笑的过时了。好的。

一个非常重要的点是,许多答案似乎都是小题大做,但并没有完全直截了当地提到,这些所谓的属性"危险"是框架设计的一个有意的部分!好的。

是的,属性可以:好的。

  • 为getter和setter指定不同的访问修饰符。这是一个优于田野的优势。一个常见的模式是拥有一个公共的getter和一个受保护的或内部的setter,这是一种非常有用的继承技术,单靠字段是无法实现的。好的。

  • 引发异常。到目前为止,这仍然是最有效的验证方法之一,尤其是在使用涉及数据绑定概念的UI框架时。在处理字段时,要确保对象保持有效状态要困难得多。好的。

  • 执行需要很长时间。这里的有效比较是与方法的比较,方法需要同样长的时间,而不是字段。除了一位作者的个人偏好之外,没有为"方法是首选"这一陈述提供依据。好的。

  • 在后续执行时从getter返回不同的值。这几乎像是一个笑话,在这一点非常接近的地方,称赞refout参数与字段的优点,字段值在refout调用之后,几乎可以保证其值与以前的值不同,而且是不可预测的。好的。

    如果我们谈论的是没有传入耦合的单线程访问的具体情况(实际上是学术性的),我们很清楚,具有可见的状态变化的副作用是不好的属性设计,而且可能我的内存正在衰退,但我似乎无法回忆起使用DateTime.Now并期望每次都会出现相同的值。至少在任何情况下,他们都不会像假设的DateTime.Now()那样把事情搞砸。好的。

  • 引起可观察到的副作用——这当然正是属性最初被发明为语言特性的原因。微软自己的财产设计指导方针表明,setter顺序不应该重要,否则就意味着时间耦合。当然,您不能单独与字段实现时间耦合,但这仅仅是因为在执行某个方法之前,您根本不能单独与字段一起导致任何有意义的行为发生。好的。

    属性访问器实际上可以帮助防止某些类型的时间耦合,方法是在采取任何操作之前强制对象进入有效状态-例如,如果一个类有一个StartDate和一个EndDate,那么在StartDate可以强制StartDate之前设置EndDate。即使在多线程或异步环境中也是如此,包括事件驱动用户界面的明显示例。好的。

属性可以做的其他事情,其中字段不能包括:好的。

  • 延迟加载,防止初始化顺序错误的最有效方法之一。
  • 变更通知,这几乎是MVVM体系结构的整个基础。
  • 继承,例如定义一个抽象的TypeName,因此派生类可以提供关于它们自己的有趣但不变的元数据。
  • 拦截,多亏了以上。
  • 索引器,每个曾经使用过ComInterop和不可避免的Item(i)调用的人都会认识到这是一件美妙的事情。
  • 使用PropertyDescriptor,这对于创建设计器和一般的XAML框架是必不可少的。

李希特显然是一个多产的作家,他对CLR和C.Y.有很多了解,但我不得不说,他最初写这个建议的时候(我不确定是不是在他最近的修订中——我真诚地希望不是),他只是不想放弃旧习惯,并且很难接受C(C + +)的约定。好的。

我的意思是,他的"属性被认为是有害的"论点基本上归结为一句话:属性看起来像字段,但它们可能不像字段。这个声明的问题是,它不是真的,或者说最多是高度误导性的。属性看起来不像字段-至少,它们不应该看起来像字段。好的。

在C语言中有两个非常强的编码约定与其他clr语言共享的类似约定,如果不遵循这些约定,fxcop会对您大喊大叫:好的。

  • 字段应该始终是私有的,而不是公共的。
  • 字段应以camelcase形式声明。属性为Pascalcase。
  • 因此,对于Foo.Bar = 42是属性访问器还是字段访问器没有歧义。它是一个属性访问器,应该像其他任何方法一样被处理——它可能很慢,可能会抛出异常,等等。这就是抽象的本质——完全取决于声明类如何反应的自由裁量权。类设计器应该应用最不令人惊讶的原则,但是调用方不应该对属性进行任何假设,除非它在tin上做了它所说的事情。那是故意的。好的。

    属性的替代方法是随处可见的getter/setter方法。这就是Java的方法,从一开始就有争议。如果这是你的包就好了,但这不是我们在.NET阵营中的表现。我们尝试,至少在静态类型系统的范围内,避免福勒所说的句法噪声。我们不需要额外的括号、额外的get/set疣或额外的方法签名——如果我们能够避免它们而不损失任何清晰度,就不需要。好的。

    你想说什么就说什么,但是foo.Bar.Baz = quux.Answers[42]总是比foo.getBar().setBaz(quux.getAnswers().getItem(42))更容易阅读。当你一天读成千上万行这样的文章时,会有不同。好的。

    (如果您对上述段落的自然反应是说,"Sure it's hard to read,but it will be easy if you split it up in multiple lines",then I'm sorry to say that you have completely missed the point.)好的。好啊。


    我没有读过这本书,你也没有引用其中你不懂的部分,所以我只能猜测。

    有些人不喜欢属性,因为它们可以使您的代码做一些令人惊讶的事情。

    如果我键入Foo.Bar,阅读它的人通常会期望它只是访问foo类的成员字段。它是一种廉价的、几乎免费的操作,而且是确定性的。我可以一遍又一遍地调用它,每次都得到相同的结果。

    相反,对于属性,它可能实际上是一个函数调用。它可能是一个无限循环。它可能会打开数据库连接。每次访问它时,它可能返回不同的值。

    这也是为什么莱纳斯讨厌C++的类似论点。你的代码会让读者感到惊讶。他讨厌运算符重载:a + b不一定意味着简单的加法。这可能意味着一些非常复杂的操作,就像C属性一样。可能有副作用。它可以做任何事情。

    老实说,我认为这是一个薄弱的论点。两种语言都充满了这样的东西。(我们是否也应该避免在C中的运算符重载?毕竟,这里也可以使用相同的参数)

    属性允许抽象。我们可以假装某个东西是一个规则的字段,并把它当作一个字段来使用,而不必担心幕后发生了什么。

    这通常被认为是一件好事,但它显然依赖于程序员编写有意义的抽象。属性的行为应类似于字段。他们不应该有副作用,不应该进行昂贵或不安全的操作。我们希望能够把它们看作是领域。

    然而,我有另一个理由发现它们不够完美。它们不能通过引用传递给其他函数。

    字段可以作为ref传递,允许被调用函数直接访问它。函数可以作为委托传递,允许被调用函数直接访问它。

    属性…不能。

    那太糟糕了。

    但这并不意味着属性是邪恶的或者不应该被使用。在很多方面,他们都很棒。


    我看不出你为什么一般不使用属性的任何原因。

    C 3+中的自动属性只简化了一点语法(LA语法糖)。


    这只是一个人的意见。我读了不少C书,还没有看到其他人说"不要使用属性"。

    我个人认为财产是C最好的东西之一。它们允许您通过您喜欢的任何机制暴露状态。您可以在第一次使用某个值时懒散地实例化它,还可以在设置值等方面进行验证。在使用和写入这些值时,我只是将属性视为setter和getter,这是一种更好的语法。

    关于属性的警告,有两个。一种可能是滥用财产,另一种可能是微妙的。

    首先,属性是方法的类型。如果在属性中放置复杂的逻辑,可能会令人惊讶,因为类的大多数用户都希望该属性相当轻。

    例如。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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(); }
       }
    }

    我发现使用这些案例的方法有助于区分。

    其次,更重要的是,如果在调试时对属性进行评估,那么这些属性可能会产生副作用。

    假设你有这样的财产:

    1
    2
    3
    4
    5
    6
    7
    8
    public int Result
    {
       get
       {
           m_numberQueries++;
           return m_result;
       }
    }

    现在假设您有一个异常,当查询太多时会发生这种异常。猜猜当您开始调试并滚动调试器中的属性时会发生什么?坏事。避免这样做!查看属性会更改程序的状态。

    这些是我唯一的警告。我认为房产的好处远远大于问题。


    我情不自禁地挑出杰弗里·里克特观点的细节:

    A property may be read-only or write-only; field access is always readable and writable.

    错误:字段不能标记为只读,因此只有对象的构造函数才能对其进行写入。

    A property method may throw an exception; field access never throws an exception.

    错误:类的实现可以将字段的访问修饰符从public更改为private。在运行时尝试读取私有字段总是会导致异常。


    这个原因一定是在一个非常具体的背景下给出的。通常情况下是相反的-建议使用属性,因为它们给您一个抽象的级别,使您能够在不影响其客户机的情况下更改类的行为…


    我不同意杰弗里·里克特的观点,但我猜他为什么不喜欢房产(我还没读过他的书)。

    尽管属性与方法(实现方面)类似,但作为类的用户,我希望其属性的行为"或多或少"类似于公共字段,例如:

    • 属性getter/setter中没有耗时的操作
    • 属性getter没有副作用(多次调用它,不会更改结果)

    不幸的是,我看到了一些不按这种方式运行的属性。但问题不在于属性本身,而在于实现它们的人员。所以这只需要一些教育。


    你不应该避免使用它们,但是你应该根据其他贡献者给出的理由,在有资格和谨慎的情况下使用它们。

    我曾经看到一个名为"客户"的属性,它在内部打开了对数据库的进程外调用,并读取了客户列表。客户端代码有一个"for(int i to customers.count)",这导致在每次迭代时对数据库进行单独调用,并访问选定的客户。这是一个令人震惊的例子,它证明了保持该属性非常轻的原则——很少超过内部字段访问。

    使用属性的一个参数是,它们允许您验证正在设置的值。另一个原因是属性的值可能是派生值,而不是单个字段,比如totalvalue=amount*quantity。


    有时我会考虑不使用属性,那就是编写.NET Compact框架代码。CF JIT编译器不执行与桌面JIT编译器相同的优化,也不优化简单属性访问器,因此在这种情况下,添加简单属性会导致使用公共字段时的少量代码膨胀。通常这不会是一个问题,但几乎总是在紧凑的框架世界中,您面临着严格的内存限制,因此,即使像这样的微小节省也很重要。


    该参数假定属性是坏的,因为它们看起来像字段,但可以执行令人惊讶的操作。此假设因.NET程序员的预期而失效:

    属性看起来不像字段。字段看起来像属性。

    ? A property method may throw an exception; field access never throws an exception.

    因此,字段就像一个保证永远不会引发异常的属性。

    ? A property cannot be passed as an out or ref parameter to a method; a field can.

    因此,字段就像一个属性,但它有附加的功能:传递到ref/out接受方法。

    ? A property method can take a long time to execute; field access always completes immediately. [...]

    所以,一个场就像一个快速属性。

    ? If called multiple times in a row, a property method may return a different value each time; a field returns the same value each time. The System.DateTime class has a readonly Now property that returns the current date and time.

    因此,字段就像一个属性,除非字段被设置为不同的值,否则它保证返回相同的值。

    ? A property method may cause observable side effects; field access never does.

    同样,字段是一种保证不会这样做的属性。

    ? A property method may require additional memory or return a reference to something that is not actually part of the object's state, so modifying the returned object has no effect on the original object; querying a field always returns a reference to an object that is guaranteed to be part of the original object's state. Working with a property that returns a copy can be very confusing to developers, and this characteristic is frequently not documented.

    这一点可能会令人惊讶,但并不是因为这是一种财产。相反,几乎没有人返回属性中的可变副本,因此0.1%的情况令人惊讶。


    我个人只在创建简单的get/set方法时使用属性。当涉及到复杂的数据结构时,我会避开它。


    调用方法而不是属性大大降低了调用代码的可读性。例如,在j*中,使用ADO.NET是一个噩梦,因为Java不支持属性和索引器(它们本质上是带参数的属性)。生成的代码非常难看,到处都是空的圆括号方法调用。

    对属性和索引器的支持是C语言在Java之上的基本优点之一。