【层面一】C#语言基础和核心语法-01(类型系统/面向对象/异常处理)

发布于:2025-09-15 ⋅ 阅读:(18) ⋅ 点赞:(0)

这是所有.NET开发的基石,必须牢固掌握。

1 类型系统

1.1 为什么需要类型?

类型系统就是一套规则,它告诉编译器和我们:

  1. 数据是什么(是数字、文本、还是自定义对象?)
  2. 能对它做什么(能计算吗?能比较吗?能调用它的方法吗?)
  3. 它占多少空间(在内存中如何布局)
  4. 它如何与其他数据交互(如何转换、继承、实现接口)

类型系统为代码提供了结构、安全性和可读性

1.2 .NET 类型系统的两大支柱:CTS 和 CLS

为了实现“跨语言”的宏伟目标,.NET 制定了两个标准:

  1. 公共类型系统 - CTS

    • 定义: 一套所有 .NET 语言都必须遵守的关于类型的定义、行为和关系的规范。
    • 目的:确保在一种语言中定义的类型(如 C# 的 class)可以在另一种语言中(如 F#)无缝使用。它定义了所有类型最终都派生自 System.Object,规定了什么是类、接口、委托、值类型、引用类型等。
    • 比喻:CTS 就像是欧盟的标准,规定了所有成员国生产的电器插头形状、电压标准。这样德国产的电器拿到法国就能直接用。
  2. 公共语言规范 - CLS

    • 定义:CTS 的一个子集。它定义了所有 .NET 语言都必须支持的最小功能集

    • 目的:确保开发者编写的代码可以被任何其他 .NET 语言使用。如果你希望代码是“符合 CLS 的”,就应该避免使用某些语言特有的特性(如 C# 的 uint 无符号整数,因为有些语言不支持)。

    • 比喻:CLS 就像是欧盟标准下的最低安全标准。一个产品只要满足这个最低标准,就可以在欧盟所有国家销售。开发者可以选择只使用这些最低标准特性,来保证最大的互操作性。

1.3 最根本的分类:值类型 vs 引用类型

这是 .NET 类型系统最核心、最重要的区别。几乎所有其他特性都源于此。

特性 值类型 引用类型
存储位置 托管堆
存储内容 直接存储数据本身 存储数据的地址(引用)
赋值行为 复制整个数据(创建副本) 复制引用(指向同一对象)
默认值 所有字段为 0 或 null null
继承 隐式密封(sealed),不能作为基类 可以派生其他类
内存管理 超出作用域时立即被回收 由垃圾回收器(GC)管理
例子 int, float, bool, char, struct, enum class, interface, delegate, array, string

比喻

  • 值类型就像你的「身份证」

    • 你复印身份证给别人,别人拿到的是副本。修改复印件,不影响你原来的身份证。

    • 身份证本身就在你手里(栈上)。

  • 引用类型就像「银行的保险箱」

    • 你告诉朋友保险箱的号码和钥匙(引用),你们用的是同一个保险箱。朋友取走里面的东西,你再去就没了。

    • 保险箱本身在银行金库里(堆上),你手里只拿着钥匙。

// 值类型示例 (struct)
public struct Point
{
    public int X;
    public int Y;
}

Point p1 = new Point { X = 10, Y = 20 };
Point p2 = p1; // 复制!p2 是 p1 的一个完整副本

p2.X = 100;    // 修改 p2 不会影响 p1
Console.WriteLine(p1.X); // 输出 10
Console.WriteLine(p2.X); // 输出 100

// 引用类型示例 (class)
public class Person
{
    public string Name;
}

Person person1 = new Person { Name = "Alice" };
Person person2 = person1; // 复制引用!现在 person2 和 person1 指向同一个对象

person2.Name = "Bob";     // 通过 person2 修改对象
Console.WriteLine(person1.Name); // 输出 "Bob",因为 person1 也指向同一个对象
Console.WriteLine(person2.Name); // 输出 "Bob"

1.4 内置类型 vs. 自定义类型

  1. 内置类型(基础类型)

.NET 提供了一组现成的、最常用的类型,它们在 C# 中有关键字对应:

C# 关键字 .NET 类型 分类 说明
int System.Int32 值类型 32 位整数
long System.Int64 值类型 64 位整数
float System.Single 值类型 32 位浮点数(需加 f 后缀)
double System.Double 值类型 64 位浮点数
decimal System.Decimal 值类型 128 位高精度小数(用于金融,需加 m 后缀)
bool System.Boolean 值类型 布尔值(true/false)
char System.Char 值类型 单个 Unicode 字符
string System.String 引用类型 不可变的 Unicode 字符串序列
object System.Object 引用类型 所有类型的终极基类

注意

string 是特殊的引用类型。它的行为有时像值类型(因为它是不可变的),任何修改操作都会产生一个新的字符串对象。

  1. 自定义类型
    开发者可以创建自己的复杂类型,这是面向对象编程的核心:
类型 关键字 主要目的 分类
class 定义数据和行为的蓝图,是OOP的主力 引用类型
结构 struct 定义轻量级的、行为简单的数据聚合 值类型
枚举 enum 定义一组命名的常数 值类型
接口 interface 定义一套公共行为的契约 引用类型
委托 delegate 定义方法签名,用于回调和方法引用 引用类型

1.5 类型转换

在不同类型之间转换是常见操作。

  1. 隐式转换

    • 由编译器自动进行的安全转换,不会丢失信息。

    • 规则:从小范围类型转向大范围类型。

    • 示例:int i = 10; long l = i; (int -> long)

  2. 显式转换(强制转换)

    • 可能丢失信息或失败,需要开发者明确指定。

    • 语法:(目标类型)源变量

    • 示例:double d = 3.14; int i = (int)d; (double -> int, 值变为 3)

    • 风险:可能导致精度丢失或溢出。

  3. 使用转换方法

    • ToString(): 任何对象都可以转换为字符串。

    • Parse() / TryParse(): 将字符串转换为其他类型(如 int.Parse(“123”))。

    • Convert 类: Convert.ToInt32(), Convert.ToDateTime() 等,功能更丰富。

  4. as 和 is 运算符

    • as: 用于引用类型之间的安全转换,失败则返回 null。

      object obj = new Person();
      Person p = obj as Person; // 安全转换
      if (p != null) { … }

    • is: 检查对象是否与给定类型兼容,返回 bool。C# 7.0 后支持模式匹配。

      if (obj is Person p) // 检查的同时转换
      {
      // 可以直接使用 p
      }

1.6 通用基类:System.Object

所有类型都直接或间接继承自 Object 类。它提供了几个最基本的方法:

  • Equals(): 判断两个对象是否“逻辑相等”。

  • GetHashCode(): 获取对象的哈希码(用于哈希表等数据结构)。

  • ToString(): 返回对象的字符串表示形式(默认返回类型全名,常被重写)。

  • GetType(): 非常重要!返回当前实例的准确类型(Type 对象)。用于反射。

  • MemberwiseClone(): 创建当前对象的浅表副本。


总结:类型系统带来的四大好处

  1. 安全性:编译器可以在编译时检查类型错误(例如,试图将字符串除以数字),将大量错误扼杀在摇篮中。

  2. 可读性:代码即文档。看到变量的类型,就能清晰地知道它的用途和能进行的操作。

  3. 内存管理:编译器知道类型的大小和生命周期,从而能高效地分配内存(值类型在栈上,引用类型在堆上)。

  4. 抽象与组织:通过类、接口等机制,允许开发者创建复杂的领域模型,更好地组织代码。


2 面向对象编程

2.1 类和对象

这是OOP最基础的概念,理解它就能理解OOP的整个世界。

  1. 类 - 蓝图
  • 是什么类是一个模板、一个蓝图、一个定义。它本身不是一个具体的东西,它只描述一类事物应该有什么样的特征和行为。

  • 生活比喻建筑设计图。图纸上定义了房子的户型、面积、有几个卧室、几个卫生间。但图纸本身不能住人。

  • 代码中:类定义了属性(数据/特征)和方法(行为/功能)。

// 这是一个“狗”类的蓝图
public class Dog
{
    // 属性 (特征)
    public string Name { get; set; } // 名字
    public string Breed { get; set; } // 品种

    // 方法 (行为)
    public void Bark()
    {
        Console.WriteLine($"{Name} 说:汪汪!");
    }

    public void Eat(string food)
    {
        Console.WriteLine($"{Name} 正在吃 {food}");
    }
}

这个 Dog 类就像一张蓝图,世界上并没有一只叫“Dog”的狗,它只是规定了“狗”应该有名字、品种,会叫、会吃

  1. 对象 - 房子
  • 是什么对象是类的一个具体实例。它是根据蓝图真正建造出来的、实实在在的东西。

  • 生活比喻:根据建筑设计图建造出来的一栋真实的房子。你可以住进去,可以装修它。

  • 代码中:使用 new 关键字来根据类创建对象。这个过程叫实例化

// 根据“狗”的蓝图,创建两只真实的狗(对象)
Dog myDog = new Dog(); // new 关键字就是“建造”的过程
myDog.Name = "阿奇";   // 给属性赋值,相当于“装修”
myDog.Breed = "边牧";
myDog.Bark();          // 调用方法 -> 输出 "阿奇 说:汪汪!"

Dog yourDog = new Dog();
yourDog.Name = "土豆";
yourDog.Breed = "柯基";
yourDog.Eat("狗粮");   // 输出 "土豆 正在吃狗粮"

核心关系:

  • 类是静态的定义,在编译时就已经确定。

  • 对象是动态的实例,在程序运行时被创建出来。

  • 一个类可以创建无数个对象,每个对象都有自己的状态(属性值)。

2.2 接口和类

这是一个容易混淆但至关重要的概念。它们不是对立的,而是互补的。

  1. 类 - 员工
  • 是什么:类是一个具体的实现者。它定义了事物是什么以及它如何做事

  • 生活比喻:一个具体的员工,比如张三。他有具体的技能,能实际完成工作。

// 一个具体的“数据库日志”实现者
public class DatabaseLogger : ILogger // 实现了 ILogger 接口
{
    public void Log(string message)
    {
        // 他知道如何把消息记录到数据库
        Console.WriteLine($"将消息 '{message}' 写入数据库...");
        // ... 实际的数据库操作代码
    }
}
  1. 接口 - 契约/标准
  • 是什么:接口是一份契约、一个标准。它只规定了实现者必须做什么,但完全不关心具体怎么做

  • 关键字:interface

  • 生活比喻职位描述(JD)。上面写着“我们需要一个会写代码的人”。它不关心你是张三还是李四,只要你能写代码就行。这份JD就是接口。

  • 代码中:接口只包含方法、属性、事件或索引器的签名,没有任何实现

// 这是一个“日志记录器”的契约/标准
public interface ILogger
{
    // 只定义了“必须有一个Log方法,接收一个string参数”
    // 没有大括号 {} 实现体!
    void Log(string message);
}
  1. 实现接口:员工满足契约
    一个类可以实现一个或多个接口,这意味着它签署了这份契约,承诺提供接口中定义的所有功能。
// FileLogger 签署了 ILogger 契约,承诺会实现 Log 方法
public class FileLogger : ILogger // 使用 ‘:‘ 表示实现接口
{
    public void Log(string message)
    {
        // 他用自己方式实现:写入文件
        Console.WriteLine($"将消息 '{message}' 写入文件...");
        // ... 实际的文件操作代码
    }
}

// 甚至可以有一个控制台日志器
public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        // 他用另一种方式实现:打印到控制台
        Console.WriteLine($"日志输出: {message}");
    }
}
  1. 为什么需要接口?—— 松耦合与多态
    接口的核心威力在于它让代码极度灵活和可扩展。

想象一个业务场景:OrderService(订单服务)需要记录日志。

没有接口的糟糕写法(紧耦合):

public class OrderService
{
    // 直接依赖一个具体的实现
    private DatabaseLogger _logger = new DatabaseLogger();

    public void ProcessOrder()
    {
        _logger.Log("开始处理订单..."); // 永远只能用DatabaseLogger
    }
}

如果想换成文件日志,必须修改 OrderService 的代码。

使用接口的强大写法(松耦合):

public class OrderService
{
    // 只依赖一个抽象的契约,而不是具体的实现
    private readonly ILogger _logger;

    // 通过构造函数注入,告诉我你需要哪种日志器,但我不管具体是哪种
    public OrderService(ILogger logger)
    {
        _logger = logger;
    }

    public void ProcessOrder()
    {
        _logger.Log("开始处理订单..."); // 神奇之处:这里不需要关心是哪个日志器
    }
}

// 在程序入口处,我们决定用哪种具体的实现
var service = new OrderService(new FileLogger()); // 想用文件日志?注入它
// var service = new OrderService(new ConsoleLogger()); // 想用控制台?换这个
// var service = new OrderService(new DatabaseLogger()); // 想用数据库?再换这个
service.ProcessOrder();

总结接口的好处:

  • 定义标准:让多个类拥有统一的行为方式。

  • 实现多态:允许不同的类对同一方法有不同的实现。OrderService 可以应对任何实现了 ILogger 的类。

  • 松耦合:使代码模块之间不直接依赖具体实现,而是依赖抽象。这使得系统更灵活、更易测试(测试时可以注入一个“模拟”的日志器)和更易扩展(未来增加新的日志类型,如 EmailLogger,完全不需要修改 OrderService)。


2.3 访问修饰符

访问修饰符决定了类、方法、属性等成员的可见性和可访问性。它实现了OOP的封装特性,就像给房子装上了不同权限的门。

修饰符 权限范围 生活比喻 代码示例
public 无限制。任何地方的代码都可以访问。 房子的前门,任何人都可以进来。 public string Name;
private 最严格。只有同一个类内部的代码可以访问。 **卧室的私人抽屉,**只有你自己能打开。 private string _secretCode;
protected 家族权限。本类内部和所有派生类(子类) 中可以访问。 家族的祖传密室,你和你的后代都可以进,外人不行。 protected inheritanceKey;
internal 项目/程序集权限。在同一个项目(程序集)内部可以访问,对外部项目不可见。 公司办公室门禁,只有本公司员工能刷开,外面的人不行。 internal EmployeeId;
protected internal protected 或 internal。只要是本程序集内部,或者是派生类(即使在其他程序集),都可以访问。 家族公司权限:要么你是家族成员(子类),要么你是公司员工(同程序集),二者满足其一即可进入。 protected internal Fund;

为什么需要访问修饰符?—— 封装与安全

  1. 隐藏复杂性:只暴露必要的部分(public 方法),隐藏内部复杂的实现细节(private 字段和方法)。使用者只需要知道怎么用,不需要知道为什么能这么用。

  2. 防止误操作:将重要的数据字段设为 private,然后通过 public 的属性(Property)来控制访问和验证逻辑,防止外部代码将其设置为无效值。


总结与关系

  • 类和对象是OOP的基础,类是蓝图,对象是实例。

  • 接口是OOP的灵魂,它定义了契约,实现了松耦合和多态,让程序变得灵活而强壮。

  • 访问修饰符是OOP的卫士,它通过封装保护了对象的内部状态,确保了代码的安全性和健壮性。


2.4 (OOP)三大特性:封装、继承、多态

面向对象的特性:封装、继承、多态

2.5 其他成员:索引器、运算符重载

C# 中的两个强大特性:索引器 和 运算符重载。
它们都能让你自定义的类用起来更像内置类型,更加直观和优雅。

2.5.1 索引器:让对象像数组一样访问

  1. 核心概念:什么是索引器?
  • 是什么:索引器允许你的对象能够像数组字典一样,使用 [ ] 符号来访问其内部的元素或数据。

  • 目的:提供一种更直观、更简洁的方式来访问对象内部封装的集合或数据。

  • 本质:索引器本质上是一个特殊的属性,它拥有 get 和 set 访问器,但其访问方式不是通过属性名,而是通过索引(可以是任何类型)。

  1. 生活比喻:智能储物柜
    想象一个智能储物柜,它有一排排的箱子。你不是通过属性(如 Locker1,Locker2)来访问每个箱子,而是通过箱子的编号来存取物品。
  • myLocker[101] = “书包”; // 把书包存进 101 号箱子

  • string item = myLocker[101]; // 从 101 号箱子取回物品

这个 [101] 就是索引器。储物柜对象内部管理着所有箱子,但对外只暴露这个简单的索引接口。

  1. 语法与实现
    索引器的声明类似于属性,但使用 this 关键字,并在方括号 [ ] 中定义参数。
public class StringArray
{
    // 内部实际存储数据的数组
    private string[] _array = new string[10];

    // 索引器定义
    // 返回值类型: string
    // 参数: int index
    public string this[int index]
    {
        get
        {
            // 读取逻辑:检查索引范围
            if (index < 0 || index >= _array.Length)
                throw new IndexOutOfRangeException();
            return _array[index];
        }
        set
        {
            // 写入逻辑:检查索引范围
            if (index < 0 || index >= _array.Length)
                throw new IndexOutOfRangeException();
            _array[index] = value;
        }
    }
}
  1. 如何使用
// 创建对象
StringArray myArray = new StringArray();

// 使用索引器赋值 (调用 set 访问器)
myArray[0] = "Hello";
myArray[1] = "World";

// 使用索引器读取 (调用 get 访问器)
Console.WriteLine(myArray[0]); // 输出 "Hello"
Console.WriteLine(myArray[1]); // 输出 "World"

// 尝试越界访问
// Console.WriteLine(myArray[100]); // 会抛出 IndexOutOfRangeException
  1. 高级用法
  • 不同类型索引:索引不一定是 int,可以是 string 或其他类型,常用于实现字典行为。
public class PersonCollection
{
    private Dictionary<string, Person> _people = new Dictionary<string, Person>();

    // 以字符串(如名字)作为索引
    public Person this[string name]
    {
        get { return _people[name]; }
        set { _people[name] = value; }
    }
}

// 使用
var collection = new PersonCollection();
collection["Alice"] = new Person("Alice", 30);
Person p = collection["Alice"];
  • 多参数索引:例如,模拟一个二维表格或棋盘。
public class GameBoard
{
    private int[,] _board = new int[3, 3];

    public int this[int row, int column]
    {
        get { return _board[row, column]; }
        set { _board[row, column] = value; }
    }
}

// 使用
var board = new GameBoard();
board[1, 2] = 5; // 在第 2 行,第 3 列放置一个棋子

总结索引器
它完美体现了封装的思想。类内部可以用任何复杂的数据结构(数组、列表、字典、数据库连接)来存储数据,但对外提供了极其简单统一的数组式访问接口。

2.5.2 运算符重载:让对象支持数学运算

  1. 核心概念:什么是运算符重载?
  • 是什么:允许你为你自定义的结构体定义诸如 +, -, ==, !=, <, > 等运算符的行为。

  • 目的:让你自定义的类型用起来像内置类型(如 int, double)一样自然,支持直观的数学或逻辑运算。

  • 本质:运算符重载实际上是一个特殊的静态方法

  1. 生活比喻:货币兑换
    你有人民币(MoneyRMB)和美元(MoneyUSD)两种对象。100 RMB + 50 USD 应该如何计算?
  • 直接相加是毫无意义的。

  • 但如果你定义了 MoneyRMB 和 MoneyUSD 之间的 + 运算符,让它自动按汇率进行转换再计算,这个操作就变得非常直观和有用。

  1. 语法与实现
    运算符重载使用 operator 关键字,并声明为 public static。
public class Vector2D
{
    public double X { get; set; }
    public double Y { get; set; }

    public Vector2D(double x, double y)
    {
        X = x;
        Y = y;
    }

    // 重载加法运算符 ‘+’
    public static Vector2D operator +(Vector2D v1, Vector2D v2)
    {
        return new Vector2D(v1.X + v2.X, v1.Y + v2.Y);
    }

    // 重载减法运算符 ‘-’
    public static Vector2D operator -(Vector2D v1, Vector2D v2)
    {
        return new Vector2D(v1.X - v2.X, v1.Y - v2.Y);
    }

    // 重载一元取反运算符 ‘-’
    public static Vector2D operator -(Vector2D v)
    {
        return new Vector2D(-v.X, -v.Y);
    }
}
  1. 如何使用
Vector2D point1 = new Vector2D(1.0, 2.0);
Vector2D point2 = new Vector2D(3.0, 4.0);

// 使用重载的 ‘+’ 运算符
Vector2D result1 = point1 + point2; // result1.X = 4.0, result1.Y = 6.0
Console.WriteLine($"({result1.X}, {result1.Y})"); // 输出 (4, 6)

// 使用重载的 ‘-’ 运算符
Vector2D result2 = point1 - point2; // result2.X = -2.0, result2.Y = -2.0

// 使用重载的一元 ‘-’ 运算符
Vector2D result3 = -point1; // result3.X = -1.0, result3.Y = -2.0
  1. 重载关系运算符(==, !=, <, > 等)

重载关系运算符通常需要成对重载(如重载 == 就必须重载 !=),并且最好同时重写 Equals() 和 GetHashCode() 方法,以保持逻辑一致性。

public class Vector2D
{
    // ... 之前的代码 ...

    // 重载 ‘==’ 运算符
    public static bool operator ==(Vector2D v1, Vector2D v2)
    {
        // 处理 null 情况
        if (ReferenceEquals(v1, v2)) return true;
        if (v1 is null || v2 is null) return false;
        
        // 定义相等的逻辑:X 和 Y 都相等
        return v1.X == v2.X && v1.Y == v2.Y;
    }

    // 重载 ‘!=’ 运算符 (必须与 ‘==’ 逻辑相反)
    public static bool operator !=(Vector2D v1, Vector2D v2)
    {
        return !(v1 == v2);
    }

    // 重写 Equals 方法,保持与 ‘==’ 逻辑一致
    public override bool Equals(object obj)
    {
        if (obj is Vector2D other)
        {
            return this == other; // 调用上面重载的 ‘==’ 运算符
        }
        return false;
    }

    // 重写 GetHashCode,如果两个对象相等,它们的哈希码也必须相等
    public override int GetHashCode()
    {
        return HashCode.Combine(X, Y);
    }
}
  1. 可重载的运算符
类别 运算符 备注
算术运算符 +, -, *, /, %
递增/递减 ++, –
位运算符 &, ,^,~,<<,>>
关系运算符 ==, !=, <, >, <=, >= 必须成对重载
true/false true, false 极少使用

不可重载的运算符:.(成员访问)、()(调用)、new(对象创建)、&&, ||(条件逻辑,但它们会通过 & 和 | 来计算)、=(赋值)等。

总结与最佳实践

特性 索引器 运算符重载
目的 让对象像数组/集合一样访问 让对象像基本数值类型一样运算
核心语法 public T this[P index] { get; set; } public static T operator +(T a, T b) { … }
适用场景 自定义集合类、封装了内部数组/字典的类 数学对象(向量、矩阵、复数)、物理量(长度、重量)、货币等
优点 简化访问,隐藏内部数据结构的复杂性 代码直观,更符合数学和物理直觉
注意事项 确保索引有效性检查,避免抛出令人困惑的异常 谨慎使用,确保运算逻辑对使用者来说显而易见。切忌滥用,例如重载 + 来表示不相关的操作(如合并两个订单)会大大降低代码可读性。始终成对重载关系运算符并重写 Equals/GetHashCode。

3. 异常处理

.NET异常处理