为什么要避免在C#中使用属性?


102

杰弗里·里希特(Jeffrey Richter)在他的出色著作《 CLR Via C#》中说,他不喜欢属性,建议不要使用它们。他给出了一些理由,但我不太了解。谁能向我解释为什么或不应该使用属性?在具有自动属性的C#3.0中,这会更改吗?

作为参考,我添加了Jeffrey Richter的意见:

•属性可以是只读或只写的;现场访问始终是可读写的。如果定义属性,则最好同时提供get和set访问器方法。

•属性方法可能会引发异常;现场访问永远不会引发异常。

•属性不能作为out或ref参数传递给方法;田野可以。例如,以下代码将无法编译:

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);
   }
}

• A property method can take a long time to execute; field access always completes immediately. A common reason to use properties is to perform thread synchronization, which can stop the thread forever, and therefore, a property should not be used if thread synchronization is required. In that situation, a method is preferred. Also, if your class can be accessed remotely (for example, your class is derived from System.MashalByRefObject), calling the property method will be very slow, and therefore, a method is preferred to a property. In my opinion, classes derived from MarshalByRefObject should never use properties.

•如果连续调用多次,则属性方法每次可能返回不同的值;字段每次都返回相同的值。System.DateTime类具有只读的Now属性,该属性返回当前日期和时间。每次查询此属性时,它将返回一个不同的值。这是一个错误,Microsoft希望他们可以通过将Now作为方法而不是属性来修复该类。

•属性方法可能会导致明显的副作用;现场访问从未如此。换句话说,类型的用户应该能够以他或她选择的任何顺序设置由类型定义的各种属性,而无需注意该类型的任何不同行为。

•属性方法可能需要额外的内存或返回对实际上不是对象状态一部分的内容的引用,因此修改返回的对象对原始对象没有影响;查询字段总是返回对对象的引用,该对象保证是原始对象状态的一部分。使用返回副本的属性可能会使开发人员感到困惑,并且通常不会记录此特征。


11
我拥有“通过C#编写CLR”的第三版,在第242页上,里希特先生说他个人不喜欢属性,但是他从不建议不使用它们。请引用阅读本书的书的版本和页码。
kirk.burleson 2012年

Answers:


173

Jeff不喜欢属性的原因是因为它们看起来像字段-因此,不了解差异的开发人员将把它们视为字段,并认为它们执行起来便宜。

我个人在这一点上与他不同意-我发现属性使客户端代码比等效的方法调用更易于阅读。我同意开发人员需要知道属性基本上是伪装的方法-但我认为教育开发人员比使方法更难于阅读代码要好。(特别是,在看到在同一条语句中调用了多个getter和setter的Java代码之后,我知道等效的C#代码将更容易阅读。理论上,Demeter定律非常好,但有时foo.Name.Length确实是正确使用的东西...)

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

这有点像反对使用扩展方法的论点-我可以理解其推理,但是在我看来,实际的好处(当很少使用时)胜过缺点。


非常感谢您的回答!关于反对使用扩展方法的论点:您是在谈论Jeffrey Richter的话题还是摘要?
abatishchev 2009年

@abatishchev:这只是关于扩展方法的一般要点。它与属性没有直接关系。
乔恩·斯基特

我走得更远。除了性能方面,我看不出为什么程序员应该知道字段或属性。他应该将其视为指定对象实例的STATE的实例属性,并且对象实现应注意对其状态的所有修改(在类的约定之内)。因此,如果重新设计了类的实现,则属性在某些情况下也可能是字段,在其他情况下也可能成为字段。然后,在字段或属性之间做出决定是国家需要保持多少保护的问题。
TheBlastOne

1
@PatrickFromberg:显然,您已经错过了大量使用只读字段的代码。没有什么可说的是属性暗示可变性。我经常有支持只读属性的只读字段-您认为这是一件坏事吗?
乔恩·斯基特

1
@Patrick:不,它带来了将API与实现分离的好处-您以后可以更改从字段计算属性的方式(例如,从一个字段计算两个相关属性)。
Jon Skeet 2014年

34

好吧,让他的论点一一列举:

属性可以是只读或只写的;现场访问始终是可读写的。

这对物业而言是一个胜利,因为您可以更精细地控制访问。

属性方法可能会引发异常。现场访问永远不会引发异常。

尽管大多数情况都是这样,但您可以很好地在未初始化的对象字段上调用方法,并引发异常。

•属性不能作为out或ref参数传递给方法;田野可以。

公平。

•属性方法可能需要很长时间才能执行;现场访问总是立即完成。

它也可以花费很少的时间。

•如果连续调用多次,则属性方法每次可能返回不同的值;字段每次都返回相同的值。

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

System.DateTime类具有只读的Now属性,该属性返回当前日期和时间。每次查询此属性时,它将返回一个不同的值。这是一个错误,Microsoft希望他们可以通过将Now作为方法而不是属性来修复该类。

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

•属性方法可能会导致明显的副作用;现场访问从未如此。换句话说,类型的用户应该能够以他或她选择的任何顺序设置由类型定义的各种属性,而无需注意该类型的任何不同行为。

公平。

•属性方法可能需要额外的内存或返回对实际上不是对象状态一部分的内容的引用,因此修改返回的对象对原始对象没有影响;查询字段总是返回对对象的引用,该对象保证是原始对象状态的一部分。使用返回副本的属性可能会使开发人员感到困惑,并且通常不会记录此特征。

多数抗议活动也可以说是针对Java的getter和setter的,而且我们在相当一段时间内都没有出现此类问题。

我认为大多数问题都可以通过更好地突出显示语法来解决(即,将属性与字段区分开),以便程序员知道期望什么。


3
“可以通过突出显示语法来解决问题”:您多久使用一次公共字段?私有字段通常具有不同的样式,例如_field。甚至只是小写field
Steven Jeuris 2011年

18

我还没有读过这本书,也没有引用过您不理解的部分,所以我不得不猜测。

有些人不喜欢属性,因为它们会使您的代码执行令人惊讶的事情。

如果输入Foo.Bar,则通常人们会期望它只是在访问Foo类的成员字段。这是一种廉价的,几乎免费的操作,并且具有确定性。我可以一遍又一遍地调用它,每次都得到相同的结果。

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

这与为什么Linus讨厌C ++相似。您的代码会使读者感到惊讶。他讨厌运算符重载:a + b不一定意味着简单加法。就像C#属性一样,这可能意味着一些非常复杂的操作。它可能有副作用。它可能会做任何事情。

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

属性允许抽象。我们可以假装某事物是常规字段,并像对待它一样使用它,而不必担心幕后情况。

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

但是,我还有另一个理由发现它们并不完美。它们不能通过引用传递给其他功能。

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

属性...不能。

糟透了

但这并不意味着属性是邪恶的,也不应该使用。在许多方面,它们都很棒。


3
我的观点是,您不应该假设它是一个以-开头的字段,因为如果您从任何其他类中调用它,则无论如何都不应访问非常量字段,因为它们应该是私有的。(也有命名约定通知您。)
Jon Skeet

1
是的,我同意。该论点似乎可以归结为“我习惯于仅将语法用于字段。因此,将其扩展为涵盖其他情况是不好的”。显而易见的答案是“好吧,那就适应它,然后再覆盖其他情况,那也不错”。;)
杰夫

我希望.net语言能够提供一种标准的方法,使类可以将属性公开为ref参数;具有特殊属性Foo的表单的成员(例如)void Foo<T>(ActionByRef<Point,T> proc, ref T param),并使编译器转换thing.Foo.X+=someVar;Foo((ref Point it, ref int param)=>it.X += param, ref someVar);。由于lambda是静态委托,因此不需要闭包,并且对象的用户将能够使用支持该属性的任何存储位置作为真实ref参数(并可以将其作为ref参数传递给其他方法)。
2012年

手工写出lambda会产生看起来很讨厌的源代码,但这就是为什么让编译器执行转换会有所帮助。公开“ callback-by-ref”属性的类的代码将非常合理。该代码在调用方是唯一难看的(缺少转换)。
2012年

请注意,属性是伪装为成员的函数调用,因此与传递类的公共函数一样,它不能通过引用传递。如果您打算这样做,可以随时使用public成员。属性是一个智能成员,它可以为传递给set的无效数据引发异常。
Diceyus

17

早在2009年,这个建议就好像是“ 谁动了我的奶酪”品种的肚子。今天,它几乎已经过时了。

一个非常重要的观点是,许多答案似乎都tip之以鼻,但并未完全解决,这些所谓的“危险”是框架设计的故意组成部分!

是的,属性可以:

  • 为getter和setter指定不同的访问修饰符。与领域相比,这是一个优势。常见的模式是拥有一个公共获取程序和一个受保护的内部的 setter,这是一种非常有用的继承技术,仅靠字段是无法实现的。

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

  • 需要很长时间才能执行。这里的有效比较是使用方法,这些方法同样需要很长时间,而不是字段。除了一个作者的个人喜好外,没有给出“首选方法”这一说法的依据。

  • 在后续执行中从其getter返回不同的值。这几乎像是在开玩笑地讲笑话,如此夸张地讲解了ref/ out参数与字段的优点,在ref/ out调用之后,其字段的值几乎可以保证与之前的值不同,并且这是不可预测的。

    如果我们在谈论没有传入耦合的单线程访问的特定情况(并且实际上是学术上的情况),那么众所周知,具有可见状态变化的副作用是不好的属性设计,也许我的记忆是褪色,但我似乎无法想起人们使用DateTime.Now并期望每次都会产生相同价值的任何例子。至少在任何情况下他们都不会像假设一样糟糕DateTime.Now()

  • 引起明显的副作用-当然,这恰恰是首先将属性作为语言功能而被发明的原因。Microsoft自己的属性设计指南指出,设置程序的顺序无关紧要,否则将意味着时间上的耦合。当然,您无法单独实现与字段的时间耦合,但这仅是因为您无法在执行某种方法之前根本不对字段进行任何有意义的行为。

    属性访问器实际上可以通过在执行任何操作之前将对象强制为有效状态来帮助防止某些类型的时间耦合-例如,如果类具有a StartDate和an EndDate,则将the设置为EndDatebefore StartDate也可以强制StartDate返回。即使在多线程或异步环境中(包括事件驱动的用户界面的明显示例),也是如此。

属性可以做的其他事情不能包括:

  • 延迟加载,这是防止初始化顺序错误的最有效方法之一。
  • 变更通知,几乎是MVVM体系结构的全部基础。
  • 继承(例如,定义一个抽象类TypeName派生类)可以提供有趣但始终不变的元数据。
  • 拦截,由于以上。
  • 索引器,曾经与COM interop一起工作并且不可避免地产生大量Item(i)呼叫的每个人都将意识到这是一件了不起的事情。
  • 使用PropertyDescriptor,这对于创建设计器和XAML框架通常是必不可少的。

里希特显然是一位多产的作家,并且对CLR和C#了解很多,但是我不得不说,似乎是他最初编写此建议时所想的(我不确定这是否在他的最新修订版中,我希望不是)他只是不想放弃旧的习惯,并且在接受C#的约定方面遇到困难(例如,相对于C ++)。

我的意思是,他的“被认为有害的属性”论点本质上可以归结为一个陈述:属性看起来像字段,但它们可能不像字段那样工作。声明的问题在于,它不是真的,或者充其量是极具误导性的。属性并不像场-至少,他们不应该看起来像场。

C#中有两个非常强大的编码约定,其他CLR语言也有类似的约定,如果您不遵守,则FXCop会大喊大叫:

  1. 字段应该永远是私有的,永远不要公开。
  2. 字段应在camelCase中声明。属性是PascalCase。

因此,对于Foo.Bar = 42属性访问器还是字段访问器没有任何歧义。它是一个属性访问器,应该像其他任何方法一样对待-它可能很慢,可能会引发异常等。这就是Abstraction的本质-完全由声明类决定如何做出反应。班级设计人员应该应用最少惊讶的原则,但是调用者不应对某个属性承担任何责任,除非该属性按照其提示行事。那是故意的。

属性的替代方法是无处不在的getter / setter方法。那是Java的方法,并且从一开始引起争议。没关系,那就好了,但这不是我们在.NET阵营中的表现。我们至少在静态类型系统的范围内尝试避免Fowler所谓的语法噪声。我们不需要额外的括号,额外的get/ set疣或额外的方法签名-如果我们可以在不造成任何损失的情况下避免使用它们,则不希望这样做。

随便说什么,但foo.Bar.Baz = quux.Answers[42]总是比看起来容易得多foo.getBar().setBaz(quux.getAnswers().getItem(42))。当您每天阅读数千行时,就会有所作为。

(如果您对以上段落的自然回答是:“确保很难阅读,但是将其分成多行会更容易”,那么我很遗憾地说,您完全错过了重点)


11

我看不出您一般不应该使用属性的任何原因。

C#3+中的自动属性仅稍微简化了语法(一种语法)。


请阅读那本书的第215-216页。我确定您知道谁是杰弗里·里希特(Jeffrey Richter)!
Quan Mai的

我已经读过(通过C#编写的CLR,第二版),不同意他的立场,也没有看到不使用属性的真正原因!
abatishchev 2009年

这是一本好书,但我目前不同意作者的观点。
康斯坦丁·塔尔库斯

作者建议不要使用“属性”的确切位置在哪里?在p.215-217上找不到任何内容
康斯坦丁·塔尔库斯

我在问题中添加了杰弗里的意见:)。您可以在印刷版的215-216页中找到它:)
Quan Mai 2009年

9

这只是一个人的意见。我已经阅读了很多C#书,但还没有看到其他人说“不要使用属性”。

我个人认为属性是C#最好的东西之一。它们允许您通过任何喜欢的机制公开状态。您可以在第一次使用某些东西时懒惰地实例化,并且可以在设置值等方面进行验证。在使用和编写它们时,我只是将属性视为设置器和获取器,这是一种更好的语法。

至于带有属性的警告,有几个。一种可能是滥用属性,另一种可能是微妙的。

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

例如

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(); }
   }
}

我发现在这些情况下使用方法有助于区分。

其次,更重要的是,如果在调试时评估属性,则可能会产生副作用。

假设您有一些类似的属性:

public int Result 
{ 
   get 
   { 
       m_numberQueries++; 
       return m_result; 
   } 
}

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

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


6

必须在非常特定的环境中给出该原因。通常是相反的方式-建议使用属性,因为它们为您提供了一个抽象级别,使您可以更改类的行为而不影响其客户端...


+1隐式地强调了客户与类属性之间的“合同”的重要性。
TheBlastOne

6

我不禁选择杰弗里·里希特的观点的细节:

属性可以是只读或只写的;现场访问始终是可读写的。

错误:字段可以标记为只读,因此只有对象的构造函数可以写入它们。

属性方法可能会引发异常。现场访问永远不会引发异常。

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


5

我不同意杰弗里·里希特(Jeffrey Richter)的观点,但我可以猜出为什么他不喜欢房地产(我还没有看过他的书)。

即使属性就像方法(在实现方面)一样,作为类的用户,我希望其属性“或多或少”像公共字段一样,例如:

  • 属性获取器/设置器内部没有耗时的操作
  • 属性获取器没有副作用(多次调用,不会改变结果)

不幸的是,我看到的属性没有那样表现。但是问题不是财产本身,而是实施这些财产的人。因此,这仅需要一些教育。


1
+1,因为它突出了干净合同的重要性。具有看似只读的属性,该属性通常在每个引用上返回一个不同的值,这是一个readwrite属性-它只是不根据自己的值进行写入,而是根据其他实例变量(直接或间接)进行写入。那不是不好的风格,但是必须记录在案(或者是合同的隐含部分)。
TheBlastOne

4

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


4

您不应该避免使用它们,但出于其他贡献者的原因,应谨慎使用它们。

我曾经看到一个名为“客户”之类的属性,该属性在内部打开了对数据库的进程外调用并读取了客户列表。客户端代码有一个“ for(int i to Customer.Count)”,它在每次迭代中以及对所选Customer的访问上引起对数据库的单独调用。那是一个令人震惊的例子,展示了保持财产非常轻的原则-很少是内部现场访问。

使用属性的一个论点是,它们允许您验证所设置的值。另一个是属性的值可能是派生值,而不是单个字段,例如TotalValue = amount *数量。


3

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


3

该论点假定属性是不好的,因为它们看起来像字段,但可以执行令人惊讶的操作。.NET程序员的预期行为使该假设无效:

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

•属性方法可能会引发异常;现场访问永远不会引发异常。

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

•属性不能作为out或ref参数传递给方法;田野可以。

因此,字段就像一个属性,但是它具有其他功能:传递给 ref / out接受方法。

•属性方法可能需要很长时间才能执行;现场访问总是立即完成。[...]

因此,字段就像快速属性。

•如果连续调用多次,则属性方法每次可能返回不同的值;字段每次都返回相同的值。System.DateTime类具有只读的Now属性,该属性返回当前日期和时间。

因此,字段就像可以保证返回相同值的属性,除非该字段设置为其他值。

•属性方法可能会导致明显的副作用;现场访问从未如此。

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

•属性方法可能需要额外的内存或返回对实际上不是对象状态一部分的内容的引用,因此修改返回的对象对原始对象没有影响;查询字段总是返回对对象的引用,该对象保证是原始对象状态的一部分。使用返回副本的属性可能会使开发人员感到困惑,并且通常不会记录此特征。

这可能令人惊讶,但不是因为它是执行此操作的属性。相反,几乎没有人返回属性中的可变副本,因此0.1%的情况令人惊讶。


0

调用方法而不是属性会大大降低调用代码的可读性。例如,在J#中,使用ADO.NET是一场噩梦,因为Java不支持属性和索引器(本质上是带有参数的属性)。结果代码非常丑陋,到处都是空括号方法。

对属性和索引器的支持是C#相对于Java的基本优势之一。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.