static 标记的成员属于类型,而不属于某个对象。实例字段每个对象各有一份;静态字段则只有一份,仅此而已。实例方法在对象上运行(order.Ship());静态方法在类型上运行(Math.Round(x)),根本不需要对象。
静态字段:只有一份,共享使用
最能看清区别的例子是计数器。每个 Order 都有自己的 Id,但目前为止已经创建了多少个订单,这个计数必须保存在一个共享的地方。
输出:
Ana #1, Ben #2, Chen #3
Orders created: 3
静态成员通过类型名访问:写 Order.Created,而不是 a.Created。C# 不允许通过实例访问静态成员(错误 CS0176),这让调用处一眼就能看出这个值是共享的。
静态方法
静态方法没有 this。它只能使用自己的参数和其他静态成员。这让它天然适合用来写根据输入计算结果的辅助函数:
输出:
61.38 EUR
7
Main 本身就是静态的:运行时在任何对象存在之前就调用它。
CS0120 错误:静态代码使用实例成员
因为 Main 是静态的,初学者往往一添加第二个方法就会遇到这个错误:
class Program
{
int visits;
void Greet() { Console.WriteLine("Hello"); }
static void Main()
{
Greet(); // error CS0120: An object reference is required for the non-static field, method, or property 'Program.Greet()'
visits++; // error CS0120: An object reference is required for the non-static field, method, or property 'Program.visits'
}
}
静态方法没有对象,所以也就没有可供它递增的 visits。修复方法有两种,哪种正确取决于这个成员是什么:
- 这个成员确实是每个对象各自的数据:创建一个对象并使用它。
var app = new Program(); app.Greet(); - 这个成员不需要对象状态:把它标记为
static。static void Greet() { ... }
为了让错误消失而把所有东西都标记为 static,在很小的程序里行得通,到了真实的程序中就会变成全局状态。判断的方法是问自己:这份数据属于某一个对象,还是属于整个程序?
反方向则总是允许的:实例方法可以随意调用静态成员。
静态类
声明为 static 的类只能包含静态成员,而且永远不能被实例化。它就是一组有名字的函数和常量。
输出:
77
100
True
编译器会强制执行这个约定:静态类中的实例成员会报错误 CS0708,对它使用 new 会报错误 CS0712。Math、Console、File、Path 和 Enumerable 都是静态类。扩展方法也必须放在静态类中。
静态构造函数与 static readonly
如果一个静态字段的值需要不止一个表达式才能算出来,可以在静态构造函数中设置它。运行时会在类型第一次被使用之前运行它,而且只运行一次:
输出:
Before first use
ShippingRates initialized
5.50
3
2026-01-01
readonly 表示这个字段只能在它的初始化器或静态构造函数中赋值。之后它就不能被重新赋值,但它指向的对象仍然可能是可变的,所以这个字典以 IReadOnlyDictionary 的形式公开。
const 与 static readonly
两者都能提供一个由整个程序共享的命名值,区别在于这个值是在什么时候确定的:
const | static readonly | |
|---|---|---|
| 值的确定时机 | 编译时 | 运行时,一次 |
| 允许的类型 | 数字、char、bool、string、枚举、null | 任何类型 |
| 隐式静态 | 是(不能写 static const) | 需要显式声明为 static |
| 复制到调用方程序集中 | 是 | 否,从字段读取 |
可用于 switch 的 case 和特性实参 | 是 | 否 |
“复制”这一行对库来说很重要:如果程序集 A 声明了 public const int MaxItems = 50;,而程序集 B 使用了它,那么 B 中包含的是字面量 50。把 A 中的值改成 100,在 B 重新编译之前对 B 没有任何影响。对于可能在版本之间变化的值,请优先使用 static readonly。
using static
using static 指令(C# 6)会导入某个类型的静态成员,这样你调用它们时就不用写类型名了:
输出:
Area: 28.27
40
在大量数学运算的代码中它读起来很顺。在其他地方要少用:单独一个 WriteLine 或 Parse,会让人看不出这个方法来自哪个类型。
静态状态与多线程
静态字段由进程中的每个线程共享。多个线程同时递增同一个静态字段会丢失更新,因为 created++ 包含读取、相加和写入三步,两个线程可能会交错执行它们。计数器请使用 Interlocked.Increment(ref created),更复杂的情况请使用 lock。这个问题最常出现在 Web 应用中:每个请求都运行在某个线程上,所以一个保存“当前用户”的静态字段会被所有用户共享。
还有一个细节:在泛型类中,每个构造出来的类型都有自己的静态字段。Cache<int>.Count 和 Cache<string>.Count 是两个不同的字段。
什么时候使用 static
适合的用法:
- 纯辅助函数,根据实参计算结果:格式化、转换、校验。
- 由整个程序共享的常量和只读查找数据。
- 工厂方法,例如
TimeSpan.FromMinutes(5),或者你自己的Temperature.FromCelsius(21)。 - 扩展方法。
不适合的用法:
- 可变的全局状态(当前用户、购物车、会变化的设置)。它会把所有接触它的代码耦合在一起,还会让测试依赖于彼此留下的数据。
- 任何你在测试中想要替换的东西,例如时钟、数据库或邮件发送器。静态调用无法被替换成假对象;而通过构造函数传入的实例可以。
常见问题
C# 中 static 是什么意思?
static 成员属于类型本身,而不属于任何对象。无论存在多少个对象,静态字段都只有一份;静态方法通过类型名调用(Math.Max(3, 7)),不需要创建实例。静态方法没有 this,所以不能直接使用实例字段。
C# 中的静态类是什么?
声明为 static 的类只能包含静态成员,而且永远不能被实例化(对它使用 new 会报错误 CS0712)。它用来把不需要对象状态的相关函数组织在一起,例如 Math、Console 或 File。扩展方法必须声明在静态类中。
如何修复“An object reference is required for the non-static field, method, or property”错误?
这是错误 CS0120:一个静态方法(通常是 Main)在使用实例成员。要么创建一个对象并通过它调用这个成员(var app = new Program(); app.Run();),要么在这个成员不需要每个对象各自的数据时,把它标记为 static。
C# 中 const 和 static readonly 有什么区别?
const 是编译期常量(数字、字符串、bool、枚举),它的值会被复制到每个使用它的程序集中。static readonly 字段在运行时赋值一次,赋值发生在它的初始化器或静态构造函数中,可以保存任何类型,例如 DateTime 或列表。永远不会改变的值用 const,其他情况用 static readonly。
C# 中什么时候应该使用 static?
只处理参数的代码(例如价格格式化这样的纯辅助函数)、对整个程序真正全局的常量和缓存,以及工厂方法,都适合用 static。凡是保存每个用户或每个请求各自状态的东西,或者你在测试中想要替换掉的东西,都不要用 static:共享的可变状态难以测试,而且不加锁的话在多线程下也不安全。