【C#】入门
文章目录
- 1. C# 语言概述
- 1.1 什么是C#
- 1.2 C# 与 C/Python 的对比
- 1.3 C# 的特点
- 2. 开发环境搭建
- 2.1 理解.NET生态系统
- 2.2 安装 .NET SDK
- 2.3 开发工具选择
- 2.4 创建第一个项目
- 3. 基础语法
- 3.1 程序结构分析
- 3.2 语法规则详解
- 3.3 命名规范
- 4. 数据类型
- 4.1 类型系统概述
- 4.2 值类型详解
- 4.2.1 整数类型
- 4.2.2 浮点类型
- 4.2.3 其他重要的值类型
- 4.3 引用类型详解
- 4.4 类型转换
- 4.4.1 隐式转换
- 4.4.2 显式转换
- 4.4.3 使用转换方法
- 5. 变量和常量
- 5.1 变量的概念和声明
- 5.1.1 显式类型声明
- 5.1.2 隐式类型声明(var关键字)
- 5.1.3 可空类型
- 5.2 常量的概念和使用
- 5.2.1 编译时常量(const)
- 5.2.2 运行时常量(readonly)
- 5.3 变量作用域和生命周期
- 5.3.1 作用域层次
- 5.3.2 变量隐藏
- 5.3.3 静态变量 vs 实例变量
- 6. 运算符
- 6.1 运算符概述
- 6.2 算术运算符
- 6.3 比较运算符
- 6.4 逻辑运算符
- 6.5 位运算符
- 6.6 赋值运算符
- 6.7 空合并运算符
- 6.8 运算符优先级
- 7. 控制结构
- 7.1 控制结构概述
- 7.2 条件语句
- 7.2.1 if语句
- 7.2.2 switch语句
- 7.3 循环语句
- 7.3.1 for循环
- 7.3.2 while循环
- 7.3.3 do-while循环
- 7.3.4 foreach循环
- 7.4 跳转语句
- 7.4.1 break语句
- 7.4.2 continue语句
- 7.4.3 return语句
- 7.4.4 goto语句(谨慎使用)
- 7.5 控制结构的最佳实践
- 8. 数组和集合
- 8.1 数组概述
- 8.2 一维数组
- 8.2.1 数组的声明和初始化
- 8.2.2 数组的访问和操作
- 8.2.3 数组的常用操作
- 8.3 多维数组
- 8.3.1 二维数组
- 8.3.2 三维及更高维数组
- 8.4 锯齿数组
- 8.5 集合类型概述
- 8.6 List<T> - 动态数组
- 8.7 Dictionary<TKey, TValue> - 键值对集合
- 8.8 其他重要集合类型
- 8.8.1 HashSet<T> - 无重复元素集合
- 8.8.2 Queue<T> - 队列
- 8.8.3 Stack<T> - 栈
- 8.9 集合的选择指南
- 9. 方法和函数
- 9.1 方法概述
- 9.2 方法的基本语法
- 9.3 方法参数详解
- 9.3.1 值参数(默认方式)
- 9.3.2 引用参数(ref关键字)
- 9.3.3 输出参数(out关键字)
- 9.3.4 参数数组(params关键字)
- 9.3.5 可选参数和命名参数
- 9.4 方法重载
- 9.5 局部函数
- 9.6 递归方法
- 9.7 方法的最佳实践
- 9.7.1 方法设计原则
- 9.7.2 错误处理和验证
- 10. 面向对象编程
- 10.1 面向对象编程概述
- 10.2 类和对象
- 10.2.1 类的定义
- 10.2.2 对象的创建和使用
- 10.3 封装详解
- 10.3.1 访问修饰符
- 10.3.2 属性的高级用法
- 10.4 静态成员
- 11. 继承和多态
- 11.1 继承的核心概念
- 11.2 多态的深层理解
- 11.3 抽象类的设计哲学
1. C# 语言概述
1.1 什么是C#
C#(C Sharp)是微软公司在2000年推出的一种面向对象的编程语言,它是.NET平台的核心语言之一。C#的设计目标是结合C++的强大功能和Visual Basic的简单易用性,同时吸收Java语言的优点。
C#语言具有以下核心特征:
- 现代化语言设计:吸收了众多编程语言的优秀特性
- 强类型系统:在编译时进行严格的类型检查,减少运行时错误
- 自动内存管理:通过垃圾收集器自动管理内存,避免内存泄漏
- 丰富的标准库:.NET框架提供了庞大的类库支持
- 跨平台支持:通过.NET Core/.NET 5+实现跨平台运行
1.2 C# 与 C/Python 的对比
特性 | C | Python | C# |
---|---|---|---|
类型系统 | 静态类型 | 动态类型 | 静态类型 |
内存管理 | 手动管理 | 自动垃圾回收 | 自动垃圾回收 |
编译方式 | 编译为机器码 | 解释执行 | 编译为中间语言(IL) |
面向对象 | 结构化编程 | 多范式 | 纯面向对象 |
语法复杂度 | 中等 | 简单 | 中等 |
平台依赖性 | 平台相关 | 跨平台 | 跨平台(.NET Core/5+) |
与C语言的主要区别:
- C#无需手动管理内存,垃圾收集器会自动处理
- C#是纯面向对象语言,所有代码都必须在类中
- C#提供了更丰富的数据类型和现代语言特性
- C#有更强的类型安全性和异常处理机制
与Python的主要区别:
- C#是静态类型语言,需要在编译时确定变量类型
- C#的语法更加严格,需要明确声明变量类型
- C#的执行性能通常优于Python
- C#更适合大型企业级应用开发
1.3 C# 的特点
C#作为一门现代编程语言,具有以下显著特点:
类型安全:C#采用强类型系统,编译器会在编译时检查类型错误,这大大减少了运行时错误的发生。与C语言相比,C#避免了许多指针操作带来的安全隐患。
自动内存管理:C#使用垃圾收集器(Garbage Collector)自动管理内存,开发者无需像C语言那样手动分配和释放内存。这极大地简化了内存管理,避免了内存泄漏和野指针等问题。
面向对象编程:C#是纯面向对象语言,支持封装、继承、多态等面向对象的核心特性。与C语言的结构化编程相比,面向对象编程更适合构建复杂的应用程序。
丰富的类库:.NET框架提供了庞大的基础类库(Base Class Library),涵盖了文件操作、网络通信、数据库访问、图形界面等各个方面,大大提高了开发效率。
现代语言特性:C#支持泛型、LINQ、async/await、lambda表达式等现代编程特性,使代码更加简洁和高效。
2. 开发环境搭建
2.1 理解.NET生态系统
在开始C#编程之前,需要理解.NET(dot net)生态系统的基本概念:
.NET Framework vs .NET Core vs .NET 5+:
- .NET Framework:Windows专用的传统版本,功能完整但不跨平台
- .NET Core:跨平台的开源版本,性能更好但功能相对精简
- .NET 5+:统一的.NET平台,结合了Framework和Core的优点
运行时环境:
- CLR(Common Language Runtime):公共语言运行时,负责执行.NET程序
- BCL(Base Class Library):基础类库,提供常用的类和方法
- IL(Intermediate Language):中间语言,C#代码编译后的形式
2.2 安装 .NET SDK
SDK(Software Development Kit)是开发C#应用程序必需的工具包,包含了编译器、运行时和开发工具。
安装步骤:
- 访问 .NET 官网
- 选择适合您操作系统的最新版本
- 下载并按照安装向导进行安装
- 安装完成后,打开命令行工具验证安装
# 验证.NET SDK是否安装成功
dotnet --version# 查看已安装的.NET版本
dotnet --info# 查看可用的项目模板
dotnet new --list
2.3 开发工具选择
选择合适的开发工具对提高开发效率非常重要。以下是主流的C#开发工具:
工具 | 特点 | 适用场景 | 优缺点 |
---|---|---|---|
Visual Studio | 功能完整的IDE | 企业级开发 | 功能强大但体积大 |
Visual Studio Code | 轻量级编辑器 | 跨平台开发 | 轻量但需要插件支持 |
JetBrains Rider | 强大的IDE | 专业开发 | 功能丰富但需要付费 |
命令行 + 编辑器 | 最轻量 | 学习和简单项目 | 灵活但缺少高级功能 |
推荐配置:
- 初学者:Visual Studio Community(免费且功能完整)
- 跨平台开发:Visual Studio Code + C#扩展
- 专业开发:Visual Studio Professional或JetBrains Rider
2.4 创建第一个项目
理解C#项目的结构和创建过程是入门的重要步骤。
# 创建新的控制台应用程序
dotnet new console -n MyFirstApp# 进入项目目录
cd MyFirstApp# 查看项目结构
dir # Windows
ls # Linux/Mac# 运行程序
dotnet run# 构建程序
dotnet build
项目文件结构说明:
Program.cs
:主程序文件,包含Main方法MyFirstApp.csproj
:项目文件,定义项目配置和依赖obj/
:编译过程中的临时文件目录bin/
:编译后的可执行文件目录
3. 基础语法
3.1 程序结构分析
C#程序具有严格的结构要求,每个程序都必须遵循特定的组织方式。让我们详细分析一个基本的C#程序:
using System; // 引用命名空间namespace MyFirstApp // 命名空间
{class Program // 类定义{static void Main(string[] args) // 主方法{Console.WriteLine("Hello, World!");}}
}
命名空间(Namespace): 命名空间是组织代码的逻辑容器,用于避免类名冲突。using System;
语句告诉编译器我们要使用System命名空间中的类,这样我们就可以直接使用Console
类而不需要写成System.Console
。
类(Class): C#是纯面向对象语言,所有代码都必须放在类中。class Program
定义了一个名为Program的类,这是程序的入口点。
Main方法: Main
方法是程序的入口点,程序从这里开始执行。static
关键字表示这是一个静态方法,可以直接通过类名调用而不需要创建对象。
3.2 语法规则详解
C#的语法规则相对严格,这有助于减少编程错误并提高代码的可读性。
大小写敏感性: C#区分大小写,这意味着Variable
和variable
是完全不同的标识符。这一点与Python类似,但与某些不区分大小写的语言(如Visual Basic)不同。
int age = 25; // 变量名:age
int Age = 30; // 变量名:Age(与上面的age不同)
string NAME = "张三"; // 变量名:NAME
语句终止符: C#使用分号(;)作为语句终止符,这一点与C语言相同。每个语句都必须以分号结束,这是强制性的。
int x = 10; // 正确
string message = "Hello"; // 正确
Console.WriteLine("World"); // 正确// int y = 20 // 错误:缺少分号
代码块分组: C#使用花括号({})来组织代码块,这与C语言和Python的缩进式语法不同。花括号必须成对出现,每个开括号都必须有对应的闭括号。
注释系统: C#提供多种注释方式,用于解释代码和生成文档:
// 这是单行注释/* 这是多行注释可以跨越多行用于较长的说明 *//// <summary>
/// 这是XML文档注释
/// 用于生成API文档
/// </summary>
/// <param name="name">参数说明</param>
/// <returns>返回值说明</returns>
public string GetGreeting(string name)
{return $"Hello, {name}!";
}
3.3 命名规范
良好的命名规范是编写可维护代码的重要基础。C#社区广泛采用以下命名约定:
元素 | 约定 | 示例 | 说明 |
---|---|---|---|
类名 | PascalCase | StudentManager | 每个单词首字母大写 |
方法名 | PascalCase | GetStudentInfo() | 每个单词首字母大写 |
变量名 | camelCase | studentName | 首字母小写,后续单词首字母大写 |
常量名 | PascalCase | MaxValue | 每个单词首字母大写 |
私有字段 | _camelCase | _studentCount | 下划线前缀+驼峰命名 |
命名规范的重要性:
- 提高代码可读性
- 便于团队协作
- 符合行业标准
- 便于代码维护
// 良好的命名示例
public class StudentManager
{private int _studentCount;private const int MaxStudentCount = 1000;public string GetStudentName(int studentId){string fullName = FindStudentById(studentId);return fullName;}
}
4. 数据类型
4.1 类型系统概述
C#具有强类型系统,这意味着每个变量都必须有明确的数据类型,类型在编译时确定且不能随意改变。这与Python的动态类型系统形成鲜明对比。
C#的类型系统分为两大类:
- 值类型(Value Types):直接存储数据值
- 引用类型(Reference Types):存储对数据的引用
这种设计在内存管理和性能方面具有重要意义。值类型通常存储在栈上,访问速度快;引用类型存储在堆上,支持更复杂的数据结构。
4.2 值类型详解
值类型直接包含数据,当你将一个值类型变量赋值给另一个变量时,会创建数据的副本(这个副本是新的对象,不是新的引用)。
4.2.1 整数类型
C#提供了多种整数类型以满足不同的需求。选择合适的整数类型不仅影响内存使用,还影响程序的性能和数值范围。
类型 | 大小(字节) | 范围 | C语言对应 | 使用场景 |
---|---|---|---|---|
sbyte | 1 | -128 到 127 | signed char | 节省内存的小整数 |
byte | 1 | 0 到 255 | unsigned char | 字节数据、颜色值 |
short | 2 | -32,768 到 32,767 | short | 较小的整数 |
ushort | 2 | 0 到 65,535 | unsigned short | 无符号小整数 |
int | 4 | -2,147,483,648 到 2,147,483,647 | int | 最常用的整数类型 |
uint | 4 | 0 到 4,294,967,295 | unsigned int | 无符号整数 |
long | 8 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | long long | 大整数 |
ulong | 8 | 0 到 18,446,744,073,709,551,615 | unsigned long long | 无符号大整数 |
选择整数类型的原则:
- 一般情况下使用
int
类型,它提供了足够的范围和良好的性能 - 当需要节省内存时,可以选择
byte
或short
- 处理大数值时使用
long
- 处理文件大小、内存地址等总是非负的数值时使用无符号类型
// 整数类型示例
int studentCount = 150; // 学生数量
byte colorValue = 255; // 颜色值(0-255)
long fileSize = 1024L * 1024 * 1024; // 文件大小(1GB)
uint population = 1400000000U; // 人口数量(无符号)
4.2.2 浮点类型
浮点数用于表示带有小数部分的数值。C#提供了三种浮点类型,各有不同的精度和用途。
类型 | 大小(字节) | 精度 | 范围 | 适用场景 |
---|---|---|---|---|
float | 4 | 7位 | ±1.5 × 10^-45 到 ±3.4 × 10^38 | 游戏开发、3D图形 |
double | 8 | 15-17位 | ±5.0 × 10^-324 到 ±1.7 × 10^308 | 科学计算、一般数值计算 |
decimal | 16 | 28-29位 | ±1.0 × 10^-28 到 ±7.9 × 10^28 | 财务计算、精确小数 |
浮点类型选择指南:
float
:适用于对精度要求不高但对性能和内存有要求的场景double
:最常用的浮点类型,适用于大多数科学和工程计算decimal
:适用于财务计算等需要精确小数表示的场景
// 浮点类型示例
float gameSpeed = 1.5f; // 游戏速度(注意f后缀)
double pi = 3.14159265359; // 圆周率
decimal price = 19.99m; // 商品价格(注意m后缀)
decimal bankBalance = 1000.50m; // 银行余额
后缀:
类型 | 后缀 | 是否必须加后缀 |
---|---|---|
float | f /F | ✅ 必须加 |
double | d /D | ❌ 可选,因为小数默认是double |
decimal | m /M | ✅ 必须加 |
4.2.3 其他重要的值类型
类型 | 大小(字节) | 描述 | 示例 | 特点 |
---|---|---|---|---|
bool | 1 | 布尔值 | true , false | 逻辑判断 |
char | 2 | Unicode字符 | 'A' , '中' | 16位Unicode字符 |
DateTime | 8 | 日期时间 | DateTime.Now | 日期时间处理 |
Guid | 16 | 全局唯一标识符 | Guid.NewGuid() | 唯一标识 |
布尔类型在逻辑判断中至关重要,与C语言不同,C#的布尔类型只能是true
或false
,不能用数值0或1代替。
字符类型char
存储单个Unicode字符,占用2个字节,可以表示世界上大部分文字字符。
// 其他值类型示例
bool isStudent = true;
char grade = 'A';
char chineseChar = '中';
DateTime birthDate = new DateTime(1990, 5, 15);
Guid uniqueId = Guid.NewGuid();
4.3 引用类型详解
引用类型存储对象的引用(类似于python的引用),而不是对象本身。多个引用类型变量可以指向同一个对象。
类型 | 描述 | 特点 | 使用场景 |
---|---|---|---|
string | 字符串 | 不可变性 | 文本处理 |
object | 所有类型的基类 | 类型擦除 | 通用容器 |
dynamic | 动态类型 | 运行时类型检查 | 与动态语言互操作 |
数组 | 数组类型 | 固定大小 | 存储多个同类型数据 |
类 | 用户定义类型 | 自定义行为 | 业务逻辑封装 |
字符串类型是最常用的引用类型,具有不可变性,即一旦创建就不能修改。这意味着字符串操作会创建新的字符串对象。
string firstName = "张";
string lastName = "三";
string fullName = firstName + lastName; // 创建新的字符串对象
object类型是所有类型的根基类,可以存储任何类型的值,但会发生装箱和拆箱操作,影响性能。
dynamic类型提供了类似Python的动态类型功能,类型检查推迟到运行时。
dynamic dynamicVar = 123;
dynamicVar = "Hello"; // 运行时改变类型
dynamicVar = DateTime.Now; // 再次改变类型
比较:
特性 | C 语言指针 | C++ 引用 | Python 引用 | C# 引用(引用类型) |
---|---|---|---|---|
表示内容 | 是变量的地址 | 是变量的别名 | 是对象的引用 | 是托管堆对象的引用 |
是否能算术运算 | ✅ 可加减 | ❌ 不可 | ❌ 不可 | ❌ 不可 |
是否能重新绑定 | ✅ 可以 | ❌ 初始化后不可变 | ✅ 变量可重新绑定 | ✅ 引用变量可重新指向 |
是否能为空指针/null | ✅ NULL | ❌(标准引用不能为 null,存在可选类型std::optional<T&> 能持有无引用状态) | ✅ None | ✅ null |
是否能直接访问地址 | ✅ | ❌(但可用 & 获取地址) | ❌ | ❌(除非 unsafe ) |
是否自动垃圾回收 | ❌ 否 | ❌ 否 | ✅ 是 | ✅ 是(.NET GC) |
本质 | 地址值 | 已初始化对象的别名 | 对象引用(指向对象在内存中的位置) | 托管堆对象的引用(指向对象在托管堆中的位置) |
可否操作底层内存 | ✅ 可 | ❌ 不可 | ❌ 不可 | ❌ 不可(除非 unsafe) |
4.4 类型转换
类型转换是编程中的重要概念,C#提供了多种转换方式来处理不同类型之间的转换。
4.4.1 隐式转换
隐式转换是自动进行的,不会丢失数据或精度。编译器会自动完成这些转换。
// 隐式转换示例
int intValue = 123;
long longValue = intValue; // int → long(安全)
double doubleValue = intValue; // int → double(安全)
4.4.2 显式转换
显式转换需要程序员明确指定,可能会丢失数据或精度。
// 显式转换示例
double doubleValue = 123.45;
int intValue = (int)doubleValue; // 结果为123,小数部分被截断long longValue = 1234567890L;
int intValue2 = (int)longValue; // 可能溢出
4.4.3 使用转换方法
C#提供了多种转换方法,提供更好的控制和错误处理。
// Convert类转换
string strValue = "123";
int intValue = Convert.ToInt32(strValue);
double doubleValue = Convert.ToDouble("123.45");// Parse方法
int intValue2 = int.Parse("123");
DateTime dateValue = DateTime.Parse("2023-12-25");// TryParse方法(推荐使用)
string input = "123abc";
if (int.TryParse(input, out int result))
{Console.WriteLine($"转换成功:{result}");
}
else
{Console.WriteLine("转换失败");
}
转换方法的选择建议:
- 使用
TryParse
方法进行安全转换,避免异常 - 使用
Convert
类进行不同类型之间的转换 - 使用
Parse
方法当你确定转换会成功时
5. 变量和常量
5.1 变量的概念和声明
变量是程序中用于存储数据的命名内存位置。与C语言相比,C#的变量声明更加灵活,支持多种声明方式。
5.1.1 显式类型声明
显式类型声明要求明确指定变量的数据类型,这是最传统也是最清晰的声明方式。
// 基本类型声明
int age = 25; // 整数
double salary = 5000.50; // 浮点数
string name = "张三"; // 字符串
bool isEmployed = true; // 布尔值
DateTime hireDate = DateTime.Now; // 日期时间// 声明但不初始化
int count; // 值类型默认为0
string message; // 引用类型默认为null
5.1.2 隐式类型声明(var关键字)
var
关键字允许编译器根据初始值自动推断变量类型。这个特性类似于Python的变量声明,但仍然是静态类型。
// var关键字示例
var age = 25; // 编译器推断为int
var name = "李四"; // 编译器推断为string
var price = 19.99; // 编译器推断为double
var isActive = true; // 编译器推断为bool// 复杂类型也可以使用var
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var dictionary = new Dictionary<string, int>();
使用var的优点:
- 简化代码,特别是处理复杂类型名称时
- 减少代码重复
- 提高代码可读性(在类型明显的情况下)
使用var的限制:
- 必须在声明时初始化
- 不能用于方法参数
- 不能用于字段声明
// var的限制示例
// var uninitializedVar; // 错误:必须初始化
// public var ClassField; // 错误:不能用于字段public void Method(var parameter) // 错误:不能用于参数
{// 方法体
}
5.1.3 可空类型
可空类型允许值类型存储null值,这对于数据库编程和API设计非常有用。
// 可空值类型
int? nullableInt = null;
double? nullableDouble = null;
bool? nullableBool = true;// 可空引用类型(C# 8.0+)
string? nullableString = null;
List<int>? nullableList = null;// 检查可空类型
if (nullableInt.HasValue)
{Console.WriteLine($"值为:{nullableInt.Value}");
}
else
{Console.WriteLine("值为null");
}
5.2 常量的概念和使用
常量是在程序执行期间不能改变值的标识符。C#提供了两种常量类型:编译时常量和运行时常量。
5.2.1 编译时常量(const)
编译时常量在编译时就确定了值,必须在声明时初始化,且只能使用编译时可以确定的值。
// 编译时常量示例
const int MAX_STUDENTS = 100;
const double PI = 3.14159265359;
const string COMPANY_NAME = "微软公司";
const bool DEBUG_MODE = true;// 使用常量
public class Calculator
{const double E = 2.71828;public double CalculateCircleArea(double radius){return PI * radius * radius;}
}
5.2.2 运行时常量(readonly)
运行时常量在运行时确定值,可以在声明时或构造函数中初始化。
public class Configuration
{// 在声明时初始化readonly DateTime startTime = DateTime.Now;// 在构造函数中初始化readonly string connectionString;readonly int maxConnections;public Configuration(string connStr, int maxConn){connectionString = connStr;maxConnections = maxConn;}// 静态readonlystatic readonly int ProcessorCount = Environment.ProcessorCount;
}
const vs readonly 比较:
特性 | const | readonly |
---|---|---|
初始化时机 | 编译时 | 运行时 |
初始化位置 | 声明时 | 声明时或构造函数 |
性能 | 更快(直接替换) | 稍慢(内存访问) |
使用场景 | 编译时已知的值 | 运行时确定的值 |
5.3 变量作用域和生命周期
变量的作用域决定了变量在程序中的可见性和可访问性。理解作用域规则对于编写正确和高效的代码非常重要。
5.3.1 作用域层次
C# 中变量的“作用域”指的是变量在代码中可访问的范围。变量按定义位置可以分为以下几类作用域:
① 类级别作用域(字段)
private int classField = 10;
private static int staticField = 20;
classField
是实例字段,每个类实例都有自己的副本。staticField
是静态字段,属于整个类,而不是某个对象。- 这两个字段都可以在类的所有非静态方法或静态方法中使用(注意访问权限限制)。
② 方法级别作用域(局部变量)
public void DemostrateScope()
{int methodVariable = 30;
}
methodVariable
是定义在方法中的局部变量。- 只能在
DemostrateScope
方法中使用,方法外部无法访问。 - 每次调用方法都会重新创建这个变量。
③ 块级作用域(如:if语句、花括号内部)
if (true)
{int blockVariable = 40;Console.WriteLine(blockVariable); // ✅ 正确
}
Console.WriteLine(blockVariable); // ❌ 错误:不在作用域内
blockVariable
只在if
的花括号{}
内部有效。- 出了这个代码块就无法访问。
④ 循环作用域(如:for循环)
for (int i = 0; i < 3; i++)
{Console.WriteLine(i); // ✅ 正确
}
Console.WriteLine(i); // ❌ 错误:i 在 for 循环外无效
i
是在for
循环中声明的变量,只在for
的作用域内有效。- 和块作用域类似,出了循环就失效。
⑤ 方法之间的作用域隔离
public void AnotherMethod()
{Console.WriteLine(classField); // ✅ 可以访问类字段// Console.WriteLine(methodVariable); // ❌ 错误:methodVariable 只属于另一个方法
}
- 每个方法有自己的作用域。
- 方法中定义的变量不能跨方法访问,即使是同一个类中。
✅ 小结
变量位置 | 示例 | 可访问范围 |
---|---|---|
类字段 | classField | 整个类内部 |
静态字段 | staticField | 整个类内部(静态上下文也能用) |
方法变量 | methodVariable | 定义它的方法中 |
块变量 | blockVariable | 花括号内 |
循环变量 | i | for 循环内 |
方法间变量隔离 | —— | 一个方法中定义的变量其他方法不能访问 |
public class ScopeExample
{// 类级别字段 - 在整个类中可访问private int classField = 10;private static int staticField = 20;public void DemostrateScope(){// 方法级别变量 - 在整个方法中可访问int methodVariable = 30;Console.WriteLine($"类字段:{classField}");Console.WriteLine($"静态字段:{staticField}");Console.WriteLine($"方法变量:{methodVariable}");// 代码块作用域if (true){// 块级别变量 - 只在此代码块中可访问int blockVariable = 40;Console.WriteLine($"块变量:{blockVariable}");// 可以访问外层作用域的变量Console.WriteLine($"访问方法变量:{methodVariable}");Console.WriteLine($"访问类字段:{classField}");}// Console.WriteLine(blockVariable); // 错误:blockVariable不在作用域内// 循环作用域for (int i = 0; i < 3; i++){// i 只在for循环内可见Console.WriteLine($"循环变量:{i}");}// Console.WriteLine(i); // 错误:i不在作用域内}public void AnotherMethod(){// 可以访问类级别的字段Console.WriteLine($"另一个方法中访问类字段:{classField}");// 但不能访问其他方法的局部变量// Console.WriteLine(methodVariable); // 错误:不在作用域内}
}
5.3.2 变量隐藏
当内层作用域中的变量名与外层作用域中的变量名相同时,会发生变量隐藏(Variable Shadowing)。
public class ShadowingExample
{private int value = 100; // 类字段public void DemonstrateShadowing(){int value = 200; // 方法局部变量,隐藏了类字段Console.WriteLine($"局部变量 value: {value}"); // 输出:200Console.WriteLine($"类字段 value: {this.value}"); // 输出:100{int value = 300; // 块级变量,隐藏了方法变量Console.WriteLine($"块级变量 value: {value}"); // 输出:300}Console.WriteLine($"方法变量 value: {value}"); // 输出:200}
}
5.3.3 静态变量 vs 实例变量
理解静态变量和实例变量的区别对于正确使用内存和设计类结构非常重要。
- 静态字段属于类,所有对象共享一份,适合保存全局状态,比如员工总数和公司名。
- 实例字段属于每个对象,每个员工有自己的 ID、姓名和薪资。
- 构造函数创建对象时,给实例字段赋值,同时更新静态字段(员工总数自增)。
- 静态属性用来访问类级别的信息,实例属性访问对象自身的数据。
- 静态成员和实例成员相互独立,静态成员不能直接访问实例成员,实例成员可以访问静态成员。
public class Employee
{// 静态字段 - 属于类,所有实例共享private static int totalEmployees = 0;private static readonly string CompanyName = "科技公司";// 实例字段 - 属于对象,每个实例都有自己的副本private int employeeId;private string name;private decimal salary;public Employee(string name, decimal salary){this.name = name;this.salary = salary;this.employeeId = ++totalEmployees; // 自动分配员工ID}// 静态属性public static int TotalEmployees => totalEmployees;public static string Company => CompanyName;// 实例属性public int EmployeeId => employeeId;public string Name => name;public decimal Salary => salary;public void DisplayInfo(){Console.WriteLine($"员工ID: {employeeId}");Console.WriteLine($"姓名: {name}");Console.WriteLine($"薪资: {salary:C}");Console.WriteLine($"公司: {CompanyName}");Console.WriteLine($"总员工数: {totalEmployees}");}
}// 使用示例
Employee emp1 = new Employee("张三", 8000);
Employee emp2 = new Employee("李四", 9500);Console.WriteLine($"当前总员工数: {Employee.TotalEmployees}"); // 输出:2
6. 运算符
6.1 运算符概述
运算符是用于操作数据的符号,C#提供了丰富的运算符集合。与C语言相比,C#增加了一些新的运算符,如空合并运算符,同时保持了大部分C语言运算符的语义。
运算符按功能可以分为以下几类:
- 算术运算符:用于数学计算
- 比较运算符:用于比较值
- 逻辑运算符:用于逻辑判断
- 位运算符:用于位级操作
- 赋值运算符:用于赋值操作
- 特殊运算符:如空合并、类型检查等
6.2 算术运算符
算术运算符用于执行基本的数学运算,与C语言的算术运算符基本相同。
运算符 | 描述 | 示例 | 结果类型 |
---|---|---|---|
+ | 加法 | a + b | 取决于操作数 |
- | 减法 | a - b | 取决于操作数 |
* | 乘法 | a * b | 取决于操作数 |
/ | 除法 | a / b | 取决于操作数 |
% | 取模 | a % b | 取决于操作数 |
++ | 自增 | ++a 或 a++ | 与操作数相同 |
-- | 自减 | --a 或 a-- | 与操作数相同 |
// 基本算术运算
int a = 10, b = 3;
Console.WriteLine($"加法: {a} + {b} = {a + b}"); // 13
Console.WriteLine($"减法: {a} - {b} = {a - b}"); // 7
Console.WriteLine($"乘法: {a} * {b} = {a * b}"); // 30
Console.WriteLine($"除法: {a} / {b} = {a / b}"); // 3 (整数除法)
Console.WriteLine($"取模: {a} % {b} = {a % b}"); // 1// 浮点除法
double x = 10.0, y = 3.0;
Console.WriteLine($"浮点除法: {x} / {y} = {x / y}"); // 3.3333...// 自增和自减
int count = 5;
Console.WriteLine($"前缀自增: ++count = {++count}"); // 6, count变为6
Console.WriteLine($"后缀自增: count++ = {count++}"); // 6, 然后count变为7
Console.WriteLine($"当前count值: {count}"); // 7
重要注意事项:
- 整数除法会截断小数部分
- 除零会抛出
DivideByZeroException
异常 - 自增和自减运算符的前缀和后缀形式有不同的求值顺序
6.3 比较运算符
比较运算符用于比较两个值,返回布尔类型的结果。
运算符 | 描述 | 示例 | 注意事项 |
---|---|---|---|
== | 相等 | a == b | 值相等比较 |
!= | 不等 | a != b | 值不等比较 |
< | 小于 | a < b | 数值比较 |
> | 大于 | a > b | 数值比较 |
<= | 小于等于 | a <= b | 数值比较 |
>= | 大于等于 | a >= b | 数值比较 |
// 数值比较
int x = 10, y = 20;
Console.WriteLine($"{x} == {y}: {x == y}"); // False
Console.WriteLine($"{x} != {y}: {x != y}"); // True
Console.WriteLine($"{x} < {y}: {x < y}"); // True
Console.WriteLine($"{x} > {y}: {x > y}"); // False// 字符串比较
string str1 = "Hello";
string str2 = "Hello";
string str3 = "World";
Console.WriteLine($"str1 == str2: {str1 == str2}"); // True
Console.WriteLine($"str1 == str3: {str1 == str3}"); // False// 引用类型比较需要注意
object obj1 = new object();
object obj2 = new object();
object obj3 = obj1;
Console.WriteLine($"obj1 == obj2: {obj1 == obj2}"); // False (不同对象)
Console.WriteLine($"obj1 == obj3: {obj1 == obj3}"); // True (同一对象)
6.4 逻辑运算符
逻辑运算符用于组合布尔表达式,支持短路求值。
运算符 | 描述 | 示例 | 短路特性 |
---|---|---|---|
&& | 逻辑与 | a && b | 如果a为false,不评估b |
` | ` | 逻辑或 | |
! | 逻辑非 | !a | 无 |
// 逻辑运算基础
bool isStudent = true;
bool hasValidId = false;
bool isAdult = true;Console.WriteLine($"是学生且有有效ID: {isStudent && hasValidId}"); // False
Console.WriteLine($"是学生或是成人: {isStudent || isAdult}"); // True
Console.WriteLine($"不是学生: {!isStudent}"); // False// 短路求值演示
bool CheckCondition()
{Console.WriteLine("CheckCondition被调用");return true;
}bool result1 = false && CheckCondition(); // CheckCondition不会被调用
bool result2 = true || CheckCondition(); // CheckCondition不会被调用
bool result3 = true && CheckCondition(); // CheckCondition会被调用
6.5 位运算符
位运算符用于对整数类型进行位级操作,这在底层编程和性能优化中很有用。
运算符 | 描述 | 示例 | 用途 |
---|---|---|---|
& | 按位与 | a & b | 掩码操作 |
` | ` | 按位或 | `a |
^ | 按位异或 | a ^ b | 切换位 |
~ | 按位非 | ~a | 取反 |
<< | 左移 | a << 2 | 快速乘法 |
>> | 右移 | a >> 2 | 快速除法 |
// 位运算示例
int a = 12; // 二进制: 1100
int b = 10; // 二进制: 1010Console.WriteLine($"a & b = {a & b}"); // 8 (1000)
Console.WriteLine($"a | b = {a | b}"); // 14 (1110)
Console.WriteLine($"a ^ b = {a ^ b}"); // 6 (0110)
Console.WriteLine($"~a = {~a}"); // -13
Console.WriteLine($"a << 2 = {a << 2}"); // 48 (左移2位相当于乘以4)
Console.WriteLine($"a >> 2 = {a >> 2}"); // 3 (右移2位相当于除以4)// 位运算的实际应用:权限系统
[Flags]
enum Permission
{None = 0, // 0000Read = 1, // 0001Write = 2, // 0010Execute = 4, // 0100Delete = 8 // 1000
}Permission userPermission = Permission.Read | Permission.Write;
Console.WriteLine($"用户权限: {userPermission}");// 检查是否有读权限
bool hasReadPermission = (userPermission & Permission.Read) != 0;
Console.WriteLine($"有读权限: {hasReadPermission}");// 添加执行权限
userPermission |= Permission.Execute;
Console.WriteLine($"添加执行权限后: {userPermission}");// 移除写权限
userPermission &= ~Permission.Write;
Console.WriteLine($"移除写权限后: {userPermission}");
6.6 赋值运算符
赋值运算符用于给变量赋值,C#提供了基本赋值和复合赋值运算符。
// 基本赋值
int x = 10;// 复合赋值运算符
x += 5; // 等同于 x = x + 5; 现在x = 15
x -= 3; // 等同于 x = x - 3; 现在x = 12
x *= 2; // 等同于 x = x * 2; 现在x = 24
x /= 4; // 等同于 x = x / 4; 现在x = 6
x %= 4; // 等同于 x = x % 4; 现在x = 2// 位运算复合赋值
x |= 8; // 等同于 x = x | 8;
x &= 15; // 等同于 x = x & 15;
x ^= 5; // 等同于 x = x ^ 5;
x <<= 1; // 等同于 x = x << 1;
x >>= 2; // 等同于 x = x >> 2;
6.7 空合并运算符
空合并运算符是C#特有的运算符,用于处理null值,这在处理可空类型时非常有用。
-
空合并运算符
??
-
用于判断左侧表达式是否为
null
。 -
如果左侧不为
null
,返回左侧值;否则返回右侧默认值。 -
常用于为可能为空的变量提供默认值。
-
-
链式空合并运算符
-
可以连续使用多个
??
判断多个变量,返回第一个非空的值。 -
适合从多个候选项中选取有效值。
-
-
空合并赋值运算符
??=
(C# 8.0+)-
只有当左侧变量为
null
时,才给它赋值。 -
等同于:
variable = variable ?? value;
-
简化了对变量是否为
null
的判断和赋值操作。
-
-
空条件运算符
?.
-
用于安全访问可能为
null
的对象成员或方法。 -
如果左侧对象为
null
,整个表达式结果为null
,避免抛出空引用异常。 -
适用于链式访问,简化了大量的空值判断。
-
-
空条件运算符链式调用
-
可多层链式调用,任一环节为
null
,整个结果为null
。 -
方便访问复杂对象结构中可能为
null
的成员。
-
总结
??
解决“取值或默认值”的问题。??=
用于“如果为空才赋值”。?.
用于“安全访问成员”,防止空引用异常。- 这三种运算符结合使用,能大幅提升代码的简洁性和安全性。
// 空合并运算符 (??)
string name = null;
string displayName = name ?? "未知用户";
Console.WriteLine($"显示名称: {displayName}"); // 输出:未知用户// 链式使用
string firstName = null;
string lastName = null;
string nickName = "小明";
string finalName = firstName ?? lastName ?? nickName ?? "无名氏";
Console.WriteLine($"最终名称: {finalName}"); // 输出:小明// 空合并赋值运算符 (??=) C# 8.0+
string message = null;
message ??= "默认消息"; // 等同于 message = message ?? "默认消息"
Console.WriteLine(message); // 输出:默认消息message ??= "新消息"; // message已经不是null,所以不会被赋值
Console.WriteLine(message); // 输出:默认消息// 空条件运算符 (?.)
string text = null;
int? length = text?.Length; // 如果text为null,则length为null
Console.WriteLine($"文本长度: {length?.ToString() ?? "null"}");// 空条件运算符的链式调用
class Person
{public string Name { get; set; }public Address Address { get; set; }
}class Address
{public string City { get; set; }
}Person person = null;
string city = person?.Address?.City; // 安全的链式调用
Console.WriteLine($"城市: {city ?? "未知"}");
6.8 运算符优先级
理解运算符优先级对于编写正确的表达式至关重要。以下是主要运算符的优先级(从高到低):
优先级 | 运算符 | 说明 |
---|---|---|
1 | () [] . | 括号、数组索引、成员访问 |
2 | ++ -- + - ! ~ | 一元运算符 |
3 | * / % | 乘法运算符 |
4 | + - | 加法运算符 |
5 | << >> | 移位运算符 |
6 | < <= > >= | 关系运算符 |
7 | == != | 相等运算符 |
8 | & | 按位与 |
9 | ^ | 按位异或 |
10 | ` | ` |
11 | && | 逻辑与 |
12 | ` | |
13 | ?? | 空合并 |
14 | ?: | 条件运算符 |
15 | = += -= 等 | 赋值运算符 |
// 优先级示例
int result1 = 2 + 3 * 4; // 14,不是20 (乘法优先)
int result2 = (2 + 3) * 4; // 20 (括号改变优先级)
bool result3 = true || false && false; // true (&&优先于||)
bool result4 = (true || false) && false; // false// 建议使用括号提高可读性
int complexResult = ((a + b) * c) / ((d - e) + f);
7. 控制结构
7.1 控制结构概述
控制结构是程序设计的基础,它们决定了程序的执行流程。C#提供了完整的控制结构,包括条件语句、循环语句和跳转语句。与C语言相比,C#的控制结构在语法上基本相同,但增加了一些现代化的特性,如switch表达式。
控制结构可以分为三大类:
- 选择结构:根据条件选择不同的执行路径
- 循环结构:重复执行特定的代码块
- 跳转结构:改变程序的正常执行顺序
7.2 条件语句
条件语句允许程序根据不同的条件执行不同的代码分支,这是实现程序逻辑的核心机制。
7.2.1 if语句
if语句是最基本的条件控制结构,它根据布尔表达式的值来决定是否执行特定的代码块。
// 基本if语句
int age = 18;
if (age >= 18)
{Console.WriteLine("您已成年");
}// if-else语句
int score = 85;
if (score >= 90)
{Console.WriteLine("优秀");
}
else if (score >= 80)
{Console.WriteLine("良好");
}
else if (score >= 60)
{Console.WriteLine("及格");
}
else
{Console.WriteLine("不及格");
}// 嵌套if语句
bool isStudent = true;
bool hasDiscount = false;if (isStudent)
{if (hasDiscount){Console.WriteLine("学生优惠价格");}else{Console.WriteLine("学生正常价格");}
}
else
{Console.WriteLine("普通价格");
}// 三元运算符(条件运算符)
string status = age >= 18 ? "成年人" : "未成年人";
int maxValue = a > b ? a : b;
if语句的最佳实践:
- 总是使用花括号,即使只有一条语句
- 避免过深的嵌套,考虑使用早期返回
- 条件表达式应该简洁明了
// 良好的实践:早期返回
public string GetGradeDescription(int score)
{if (score < 0 || score > 100){return "无效分数";}if (score >= 90){return "优秀";}if (score >= 80){return "良好";}if (score >= 60){return "及格";}return "不及格";
}
7.2.2 switch语句
switch语句提供了一种更清晰的方式来处理多个离散值的条件判断。
(1)传统的switch:
// 传统switch语句
string dayOfWeek = "Monday";
string dayInChinese;switch (dayOfWeek)
{case "Monday":dayInChinese = "星期一";break;case "Tuesday":dayInChinese = "星期二";break;case "Wednesday":dayInChinese = "星期三";break;case "Thursday":dayInChinese = "星期四";break;case "Friday":dayInChinese = "星期五";break;case "Saturday":case "Sunday":dayInChinese = "周末";break;default:dayInChinese = "未知";break;
}
(2) switch表达式 (C# 8.0+) - 更简洁的语法
string dayInChinese2 = dayOfWeek switch
{"Monday" => "星期一","Tuesday" => "星期二","Wednesday" => "星期三","Thursday" => "星期四","Friday" => "星期五","Saturday" or "Sunday" => "周末",_ => "未知"
};
(3)基于类型的switch (C# 7.0+)
object value = 42;
string description = value switch
{int i when i > 0 => $"正整数: {i}",int i when i < 0 => $"负整数: {i}",int => "零",string s => $"字符串: {s}",bool b => $"布尔值: {b}",_ => "未知类型"
};
(4)元组模式匹配 (C# 8.0+)
public string GetQuadrant(int x, int y) => (x, y) switch
{(> 0, > 0) => "第一象限",(< 0, > 0) => "第二象限",(< 0, < 0) => "第三象限",(> 0, < 0) => "第四象限",(0, 0) => "原点",(0, _) => "X轴",(_, 0) => "Y轴"
};
7.3 循环语句
循环语句允许程序重复执行特定的代码块,直到满足某个条件为止。
7.3.1 for循环
for循环是最常用的循环结构,特别适合已知循环次数的情况。
// 基本for循环
for (int i = 0; i < 10; i++)
{Console.WriteLine($"第{i + 1}次循环");
}// 多变量for循环
for (int i = 0, j = 10; i < j; i++, j--)
{Console.WriteLine($"i={i}, j={j}");
}// 无限循环(需要内部break)
for (;;)
{string input = Console.ReadLine();if (input == "quit")break;Console.WriteLine($"您输入了: {input}");
}// for循环的实际应用:数组处理
int[] numbers = { 1, 2, 3, 4, 5 };
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
{sum += numbers[i];Console.WriteLine($"numbers[{i}] = {numbers[i]}, 累计和 = {sum}");
}// 嵌套for循环:二维数组
int[,] matrix = {{ 1, 2, 3 },{ 4, 5, 6 },{ 7, 8, 9 }
};for (int row = 0; row < matrix.GetLength(0); row++)
{for (int col = 0; col < matrix.GetLength(1); col++){Console.Write($"{matrix[row, col]} ");}Console.WriteLine(); // 换行
}
7.3.2 while循环
while循环在条件为真时重复执行代码块,适合不确定循环次数的情况。
// 基本while循环
int count = 0;
while (count < 5)
{Console.WriteLine($"Count: {count}");count++;
}// 用户输入验证
int userInput;
while (true)
{Console.Write("请输入一个正数(输入0退出):");if (int.TryParse(Console.ReadLine(), out userInput)){if (userInput == 0)break;if (userInput > 0){Console.WriteLine($"您输入的正数是:{userInput}");break;}else{Console.WriteLine("请输入正数!");}}else{Console.WriteLine("请输入有效的数字!");}
}// 读取文件直到结束
using (var reader = new StreamReader("data.txt"))
{string line;while ((line = reader.ReadLine()) != null){Console.WriteLine(line);}
}
7.3.3 do-while循环
do-while循环先执行一次代码块,然后检查条件,保证至少执行一次。
// 基本do-while循环
int number;
do
{Console.Write("请输入一个1-10之间的数字:");number = int.Parse(Console.ReadLine());if (number < 1 || number > 10){Console.WriteLine("输入范围不正确,请重新输入!");}
} while (number < 1 || number > 10);Console.WriteLine($"您输入的有效数字是:{number}");// 菜单系统示例
char choice;
do
{Console.WriteLine("\n=== 主菜单 ===");Console.WriteLine("1. 新建文件");Console.WriteLine("2. 打开文件");Console.WriteLine("3. 保存文件");Console.WriteLine("4. 退出");Console.Write("请选择(1-4): ");choice = Console.ReadKey().KeyChar;Console.WriteLine();switch (choice){case '1':Console.WriteLine("新建文件...");break;case '2':Console.WriteLine("打开文件...");break;case '3':Console.WriteLine("保存文件...");break;case '4':Console.WriteLine("程序退出...");break;default:Console.WriteLine("无效选择,请重新选择!");break;}
} while (choice != '4');
7.3.4 foreach循环
foreach循环是C#特有的循环结构,用于遍历集合或数组,语法简洁且安全。
// 遍历数组
string[] fruits = { "苹果", "香蕉", "橘子", "葡萄" };
foreach (string fruit in fruits)
{Console.WriteLine($"水果:{fruit}");
}// 遍历List
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach (int number in numbers)
{Console.WriteLine($"数字:{number}");
}// 遍历Dictionary
Dictionary<string, int> ages = new Dictionary<string, int>
{{ "张三", 25 },{ "李四", 30 },{ "王五", 28 }
};foreach (KeyValuePair<string, int> pair in ages)
{Console.WriteLine($"{pair.Key}的年龄是{pair.Value}岁");
}// 使用var简化语法
foreach (var pair in ages)
{Console.WriteLine($"{pair.Key}的年龄是{pair.Value}岁");
}// 遍历字符串(字符串是字符的集合)
string text = "Hello";
foreach (char c in text)
{Console.WriteLine($"字符:{c}");
}// 带索引的遍历(需要时使用for循环或LINQ)
for (int i = 0; i < fruits.Length; i++)
{Console.WriteLine($"索引{i}:{fruits[i]}");
}
7.4 跳转语句
跳转语句用于改变程序的正常执行流程,提供了更灵活的控制方式。
7.4.1 break语句
break语句用于立即退出最近的循环或switch语句。
// 在for循环中使用break
for (int i = 0; i < 10; i++)
{if (i == 5){Console.WriteLine("遇到5,退出循环");break;}Console.WriteLine($"i = {i}");
}// 在while循环中使用break
int count = 0;
while (true) // 无限循环
{count++;Console.WriteLine($"计数:{count}");if (count >= 3){Console.WriteLine("达到限制,退出循环");break;}
}// 在嵌套循环中使用break(只退出内层循环)
for (int i = 0; i < 3; i++)
{Console.WriteLine($"外层循环:{i}");for (int j = 0; j < 5; j++){if (j == 2){Console.WriteLine(" 内层循环遇到2,退出内层");break; // 只退出内层循环}Console.WriteLine($" 内层循环:{j}");}
}// 需要退出多层循环时使用标志变量或goto
bool shouldExit = false;
for (int i = 0; i < 3 && !shouldExit; i++)
{for (int j = 0; j < 5; j++){if (i == 1 && j == 2){shouldExit = true;break;}Console.WriteLine($"i={i}, j={j}");}
}
7.4.2 continue语句
continue语句用于跳过当前循环迭代的剩余部分,直接进入下一次迭代。
// 在for循环中使用continue
for (int i = 0; i < 10; i++)
{if (i % 2 == 0){continue; // 跳过偶数}Console.WriteLine($"奇数:{i}");
}// 在foreach循环中使用continue
string[] words = { "hello", "", "world", "test", "" };
foreach (string word in words)
{if (string.IsNullOrEmpty(word)){continue; // 跳过空字符串}Console.WriteLine($"单词:{word}");
}// 处理数据时跳过无效项
List<int> numbers = new List<int> { 1, -2, 3, -4, 5, 0, 7 };
int sum = 0;
int validCount = 0;foreach (int number in numbers)
{if (number <= 0){Console.WriteLine($"跳过非正数:{number}");continue;}sum += number;validCount++;Console.WriteLine($"添加正数:{number},当前和:{sum}");
}Console.WriteLine($"正数总和:{sum},正数个数:{validCount}");
7.4.3 return语句
return语句用于从方法中返回值并结束方法的执行。
// 有返回值的方法
public int FindMax(int[] array)
{if (array == null || array.Length == 0){return -1; // 早期返回}int max = array[0];for (int i = 1; i < array.Length; i++){if (array[i] > max){max = array[i];}}return max;
}// 无返回值的方法中使用return
public void ProcessData(string data)
{if (string.IsNullOrEmpty(data)){Console.WriteLine("数据为空,无法处理");return; // 提前结束方法}// 处理数据的逻辑Console.WriteLine($"处理数据:{data}");if (data.Length > 100){Console.WriteLine("数据过长,停止处理");return; // 条件性早期返回}Console.WriteLine("数据处理完成");
}// 使用return简化复杂逻辑
public string GetDiscountLevel(decimal amount, bool isMember)
{if (amount < 100){return "无折扣";}if (!isMember){return amount >= 500 ? "普通折扣" : "小额折扣";}if (amount >= 1000){return "VIP折扣";}return "会员折扣";
}
7.4.4 goto语句(谨慎使用)
虽然goto语句在现代编程中很少使用,但在某些特殊情况下仍然有用。
// goto的基本用法(一般不推荐)
int i = 0;
start:
Console.WriteLine($"i = {i}");
i++;
if (i < 3)goto start;// goto在错误处理中的应用
public bool ProcessFile(string filename)
{FileStream file = null;StreamReader reader = null;try{file = new FileStream(filename, FileMode.Open);if (file == null)goto cleanup;reader = new StreamReader(file);if (reader == null)goto cleanup;// 处理文件...string content = reader.ReadToEnd();Console.WriteLine(content);return true;}catch (Exception ex){Console.WriteLine($"错误:{ex.Message}");goto cleanup;}cleanup:reader?.Close();file?.Close();return false;
}// 更好的替代方案:using语句
public bool ProcessFileWithUsing(string filename)
{try{using (var file = new FileStream(filename, FileMode.Open))using (var reader = new StreamReader(file)){string content = reader.ReadToEnd();Console.WriteLine(content);return true;}}catch (Exception ex){Console.WriteLine($"错误:{ex.Message}");return false;}
}
7.5 控制结构的最佳实践
在使用控制结构时,应该遵循以下最佳实践:
可读性优先:
// 好的做法:清晰的条件表达式
if (user != null && user.IsActive && user.HasPermission)
{ProcessUser(user);
}// 避免:复杂的嵌套条件
if (user != null)
{if (user.IsActive){if (user.HasPermission){ProcessUser(user);}}
}
避免深层嵌套:
// 好的做法:早期返回
public string ValidateUser(User user)
{if (user == null)return "用户不能为空";if (string.IsNullOrEmpty(user.Name))return "用户名不能为空";if (user.Age < 0)return "年龄不能为负数";return "验证通过";
}// 避免:深层嵌套
public string ValidateUserBad(User user)
{if (user != null){if (!string.IsNullOrEmpty(user.Name)){if (user.Age >= 0){return "验证通过";}else{return "年龄不能为负数";}}else{return "用户名不能为空";}}else{return "用户不能为空";}
}
选择合适的循环类型:
// 已知循环次数:使用for
for (int i = 0; i < 10; i++) { }// 遍历集合:使用foreach
foreach (var item in collection) { }// 条件循环:使用while
while (condition) { }// 至少执行一次:使用do-while
do { } while (condition);
8. 数组和集合
8.1 数组概述
数组是存储相同类型多个元素的数据结构。在C#中,数组是引用类型,这意味着数组变量存储的是对数组对象的引用,而不是数组本身。与C语言相比,C#的数组更加安全,提供了边界检查和丰富的操作方法。
数组具有以下特点:
- 类型安全:只能存储指定类型的元素
- 固定大小:一旦创建,大小不能改变
- 索引访问:通过索引快速访问元素
- 引用类型:数组变量存储引用,多个变量可以引用同一个数组
8.2 一维数组
一维数组是最基本的数组类型,元素按线性顺序存储。
8.2.1 数组的声明和初始化
// 声明数组变量
int[] numbers; // 声明int类型数组
string[] names; // 声明string类型数组// 创建数组实例
numbers = new int[5]; // 创建包含5个元素的数组,初始值为0
names = new string[3]; // 创建包含3个元素的数组,初始值为null// 声明并初始化
int[] scores = new int[4]; // 创建4个元素的数组
double[] prices = new double[] { 19.99, 29.99, 39.99 }; // 创建并初始化// 简化的初始化语法
int[] values = { 1, 2, 3, 4, 5 }; // 编译器自动推断大小
string[] fruits = { "苹果", "香蕉", "橘子" };// 使用var关键字
var grades = new int[] { 85, 90, 78, 92, 88 };
var cities = new[] { "北京", "上海", "广州", "深圳" }; // 编译器推断为string[]
8.2.2 数组的访问和操作
- 访问数组元素
- 使用索引访问,索引从 0 开始。
- 最后一个元素的索引是
Length - 1
。
- 修改数组元素
- 可通过索引修改指定位置的值,赋新值即可。
- 数组的常用属性
Length
:元素个数。Rank
:数组的维数,一维数组为 1。
- 遍历数组的两种方式
for
循环:适合需要索引的位置。foreach
循环:适合只处理元素值的场景。
- 数组的复制方法
Array.Copy
:从源数组复制到目标数组。Clone
:返回一个新数组,是浅拷贝。CopyTo
:将当前数组内容复制到另一个数组中。
int[] numbers = { 10, 20, 30, 40, 50 };// 访问数组元素
Console.WriteLine($"第一个元素:{numbers[0]}"); // 输出:10
Console.WriteLine($"最后一个元素:{numbers[numbers.Length - 1]}"); // 输出:50// 修改数组元素
numbers[0] = 100;
numbers[2] = 300;
Console.WriteLine($"修改后的第一个元素:{numbers[0]}"); // 输出:100// 数组属性和方法
Console.WriteLine($"数组长度:{numbers.Length}"); // 输出:5
Console.WriteLine($"数组排名(维度):{numbers.Rank}"); // 输出:1// 遍历数组
Console.WriteLine("使用for循环遍历:");
for (int i = 0; i < numbers.Length; i++)
{Console.WriteLine($"numbers[{i}] = {numbers[i]}");
}Console.WriteLine("使用foreach循环遍历:");
foreach (int number in numbers)
{Console.WriteLine($"值:{number}");
}// 数组的复制
int[] originalArray = { 1, 2, 3, 4, 5 };
int[] copiedArray = new int[originalArray.Length];// 方法1:使用Array.Copy
Array.Copy(originalArray, copiedArray, originalArray.Length);// 方法2:使用Clone(浅拷贝)
int[] clonedArray = (int[])originalArray.Clone();// 方法3:使用CopyTo
int[] anotherCopy = new int[originalArray.Length];
originalArray.CopyTo(anotherCopy, 0);
8.2.3 数组的常用操作
- 查找操作
- 使用
Array.IndexOf
可以查找某个元素第一次出现的索引。 - 使用
Array.Exists
可判断是否存在满足条件的元素,例如是否有大于 5 的数。 - 使用
Array.Find
可以返回第一个满足条件的元素值。
- 使用
- 排序操作
- 使用
Array.Sort
可以对数组进行升序排序。 - 排序前若不想改变原数组,可先用
Clone
创建副本再排序。
- 使用
- 反转操作
- 使用
Array.Reverse
可将数组元素顺序反转。 - 同样建议先复制数组,再进行反转操作以保留原数组。
- 使用
- 清空操作
- 使用
Array.Clear
可将指定范围内的元素设置为类型的默认值(如int
为 0)。 - 可以一次性清空整个数组的内容。
- 使用
int[] numbers = { 3, 1, 4, 1, 5, 9, 2, 6 };// 查找操作
int searchValue = 4;
int index = Array.IndexOf(numbers, searchValue);
Console.WriteLine($"{searchValue}的索引:{index}");bool exists = Array.Exists(numbers, x => x > 5);
Console.WriteLine($"是否存在大于5的数:{exists}");int firstLarge = Array.Find(numbers, x => x > 5);
Console.WriteLine($"第一个大于5的数:{firstLarge}");// 排序操作
int[] sortedNumbers = (int[])numbers.Clone();
Array.Sort(sortedNumbers);
Console.WriteLine($"排序后:{string.Join(", ", sortedNumbers)}");// 反转操作
int[] reversedNumbers = (int[])numbers.Clone();
Array.Reverse(reversedNumbers);
Console.WriteLine($"反转后:{string.Join(", ", reversedNumbers)}");// 清空操作
Array.Clear(numbers, 0, numbers.Length); // 将所有元素设置为默认值
Console.WriteLine($"清空后:{string.Join(", ", numbers)}");
8.3 多维数组
多维数组允许存储多维度的数据,如矩阵、表格等。
8.3.1 二维数组
// 声明和初始化二维数组
int[,] matrix = new int[3, 4]; // 3行4列的矩阵// 初始化时赋值
int[,] table = {{ 1, 2, 3, 4 },{ 5, 6, 7, 8 },{ 9, 10, 11, 12 }
};// 另一种初始化方式
int[,] grid = new int[,] {{ 1, 2 },{ 3, 4 },{ 5, 6 }
};// 访问和修改元素
Console.WriteLine($"第2行第3列的元素:{table[1, 2]}"); // 输出:7
table[0, 0] = 100;
Console.WriteLine($"修改后的第1行第1列:{table[0, 0]}"); // 输出:100// 获取维度信息
Console.WriteLine($"行数:{table.GetLength(0)}"); // 输出:3
Console.WriteLine($"列数:{table.GetLength(1)}"); // 输出:4
Console.WriteLine($"总元素数:{table.Length}"); // 输出:12// 遍历二维数组
Console.WriteLine("遍历二维数组:");
for (int row = 0; row < table.GetLength(0); row++)
{for (int col = 0; col < table.GetLength(1); col++){Console.Write($"{table[row, col],4}");}Console.WriteLine();
}// 使用foreach遍历(按存储顺序)
Console.WriteLine("使用foreach遍历:");
foreach (int value in table)
{Console.Write($"{value} ");
}
Console.WriteLine();
8.3.2 三维及更高维数组
// 三维数组:例如表示RGB颜色的立方体
int[,,] colorCube = new int[256, 256, 256]; // R, G, B各256个值// 初始化三维数组
int[,,] cube = {{{ 1, 2 },{ 3, 4 }},{{ 5, 6 },{ 7, 8 }}
};// 访问三维数组元素
cube[0, 1, 0] = 100;
Console.WriteLine($"cube[0,1,0] = {cube[0, 1, 0]}");// 遍历三维数组
for (int i = 0; i < cube.GetLength(0); i++)
{for (int j = 0; j < cube.GetLength(1); j++){for (int k = 0; k < cube.GetLength(2); k++){Console.WriteLine($"cube[{i},{j},{k}] = {cube[i, j, k]}");}}
}
8.4 锯齿数组
锯齿数组(Jagged Array)是数组的数组,每个子数组可以有不同的长度。
// 声明锯齿数组
int[][] jaggedArray = new int[3][];// 初始化子数组
jaggedArray[0] = new int[2] { 1, 2 };
jaggedArray[1] = new int[3] { 3, 4, 5 };
jaggedArray[2] = new int[4] { 6, 7, 8, 9 };// 简化的初始化方式
int[][] numbers = {new int[] { 1, 2 },new int[] { 3, 4, 5 },new int[] { 6, 7, 8, 9 }
};// 访问锯齿数组元素
Console.WriteLine($"第2行第3列:{numbers[1][2]}"); // 输出:5// 获取信息
Console.WriteLine($"总行数:{numbers.Length}"); // 输出:3
Console.WriteLine($"第2行长度:{numbers[1].Length}"); // 输出:3// 遍历锯齿数组
Console.WriteLine("遍历锯齿数组:");
for (int i = 0; i < numbers.Length; i++)
{Console.Write($"第{i + 1}行:");for (int j = 0; j < numbers[i].Length; j++){Console.Write($"{numbers[i][j]} ");}Console.WriteLine();
}// 使用foreach遍历
Console.WriteLine("使用foreach遍历:");
foreach (int[] row in numbers)
{foreach (int value in row){Console.Write($"{value} ");}Console.WriteLine();
}// 锯齿数组 vs 多维数组
// 锯齿数组:int[][] - 更灵活,子数组长度可不同
// 多维数组:int[,] - 更高效,所有行列长度相同
8.5 集合类型概述
虽然数组功能强大,但它们的大小是固定的。.NET提供了丰富的集合类型来处理动态大小的数据集合。集合类型位于System.Collections.Generic
命名空间中。
集合的优势:
- 动态大小:可以根据需要增加或减少元素
- 类型安全:泛型集合提供编译时类型检查
- 丰富的方法:提供了查找、排序、过滤等操作
- 性能优化:针对不同场景优化的数据结构
8.6 List - 动态数组
List是最常用的集合类型,提供了类似数组的索引访问,同时支持动态调整大小。
using System.Collections.Generic;// 创建List
List<string> names = new List<string>();
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
List<double> prices = new List<double>(10); // 指定初始容量// 添加元素
names.Add("张三");
names.Add("李四");
names.AddRange(new[] { "王五", "赵六" });// 插入元素
names.Insert(1, "新名字"); // 在索引1处插入// 访问元素
Console.WriteLine($"第一个名字:{names[0]}");
Console.WriteLine($"最后一个名字:{names[names.Count - 1]}");// 修改元素
names[0] = "张三丰";// 查找操作
bool hasZhangSan = names.Contains("张三丰");
int index = names.IndexOf("李四");
int lastIndex = names.LastIndexOf("王五");// 查找满足条件的元素
List<int> evenNumbers = numbers.FindAll(x => x % 2 == 0);
int firstEven = numbers.Find(x => x % 2 == 0);
bool hasLargeNumber = numbers.Exists(x => x > 3);// 删除操作
names.Remove("王五"); // 删除第一个匹配的元素
names.RemoveAt(0); // 删除指定索引的元素
names.RemoveAll(x => x.StartsWith("张")); // 删除所有满足条件的元素
names.RemoveRange(0, 2); // 删除指定范围的元素// 排序和反转
List<int> sortableNumbers = new List<int> { 3, 1, 4, 1, 5, 9 };
sortableNumbers.Sort(); // 升序排序
sortableNumbers.Reverse(); // 反转// 自定义排序
List<string> words = new List<string> { "apple", "banana", "cherry" };
words.Sort((x, y) => x.Length.CompareTo(y.Length)); // 按长度排序// 转换操作
string[] nameArray = names.ToArray(); // 转换为数组
List<string> copiedList = new List<string>(names); // 复制构造// 性能相关
Console.WriteLine($"当前容量:{names.Capacity}");
Console.WriteLine($"元素数量:{names.Count}");
names.TrimExcess(); // 减少容量到实际大小
8.7 Dictionary<TKey, TValue> - 键值对集合
Dictionary提供了基于键的快速查找,类似于其他语言中的哈希表或映射。
// 创建Dictionary
Dictionary<string, int> ages = new Dictionary<string, int>();
Dictionary<string, string> capitals = new Dictionary<string, string>
{{ "中国", "北京" },{ "美国", "华盛顿" },{ "日本", "东京" },{ "法国", "巴黎" }
};// 添加元素
ages["张三"] = 25;
ages["李四"] = 30;
ages.Add("王五", 28);// 尝试添加(如果键已存在会抛出异常)
try
{ages.Add("张三", 26); // 会抛出异常
}
catch (ArgumentException)
{Console.WriteLine("键已存在");
}// 安全添加
if (!ages.ContainsKey("赵六"))
{ages["赵六"] = 35;
}// 访问元素
Console.WriteLine($"张三的年龄:{ages["张三"]}");// 安全访问
if (ages.TryGetValue("孙七", out int age))
{Console.WriteLine($"孙七的年龄:{age}");
}
else
{Console.WriteLine("未找到孙七的年龄信息");
}// 更新元素
ages["张三"] = 26; // 如果键存在则更新,不存在则添加// 删除元素
bool removed = ages.Remove("王五");
Console.WriteLine($"删除王五:{removed}");// 检查键和值
bool hasKey = ages.ContainsKey("李四");
bool hasValue = ages.ContainsValue(30);// 遍历Dictionary
Console.WriteLine("遍历键值对:");
foreach (KeyValuePair<string, int> pair in ages)
{Console.WriteLine($"{pair.Key}: {pair.Value}岁");
}// 使用var简化
foreach (var pair in ages)
{Console.WriteLine($"{pair.Key}: {pair.Value}岁");
}// 只遍历键
Console.WriteLine("所有姓名:");
foreach (string name in ages.Keys)
{Console.WriteLine(name);
}// 只遍历值
Console.WriteLine("所有年龄:");
foreach (int ageValue in ages.Values)
{Console.WriteLine($"{ageValue}岁");
}// 字典的实际应用:计数器
string text = "hello world";
Dictionary<char, int> charCount = new Dictionary<char, int>();foreach (char c in text)
{if (char.IsLetter(c)){if (charCount.ContainsKey(c)){charCount[c]++;}else{charCount[c] = 1;}}
}Console.WriteLine("字符统计:");
foreach (var pair in charCount)
{Console.WriteLine($"'{pair.Key}': {pair.Value}次");
}
8.8 其他重要集合类型
8.8.1 HashSet - 无重复元素集合
HashSet用于存储唯一元素,提供高效的查找、添加和删除操作。
// 创建HashSet
HashSet<string> uniqueNames = new HashSet<string>();
HashSet<int> numbers = new HashSet<int> { 1, 2, 3, 4, 5 };// 添加元素
uniqueNames.Add("张三");
uniqueNames.Add("李四");
uniqueNames.Add("张三"); // 重复元素不会被添加Console.WriteLine($"唯一名字数量:{uniqueNames.Count}"); // 输出:2// 批量添加
string[] moreNames = { "王五", "张三", "赵六" };
foreach (string name in moreNames)
{bool added = uniqueNames.Add(name);Console.WriteLine($"添加{name}:{(added ? "成功" : "已存在")}");
}// 集合操作
HashSet<int> set1 = new HashSet<int> { 1, 2, 3, 4 };
HashSet<int> set2 = new HashSet<int> { 3, 4, 5, 6 };// 并集
HashSet<int> union = new HashSet<int>(set1);
union.UnionWith(set2);
Console.WriteLine($"并集:{string.Join(", ", union)}"); // 1, 2, 3, 4, 5, 6// 交集
HashSet<int> intersection = new HashSet<int>(set1);
intersection.IntersectWith(set2);
Console.WriteLine($"交集:{string.Join(", ", intersection)}"); // 3, 4// 差集
HashSet<int> difference = new HashSet<int>(set1);
difference.ExceptWith(set2);
Console.WriteLine($"差集:{string.Join(", ", difference)}"); // 1, 2
8.8.2 Queue - 队列
队列实现先进先出(FIFO)的数据结构。
// 创建队列
Queue<string> customerQueue = new Queue<string>();// 入队
customerQueue.Enqueue("客户1");
customerQueue.Enqueue("客户2");
customerQueue.Enqueue("客户3");Console.WriteLine($"队列中有{customerQueue.Count}个客户");// 查看队首元素(不移除)
string nextCustomer = customerQueue.Peek();
Console.WriteLine($"下一个客户:{nextCustomer}");// 出队
while (customerQueue.Count > 0)
{string customer = customerQueue.Dequeue();Console.WriteLine($"正在服务:{customer}");
}// 队列的实际应用:任务处理
Queue<Action> taskQueue = new Queue<Action>();// 添加任务
taskQueue.Enqueue(() => Console.WriteLine("执行任务1"));
taskQueue.Enqueue(() => Console.WriteLine("执行任务2"));
taskQueue.Enqueue(() => Console.WriteLine("执行任务3"));// 执行任务
while (taskQueue.Count > 0)
{ Action task = taskQueue.Dequeue(); task.Invoke();
}
8.8.3 Stack - 栈
栈实现后进先出(LIFO)的数据结构。
// 创建栈
Stack<string> browserHistory = new Stack<string>();// 压栈(添加页面)
browserHistory.Push("首页");
browserHistory.Push("产品页");
browserHistory.Push("详情页");
browserHistory.Push("购物车");Console.WriteLine($"浏览历史有{browserHistory.Count}个页面");// 查看栈顶元素(不移除)
string currentPage = browserHistory.Peek();
Console.WriteLine($"当前页面:{currentPage}");// 后退(出栈)
while (browserHistory.Count > 1)
{string page = browserHistory.Pop();Console.WriteLine($"从{page}后退到{browserHistory.Peek()}");
}// 栈的实际应用:撤销操作
Stack<string> undoStack = new Stack<string>();// 模拟用户操作
undoStack.Push("输入文字A");
undoStack.Push("输入文字B");
undoStack.Push("删除文字");
undoStack.Push("插入图片");// 撤销操作
Console.WriteLine("撤销操作:");
while (undoStack.Count > 0)
{string operation = undoStack.Pop();Console.WriteLine($"撤销:{operation}");
}
8.9 集合的选择指南
选择合适的集合类型对性能和代码可读性都很重要:
集合类型 | 适用场景 | 时间复杂度 | 特点 |
---|---|---|---|
Array | 固定大小,频繁索引访问 | O(1)访问 | 内存效率高 |
List<T> | 动态大小,频繁索引访问 | O(1)访问,O(n)插入/删除 | 最常用 |
Dictionary<K,V> | 键值对存储,快速查找 | O(1)平均查找 | 基于哈希表 |
HashSet<T> | 唯一元素,集合操作 | O(1)平均操作 | 去重,集合运算 |
Queue<T> | 先进先出处理 | O(1)入队出队 | 任务队列 |
Stack<T> | 后进先出处理 | O(1)压栈出栈 | 撤销操作 |
LinkedList<T> | 频繁插入删除 | O(1)插入删除 | 双向链表 |
9. 方法和函数
9.1 方法概述
在C#中,"方法"是执行特定任务的代码块。与C语言的函数概念相似,但C#中所有的方法都必须属于某个类。方法是实现代码重用、模块化和结构化编程的基础。
方法的核心作用:
- 代码重用:避免重复编写相同的代码
- 模块化:将复杂问题分解为小的、可管理的部分
- 抽象:隐藏实现细节,提供清晰的接口
- 测试:独立的方法更容易进行单元测试
9.2 方法的基本语法
方法的完整语法包括访问修饰符、返回类型、方法名、参数列表和方法体。
方法的基本定义语法
- 格式为:
[访问修饰符] [其他修饰符] 返回类型 方法名(参数列表)
。 - 方法体写在花括号内,若有返回值,用
return
返回对应类型的结果。
- 访问修饰符(Access Modifier)
- 用于控制方法的访问范围。常见的有:
public
:公开访问,任何地方都可以调用。private
:私有访问,只能在当前类中使用(默认)。protected
:受保护,只能在当前类和其子类中访问。internal
:只能在当前程序集(项目)中访问。protected internal
:当前程序集或派生类可访问。private protected
:当前类或其派生类中,并且只能在当前程序集内访问。
- 用于控制方法的访问范围。常见的有:
- 修饰符(Modifier)
- 控制方法的行为或特性,常用的包括:
static
:静态方法,不依赖对象实例,可直接通过类调用。abstract
:抽象方法,没有方法体,必须在派生类中实现(只能在抽象类中使用)。virtual
:虚方法,允许子类重写。override
:重写从基类继承的虚方法或抽象方法。sealed
:禁止方法在子类中被再次重写(仅用于已重写的方法)。async
:声明方法是异步的,通常与await
一起使用。extern
:声明方法由外部实现(如调用非托管代码)。
- 控制方法的行为或特性,常用的包括:
- 返回类型(Return Type)
- 表示方法执行后返回的数据类型。可以是任何合法类型,如:
- 基本类型:
int
、double
、bool
、string
等 - 引用类型:如
object
、自定义类 void
:表示无返回值Task
或Task<T>
:用于异步方法
- 基本类型:
- 表示方法执行后返回的数据类型。可以是任何合法类型,如:
- 方法名(Method Name)
- 方法的名称,命名需符合标识符规范,建议使用 Pascal 命名法(首字母大写)。
- 方法名应简洁、语义明确,如
Add
、PrintMessage
、CalculateArea
。
- 参数列表(Parameter List)
- 括号中的参数定义,用于向方法传递值。每个参数包含类型和名称。
- 参数可以有多个,用逗号分隔。
- 可选支持:
- 默认参数值,例如
int count = 10
ref
:按引用传递(必须先赋值)out
:输出参数(不需要提前赋值)params
:可变参数列表(数组形式接收任意个数的同类型参数)
- 默认参数值,例如
// 基本方法定义语法
[访问修饰符] [修饰符] 返回类型 方法名(参数列表)
{// 方法体return 返回值; // 如果有返回值
}// 示例:简单的数学运算方法
public static int Add(int a, int b)
{int result = a + b;return result;
}// 无返回值的方法
public static void PrintMessage(string message)
{Console.WriteLine($"消息:{message}");
}// 带多个参数的方法
public static double CalculateArea(double length, double width)
{return length * width;
}// 使用方法
int sum = Add(10, 20);
PrintMessage("Hello World");
double area = CalculateArea(5.5, 3.2);
9.3 方法参数详解
C#提供了多种参数传递方式,每种方式都有其特定的用途和行为。
9.3.1 值参数(默认方式)
值参数是最常见的参数传递方式,传递的是参数值的副本。
public static void ModifyValue(int value)
{value = 100; // 只修改副本,不影响原始值Console.WriteLine($"方法内部:value = {value}");
}// 测试值参数
int originalValue = 10;
Console.WriteLine($"调用前:originalValue = {originalValue}");
ModifyValue(originalValue);
Console.WriteLine($"调用后:originalValue = {originalValue}");// 输出:
// 调用前:originalValue = 10
// 方法内部:value = 100
// 调用后:originalValue = 10// 值参数的典型应用
public static double CalculateCircleArea(double radius)
{const double PI = 3.14159265359;return PI * radius * radius;
}public static bool IsEven(int number)
{return number % 2 == 0;
}public static string FormatCurrency(decimal amount)
{return $"¥{amount:N2}";
}
9.3.2 引用参数(ref关键字)
引用参数传递变量的引用,方法内部的修改会影响原始变量。
public static void SwapValues(ref int a, ref int b)
{int temp = a;a = b;b = temp;
}// 使用ref参数
int x = 10, y = 20;
Console.WriteLine($"交换前:x = {x}, y = {y}");
SwapValues(ref x, ref y);
Console.WriteLine($"交换后:x = {x}, y = {y}");// 引用参数的实际应用
public static void UpdateBankBalance(ref decimal balance, decimal amount, bool isDeposit)
{if (isDeposit){balance += amount;Console.WriteLine($"存款 ¥{amount:N2},余额:¥{balance:N2}");}else{if (balance >= amount){balance -= amount;Console.WriteLine($"取款 ¥{amount:N2},余额:¥{balance:N2}");}else{Console.WriteLine("余额不足");}}
}decimal accountBalance = 1000m;
UpdateBankBalance(ref accountBalance, 500m, true); // 存款
UpdateBankBalance(ref accountBalance, 200m, false); // 取款
Console.WriteLine($"最终余额:¥{accountBalance:N2}");
9.3.3 输出参数(out关键字)
输出参数用于从方法中返回多个值,必须在方法内部赋值。
// 基本的out参数使用
public static bool TryDivide(int dividend, int divisor, out int quotient, out int remainder)
{if (divisor == 0){quotient = 0;remainder = 0;return false;}quotient = dividend / divisor;remainder = dividend % divisor;return true;
}// 使用out参数
if (TryDivide(17, 5, out int q, out int r))
{Console.WriteLine($"17 ÷ 5 = {q} 余 {r}");
}
else
{Console.WriteLine("除法失败");
}// out参数的实际应用:字符串解析
public static bool TryParsePersonInfo(string input, out string name, out int age)
{name = string.Empty;age = 0;if (string.IsNullOrWhiteSpace(input))return false;string[] parts = input.Split(',');if (parts.Length != 2)return false;name = parts[0].Trim();return int.TryParse(parts[1].Trim(), out age);
}// 使用示例
string personData = "张三, 25";
if (TryParsePersonInfo(personData, out string personName, out int personAge))
{Console.WriteLine($"姓名:{personName},年龄:{personAge}");
}// 返回多个计算结果
public static void CalculateStatistics(int[] numbers, out double average, out int min, out int max)
{if (numbers == null || numbers.Length == 0){average = 0;min = 0;max = 0;return;}int sum = 0;min = numbers[0];max = numbers[0];foreach (int number in numbers){sum += number;if (number < min) min = number;if (number > max) max = number;}average = (double)sum / numbers.Length;
}
9.3.4 参数数组(params关键字)
params允许方法接受可变数量的参数,提供了类似C语言中可变参数函数的功能。
// 基本params使用
public static int Sum(params int[] numbers)
{int total = 0;foreach (int number in numbers){total += number;}return total;
}// 多种调用方式
int result1 = Sum(1, 2, 3, 4, 5); // 传递多个参数
int result2 = Sum(new int[] { 1, 2, 3 }); // 传递数组
int result3 = Sum(); // 不传递参数Console.WriteLine($"结果1:{result1},结果2:{result2},结果3:{result3}");// 字符串格式化方法
public static string FormatMessage(string template, params object[] args)
{if (args.Length == 0)return template;try{return string.Format(template, args);}catch{return template;}
}// 使用示例
string msg1 = FormatMessage("Hello, {0}!", "张三");
string msg2 = FormatMessage("用户{0},年龄{1},来自{2}", "李四", 25, "北京");
string msg3 = FormatMessage("简单消息");// 日志记录方法
public static void LogInfo(string message, params object[] additionalInfo)
{DateTime now = DateTime.Now;Console.WriteLine($"[{now:yyyy-MM-dd HH:mm:ss}] INFO: {message}");if (additionalInfo.Length > 0){Console.WriteLine("附加信息:");for (int i = 0; i < additionalInfo.Length; i++){Console.WriteLine($" {i + 1}. {additionalInfo[i]}");}}
}// 使用日志方法
LogInfo("用户登录");
LogInfo("数据处理完成", "处理数量:100", "耗时:2.5秒", "状态:成功");
9.3.5 可选参数和命名参数
可选参数允许为参数提供默认值,命名参数允许按名称传递参数。
// 可选参数
public static void CreateUser(string name, int age = 18, string city = "北京", bool isActive = true)
{Console.WriteLine($"用户信息:");Console.WriteLine($" 姓名:{name}");Console.WriteLine($" 年龄:{age}");Console.WriteLine($" 城市:{city}");Console.WriteLine($" 状态:{(isActive ? "激活" : "未激活")}");Console.WriteLine();
}// 多种调用方式
CreateUser("张三"); // 使用所有默认值
CreateUser("李四", 25); // 部分使用默认值
CreateUser("王五", 30, "上海"); // 指定前面的参数
CreateUser("赵六", 28, "广州", false); // 指定所有参数// 命名参数:可以跳过某些参数或改变顺序
CreateUser(name: "孙七", city: "深圳"); // 跳过age参数
CreateUser(city: "杭州", name: "周八", age: 35); // 改变参数顺序// 复杂的可选参数示例
public static string GenerateReport(string title,DateTime? startDate = null,DateTime? endDate = null,string format = "PDF",bool includeCharts = true,int maxPages = 100)
{DateTime start = startDate ?? DateTime.Now.AddDays(-30);DateTime end = endDate ?? DateTime.Now;string report = $"报告标题:{title}\n";report += $"时间范围:{start:yyyy-MM-dd} 到 {end:yyyy-MM-dd}\n";report += $"格式:{format}\n";report += $"包含图表:{(includeCharts ? "是" : "否")}\n";report += $"最大页数:{maxPages}\n";return report;
}// 使用命名参数
string report = GenerateReport(title: "月度销售报告",format: "Excel",includeCharts: false,startDate: DateTime.Now.AddDays(-7)
);
9.4 方法重载
方法重载允许在同一个类中定义多个同名方法,只要它们的参数列表不同即可。
public class Calculator
{// 重载Add方法:不同参数类型public static int Add(int a, int b){Console.WriteLine("调用int版本的Add");return a + b;}public static double Add(double a, double b){Console.WriteLine("调用double版本的Add");return a + b;}public static string Add(string a, string b){Console.WriteLine("调用string版本的Add");return a + b;}// 重载Add方法:不同参数数量public static int Add(int a, int b, int c){Console.WriteLine("调用三参数版本的Add");return a + b + c;}// 重载Add方法:参数数组public static int Add(params int[] numbers){Console.WriteLine("调用params版本的Add");int sum = 0;foreach (int number in numbers){sum += number;}return sum;}
}// 使用重载方法
int result1 = Calculator.Add(5, 3); // 调用int版本
double result2 = Calculator.Add(5.5, 3.2); // 调用double版本
string result3 = Calculator.Add("Hello", "World"); // 调用string版本
int result4 = Calculator.Add(1, 2, 3); // 调用三参数版本
int result5 = Calculator.Add(1, 2, 3, 4, 5); // 调用params版本// 实际应用:数据转换方法
public class DataConverter
{public static string ToString(int value){return value.ToString();}public static string ToString(int value, string format){return value.ToString(format);}public static string ToString(double value, int decimalPlaces){return Math.Round(value, decimalPlaces).ToString();}public static string ToString(DateTime value){return value.ToString("yyyy-MM-dd");}public static string ToString(DateTime value, string format){return value.ToString(format);}
}
9.5 局部函数
局部函数是在方法内部定义的函数,只能在定义它们的方法内部访问。
public static int CalculateFactorial(int n)
{// 局部函数:输入验证bool IsValidInput(int number){return number >= 0 && number <= 20; // 限制范围防止溢出}// 局部函数:递归计算int CalculateFactorialRecursive(int number){if (number <= 1)return 1;return number * CalculateFactorialRecursive(number - 1);}// 主逻辑if (!IsValidInput(n)){throw new ArgumentException($"输入值{n}无效,必须在0-20之间");}return CalculateFactorialRecursive(n);
}// 复杂示例:文件处理
public static List<string> ProcessTextFile(string filePath)
{// 局部函数:验证文件bool ValidateFile(string path){return !string.IsNullOrEmpty(path) && File.Exists(path) && Path.GetExtension(path).ToLower() == ".txt";}// 局部函数:清理文本行string CleanLine(string line){if (string.IsNullOrWhiteSpace(line))return string.Empty;return line.Trim().Replace("\t", " ").Replace(" ", " ");}// 局部函数:过滤有效行bool IsValidLine(string line){return !string.IsNullOrEmpty(line) && line.Length > 3 && !line.StartsWith("#");}// 主逻辑if (!ValidateFile(filePath)){throw new ArgumentException("无效的文件路径");}var result = new List<string>();string[] lines = File.ReadAllLines(filePath);foreach (string line in lines){string cleanedLine = CleanLine(line);if (IsValidLine(cleanedLine)){result.Add(cleanedLine);}}return result;
}
9.6 递归方法
递归是方法调用自身的编程技术,适用于解决可以分解为相似子问题的复杂问题。
// 经典递归:斐波那契数列
public static long Fibonacci(int n)
{// 基础情况if (n <= 0) return 0;if (n == 1) return 1;// 递归情况return Fibonacci(n - 1) + Fibonacci(n - 2);
}// 优化的斐波那契:使用备忘录模式
public static long FibonacciOptimized(int n)
{Dictionary<int, long> memo = new Dictionary<int, long>();long FibonacciMemo(int num){if (num <= 0) return 0;if (num == 1) return 1;if (memo.ContainsKey(num))return memo[num];long result = FibonacciMemo(num - 1) + FibonacciMemo(num - 2);memo[num] = result;return result;}return FibonacciMemo(n);
}// 递归遍历目录
public static void ListDirectoryContents(string directoryPath, int level = 0)
{try{// 创建缩进string indent = new string(' ', level * 2);// 显示当前目录Console.WriteLine($"{indent}📁 {Path.GetFileName(directoryPath)}");// 遍历文件string[] files = Directory.GetFiles(directoryPath);foreach (string file in files){Console.WriteLine($"{indent} 📄 {Path.GetFileName(file)}");}// 递归遍历子目录string[] directories = Directory.GetDirectories(directoryPath);foreach (string directory in directories){ListDirectoryContents(directory, level + 1);}}catch (UnauthorizedAccessException){Console.WriteLine($"{new string(' ', level * 2)} ❌ 无权限访问");}
}// 递归计算目录大小
public static long CalculateDirectorySize(string directoryPath)
{long size = 0;try{// 累加所有文件大小string[] files = Directory.GetFiles(directoryPath);foreach (string file in files){FileInfo fileInfo = new FileInfo(file);size += fileInfo.Length;}// 递归计算子目录大小string[] directories = Directory.GetDirectories(directoryPath);foreach (string directory in directories){size += CalculateDirectorySize(directory);}}catch (UnauthorizedAccessException){// 忽略无权限的目录}return size;
}// 递归数据结构:二叉树
public class TreeNode
{public int Value { get; set; }public TreeNode Left { get; set; }public TreeNode Right { get; set; }public TreeNode(int value){Value = value;}
}public static class BinaryTree
{// 递归插入节点public static TreeNode Insert(TreeNode root, int value){if (root == null)return new TreeNode(value);if (value < root.Value)root.Left = Insert(root.Left, value);else if (value > root.Value)root.Right = Insert(root.Right, value);return root;}// 递归中序遍历public static void InOrderTraversal(TreeNode root){if (root == null) return;InOrderTraversal(root.Left); // 遍历左子树Console.Write($"{root.Value} "); // 访问根节点InOrderTraversal(root.Right); // 遍历右子树}// 递归查找节点public static bool Search(TreeNode root, int value){if (root == null) return false;if (root.Value == value) return true;return value < root.Value ? Search(root.Left, value) : Search(root.Right, value);}
}
9.7 方法的最佳实践
编写高质量方法的指导原则:
9.7.1 方法设计原则
// 1. 单一职责原则:一个方法只做一件事
// 好的做法
public static bool IsValidEmail(string email)
{if (string.IsNullOrWhiteSpace(email))return false;return email.Contains("@") && email.Contains(".") && email.Length > 5;
}public static string FormatEmail(string email)
{return email?.Trim().ToLower();
}// 避免:一个方法做多件事
public static string ValidateAndFormatEmail(string email) // 不推荐
{// 验证和格式化混在一起
}// 2. 方法名应该描述其功能
// 好的方法名
public static decimal CalculateTotalPrice(decimal basePrice, decimal taxRate)
public static bool IsUserAuthorized(User user, string permission)
public static void SendNotificationEmail(string recipient, string subject, string body)// 避免的方法名
public static decimal DoCalculation(decimal a, decimal b) // 不明确
public static bool Check(User user, string permission) // 太模糊
public static void Process(string a, string b, string c) // 没有意义// 3. 参数数量控制
// 好的做法:参数数量少
public static User CreateUser(string name, string email, DateTime birthDate)// 当参数过多时,考虑使用对象
public class UserCreationRequest
{public string Name { get; set; }public string Email { get; set; }public DateTime BirthDate { get; set; }public string Phone { get; set; }public string Address { get; set; }public string Department { get; set; }
}public static User CreateUser(UserCreationRequest request)
{// 实现逻辑return new User();
}
9.7.2 错误处理和验证
// 输入验证
public static double CalculateSquareRoot(double number)
{if (number < 0){throw new ArgumentException("不能计算负数的平方根", nameof(number));}return Math.Sqrt(number);
}// 安全的方法设计
public static bool TryCalculateSquareRoot(double number, out double result)
{result = 0;if (number < 0)return false;result = Math.Sqrt(number);return true;
}// 空值检查
public static string GetFullName(string firstName, string lastName)
{if (string.IsNullOrWhiteSpace(firstName))throw new ArgumentException("名字不能为空", nameof(firstName));if (string.IsNullOrWhiteSpace(lastName))throw new ArgumentException("姓氏不能为空", nameof(lastName));return $"{firstName} {lastName}";
}// 防御性编程
public static int[] SortArray(int[] input)
{if (input == null)return new int[0]; // 或者抛出异常if (input.Length <= 1)return (int[])input.Clone();int[] result = (int[])input.Clone();Array.Sort(result);return result;
}
10. 面向对象编程
10.1 面向对象编程概述
面向对象编程(Object-Oriented Programming, OOP)是一种编程范式,它使用"对象"来设计应用程序和计算机程序。C#是一门纯面向对象的语言,这意味着几乎所有的代码都必须在类中定义。
与C语言的过程式编程相比,面向对象编程具有以下优势:
- 更好的代码组织:通过类和对象来组织相关的数据和功能
- 代码重用:通过继承和组合实现代码复用
- 模块化:将复杂系统分解为独立的、可管理的模块
- 维护性:封装隐藏了实现细节,降低了代码耦合度
- 扩展性:通过多态和接口实现灵活的扩展机制
OOP的四大核心原则:
- 封装(Encapsulation):将数据和操作数据的方法封装在一起
- 继承(Inheritance):子类可以继承父类的属性和方法
- 多态(Polymorphism):同一接口可以有不同的实现
- 抽象(Abstraction):隐藏复杂的实现细节,只暴露必要的接口
10.2 类和对象
类是对象的蓝图或模板,定义了对象应该具有的属性和行为。对象是类的实例,是实际存在的个体。
10.2.1 类的定义
// 基本的类定义
public class Student
{// 字段(Fields)- 存储对象的状态private string name;private int age;private string studentId;private List<string> courses;// 属性(Properties)- 提供对字段的受控访问public string Name{get { return name; }set { if (string.IsNullOrWhiteSpace(value))throw new ArgumentException("姓名不能为空");name = value.Trim();}}public int Age{get { return age; }set { if (value < 0 || value > 150)throw new ArgumentException("年龄必须在0-150之间");age = value;}}public string StudentId{get { return studentId; }private set { studentId = value; } // 私有设置器}// 自动属性(Auto-implemented Properties)public string Email { get; set; }public DateTime EnrollmentDate { get; set; }// 只读属性public int CourseCount => courses?.Count ?? 0;// 构造函数(Constructors)public Student(){courses = new List<string>();EnrollmentDate = DateTime.Now;StudentId = GenerateStudentId();}public Student(string name, int age) : this() // 构造函数链{Name = name;Age = age;}public Student(string name, int age, string email) : this(name, age){Email = email;}// 方法(Methods)public void AddCourse(string courseName){if (string.IsNullOrWhiteSpace(courseName))throw new ArgumentException("课程名称不能为空");if (!courses.Contains(courseName)){courses.Add(courseName);Console.WriteLine($"{Name} 已添加课程:{courseName}");}else{Console.WriteLine($"{Name} 已经选择了课程:{courseName}");}}public void RemoveCourse(string courseName){if (courses.Remove(courseName)){Console.WriteLine($"{Name} 已移除课程:{courseName}");}else{Console.WriteLine($"{Name} 没有选择课程:{courseName}");}}public List<string> GetCourses(){return new List<string>(courses); // 返回副本,保护内部数据}public void DisplayInfo(){Console.WriteLine($"学生信息:");Console.WriteLine($" 姓名:{Name}");Console.WriteLine($" 年龄:{Age}");Console.WriteLine($" 学号:{StudentId}");Console.WriteLine($" 邮箱:{Email ?? "未设置"}");Console.WriteLine($" 入学日期:{EnrollmentDate:yyyy-MM-dd}");Console.WriteLine($" 选课数量:{CourseCount}");if (CourseCount > 0){Console.WriteLine($" 课程列表:{string.Join(", ", courses)}");}}// 私有辅助方法private string GenerateStudentId(){return $"STU{DateTime.Now:yyyyMMdd}{new Random().Next(1000, 9999)}";}// 重写ToString方法public override string ToString(){return $"Student[Name={Name}, Age={Age}, ID={StudentId}]";}
}
10.2.2 对象的创建和使用
// 创建对象的不同方式
class Program
{static void Main(){// 使用默认构造函数Student student1 = new Student();student1.Name = "张三";student1.Age = 20;student1.Email = "zhangsan@example.com";// 使用参数化构造函数Student student2 = new Student("李四", 21);student2.Email = "lisi@example.com";// 使用所有参数的构造函数Student student3 = new Student("王五", 19, "wangwu@example.com");// 对象初始化器语法Student student4 = new Student{Name = "赵六",Age = 22,Email = "zhaoliu@example.com"};// 使用对象student1.AddCourse("C#编程");student1.AddCourse("数据结构");student1.AddCourse("算法设计");student2.AddCourse("C#编程");student2.AddCourse("Web开发");// 显示学生信息student1.DisplayInfo();Console.WriteLine();student2.DisplayInfo();// 获取课程列表List<string> courses = student1.GetCourses();Console.WriteLine($"\n{student1.Name}的课程:");foreach (string course in courses){Console.WriteLine($" - {course}");}}
}
10.3 封装详解
封装是面向对象编程的核心原则之一,它将数据和操作数据的方法组合在一起,并隐藏对象的内部实现细节。
10.3.1 访问修饰符
C#提供了多种访问修饰符来控制类成员的可见性:
修饰符访问范围描述
public任何地方完全公开,无访问限制
private当前类内部最严格的访问控制
protected当前类及其派生类继承层次内可访问
internal当前程序集同一程序集内可访问
protected internal当前程序集或派生类两种访问权限的并集
private protected当前程序集内的派生类两种访问权限的交集
public class BankAccount
{// 私有字段 - 只能在类内部访问private decimal balance;private string accountNumber;private DateTime createdDate;// 受保护字段 - 派生类可以访问protected string ownerName;protected bool isActive;// 内部字段 - 同一程序集内可访问internal string bankCode;// 公共属性 - 提供对私有字段的受控访问public decimal Balance { get { return balance; }private set { balance = value; } // 外部只读,内部可写}public string AccountNumber { get { return accountNumber; }}public string OwnerName { get { return ownerName; }set {if (string.IsNullOrWhiteSpace(value))throw new ArgumentException("账户所有者姓名不能为空");ownerName = value.Trim();}}// 只读属性public DateTime CreatedDate => createdDate;public int AccountAge => (DateTime.Now - createdDate).Days;// 构造函数public BankAccount(string ownerName, string bankCode){this.OwnerName = ownerName;this.bankCode = bankCode;this.accountNumber = GenerateAccountNumber();this.balance = 0;this.createdDate = DateTime.Now;this.isActive = true;}// 公共方法 - 提供账户操作接口public bool Deposit(decimal amount){if (!ValidateAmount(amount)){Console.WriteLine("存款金额必须大于0");return false;}if (!isActive){Console.WriteLine("账户已被冻结,无法存款");return false;}balance += amount;LogTransaction("存款", amount);return true;}public bool Withdraw(decimal amount){if (!ValidateAmount(amount)){Console.WriteLine("取款金额必须大于0");return false;}if (!isActive){Console.WriteLine("账户已被冻结,无法取款");return false;}if (amount > balance){Console.WriteLine("余额不足");return false;}balance -= amount;LogTransaction("取款", amount);return true;}public bool Transfer(BankAccount targetAccount, decimal amount){if (targetAccount == null){Console.WriteLine("目标账户不能为空");return false;}if (this.Withdraw(amount)){if (targetAccount.Deposit(amount)){Console.WriteLine($"成功转账 ¥{amount:N2} 到账户 {targetAccount.AccountNumber}");return true;}else{// 如果存款失败,需要回滚取款操作this.Deposit(amount);Console.WriteLine("转账失败:目标账户存款失败");return false;}}return false;}// 私有方法 - 内部实现细节private bool ValidateAmount(decimal amount){return amount > 0 && amount <= 1000000; // 限制单次操作金额}private string GenerateAccountNumber(){return $"{bankCode}{DateTime.Now:yyyyMMdd}{new Random().Next(10000, 99999)}";}private void LogTransaction(string type, decimal amount){Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {type}: ¥{amount:N2}, 余额: ¥{balance:N2}");}// 受保护方法 - 派生类可以重写或调用protected virtual void OnAccountStatusChanged(bool newStatus){Console.WriteLine($"账户状态已变更为:{(newStatus ? "激活" : "冻结")}");}// 内部方法 - 程序集内的其他类可以调用internal void FreezeAccount(){if (isActive){isActive = false;OnAccountStatusChanged(isActive);}}internal void ActivateAccount(){if (!isActive){isActive = true;OnAccountStatusChanged(isActive);}}
}
10.3.2 属性的高级用法
属性提供了对字段的受控访问,是封装的重要体现。
public class Product
{private decimal price;private int stockQuantity;private string name;// 带验证的属性public string Name{get { return name; }set{if (string.IsNullOrWhiteSpace(value))throw new ArgumentException("产品名称不能为空");if (value.Length > 100)throw new ArgumentException("产品名称不能超过100个字符");name = value.Trim();}}// 带业务逻辑的属性public decimal Price{get { return price; }set{if (value < 0)throw new ArgumentException("价格不能为负数");if (value > 1000000)throw new ArgumentException("价格不能超过1,000,000");decimal oldPrice = price;price = value;// 触发价格变更事件OnPriceChanged(oldPrice, value);}}// 只读计算属性public decimal TotalValue => price * stockQuantity;public bool IsInStock => stockQuantity > 0;public string Status => IsInStock ? "有货" : "缺货";// 初始化器属性(C# 9.0+)public string Category { get; init; }public string Brand { get; init; }// 库存属性,带库存变更通知public int StockQuantity{get { return stockQuantity; }set{if (value < 0)throw new ArgumentException("库存数量不能为负数");int oldStock = stockQuantity;stockQuantity = value;// 库存预警if (value <= 10 && oldStock > 10){OnLowStockWarning();}else if (value == 0 && oldStock > 0){OnOutOfStock();}}}// 事件public event EventHandler<PriceChangedEventArgs> PriceChanged;public event EventHandler LowStockWarning;public event EventHandler OutOfStock;// 构造函数public Product(string name, decimal price, string category, string brand){Name = name;Price = price;Category = category;Brand = brand;StockQuantity = 0;}// 库存操作方法public void AddStock(int quantity){if (quantity <= 0)throw new ArgumentException("添加的库存数量必须大于0");StockQuantity += quantity;Console.WriteLine($"已添加库存 {quantity} 件,当前库存:{StockQuantity}");}public bool SellProduct(int quantity){if (quantity <= 0){Console.WriteLine("销售数量必须大于0");return false;}if (quantity > StockQuantity){Console.WriteLine($"库存不足,当前库存:{StockQuantity},请求数量:{quantity}");return false;}StockQuantity -= quantity;Console.WriteLine($"已销售 {quantity} 件,当前库存:{StockQuantity}");return true;}// 事件触发方法protected virtual void OnPriceChanged(decimal oldPrice, decimal newPrice){PriceChanged?.Invoke(this, new PriceChangedEventArgs(oldPrice, newPrice));Console.WriteLine($"产品 {Name} 价格从 ¥{oldPrice:N2} 变更为 ¥{newPrice:N2}");}protected virtual void OnLowStockWarning(){LowStockWarning?.Invoke(this, EventArgs.Empty);Console.WriteLine($"⚠️ 警告:产品 {Name} 库存不足({StockQuantity} 件)");}protected virtual void OnOutOfStock(){OutOfStock?.Invoke(this, EventArgs.Empty);Console.WriteLine($"❌ 警告:产品 {Name} 已售罄");}
}// 事件参数类
public class PriceChangedEventArgs : EventArgs
{public decimal OldPrice { get; }public decimal NewPrice { get; }public decimal ChangeAmount => NewPrice - OldPrice;public decimal ChangePercentage => OldPrice == 0 ? 0 : (ChangeAmount / OldPrice) * 100;public PriceChangedEventArgs(decimal oldPrice, decimal newPrice){OldPrice = oldPrice;NewPrice = newPrice;}
}
10.4 静态成员
静态成员属于类本身,而不是类的实例。它们在类第一次被使用时初始化,并且在程序的整个生命周期中都存在。
public class Counter
{// 静态字段 - 所有实例共享private static int totalInstances = 0;private static readonly object lockObject = new object();// 实例字段private int instanceId;private int value;// 静态属性public static int TotalInstances { get { return totalInstances; }}// 实例属性public int InstanceId => instanceId;public int Value => value;// 静态构造函数 - 只执行一次static Counter(){Console.WriteLine("Counter类的静态构造函数被调用");totalInstances = 0;}// 实例构造函数public Counter(){lock (lockObject) // 线程安全{totalInstances++;instanceId = totalInstances;}value = 0;Console.WriteLine($"创建了第 {instanceId} 个Counter实例");}// 静态方法public static void ResetAllCounters(){Console.WriteLine("重置所有计数器的功能需要在实例中实现");// 静态方法不能直接访问实例成员}public static Counter CreateWithInitialValue(int initialValue){Counter counter = new Counter();counter.value = initialValue;return counter;}// 实例方法public void Increment(){value++;}public void Decrement(){value--;}public void Reset(){value = 0;}
}// 静态类示例 - 工具类
public static class MathHelper
{// 静态常量public const double PI = 3.14159265358979323846;public const double E = 2.71828182845904523536;// 静态只读字段public static readonly Random Random = new Random();// 静态方法public static double CalculateCircleArea(double radius){if (radius < 0)throw new ArgumentException("半径不能为负数");return PI * radius * radius;}public static double CalculateCircleCircumference(double radius){if (radius < 0)throw new ArgumentException("半径不能为负数");return 2 * PI * radius;}public static int GetRandomNumber(int min, int max){return Random.Next(min, max + 1);}public static double GetRandomDouble(){return Random.NextDouble();}public static bool IsPrime(int number){if (number < 2) return false;if (number == 2) return true;if (number % 2 == 0) return false;for (int i = 3; i * i <= number; i += 2){if (number % i == 0)return false;}return true;}public static long Factorial(int n){if (n < 0)throw new ArgumentException("不能计算负数的阶乘");if (n > 20)throw new ArgumentException("输入值过大,会导致溢出");long result = 1;for (int i = 2; i <= n; i++){result *= i;}return result;}
}// 单例模式示例
public class DatabaseConnection
{private static DatabaseConnection instance;private static readonly object lockObject = new object();private string connectionString;private bool isConnected;// 私有构造函数,防止外部实例化private DatabaseConnection(){connectionString = "Server=localhost;Database=MyApp;";isConnected = false;Console.WriteLine("数据库连接实例已创建");}// 获取唯一实例public static DatabaseConnection Instance{get{if (instance == null){lock (lockObject){if (instance == null){instance = new DatabaseConnection();}}}return instance;}}public void Connect(){if (!isConnected){isConnected = true;Console.WriteLine("数据库连接已建立");}else{Console.WriteLine("数据库已经连接");}}public void Disconnect(){if (isConnected){isConnected = false;Console.WriteLine("数据库连接已断开");}else{Console.WriteLine("数据库连接已经断开");}}public void ExecuteQuery(string query){if (!isConnected){Console.WriteLine("请先连接数据库");return;}Console.WriteLine($"执行查询:{query}");}
}// 使用示例
class Program
{static void Main(){// 使用Counter类Console.WriteLine($"当前实例数量:{Counter.TotalInstances}");Counter c1 = new Counter();Counter c2 = new Counter();Counter c3 = Counter.CreateWithInitialValue(10);Console.WriteLine($"总实例数量:{Counter.TotalInstances}");// 使用静态工具类double area = MathHelper.CalculateCircleArea(5.0);Console.WriteLine($"圆的面积:{area:F2}");int randomNum = MathHelper.GetRandomNumber(1, 100);Console.WriteLine($"随机数:{randomNum}");bool isPrime = MathHelper.IsPrime(17);Console.WriteLine($"17是素数:{isPrime}");// 使用单例模式DatabaseConnection db1 = DatabaseConnection.Instance;DatabaseConnection db2 = DatabaseConnection.Instance;Console.WriteLine($"db1和db2是同一个实例:{ReferenceEquals(db1, db2)}");db1.Connect();db2.ExecuteQuery("SELECT * FROM Users");db1.Disconnect();}
}
11. 继承和多态
11.1 继承的核心概念
继承是面向对象编程的三大支柱之一,它允许我们创建新类来扩展现有类的功能。与C语言不同,C#提供了内建的继承机制,这使得代码重用变得更加自然和高效。
继承的本质:子类自动获得父类的所有非私有成员,包括字段、属性、方法等。这种"is-a"关系让我们能够建立清晰的类层次结构。
与其他语言的对比:
- C语言:没有内建继承,只能通过结构体嵌套模拟
- Python:支持多重继承,语法更灵活但可能导致复杂性
- C#:单继承设计,简单清晰,通过接口实现多重继承的效果
11.2 多态的深层理解
多态让我们能够用统一的接口处理不同类型的对象。这不仅仅是语法糖,而是一种强大的设计思想。
多态的实现机制:
- 虚方法:使用virtual关键字声明,可以在派生类中被重写
- 方法重写:使用override关键字,提供新的实现
- 动态绑定:运行时根据实际对象类型调用对应的方法
多态的价值:
- 降低代码耦合度
- 提高代码的可扩展性
- 实现开放-封闭原则(对扩展开放,对修改封闭)
11.3 抽象类的设计哲学
抽象类处于具体类和接口之间,它既可以包含实现代码,又可以定义必须被子类实现的抽象方法。
抽象类的使用场景:
- 当多个相关类需要共享代码时
- 当需要定义一个模板,强制子类实现某些方法时
- 当需要为一组相关类提供公共基础设施时
设计原则:
- 抽象类应该表示一个概念,而不是具体的事物
- 抽象方法应该代表子类必须实现的核心行为
- 具体方法应该提供通用的、可复用的功能