从 `new` 关键字开始,把 C# 类与对象真正讲清楚

江屿
江屿
发布时间:2026-08-24| 148人浏览过
原创

前言

很多人刚学 C# 时,最容易混淆的不是语法本身,而是“类只是定义”与“对象才是实际实例”这两个层次。本文就从 `new` 关键字切入,把类、对象、构造函数、属性和静态成员串起来讲清楚,让你不仅知道怎么写,还能判断代码背后到底发生了什么。

很多人刚学 C# 时,最容易混淆的不是语法本身,而是“类只是定义”与“对象才是实际实例”这两个层次。本文就从 new 关键字切入,把类、对象、构造函数、属性和静态成员串起来讲清楚,让你不仅知道怎么写,还能判断代码背后到底发生了什么。

为了便于理解,文中继续使用 Car 这个例子:先看类怎么声明,再看对象怎样被创建,最后补上 C# 中几个最常见、也最容易在实战里遇到的相关概念。

类是什么:先用 class 定义蓝图

在 C# 中,类是用 class 关键字定义出来的。它本身不是一辆真正的车,更像是一份“制造说明书”或“蓝图”,描述这个类型应该有哪些数据、能做哪些事。

声明一个最简单的类

先看一个最基础的定义:

展示 C# 中类、字段、方法与对象实例之间关系的信息图
类、对象与 `new` 的对应关系用一张图看清“类是蓝图、对象是实例”以及每个实例拥有独立字段状态。
// 这就是一份最简单的“汽车蓝图”
// public 是一个访问修舍符,意味着这个蓝图在任何地方都可以被看到和使用
public class Car
{
    // 类的成员将在这里定义...
}

即使这个 Car 还是空的,它也已经是一个合法的 C# 类型。后面所有对象,都是围绕这个类型创建出来的。

类的成员:数据和行为分别放在哪里

类里通常至少会有两类核心成员:字段负责保存状态,方法负责定义行为。把这两部分区分清楚,后面理解对象就会顺很多。

展示构造函数、属性和静态成员职责分工的 C# 信息图
构造函数、属性与 `static` 怎么分工这一节最容易混淆的三个成员类型,可以放在一起对照理解各自职责。

字段:保存对象自己的数据

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

对比引用类型和值类型赋值行为的 C# 信息图
引用类型和值类型的赋值差异赋值后为什么会互相影响,通常就卡在这一张对比图上。
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?能把这些问题想清楚,类与对象这部分就算真正入门了。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。