很多人刚学 C# 时,最容易混淆的不是语法本身,而是“类只是定义”与“对象才是实际实例”这两个层次。本文就从 new 关键字切入,把类、对象、构造函数、属性和静态成员串起来讲清楚,让你不仅知道怎么写,还能判断代码背后到底发生了什么。
为了便于理解,文中继续使用 Car 这个例子:先看类怎么声明,再看对象怎样被创建,最后补上 C# 中几个最常见、也最容易在实战里遇到的相关概念。
类是什么:先用 class 定义蓝图
在 C# 中,类是用 class 关键字定义出来的。它本身不是一辆真正的车,更像是一份“制造说明书”或“蓝图”,描述这个类型应该有哪些数据、能做哪些事。
声明一个最简单的类
先看一个最基础的定义:

// 这就是一份最简单的“汽车蓝图”
// public 是一个访问修舍符,意味着这个蓝图在任何地方都可以被看到和使用
public class Car
{
// 类的成员将在这里定义...
}
即使这个 Car 还是空的,它也已经是一个合法的 C# 类型。后面所有对象,都是围绕这个类型创建出来的。
类的成员:数据和行为分别放在哪里
类里通常至少会有两类核心成员:字段负责保存状态,方法负责定义行为。把这两部分区分清楚,后面理解对象就会顺很多。

字段:保存对象自己的数据
字段是类内部用来存储数据的变量。很多时候,字段不会直接对外公开,而是先用 private 保护起来,让外部不能随意改动。

public class Car
{
// 私有字段,用于存储汽车的颜色和当前速度
// 就像是内部零件,不对外直接暴露
private string _color;
private int _speed;
}
这里的 _color 和 _speed 就是对象内部状态。只要后面创建出不同的 Car 对象,每个对象都会有自己独立的一份这两个字段。
方法:定义对象可以做什么
如果说字段回答的是“这个对象有什么数据”,那么方法回答的就是“这个对象能执行什么动作”。
public class Car
{
// ... 字段 ...
// 公开的方法,定义了汽车的行为
public void Accelerate()
{
_speed += 10;
Console.WriteLine($"加速!当前速度: {_speed} km/h");
}
public void Brake()
{
_speed -= 10;
if (_speed < 0) _speed = 0;
Console.WriteLine($"刹车!当前速度: {_speed} km/h");
}
}
Accelerate() 会把速度加到 _speed 上,Brake() 则会减速,并保证速度不会低于 0。这说明方法不仅是“能调用的函数”,也是对象修改自身状态的入口。
为什么要用 new:对象实例是怎么创建出来的
理解类之后,下一步就是理解对象。类只是定义,真正能在程序里使用的是对象实例,而创建实例要靠 new 关键字。
这个过程叫实例化(Instantiation)。你可以把它理解为:按照 Car 这份蓝图,实际造出一辆可操作的车。
// 声明一个 Car 类型的变量 myCar
// 并使用 new 关键字创建一个 Car 类的实例(对象),然后将其赋给 myCar
Car myCar = new Car();
// 再创建一辆车
Car yourCar = new Car();
// 现在,myCar 和 yourCar 就是两个独立的 Car 对象
// 它们都拥有自己的 _color 和 _speed 字段
// 调用 myCar 的方法,不会影响 yourCar
myCar.Accelerate(); // 输出: 加速!当前速度: 10 km/h
myCar.Accelerate(); // 输出: 加速!当前速度: 20 km/h
yourCar.Brake(); // 输出: 刹车!当前速度: 0 km/h
Console.WriteLine(myCar); // 输出类似: YourProjectName.Car
这里最关键的一点是:myCar 和 yourCar 虽然类型相同,但它们是两个不同的对象。myCar 连续加速两次后速度变成 20,并不会把 yourCar 的速度也一起改掉。
更准确地说,myCar 和 yourCar 是两个引用,它们分别指向内存中的两个 Car 实例。只要是不同实例,就各自维护自己的字段值。
对象创建后最常见的三个核心成员
学会 new 之后,通常还要补上三个高频概念:构造函数负责初始化,属性负责安全访问数据,static 则处理“属于类本身”的共享成员。
构造函数:对象一创建就执行的初始化入口
当你写下 new Car() 或 new Car("红色") 时,实际调用的是构造函数。它的主要任务是在对象刚被创建时完成初始化。
构造函数有两个非常好认的特征:
- 没有返回类型,连
void都不能写。 - 名字必须和类名完全相同。
public class Car
{
private string _color;
private int _speed;
// 这是一个构造函数
// 它在 new Car("Red") 时被调用
public Car(string color)
{
// this 关键字代表当前正在被创建的对象实例
this._color = color;
this._speed = 0; // 设定初始速度为0
Console.WriteLine($"一辆{_color}的汽车被制造出来了!");
}
// ... 方法 ...
}
// === 使用构造函数创建对象 ===
Car redCar = new Car("红色"); // 输出: 一辆红色的汽车被制造出来了!
Car blueCar = new Car("蓝色"); // 输出: 一辆蓝色的汽车被制造出来了!
这里的构造函数把传入的 color 写进 _color,同时把 _speed 初始化为 0。这样对象一旦创建出来,就已经处于一个可用状态。
还有一个容易被忽略的点:如果你完全不写任何构造函数,C# 编译器会自动提供一个无参数的默认构造函数。但只要你自己定义了构造函数,是否还能直接写 new Car(),就要看你有没有保留对应的无参版本。
属性:在封装和可用性之间做平衡
前面把字段设成 private,是为了避免外部代码随意破坏对象状态。但数据总不能永远关在类里面,所以 C# 提供了属性(Properties)作为更规范的读写入口。
属性看起来像字段,实际背后通过 get 和 set 控制读取和写入时的逻辑。
public class Car
{
private string _color;
private int _speed;
// 为 _speed 字段创建一个名为 Speed 的公开属性
public int Speed
{
// get 访问器:当读取属性值时执行
get
{
return _speed;
}
// set 访问器:当给属性赋值时执行
set
{
// 'value' 是一个关键字,代表赋过来的值
if (value < 0)
{
Console.WriteLine("速度不能为负数!");
}
else
{
_speed = value;
}
}
}
// ... 构造函数和方法 ...
}
// === 使用属性 ===
Car myCar = new Car("银色");
myCar.Speed = 50; // 调用 set 访问器,将 50 赋给 value
Console.WriteLine($"当前速度: {myCar.Speed}"); // 调用 get 访问器
myCar.Speed = -10; // 调用 set 访问器,触发验证逻辑
// 输出: 速度不能为负数!
这段代码的重点不只是“写法”,而是属性把验证逻辑放在了赋值入口。外部仍然可以写 myCar.Speed = 50 这种自然语法,但类内部已经把非法值挡住了。
如果属性不需要额外逻辑,C# 还提供了更简洁的自动属性:
// 编译器会自动在后台创建一个名为<Color>k__BackingField的私有字段
public string Color { get; set; }
这种写法非常常见,适合只想公开读写、不需要手动控制 get/set 细节的场景。
static:有些成员属于类,不属于某个对象
大多数情况下,字段和方法都是实例成员,也就是每创建一个对象,就拥有自己的一份数据和可调用行为。但也有一些信息并不属于单个对象,而是属于整个类。
例如“总共生产了多少辆车”,显然不该挂在某一辆具体汽车身上,这时就应该用 static。
public class Car
{
// 静态字段:所有Car对象共享这一个字段
public static int NumberOfCarsProduced = 0;
public string Color { get; set; }
public Car(string color)
{
this.Color = color;
NumberOfCarsProduced++; // 每制造一辆车,就让共享的计数器加一
}
// 静态方法:可以直接通过类名调用,无需创建对象
public static void DisplayProductionInfo()
{
Console.WriteLine($"总共生产了 {NumberOfCarsProduced} 辆汽车。");
}
}
// === 使用静态成员 ===
Car.DisplayProductionInfo(); // 输出: 总共生产了 0 辆汽车。
Car car1 = new Car("红色");
Car car2 = new Car("黑色");
Car.DisplayProductionInfo(); // 输出: 总共生产了 2 辆汽车。
NumberOfCarsProduced 只有一份,所有 Car 对象共享它。DisplayProductionInfo() 也不依赖某一辆车的状态,所以可以直接通过类名调用,而不是先 new 出对象再调用。
为什么修改一个变量会影响另一个:引用类型和值类型的区别
学类和对象时,另一个高频疑问是:为什么有些赋值之后两个变量会互相影响,有些又完全不会?答案通常就在“引用类型”和“值类型”的区别里。
引用类型:复制的是引用,不是对象本体
class 属于引用类型。变量里保存的不是对象内容本身,而是一个指向对象的引用,可以简单理解成地址。
因此,当你把一个引用类型变量赋给另一个变量时,复制的是引用,两者会指向同一个对象。
Car carA = new Car("红色");
Car carB = carA; // 复制引用,carA 和 carB 指向同一个对象
carB.Color = "蓝色";
Console.WriteLine(carA.Color); // 输出: 蓝色
这里改的是 carB.Color,但 carA.Color 也变成了“蓝色”,因为 carA 和 carB 根本不是两辆车,而是同一辆车的两个引用。
值类型:赋值时复制完整数据
int、double、bool,以及 struct,都属于值类型。值类型变量存的是数据本身,所以赋值时会复制出一份完整副本。
int a = 10;
int b = a; // 复制值,a 和 b 是两个独立的 10
b = 20;
Console.WriteLine(a); // 输出: 10
修改 b 不会影响 a,因为它们已经是两份独立的数据。这也是为什么在排查“对象被谁改了”这类问题时,先确认类型是引用类型还是值类型,往往能迅速缩小范围。
项目里怎么组织类:命名空间与继承
当代码从几个类增长到几十个、几百个时,问题就不再只是“怎么写类”,还包括“怎么管理这些类”。C# 里最常见的两个组织手段是命名空间和继承。
命名空间:给类分组,避免重名冲突
命名空间(Namespaces)可以理解为类的逻辑分组机制。它最直接的作用,是在大型项目里避免不同模块出现同名类冲突。
namespace MyCarFactory.Luxury
{
public class Car { /* ... */ }
}
namespace MyCarFactory.Utility
{
public class Car { /* ... */ }
}
这里虽然两个类都叫 Car,但由于分别位于 MyCarFactory.Luxury 和 MyCarFactory.Utility,编译器仍然能区分它们。
继承:在已有类的基础上继续扩展
继承(Inheritance)表示一个类可以从另一个类派生,复用后者已有的非私有成员,并在此基础上扩展自己的能力。
// SportsCar 继承自 Car
public class SportsCar : Car
{
public void ActivateTurbo()
{
// ...
}
}
SportsCar 在拥有 Car 基础能力的同时,又增加了自己的 ActivateTurbo() 方法。对于刚接触面向对象的人来说,先把继承理解成“在现有类型上扩展特性”就够用了。
从 new 回看类与对象,重点该怎么记
把全文收束起来,其实可以抓住这样一条主线:class 用来定义类型,new 用来创建实例;字段保存状态,方法描述行为;构造函数负责初始化,属性负责受控访问;static 管的是全类共享成员;而引用类型和值类型,则决定赋值和修改时的实际效果。
如果你之后在 C# 代码里再次看到 new Car(),不妨顺手问自己几个问题:这里调用的是哪个构造函数?创建出来的是独立对象还是共享引用?成员是应该挂在实例上,还是应该声明成 static?能把这些问题想清楚,类与对象这部分就算真正入门了。









