此外,我不确定在 "不要将静态类视为杂项存储桶" 中 这个指南 的含义是什么——我有一些只是一堆零散静态函数的类...
将类设置为static
是完全可以的,在事实上,如果你查看System.Math
,你会发现它也是static
的:
public static class Math
这个指南想要表达的是,你不应该把所有的静态方法都放在一个静态类中,让它承担所有任务并充当静态方法的桶。相反,如果适合的话,应该创建一些较小的util类,其中包含与同一功能相关的方法,就像在BCL中的System.Math
和其他几个类中所做的那样。
我应该将整个类声明为静态的吗?
是的。在一个类中添加 static
表示它只包含静态成员,你不能实例化它。如果没有它,使用你的类的用户可能会感到困惑,并尝试创建类的实例或变量。有了 static
,这是不可能的。
看起来这正是你的情况。
将 static 修饰符添加到类中是否会影响性能?
不会,对静态方法的调用始终具有相同的性能特征,无论包含类是否为 static
。实际上,在 CIL 级别上不存在静态类的整个概念,它们只是密封的抽象类(这种组合在 C# 中无法编译)。
但即使有差异,也非常微小。不要过早优化,特别是当涉及微观优化时。
辅助类通常是静态类,因此您不需要实例化它们。实例化托管的.NET对象(特别是辅助类)并没有太大的成本,这只是一种方便。
很容易就可以组合一个带有最小辅助方法的静态类并完成工作。它们在代码中有其位置,并且可以在确定性输入/输出时使用,例如字符串的ComputeHash,数字的Find Average等。
但是,静态类被反对的一个原因是它们通常会干扰单元测试并引起各种问题。(Fakes,Moles,Private Accessors等)
即使是辅助类,基于接口的方法也有助于整个代码的单元测试。这对于涉及工作流程的大型项目尤其如此,其中静态辅助方法仅是工作流程的一部分。
例如,假设您需要检查当前年份是否为闰年。编写一个快速的静态方法是很诱人的。
public static class DateHelper
{
public static bool IsLeapYear()
{
var currentDate = DateTime.UtcNow;
// check if currentDate's year is a leap year using some unicorn logic
return true; // or false
}
}
如果你的代码中有类似这样的方法:
public class Birthday
{
public int GetLeapYearDaysData()
{
// some self-logic..
// now call our static method
var isLeapYear = DateHelper.IsLeapYear();
// based on this value, you might return 100 or 200.
if (isLeapYear)
{
return 100;
}
return 200;
}
}
现在,如果你尝试对这个方法 public int GetLeapYearDaysData() 进行单元测试,你可能会遇到麻烦,因为返回值是不确定的,即取决于当前年份,而且不建议让单元测试的行为随着时间的推移变得不可预测或恶化。
// this unit test is flaky
[Test]
public void TestGetLeapYearDaysData()
{
var expected = 100;
// we don't know if this method will return 100 or 200.
var actual = new Birthday().GetLeapYearDaysData();
Assert.AreEqual(expected, actual);
}
public interface IDateHelper
{
bool IsLeapYear();
}
public class DateHelper : IDateHelper
{
public bool IsLeapYear()
{
var currentDate = DateTime.UtcNow;
// check if currentDate's year is a leap year using some unicorn logic
return true; // or false
}
}
public class Birthday
{
private IDateHelper _dateHelper;
// any caller can inject their own version of dateHelper.
public Birthday(IDateHelper dateHelper)
{
this._dateHelper = dateHelper;
}
public int GetLeapYearDaysData()
{
// some self-logic..
// now call our injected helper's method.
var isLeapYear = this._dateHelper.IsLeapYear();
// based on this value, you might return 100 or 200.
if (isLeapYear)
{
return 100;
}
return 200;
}
}
// now see how are unit tests can be more robust and reliable
// this unit test is more robust
[Test]
public void TestGetLeapYearDaysData()
{
var expected = 100;
// use any mocking framework or stubbed class
// to reliably tell the unit test that 100 needs to be returned.
var mockDateHelper = new Mock<IDateHelper>();
// make the mock helper return true for leap year check.
// we're no longer at the mercy of current date time.
mockDateHelper.Setup(m=>m.IsLeapYear()).Returns(true);
// inject this mock DateHelper in our BirthDay class
// we know for sure the value that'll be returned.
var actual = new Birthday(mockDateHelper).GetLeapYearDaysData();
Assert.AreEqual(expected, actual);
}
正如您所看到的,一旦辅助方法基于接口,它们就很容易进行测试。在一个大项目的过程中,许多这样的较小静态方法最终会导致测试关键功能流程的瓶颈。
因此,提前意识到这个陷阱并进行额外的投资是值得的。基本上,要确定哪些类/方法需要是静态的,哪些不需要。