使用静态属性或方法在面向对象编程中有哪些不方便之处?

3

我需要解释一下为什么我不使用静态方法/属性。例如,

String s=String.Empty;

这个属性(属于.Net框架)是否有误?应该是这样的吗?
String s= new EmptySting();

或者

IEmptyStringFactory factory=new EmptyStringFactory();

String s= factory.Create();

这些表达式是为了说明有时需要这种意思。我的问题是在什么情况下应该使用静态,什么情况下不应该? - Sessiz Saat
9个回答

9

为什么每次想使用空字符串时都要创建一个新对象呢?基本上,空字符串是一个单例对象。

正如威尔所说,静态变量在测试方面确实可能会有问题,但这并不意味着您应该在所有地方都使用静态变量。

(个人而言,我更喜欢使用""而不是string.Empty,但这是一个已经被讨论过的话题。)


好的。抱歉,但是...嗯...什么?“that doesn't mean you should statics everywhere”似乎缺少一个动词。你能澄清一下你的回答吗?不想太过刻意,但也许你应该多加一点关于静态属性的内容,而不仅仅是String.Empty。这个回答似乎有点不完整,几乎值得被踩——虽然这不会影响你的声誉,但只是为了让你保持诚实 :) - Erich Mirabal

7

我认为使用静态内容最糟糕的一点是可能导致类之间紧密耦合。例如在 System.Web.Abstractions 推出前使用 ASP.NET 时,这样会使所有类变得难以测试,可能会更容易引起系统范围内的错误。


2

你的三个不同示例的语义非常不同。我会尝试按照我的实践方式进行分解。


String s=String.Empty;

这是一个单例模式。当你希望确保某些东西只有一个实例时,可以使用它。在这种情况下,由于字符串是不可变的,只需要一个“空”字符串即可。不要滥用单例模式,因为它们很难测试。但是当它们有意义时,它们非常强大。


String s= new EmptySting();

这是您的标准构造函数。尽可能使用此构造函数。只有当单例模式的情况非常压倒性时,才需要重构为单例模式。在string.Empty的情况下,使用单例非常合理,因为字符串的状态不能被引用类更改。


IEmptyStringFactory factory=new EmptyStringFactory();
String s= factory.Create();

实例工厂和静态工厂像单例一样,应该谨慎使用。通常情况下,它们应该在类的构造复杂且依赖于多个步骤和可能状态时使用。
如果对象的构造依赖于调用者可能不知道的状态,则应使用实例工厂(如您的示例中所示)。当构造复杂但调用者知道会影响构造条件时,则应使用静态工厂(例如 StringFactory.CreateEmpty()StringFactory.Create("foo"))。然而,在字符串的情况下,构造足够简单,使用工厂就会强行解决没有问题的问题。

我从不主张除了在性能优化场景下使用单例。软件设计充满了需求变更。当需求发生变化时,你认为现在只需要一个的东西很快就会变成两个或更多。从单例模式重构代码到普通实例模式是一项重大的变更,并且随着项目规模的增大而变得更加糟糕。 - clemahieu
@clemahieu,我认为单例模式是最糟糕的模式之一(考虑到它经常被错误地使用和被频繁地错误使用)。然而,“永远不”有点教条主义。在某些情况下,使用单例并不是预先优化。其中之一是“Empty”或“Null Object”模式。另一个是当您必须确保一个类的实例只存在一个时。我认为我同意您的观点,但“永远不”是一个过于强大的词。 - Michael Meadows

2
好的,对于 String.Empty 来说,它更像是一个常量(类似于 Math.PI 或者 Math.E),是为该类型定义的。创建一个子类来表示一个特定值通常是不好的。
至于你另外一个(主要)问题中关于它们为何“不方便”的部分:
我发现只有当静态属性和方法被滥用以创建更加功能化的解决方案而不是采用 C# 所意味的面向对象方法时,它们才会变得不方便。
我的大多数静态成员要么就是如上所述的常量,要么就是工厂方法(例如 Int.TryParse)。
如果这个类有很多静态属性或方法,用于定义由该类所表示的“对象”,那么我认为这通常是不好的设计。
其中一件让我感到困扰的事情是,有时候这些静态方法/属性过于与某种做事方式绑定,而没有提供一种轻松的方式来创建实例,从而提供易于覆盖行为的方式。例如,想象一下,你想在计算数学时使用角度而不是弧度。由于 Math 全都是静态的,你无法这样做,而必须每次都进行转换。如果 Math 是基于实例的,你可以创建一个新的 Math 对象,该对象默认为你想要的弧度或角度,并且仍然可以具有典型行为的静态属性。
例如,我希望我能这样说:
Math mD = new Math(AngleMode.Degrees); // ooooh, use one with degrees instead
double x = mD.Sin(angleInDegrees);

但我必须写成这样:
double x = Math.Sin(angleInDegrees * Math.PI / 180);

当然,你可以编写扩展方法和常量来进行转换,但是你明白我的意思吧。这可能不是最好的例子,但我希望它能传达不能使用默认变体方法的问题。它创建了一个功能性构造并与通常的面向对象方法不同。(作为旁注,在这个例子中,我会为每种模式设置一个静态属性。在我看来,这将是静态属性的一个不错的用法)。

我认为您可能指的是“更加过程化的解决方案”而不是“更加函数式的解决方案”。静态方法和属性类似于过程式编程范例中的“对结构体进行操作的方法”。而函数式范例则依赖于集合上的匹配操作。 - Michael Meadows
是的,我所指的“基于函数”的意思是指过程。而不是F#中的“函数式编程”。 - Erich Mirabal

1
通常来说,创建一个新的空字符串是一个不好的主意 - 这会在堆上创建额外的对象,因此对于垃圾收集器来说需要额外的工作。当你想要一个空字符串时,你应该总是使用 String.Empty 或 "",因为它们是对现有对象的引用。

编译器可以轻松地对此进行字符串池化。我猜它已经这样做了。 - clemahieu
但是,如果您使用String()创建一个新字符串,那么(如果我没记错的话)您需要显式地使用String.Intern进行内部化,以获取String.Empty和""所使用的“共享”版本。 - thecoop

1

一般来说,静态的目的是确保程序中只有一个静态“东西”的实例。

  • 静态字段在类型的所有实例中保持相同的值
  • 静态方法和属性不需要实例即可调用
  • 静态类型只能包含静态方法/属性/字段

当您知道创建的“东西”在程序的生命周期内永远不会改变时,静态非常有用。例如,在您的示例中,System.String定义了一个私有静态字段来存储空字符串,该字符串仅分配一次,并通过静态属性公开。

正如提到的那样,静态存在测试问题。例如,很难模拟静态类型,因为它们无法实例化或派生。还很难将模拟引入某些静态方法,因为它们使用的字段也必须是静态的。(您可以使用静态setter属性来解决此问题,但我个人尽量避免这样做,因为它通常会破坏封装性)。

大多数情况下,使用静态是可以的。您需要根据程序的复杂性决定何时进行使用静态和实例实体的权衡。


+1,这个回答不应该被踩。显然,“何时使用静态成员/类”的概念是那种容易引起争议的问题之一,人们往往会攻击任何稍微持有不同意见的人。我并不完全同意你对于使用静态成员/类的“可接受程度”,但你所说的话并没有错误之处。 - Michael Meadows

1
在纯粹的面向对象方法中,静态方法打破了面向对象范式,因为你将实际数据附加到数据定义上。类是一组符合语义的对象的定义。就像数学集合中包含一个或零个元素一样,可以有只包含一个或零个可能状态的类。
共享公共对象并允许多个操作者对其状态进行操作的方法是传递引用。
静态方法的主要问题来自于,如果将来需要两个怎么办?我们正在编写计算机程序,人们会认为,如果我们可以制作一些东西,那么我们应该能够非常简单地制作两个,但使用静态方法则不是这种情况。将某些内容从静态状态更改为普通实例状态需要完全重写相关的类。
我可能会假设我只想永远使用一个SqlConnection池,但现在如果我想要一个高优先级池和一个低优先级池怎么办?如果连接池是实例化而不是静态的,则解决方案将很简单,否则我必须将池与连接实例化耦合。我最好希望库编写者有预见性,否则我必须重新实现池。
编辑: 单一继承语言中的静态方法是提供代码重用的一种解决方法。通常,如果有方法想要在不同的类之间共享通用代码,您可以通过多重继承或混合来实现。而单一继承语言则强制您调用静态方法;无法使用带状态的多个抽象类。

1

使用静态方法有以下缺点:

  1. 静态方法不允许扩展方法。
  2. 在创建第一个实例之前会自动调用静态构造函数以初始化类(当然取决于被调用的静态类)。
  3. 静态类数据在执行范围的整个生命周期内存在,这浪费了内存。

使用静态方法的原因

  1. 对于辅助方法来说,静态方法很好,因为您不想创建非静态类的本地副本,仅调用单个辅助方法。
  2. 额,静态类使单例模式成为可能。

0

从面向场景的设计角度,选择静态方法还是实例方法的标准应该是:如果可以在不创建类的实例的情况下调用方法,则将其设置为静态方法。否则,请将其设置为实例方法。第一种选项使调用成为一次性过程,并避免 .ctor 调用。

这里还有另一个有用的标准,即责任是否在正确的位置上。例如,您有一个帐户类。假设您需要实现货币转换功能,例如从美元到欧元。您会将它作为 Account 类的成员吗?account.ConvertTo(Currency.Euro)? 还是您要创建一个封装该责任的不同类?CurrencyConverter.Convert(account, Currency.Euro)? 对我来说,后者更好,因为它在不同类中封装了责任,而在前者中,我会将货币转换知识分散到不同的账户中。


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