【层面一】C#语言基础和核心语法-01(类型系统/面向对象/异常处理)
文章目录
- 1 类型系统
- 1.1 为什么需要类型?
- 1.2 .NET 类型系统的两大支柱:CTS 和 CLS
- 1.3 最根本的分类:值类型 vs 引用类型
- 1.4 内置类型 vs. 自定义类型
- 1.5 类型转换
- 1.6 通用基类:System.Object
- 2 面向对象编程
- 2.1 类和对象
- 2.2 接口和类
- 2.3 访问修饰符
- 2.4 (OOP)三大特性:封装、继承、多态
- 2.5 其他成员:索引器、运算符重载
- 2.5.1 索引器:让对象像数组一样访问
- 2.5.2 运算符重载:让对象支持数学运算
- 3. 异常处理
这是所有.NET开发的基石,必须牢固掌握。
1 类型系统
1.1 为什么需要类型?
类型系统就是一套规则,它告诉编译器和我们:
- 数据是什么(是数字、文本、还是自定义对象?)
- 能对它做什么(能计算吗?能比较吗?能调用它的方法吗?)
- 它占多少空间(在内存中如何布局)
- 它如何与其他数据交互(如何转换、继承、实现接口)
类型系统为代码提供了结构、安全性和可读性。
1.2 .NET 类型系统的两大支柱:CTS 和 CLS
为了实现“跨语言”的宏伟目标,.NET 制定了两个标准:
-
公共类型系统 - CTS
- 定义: 一套所有 .NET 语言都必须遵守的关于类型的定义、行为和关系的规范。
- 目的:确保在一种语言中定义的类型(如 C# 的 class)可以在另一种语言中(如 F#)无缝使用。它定义了所有类型最终都派生自 System.Object,规定了什么是类、接口、委托、值类型、引用类型等。
- 比喻:CTS 就像是欧盟的标准,规定了所有成员国生产的电器插头形状、电压标准。这样德国产的电器拿到法国就能直接用。
-
公共语言规范 - 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. 自定义类型
- 内置类型(基础类型)
.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 是特殊的引用类型。它的行为有时像值类型(因为它是不可变的),任何修改操作都会产生一个新的字符串对象。
- 自定义类型
开发者可以创建自己的复杂类型,这是面向对象编程的核心:
类型 | 关键字 | 主要目的 | 分类 |
---|---|---|---|
类 | class | 定义数据和行为的蓝图,是OOP的主力 | 引用类型 |
结构 | struct | 定义轻量级的、行为简单的数据聚合 | 值类型 |
枚举 | enum | 定义一组命名的常数 | 值类型 |
接口 | interface | 定义一套公共行为的契约 | 引用类型 |
委托 | delegate | 定义方法签名,用于回调和方法引用 | 引用类型 |
1.5 类型转换
在不同类型之间转换是常见操作。
-
隐式转换
-
由编译器自动进行的安全转换,不会丢失信息。
-
规则:从小范围类型转向大范围类型。
-
示例:int i = 10; long l = i; (int -> long)
-
-
显式转换(强制转换)
-
可能丢失信息或失败,需要开发者明确指定。
-
语法:(目标类型)源变量
-
示例:double d = 3.14; int i = (int)d; (double -> int, 值变为 3)
-
风险:可能导致精度丢失或溢出。
-
-
使用转换方法
-
ToString(): 任何对象都可以转换为字符串。
-
Parse() / TryParse(): 将字符串转换为其他类型(如 int.Parse(“123”))。
-
Convert 类: Convert.ToInt32(), Convert.ToDateTime() 等,功能更丰富。
-
-
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(): 创建当前对象的浅表副本。
总结:类型系统带来的四大好处
-
安全性:编译器可以在编译时检查类型错误(例如,试图将字符串除以数字),将大量错误扼杀在摇篮中。
-
可读性:代码即文档。看到变量的类型,就能清晰地知道它的用途和能进行的操作。
-
内存管理:编译器知道类型的大小和生命周期,从而能高效地分配内存(值类型在栈上,引用类型在堆上)。
-
抽象与组织:通过类、接口等机制,允许开发者创建复杂的领域模型,更好地组织代码。
2 面向对象编程
2.1 类和对象
这是OOP最基础的概念,理解它就能理解OOP的整个世界。
- 类 - 蓝图
-
是什么:类是一个模板、一个蓝图、一个定义。它本身不是一个具体的东西,它只描述一类事物应该有什么样的特征和行为。
-
生活比喻:建筑设计图。图纸上定义了房子的户型、面积、有几个卧室、几个卫生间。但图纸本身不能住人。
-
代码中:类定义了属性(数据/特征)和方法(行为/功能)。
// 这是一个“狗”类的蓝图
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”的狗,它只是规定了“狗”应该有名字、品种,会叫、会吃
- 对象 - 房子
-
是什么:对象是类的一个具体实例。它是根据蓝图真正建造出来的、实实在在的东西。
-
生活比喻:根据建筑设计图建造出来的一栋真实的房子。你可以住进去,可以装修它。
-
代码中:使用 new 关键字来根据类创建对象。这个过程叫实例化。
// 根据“狗”的蓝图,创建两只真实的狗(对象)
Dog myDog = new Dog(); // new 关键字就是“建造”的过程
myDog.Name = "阿奇"; // 给属性赋值,相当于“装修”
myDog.Breed = "边牧";
myDog.Bark(); // 调用方法 -> 输出 "阿奇 说:汪汪!"Dog yourDog = new Dog();
yourDog.Name = "土豆";
yourDog.Breed = "柯基";
yourDog.Eat("狗粮"); // 输出 "土豆 正在吃狗粮"
核心关系:
-
类是静态的定义,在编译时就已经确定。
-
对象是动态的实例,在程序运行时被创建出来。
-
一个类可以创建无数个对象,每个对象都有自己的状态(属性值)。
2.2 接口和类
这是一个容易混淆但至关重要的概念。它们不是对立的,而是互补的。
- 类 - 员工
-
是什么:类是一个具体的实现者。它定义了事物是什么以及它如何做事。
-
生活比喻:一个具体的员工,比如张三。他有具体的技能,能实际完成工作。
// 一个具体的“数据库日志”实现者
public class DatabaseLogger : ILogger // 实现了 ILogger 接口
{public void Log(string message){// 他知道如何把消息记录到数据库Console.WriteLine($"将消息 '{message}' 写入数据库...");// ... 实际的数据库操作代码}
}
- 接口 - 契约/标准
-
是什么:接口是一份契约、一个标准。它只规定了实现者必须做什么,但完全不关心具体怎么做。
-
关键字:interface
-
生活比喻:职位描述(JD)。上面写着“我们需要一个会写代码的人”。它不关心你是张三还是李四,只要你能写代码就行。这份JD就是接口。
-
代码中:接口只包含方法、属性、事件或索引器的签名,没有任何实现
// 这是一个“日志记录器”的契约/标准
public interface ILogger
{// 只定义了“必须有一个Log方法,接收一个string参数”// 没有大括号 {} 实现体!void Log(string message);
}
- 实现接口:员工满足契约
一个类可以实现一个或多个接口,这意味着它签署了这份契约,承诺提供接口中定义的所有功能。
// 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}");}
}
- 为什么需要接口?—— 松耦合与多态
接口的核心威力在于它让代码极度灵活和可扩展。
想象一个业务场景: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; |
为什么需要访问修饰符?—— 封装与安全
-
隐藏复杂性:只暴露必要的部分(public 方法),隐藏内部复杂的实现细节(private 字段和方法)。使用者只需要知道怎么用,不需要知道为什么能这么用。
-
防止误操作:将重要的数据字段设为 private,然后通过 public 的属性(Property)来控制访问和验证逻辑,防止外部代码将其设置为无效值。
总结与关系
-
类和对象是OOP的基础,类是蓝图,对象是实例。
-
接口是OOP的灵魂,它定义了契约,实现了松耦合和多态,让程序变得灵活而强壮。
-
访问修饰符是OOP的卫士,它通过封装保护了对象的内部状态,确保了代码的安全性和健壮性。
2.4 (OOP)三大特性:封装、继承、多态
面向对象的特性:封装、继承、多态
2.5 其他成员:索引器、运算符重载
C# 中的两个强大特性:索引器 和 运算符重载。
它们都能让你自定义的类用起来更像内置类型,更加直观和优雅。
2.5.1 索引器:让对象像数组一样访问
- 核心概念:什么是索引器?
-
是什么:索引器允许你的对象能够像数组或字典一样,使用 [ ] 符号来访问其内部的元素或数据。
-
目的:提供一种更直观、更简洁的方式来访问对象内部封装的集合或数据。
-
本质:索引器本质上是一个特殊的属性,它拥有 get 和 set 访问器,但其访问方式不是通过属性名,而是通过索引(可以是任何类型)。
- 生活比喻:智能储物柜
想象一个智能储物柜,它有一排排的箱子。你不是通过属性(如 Locker1,Locker2)来访问每个箱子,而是通过箱子的编号来存取物品。
-
myLocker[101] = “书包”; // 把书包存进 101 号箱子
-
string item = myLocker[101]; // 从 101 号箱子取回物品
这个 [101] 就是索引器。储物柜对象内部管理着所有箱子,但对外只暴露这个简单的索引接口。
- 语法与实现
索引器的声明类似于属性,但使用 this 关键字,并在方括号 [ ] 中定义参数。
public class StringArray
{// 内部实际存储数据的数组private string[] _array = new string[10];// 索引器定义// 返回值类型: string// 参数: int indexpublic 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;}}
}
- 如何使用
// 创建对象
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
- 高级用法
- 不同类型索引:索引不一定是 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 运算符重载:让对象支持数学运算
- 核心概念:什么是运算符重载?
-
是什么:允许你为你自定义的类或结构体定义诸如 +, -, ==, !=, <, > 等运算符的行为。
-
目的:让你自定义的类型用起来像内置类型(如 int, double)一样自然,支持直观的数学或逻辑运算。
-
本质:运算符重载实际上是一个特殊的静态方法。
- 生活比喻:货币兑换
你有人民币(MoneyRMB)和美元(MoneyUSD)两种对象。100 RMB + 50 USD 应该如何计算?
-
直接相加是毫无意义的。
-
但如果你定义了 MoneyRMB 和 MoneyUSD 之间的 + 运算符,让它自动按汇率进行转换再计算,这个操作就变得非常直观和有用。
- 语法与实现
运算符重载使用 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);}
}
- 如何使用
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
- 重载关系运算符(==, !=, <, > 等)
重载关系运算符通常需要成对重载(如重载 == 就必须重载 !=),并且最好同时重写 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);}
}
- 可重载的运算符
类别 | 运算符 | 备注 |
---|---|---|
算术运算符 | +, -, *, /, % | |
递增/递减 | ++, – | |
位运算符 | &, | ,^,~,<<,>> |
关系运算符 | ==, !=, <, >, <=, >= | 必须成对重载 |
true/false | true, false | 极少使用 |
不可重载的运算符:.(成员访问)、()(调用)、new(对象创建)、&&, ||(条件逻辑,但它们会通过 & 和 | 来计算)、=(赋值)等。
总结与最佳实践
特性 | 索引器 | 运算符重载 |
---|---|---|
目的 | 让对象像数组/集合一样访问 | 让对象像基本数值类型一样运算 |
核心语法 | public T this[P index] { get; set; } | public static T operator +(T a, T b) { … } |
适用场景 | 自定义集合类、封装了内部数组/字典的类 | 数学对象(向量、矩阵、复数)、物理量(长度、重量)、货币等 |
优点 | 简化访问,隐藏内部数据结构的复杂性 | 代码直观,更符合数学和物理直觉 |
注意事项 | 确保索引有效性检查,避免抛出令人困惑的异常 | 谨慎使用,确保运算逻辑对使用者来说显而易见。切忌滥用,例如重载 + 来表示不相关的操作(如合并两个订单)会大大降低代码可读性。始终成对重载关系运算符并重写 Equals/GetHashCode。 |
3. 异常处理
.NET异常处理