原文链接:Static Methods will Shock You

声明:本文与右侧相关图书《深入理解C#(第2版)》二者之间没有直接关系,之所以选中此书作为相关图书,是因为二者有个共同特点【Depth】,即对事物的深入探查(再说右边空一块儿也不美观,呵呵)。


坦率地说,我不喜欢静态方法。它们让我坐立不安。在面向对象的宇宙中,那些静态方法是反物质(anti-matter)。

它们并不坏,但要是使用不当,它们会变得很危险。

静态方法何时是有益的

当使用静态方法或变量时只有两种情况不令人憎恶。

  1. 声明一个真正的全局常量,而非全局变量。一个全局常量。例如:Math.PI。实际说来,这是一个合理的简写,宇宙有一个实例,而且这个宇宙单例(singleton)包含一个数学概念单例(singleton),其中有一个不会更改的属性PI(π,圆周率)。对我们而言这个概念似乎很奇怪,因为我们已经习惯于在面向对象职责的上下文中不去思考PI。如果我们设计过一些奇怪的游戏,其中有若干拥有不同数学概念和常量的备选宇宙,那么前面提到的情形会变得更明显。
  2. 对象创建。 静态方法是一种有价值且有效的对象创建方法。重载接受不同参数的构造函数的意图并不非常清晰,而通常使用静态构造函数替换它们会使意图更加清晰。
public void BlogEntry(string title, string contents);
public void BlogEntry(string fileName);

当按以下形式编码时,代码意图会更加清晰

public static BlogEntry FromTitleAndContents(string title, string contents);
public static BlogEntry FromFile(string file);

在使用中,它会变得格外明白

BlogEntry newEntry = new BlogEntry("Hello World", "I like to write programs");
BlogEntry newEntry = new BlogEntry("helloworld.xml");
// vs.
BlogEntry newEntry = BlogEntry.FromTitleAndContents("Hello World", "I like to write programs");
BlogEntry newEntry = BlogEntry.FromFile("helloworld.xml");

静态方法何时是有害的

除了那两种用法以外。在我看来,任何其他用法都是令人憎恶的。(我想这里我会略过C#扩展方法extension methods),因为在合适的条件下它们会非常有用,但我保留在未来作出判断的权利。)

静态方法通常预示着一个”无家可归“(不知所属)的方法。它在那里袖手旁观,试图属于它所在的那个类,但是它实际上不属于那里,因为它没有使用那个类的内部状态。当我们从单一职责原则(Single Responsibility Principle,缩写为SRP)的角度来审视我们的类时,一个静态方法通常是违反单一职责原则的,因为静态方法往往会拥有一个与其附属类不同的职责。

一种方式是将静态方法想象为全局处理过程(global procedures)。从本质上讲,可以在任何地方的任意位置调用一个静态方法。静态方法仅仅是伪装成某个类的一部分,那个类实际上只是被作为通过某种逻辑分组来组织该方法的”标签(tag)“使用。我之所以从这些方面着眼于静态方法,是因为创建全局处理过程与面向对象设计截然相反。

静态方法的另一主要问题是可测试性(testability)。当构建软件时,可测试性是件大事。测试静态方法的难度是众所周知的,尤其是当它们创建具体类的新实例时。如果你曾在遗留代码上工作,并试图为一个静态方法编写一个单元测试,那么你就会懂我的痛。

静态方法也不是多态的。如果你在某个类上创建了一个静态方法,那么无法重写其行为。你被那个实现的一个硬编码引用卡住而动弹不得。

最终,静态方法增加了应用程序的复杂度。应用程序中的静态方法越多,在那个应用程序上工作的程序员须要了解的东西也就越多。当在某个类中使用实例方法时,拥有该对象的实例将允许程序员来决定所有可以采取的行动。当使用静态方法时,程序员必须知晓那些甚至可能都不在他正在工作的对象上,但用于操纵该对象的秘密方法。让我们来看一个常见的例子:Date(日期)类和DateUtilities(日期实用工具)类。

在我曾经工作过的多个应用程序中,都有Date类和DateUtilities类。Date类的存在有助于提供一种日期和时间的实现,而DateUtilities类的存在有助于操纵日期实例,并且做些事情,例如算出每个月的第一天、或是确定假期、或是检查两个日期是否重叠。我不知道有多少次我想试图编写一个方法去做某件DateUtilities类已经做过的事情,因为我不知道去找一个被称为DateUtilities的静态类,其静态方法已完成了我想做的工作。作为一名程序员,我必须记住,经常检查DateUtilities类中的所有方法,以便查明其中是否有我所需的方法。当你工作在一个大型应用程序中,并与许多混在帮助类中的静态方法一起工作时,这会导致很大的开销。或许,我正蔓延至这里的另一个问题,但关键是,与使用某个具有实际功能的类上的一个方法相比,记住那些飘忽世外的实用工具方法的存在要困难得多。(旁注:C#扩展方法可以稍微解决此类问题,你可以输入”.“来查看那些可以操纵该类的静态方法。)

最后的话

因此,请谨记当你创建一个静态方法时务必慎重考虑。我不主张决不使用它们。我主张要有一个充分的理由,并首先检查该静态方法是否实际属于它可以使用其状态信息的另一个类。

应用扩展方法的基本准则——本人增补

通常,建议您只在不得已的情况下才谨慎地实现扩展方法。对于必须扩展现有类型的客户端代码,只要有可能,都应该通过创建一个继承自现有类型的新类型来达到此目的(即不要轻易实现扩展方法,它只是增强版的静态方法而已!)。更多信息,参阅继承(C#编程指南)

当使用扩展方法来扩展你无法更改其源代码的类型时,你需要承担该类型实现中的更改会导致扩展方法失效的风险。

如果你确实为给定类型实现了扩展方法,请记住以下两点:

  • 如果扩展方法与该类型中定义的方法具有相同的签名,则扩展方法永远不会被调用。
  • 扩展方法是在命名空间级别被引入到(当前编码的)作用域中的。例如,如果你在同一个名为 Extensions 的命名空间下有多个包含扩展方法的静态类,那么通过 using Extensions; 指令将把这些扩展方法全部引入到作用域中。

预知后事如何,请看下回《清除静态方法三板斧之二——我是否应该保留助手类》