基础知识
数据类型
C# 类型系统
C# 中的引用类型不仅包括类和接口,还包括:
- 类(class)
- 接口(interface)
- 委托(delegate)
- 数组(array,包含值类型数组)
- 字符串(string)
- 记录(record,C# 9.0 引入)
- 动态类型(dynamic)
C# 的类型系统分为两大类:
值类型
- 包括结构体(struct)、枚举(enum)和基本类型(如 int、double 等)
- 直接存储数据,通常分配在栈上(成员除外)
引用类型
- 包括上述所有引用类型
- 存储对象引用,对象本身存于堆中
引用类型的设计优势:
- 多态性:基类引用可指向派生类对象
- 轻量传递:只传递内存地址无需复制整个对象
- 内存管理:依靠垃圾回收自动管理生命周期
- 支持继承与接口实现
- 避免对象切片问题
自 C# 7.0 起引入 ref struct,从 C# 9.0 起扩展为 record 和 record struct,进一步增强类型系统。
C# 可空类型
C# 可空类型让值类型(如 int、double、bool 等)能表示缺失或无效值,主要特点如下:
- 定义方式:在值类型后添加问号,例如:
int?
表示可空整数。 - 应用场景:适用于数据库 NULL 值或表单中允许空值的情况。
- 属性说明:
- 可以为有效值或 null
- 使用
.HasValue
检查是否有值,使用.Value
获取实际值(前提是.HasValue
为 true,否则会异常)
- 常用操作:
??
运算符提供默认值,如:int? a = null; int b = a ?? 0;
- 支持比较及算术运算,但需注意 null 的处理
- 数据库交互:方便表达未填写或缺失字段
示例
// 示例代码
int? number = null;
if (number.HasValue)
{
Console.WriteLine("Value: " + number.Value);
}
else
{
Console.WriteLine("Value is null");
}
类型转换
double.Parse 和 (double) 区别
- 用途:
double.Parse
:从字符串解析为 double,格式不合法会抛异常(double)
:将数值类型强制转换为 double,无需解析字符串
- 输入类型:
double.Parse
:只能解析字符串(double)
:可转换多种数值类型
- 异常处理:
double.Parse
: 解析失败时抛出 FormatException(double)
: 无异常,直接转换
示例:
string strNumber = "123.45";
double resultA = double.Parse(strNumber);
int intNumber = 123;
double resultB = (double)intNumber;
Convert.ToInt32 和 int.Parse 区别
- 输入范围:
Convert.ToInt32
接收字符串、bool、double 等 - 处理 null 值:
int.Parse(null)
: 抛 ArgumentNullExceptionConvert.ToInt32(null)
: 返回 0
- 其余异常:空字符串或非数字字符串时均抛异常
示例:
int result1 = int.Parse("123");
int result2 = Convert.ToInt32("123");
int result3 = int.Parse(nullString); // 抛异常
int result4 = Convert.ToInt32(nullString); // 返回 0
向上转型与向下转型
向上转型 (Upcasting)
向上转型是指将子类对象的引用赋值给父类变量的过程,其主要特点如下:
- 隐式转换,无需显式类型转换符。
- 始终安全,不会抛出异常。
- 只能访问父类中定义的成员。
示例
// 向上转型示例
Dog dog = new Dog();
Animal animal = dog; // 隐式向上转型,安全
animal.Eat(); // 可调用Animal中的成员
// animal.Bark(); // 编译错误,无法访问子类特有方法
向下转型 (Downcasting)
向下转型是将父类引用转换为子类变量的过程,需要注意安全性:
- 使用显式类型转换符。
- 如果类型不匹配,可能抛出InvalidCastException异常。
- 推荐在转换前使用is或as运算符进行检查。
示例
// 向下转型示例
Animal animal = new Dog(); // 父类引用指向子类实例
Dog dog = (Dog)animal; // 显式向下转型
// 安全的向下转型方式
if (animal is Dog)
{
Dog safeDog = (Dog)animal;
safeDog.Bark();
}
// 使用 as 操作符
Dog asDog = animal as Dog;
if (asDog != null)
{
asDog.Bark();
}
// C# 7.0以后的模式匹配
if (animal is Dog dogInstance)
{
dogInstance.Bark();
}
as 与 (Type) 显式转换的区别
特性 | as 运算符 | 显式转换 (Type) |
---|---|---|
失败行为 | 返回 null | 抛出 InvalidCastException 异常 |
适用类型 | 仅适用于引用类型和可空值类型 | 适用于所有类型 |
自定义转换 | 不支持用户定义的转换 | 支持用户定义的转换操作符 |
性能 | 通常略快,因不抛出异常 | 异常情况下可能略慢 |
as 运算符示例
// as 运算符使用示例
object obj = new Dog();
// 使用 as 进行安全转换
Dog dog = obj as Dog;
if (dog != null)
{
dog.Bark(); // 成功转换后使用
}
else
{
Console.WriteLine("转换失败但不抛出异常");
}
// 注意:as 不适用于值类型,除非使用可空值类型
int? i = obj as int?;
显式转换示例
object obj = new Dog();
try
{
// 使用显式转换
Dog dog = (Dog)obj; // 如果obj不是Dog类型则抛出异常
dog.Bark();
// 示例:值类型转换
object num = 123;
int i = (int)num;
// 示例:支持用户定义转换操作符
MyClass1 mc1 = new MyClass1();
MyClass2 mc2 = (MyClass2)mc1;
}
catch (InvalidCastException)
{
Console.WriteLine("转换失败,抛出异常");
}
Parent p = new Child(); 的本质
这句代码实现了向上转型,其背后包含以下技术要点:
- 内存分配与对象创建:new Child()在堆上创建完整的Child对象,包含子类和父类成员。
- 引用类型与实际类型分离:虽然引用类型为Parent,但实际对象类型是Child。
- 访问限制:Parent引用只能访问在Parent类中定义的成员;需要显式转换后访问Child的特有成员。
- 多态性:重写方法调用的是子类实现,而隐藏方法调用的是父类实现。
示例
class Parent {
public virtual void VirtualMethod() { Console.WriteLine("Parent's virtual method"); }
public void NormalMethod() { Console.WriteLine("Parent's normal method"); }
}
class Child : Parent {
public override void VirtualMethod() { Console.WriteLine("Child's override method"); }
public new void NormalMethod() { Console.WriteLine("Child's new method"); }
public void ChildOnlyMethod() { Console.WriteLine("Child-specific method"); }
}
// 使用示例
Parent p = new Child();
p.VirtualMethod(); // 输出: "Child's override method" (多态调用)
// p.ChildOnlyMethod(); // 编译错误,Parent类中没有此方法
// 向下转型后可访问子类特有方法
((Child)p).ChildOnlyMethod();
子类特有属性是否丢失?
物理上子类特有的属性和方法不会丢失,但通过父类引用时不可直接访问,需要向下转型才能访问。
示例
public class Pet
{
public string Name { get; set; }
public virtual void MakeSound() { Console.WriteLine("Some generic sound"); }
}
public class Dog : Pet
{
public bool CanFetch { get; set; } // Dog特有属性
public void Bark() { Console.WriteLine("Woof!"); } // Dog特有方法
public override void MakeSound() { Console.WriteLine("Woof!"); }
}
// 调用示例
Pet myPet = GetPetById("1"); // 获取Dog对象,但引用类型是Pet
myPet.Name = "Buddy";
myPet.MakeSound();
// myPet.CanFetch = true; // 编译错误,Pet类型中无CanFetch属性
// myPet.Bark(); // 编译错误,Pet类型中无Bark方法
总结
- 向上转型通过隐式转换保证类型安全,但只能调用父类成员。
- 向下转型需要显式转换,转换前应当做好类型检查。
- as运算符和C#模式匹配均能提高代码的健壮性,避免运行时异常。
- 建议合理设计类层次结构,避免不必要的强制类型转换。
dynamic 和 var
类型检查时机
- var: 在编译时确定类型,是一种静态类型
- dynamic: 在运行时确定类型,是一种动态类型
主要区别
1. 类型推断
- var需要编译器从初始化表达式推断具体类型,必须在声明时初始化
- dynamic不需要编译时确定类型,可以接受任何类型的值
2. 用法范围
- var只能用于局部变量声明
- dynamic可用于字段、参数、返回值类型等多种场景
3. 编译器检查
- var变量的成员访问在编译时检查
- dynamic变量的成员访问在运行时检查,编译时不进行验证
4. 性能影响
- var与直接使用具体类型没有性能差异
- dynamic因涉及运行时解析,会有一定性能开销
代码示例
// var示例 - 编译时确定类型
var name = "Hello"; // 编译器推断为string类型
// name = 123; // 编译错误,不能将int赋值给string
// dynamic示例 - 运行时确定类型
dynamic value = "Hello";
value = 123; // 正确,可以改变类型
value.NonExistentMethod(); // 编译通过,但运行时可能抛出异常
适用场景
- 使用var:当类型名称很长或明显可以从右侧推断时,提高代码可读性
- 使用dynamic:处理COM互操作、反射场景、处理JSON等动态数据
注意:过度使用dynamic会降低代码的类型安全性和性能,应当谨慎使用。
变量
变量定义
隐式类型变量
使用 var,根据赋值自动推断变量类型。
// 隐式类型变量示例
var number = 10; // 编译器推断为 int 类型
显式类型变量
直接声明具体类型的变量。
// 显示类型变量示例
int age = 25;
double price = 19.99;
string name = "Alice";
动态类型变量
使用 dynamic,在运行时确定变量类型,允许重新赋值为其他类型。
// 动态类型变量示例
dynamic obj = "Hello";
obj = 10; // 可以重新赋值为不同类型的值
object 类型变量
object 是所有类型的基类,可存储任意类型数据。
// object 类型示例
object obj = "Hello";
obj = 10; // 重新赋值为不同类型的值
常量
使用 const 定义在编译时确定且不可更改的常量。
// 常量示例
const int MaxValue = 100;
只读变量
readonly 变量只能在声明时或构造函数中赋值,一经赋值不可修改。
// 只读变量示例
readonly int MinValue = 0;
静态变量
使用 static 声明属于类而非实例的变量。
// 静态变量示例
static int count = 0;
引用参数和输出参数
使用 ref 和 out 定义方法的引用参数和输出参数。
// 引用参数和输出参数示例
void Modify(ref int x) { x = 10; }
void GetValue(out int y) { y = 20; }
变量初始化机制
类成员变量
类的成员变量(包括实例成员和静态成员)会自动赋予默认值。规则如下:
- 引用类型: 默认为
null
- 数值类型: 默认为
0
- bool 类型: 默认为
false
- char 类型: 默认为
'\0'
- 结构体: 每个字段被初始化为对应类型的默认值
示例
class Person
{
private int age; // 自动初始化为 0
private string name; // 自动初始化为 null
private bool isActive; // 自动初始化为 false
public void PrintValues()
{
Console.WriteLine($"Age: {age}, Name: {name}, IsActive: {isActive}");
// 输出: Age: 0, Name: , IsActive: False
}
}
局部变量
局部变量在声明时不会自动初始化,必须在使用前显式赋值,否者编译器报错。
示例
void Method()
{
int localVar; // 未初始化
// Console.WriteLine(localVar); // 编译错误:使用未赋值的局部变量
string localName; // 未初始化
// Console.WriteLine(localName); // 编译错误:使用未赋值的局部变量
// 正确做法
localVar = 10;
localName = "John";
Console.WriteLine($"{localVar}, {localName}");
}
注意事项
- 性能考虑: 局部变量不自动初始化,可避免不必要的性能开销。
- 安全性: 强制显式初始化局部变量有助于防止逻辑错误。
- 一致性: 类成员变量自动初始化确保对象在创建时状态一致。
总结
理解C#中的变量初始化机制,有助于编写更健壮和高性能的代码,建议在使用局部变量时始终进行显式初始化,同时充分利用类成员变量的默认值以简化代码。
命名规范
属性(Properties)命名
属性作为类对外的公共接口,建议使用大写字母开头(PascalCase)。
public string Name { get; set; } // 正确的属性命名
public int Age { get; set; } // 正确的属性命名
局部变量和方法参数命名
局部变量和方法参数仅在方法内部使用,采用小写字母开头的 camelCase 风格,便于区分与属性。
string name = "张三"; // 正确的局部变量命名
int age = 20; // 正确的局部变量命名
私有字段命名
对于类中的私有字段,可以采用下划线前缀结合 camelCase 的方式。这有助于区分局部变量或方法参数。
private string _firstName;
private int _studentAge;
示例:Student 类
下面的示例展示了如何在构造函数中同时使用公共属性和局部变量的命名规范。
public class Student
{
// 属性使用 PascalCase
public string Name { get; set; }
public int Age { get; set; }
// 私有字段采用 _camelCase
private string _studentId;
// 构造函数参数使用 camelCase
public Student(string name, int age)
{
// this.Name 表示属性,而 name 是参数
this.Name = name;
this.Age = age;
// ...其他初始化代码...
}
public void DisplayInfo()
{
// 局部变量使用 camelCase
string displayText = $"姓名:{this.Name}, 年龄:{this.Age}";
Console.WriteLine(displayText);
}
}
补充说明
- 属性(Property)作为类对外接口,应重点区分。
- 私有字段采用下划线前缀可有效提高代码阅读性。
- 保持构造函数参数和局部变量的统一命名风格有助于维护。
算法基础
排序算法
冒泡排序
public static void BubbleSort(int[] arr)
{
int n = arr.Length;
for (int i = 0; i < n - 1; i++)
{
// 每次遍历都会将最大的元素"冒泡"到末尾
for (int j = 0; j < n - 1 - i; j++)
{
// 比较相邻元素,如果前面的大于后面的则交换
if (arr[j] > arr[j + 1])
{
// 交换元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
字符串与格式化
格式化输出
基础格式化方法
String.Format 方法
- 使用占位符
{0}
,{1}
,{2}
等来指定变量的位置。
string name = "Alice";
int age = 25;
string formattedString = String.Format("Name: {0}, Age: {1}", name, age);
Console.WriteLine(formattedString); // 输出: Name: Alice, Age: 25
Console.WriteLine 方法
- 直接在
Console.WriteLine
中使用占位符。
string name = "Bob";
int age = 30;
Console.WriteLine("Name: {0}, Age: {1}", name, age); // 输出: Name: Bob, Age: 30
字符串插值(String Interpolation)
- 使用
$
符号直接在字符串中嵌入表达式,C# 6.0引入的现代化语法。
string name = "Charlie";
int age = 35;
Console.WriteLine($"Name: {name}, Age: {age}"); // 输出: Name: Charlie, Age: 35
格式化字符串
- 使用格式化字符串来指定数值的显示格式,例如日期、货币、小数位数等。
double price = 19.99;
DateTime now = DateTime.Now;
Console.WriteLine($"Price: {price:C}"); // 输出: Price: ¥19.99 (根据当前区域设置)
Console.WriteLine($"Date: {now:yyyy-MM-dd}"); // 输出: Date: 2023-10-23 (当前日期)
ToString 方法
- 使用
ToString
方法并传入格式化字符串。
double value = 123.456;
string formattedValue = value.ToString("F2"); // 保留两位小数
Console.WriteLine(formattedValue); // 输出: 123.46
StringBuilder.AppendFormat 方法
- 使用
StringBuilder
的AppendFormat
方法高效进行格式化,适合多次拼接场景。
StringBuilder sb = new StringBuilder();
sb.AppendFormat("Name: {0}, Age: {1}", "Dave", 40);
sb.AppendFormat(", Salary: {0:C}", 5000);
Console.WriteLine(sb.ToString()); // 输出: Name: Dave, Age: 40, Salary: ¥5,000.00
复合格式化
- 使用复合格式化字符串来格式化多个值。
string name = "Eve";
int age = 45;
Console.WriteLine("Name: {0}, Age: {1:D3}", name, age); // 输出: Name: Eve, Age: 045
标准格式说明符详解
格式说明符 | 名称 | 示例 | 说明 |
---|---|---|---|
C | 货币 | {0:C} → ¥123.45 | 货币格式,根据当前区域文化设置显示 |
D | 十进制 | {0:D5} → 00123 | 整数的十进制格式,可指定最小位数 |
E | 科学计数法 | {0:E2} → 1.23E+002 | 科学记数法,可指定小数点后的位数 |
F | 定点 | {0:F2} → 123.45 | 固定点格式,可指定小数点后的位数 |
G | 常规 | {0:G} → 123.456 | 根据值自动选择最有效的表示方式 |
N | 数字 | {0:N} → 123,456.79 | 带千位分隔符的数字格式 |
P | 百分比 | {0:P2} → 12.35% | 乘以100后以百分比显示,可指定小数点后的位数 |
X | 十六进制 | {0:X} → 7B | 十六进制格式,可指定位数 |
日期时间格式化
格式说明符 | 示例 | 结果 |
---|---|---|
d | {0:d} | 2023/10/23 (短日期) |
D | {0:D} | 2023年10月23日 星期一 (长日期) |
t | {0:t} | 14:30 (短时间) |
T | {0:T} | 14:30:45 (长时间) |
f | {0:f} | 2023年10月23日 14:30 (完整日期,短时间) |
F | {0:F} | 2023年10月23日 14:30:45 (完整日期,长时间) |
g | {0:g} | 2023/10/23 14:30 (短日期,短时间) |
G | {0:G} | 2023/10/23 14:30:45 (短日期,长时间) |
yyyy-MM-dd | {0:yyyy-MM-dd} | 2023-10-23 (自定义格式) |
yyyy年MM月dd日 HH:mm:ss | {0:yyyy年MM月dd日 HH:mm:ss} | 2023年10月23日 14:30:45 (自定义格式) |
复合格式语法详解
复合格式字符串语法:{index[,alignment][:formatString]}
- index: 参数位置,从0开始
- alignment: 对齐和宽度
- 正数: 右对齐
- 负数: 左对齐
- 数值大小: 字符宽度
- formatString: 格式说明符
对齐与宽度示例
// 左对齐与右对齐
Console.WriteLine("|{0,-10}|{1,10}|", "左边", "右边");
// 输出: |左边 | 右边|
// 表格式对齐
Console.WriteLine("{0,-10}{1,10}{2,15}", "姓名", "年龄", "工资");
Console.WriteLine("{0,-10}{1,10:D}{2,15:C}", "张三", 25, 8000);
Console.WriteLine("{0,-10}{1,10:D}{2,15:C}", "李四", 30, 10000);
// 输出:
// 姓名 年龄 工资
// 张三 025 ¥8,000.00
// 李四 030 ¥10,000.00
自定义格式化
自定义数字格式
字符 | 描述 | 示例 |
---|---|---|
0 | 零占位符 | {0:00.00} → 01.50 |
# | 数字占位符 | {0:#.##} → 1.5 |
. | 小数点 | {0:0.0} → 1.5 |
, | 千位分隔符或缩放 | {0:#,#} → 1,234 |
% | 百分比 | {0:0%} → 15% |
‰ | 千分比 | {0:0‰} → 15‰ |
E | 科学记数法 | {0:0.00E+00} → 1.50E+01 |
示例代码
double value = 1234.5678;
Console.WriteLine($"固定两位小数: {value:0.00}"); // 1234.57
Console.WriteLine($"最少1位整数,最多2位小数: {value:#.##}"); // 1234.57
Console.WriteLine($"千位分隔: {value:#,#.00}"); // 1,234.57
Console.WriteLine($"科学记数法: {value:0.00E+00}"); // 1.23E+03
Console.WriteLine($"自定义格式: {value:###-##-##}"); // 123-45-68
自定义日期格式
格式字符 | 描述 | 示例 |
---|---|---|
y | 年份 | yyyy → 2023 |
M | 月份 | MM → 10 |
d | 日 | dd → 23 |
h | 12小时制小时 | hh → 02 |
H | 24小时制小时 | HH → 14 |
m | 分钟 | mm → 30 |
s | 秒 | ss → 45 |
f | 小数秒 | fff → 678 |
t | AM/PM指示器 | tt → PM |
z | 时区 | zzz → +08:00 |
示例代码
DateTime now = DateTime.Now;
Console.WriteLine($"ISO 8601格式: {now:yyyy-MM-ddTHH:mm:ss}"); // 2023-10-23T14:30:45
Console.WriteLine($"中文日期格式: {now:yyyy年MM月dd日 dddd HH时mm分}"); // 2023年10月23日 星期一 14时30分
Console.WriteLine($"文件名安全格式: {now:yyyy-MM-dd_HH-mm-ss}"); // 2023-10-23_14-30-45
Console.WriteLine($"长日期短时间: {now:yyyy年MM月dd日 tt hh:mm}"); // 2023年10月23日 下午 02:30
不同区域文化下的格式化
using System.Globalization;
double price = 1234.56;
DateTime date = new DateTime(2023, 10, 23);
// 美国区域设置
CultureInfo us = new CultureInfo("en-US");
Console.WriteLine($"美元: {price.ToString("C", us)}"); // $1,234.56
Console.WriteLine($"美国日期: {date.ToString("D", us)}"); // Monday, October 23, 2023
// 中国区域设置
CultureInfo cn = new CultureInfo("zh-CN");
Console.WriteLine($"人民币: {price.ToString("C", cn)}"); // ¥1,234.56
Console.WriteLine($"中国日期: {date.ToString("D", cn)}"); // 2023年10月23日
// 德国区域设置
CultureInfo de = new CultureInfo("de-DE");
Console.WriteLine($"欧元: {price.ToString("C", de)}"); // 1.234,56 €
Console.WriteLine($"德国日期: {date.ToString("D", de)}"); // Montag, 23. Oktober 2023
实现IFormattable接口进行自定义格式化
public class Person : IFormattable
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string ToString(string format, IFormatProvider formatProvider)
{
if (string.IsNullOrEmpty(format)) format = "F";
switch (format.ToUpper())
{
case "F": // Full name
return $"{FirstName} {LastName}, {Age}岁";
case "S": // Short name
return $"{LastName}{FirstName}";
case "A": // Age only
return $"{Age}";
default:
throw new FormatException($"'{format}' 不是有效的格式说明符。");
}
}
public override string ToString()
{
return ToString("F", CultureInfo.CurrentCulture);
}
}
// 使用示例
Person person = new Person { FirstName = "张", LastName = "三", Age = 30 };
Console.WriteLine($"全名: {person:F}"); // 全名: 张 三, 30岁
Console.WriteLine($"简称: {person:S}"); // 简称: 三张
Console.WriteLine($"年龄: {person:A}"); // 年龄: 30
格式化输出最佳实践
选择最合适的方法
- 简单字符串连接:用于少量拼接
- 字符串插值(
$
):提高代码可读性,适合中等复杂度 - StringBuilder:高性能场景,需要多次追加
性能考虑
- 字符串操作较少时,
$
插值语法最直观 - 循环中大量拼接,使用
StringBuilder
- 避免不必要的
string.Format
嵌套调用
- 字符串操作较少时,
可读性提示
- 使用命名变量而非仅用索引
- 保持一致的格式风格
- 为复杂格式添加注释
日志与国际化
- 考虑使用
IFormatProvider
来适应不同区域 - 分离格式字符串和参数,便于本地化
- 使用专业日志框架处理复杂格式化
- 考虑使用
实用场景示例
表格输出
Console.WriteLine("{0,-15}{1,-10}{2,10}{3,15}", "商品名称", "单价", "数量", "总价");
Console.WriteLine(new string('-', 50));
Console.WriteLine("{0,-15}{1,-10:C}{2,10}{3,15:C}", "笔记本电脑", 6999, 2, 6999*2);
Console.WriteLine("{0,-15}{1,-10:C}{2,10}{3,15:C}", "手机", 2999, 5, 2999*5);
Console.WriteLine(new string('-', 50));
Console.WriteLine("{0,-15}{1,-10}{2,10}{3,15:C}", "总计", "", "", 6999*2+2999*5);
进度条显示
for (int i = 0; i <= 100; i += 10)
{
int width = Console.WindowWidth - 10;
int progressWidth = (int)(width * i / 100.0);
Console.Write("\r[{0,-" + width + "}] {1,3}%", new string('#', progressWidth), i);
Thread.Sleep(200); // 模拟进度
}
Console.WriteLine("\n完成!");
文件大小格式化
static string FormatFileSize(long bytes)
{
string[] suffixes = { "B", "KB", "MB", "GB", "TB" };
int i = 0;
double size = bytes;
while (size >= 1024 && i < suffixes.Length - 1)
{
size /= 1024;
i++;
}
return $"{size:0.##} {suffixes[i]}";
}
Console.WriteLine(FormatFileSize(1500)); // 1.46 KB
Console.WriteLine(FormatFileSize(2048576)); // 1.95 MB
Console.WriteLine(FormatFileSize(1073741824)); // 1 GB
数组与集合
数组
一维数组
简单初始化
编译器自动推断类型和大小,写法简洁:
int[] array1 = { 1, 3, -1, 5, -2 };
显式类型初始化
使用 new
显式创建数组实例:
int[] array2 = new int[] { 1, 3, -1, 5, -2 };
先声明后初始化
先声明变量,后面再赋值:
int[] array3;
array3 = new int[] { 1, 3, -1, 5, -2 };
指定大小后逐个赋值
声明时指定数组长度,然后通过索引赋值:
int[] array4 = new int[5];
array4[0] = 1;
array4[1] = 3;
array4[2] = -1;
array4[3] = 5;
array4[4] = -2;
使用 Array 类的静态方法
通过 Array.CreateInstance
方法创建数组,再通过 SetValue
方法赋值:
int[] array5 = (int[])Array.CreateInstance(typeof(int), 5);
array5.SetValue(1, 0);
array5.SetValue(3, 1);
array5.SetValue(-1, 2);
array5.SetValue(5, 3);
array5.SetValue(-2, 4);
多维数组
二维数组定义与初始化
声明并分配内存
直接声明一个固定行列的二维数组:
int[,] array = new int[4, 3];
声明并立即初始化(带初始值)
方式一:
指定行列数
c#int[,] array1 = new int[2, 3] { { 1, 2, 3 }, { 4, 5, 6 } };
方式二:
让编译器自动推断行列数
c#int[,] array2 = new int[,] { { 1, 2, 3 }, { 4, 5, 6 } };
方式三:
省略 new 关键字中的类型部分
c#int[,] array3 = { { 1, 2, 3 }, { 4, 5, 6 } };
先声明后初始化
先声明二维数组,再进行初始化:
int[,] array;
array = new int[2, 3] { { 1, 2, 3 }, { 4, 5, 6 } };
使用 Array 类创建
通过 Array.CreateInstance
方法动态创建二维数组:
Array myArray = Array.CreateInstance(typeof(int), 4, 3);
访问与操作二维数组
访问元素:
例如访问第二行第三列的元素(注意索引从 0 开始)
c#int element = array[1, 2];
修改元素:
修改第一行第二列的值
c#array[0, 1] = 10;
获取数组维度大小:
使用
c#GetLength
方法分别获取行数和列数
c#int rows = array.GetLength(0); // 行数 int columns = array.GetLength(1); // 列数
交错数组(Jagged Arrays)
交错数组是“数组的数组”,每一行可以有不同的长度,不同于固定行列的多维数组。
定义与初始化
int[][] jaggedArray = new int[3][];
// 为每一行分配内存并初始化
jaggedArray[0] = new int[] { 1, 2, 3 };
jaggedArray[1] = new int[] { 4, 5 };
jaggedArray[2] = new int[] { 6, 7, 8, 9 };
数组复制与动态操作
数组复制
使用 Array.Copy
方法将一个数组的内容复制到另一个数组:
string[] musicList = { "Island", "Ocean", "Pretty", "Sun" };
string[] insertedMusicList = new string[musicList.Length + 1];
// 复制原数组内容到新数组
Array.Copy(musicList, insertedMusicList, musicList.Length);
插入新元素(示例)
在复制后的数组中找到合适的位置插入新元素:
// 假设按字典顺序插入新歌曲
string insertName = Console.ReadLine();
int insertIndex = musicList.Length;
for (int i = 0; i < musicList.Length; i++)
{
if (string.Compare(musicList[i], insertName) > 0)
{
insertIndex = i;
break;
}
}
for (int i = insertedMusicList.Length - 1; i > insertIndex; i--)
{
insertedMusicList[i] = insertedMusicList[i - 1];
}
insertedMusicList[insertIndex] = insertName;
可以动态声明数组大小吗
在 C# 中,可以使用语法 int[] scores = new int[a];
来动态声明数组的大小,其中 a
必须是一个非负整数。这意味着你可以在运行时根据需要动态指定数组的大小。这种方式在你知道要创建数组大小的条件时非常有用。
int a = 5; // 可以根据实际需要动态设置这个值
int[] scores = new int[a]; // 创建一个大小为 a 的整数数组
在这个例子中,a
可以根据程序运行时输入或其他条件动态设置,数组 scores
的大小将根据 a
的值来确定。
在 C# 中,声明数组时不能使用 int[5] a
这种语法。相反,C# 中声明数组的正确方式是先指定数据类型及数组符号 []
,然后在初始化数组时才指定大小。以下是正确的用法:
int[] a = new int[5]; // 声明一个长度为5的整型数组
这种方式声明了一个包含 5 个元素的整数数组 a
。数组的大小是在使用 new
关键字创建数组实例时指定的,而不是在变量声明时。
交错数组和二维数组的区别
在 C# 中,交错数组和二维数组是两种不同的多维数组类型,它们在内存结构、声明语法和使用方式上有显著的不同。
二维数组 (int[,]
)
二维数组是一个矩形数组,所有行的列数必须相同。
特点:
- 内存布局:在内存中是连续的矩形块
- 声明方式:使用逗号分隔维度
int[,]
- 访问元素:使用
array[i, j]
语法 - 必须是矩形:每行列数必须相同
示例代码:
// 创建 3x4 的二维数组
int[,] matrix = new int[3, 4];
// 初始化二维数组
int[,] matrix2 = new int[,] {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
// 访问元素
int element = matrix[1, 2]; // 访问第2行第3列的元素
// 获取维度
int rows = matrix.GetLength(0); // 行数
int columns = matrix.GetLength(1); // 列数
交错数组 (int[][]
)
交错数组是"数组的数组",每行可以有不同的列数。
特点:
- 内存布局:每行是独立的数组,不连续
- 声明方式:使用多个方括号
int[][]
- 访问元素:使用
array[i][j]
语法 - 灵活性:每行可以有不同的长度
- 需要单独初始化:每行需要单独分配内存
示例代码:
// 创建有3行的交错数组
int[][] jaggedArray = new int[3][];
// 为每行分配不同长度的数组
jaggedArray[0] = new int[4] { 1, 2, 3, 4 };
jaggedArray[1] = new int[2] { 5, 6 };
jaggedArray[2] = new int[3] { 7, 8, 9 };
// 访问元素
int element = jaggedArray[1][1]; // 访问第2行第2列的元素
// 获取维度
int rows = jaggedArray.Length; // 行数
int columns1 = jaggedArray[0].Length; // 第1行的列数
int columns2 = jaggedArray[1].Length; // 第2行的列数
- 主要区别
特性 | 二维数组 (int[,] ) | 交错数组 (int[][] ) |
---|---|---|
内存分配 | 单次分配,连续存储 | 多次分配,不连续存储 |
内存效率 | 更高效,节省内存 | 每行有额外的数组开销 |
行列大小 | 所有行必须有相同列数 | 每行可以有不同的列数 |
访问语法 | array[i, j] | array[i][j] |
性能 | 通常对于固定大小的数据处理更快 | 不规则数据结构更灵活 |
获取尺寸 | GetLength(0) , GetLength(1) | Length 和 每行的 Length |
- 使用场景
- 二维数组:适合需要固定矩形布局的场景,如图像处理、棋盘游戏、矩阵运算。
- 交错数组:适合每行长度可能不同的场景,如存储不规则形状的数据、三角形数据结构等。
集合类
集合类基础
C#提供了丰富的集合类,分为非泛型集合(位于 System.Collections
命名空间)和泛型集合(位于 System.Collections.Generic
命名空间)两大类。随着.NET的发展,泛型集合因其类型安全和性能优势已成为首选。
泛型集合概述
泛型集合提供类型安全的数据操作,避免装箱拆箱操作,提高性能。在 C# 中,List<T>
、Dictionary<TKey, TValue>
、HashSet<T>
等是最常用的泛型集合。
在 C# 中,new List<T>
是用于创建泛型集合 List<T>
的语法。List<T>
是一个泛型类,表示一个可以存储任意类型对象的动态数组。T
是类型参数,可以是任何类型。
泛型集合允许您创建类型安全的集合,而不需要在集合中进行类型转换。List<T>
是最常用的泛型集合之一。
以下是创建和使用 List<T>
的基本语法:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 创建一个存储整数的列表
List<int> intList = new List<int>();
// 添加元素到列表
intList.Add(1);
intList.Add(2);
intList.Add(3);
// 访问列表中的元素
foreach (int item in intList)
{
Console.WriteLine(item);
}
// 创建一个存储字符串的列表
List<string> stringList = new List<string> { "apple", "banana", "cherry" };
// 访问列表中的元素
foreach (string item in stringList)
{
Console.WriteLine(item);
}
// 使用索引访问和修改元素
Console.WriteLine($"第一个元素: {stringList[0]}");
stringList[0] = "apricot";
// 使用Contains方法检查元素
bool containsBanana = stringList.Contains("banana"); // true
// 使用Insert在特定位置插入元素
stringList.Insert(1, "blueberry");
// 使用Remove删除特定元素
stringList.Remove("cherry");
// 使用RemoveAt删除特定位置的元素
stringList.RemoveAt(0);
}
}
常用集合类型详解
线性集合对比:Array、ArrayList 和 List<T>
在现代C#开发中,除非需要固定大小数组的特性,否则List<T>
几乎总是比Array和ArrayList更好的选择。
特性 | Array | ArrayList | List<T> |
---|---|---|---|
命名空间 | System | System.Collections | System.Collections.Generic |
内存分配 | 固定大小,连续内存 | 动态大小 | 动态大小 |
大小调整 | 不可调整,创建后大小固定 | 可动态调整 | 可动态调整 |
类型安全 | 强类型,只能存储同一类型 | 非类型安全,可存储任意类型 | 强类型,泛型实现 |
元素类型 | 同一类型 | Object(可存不同类型) | 指定的泛型类型 |
装箱/拆箱 | 无(值类型数组) | 存在装箱/拆箱,影响性能 | 无装箱/拆箱 |
性能 | 最高(直接内存访问) | 最低(装箱/拆箱开销) | 高(接近数组) |
类型检查 | 编译时 | 运行时 | 编译时 |
索引器 | 支持 | 支持 | 支持 |
常用方法 | Length, Clone, CopyTo | Add, Remove, Insert, Sort, Clear | Add, Remove, Insert, Find, Sort, ForEach |
创建语法 | int[] arr = new int[5] | ArrayList list = new ArrayList() | List<int> list = new List<int>() |
适用场景 | 固定大小集合,性能要求高 | 需要存储不同类型(不推荐使用) | 动态集合,类型统一 |
引入时间 | .NET 1.0 | .NET 1.0 | .NET 2.0 |
内存效率 | 高 | 低(装箱开销) | 高 |
推荐程度 | 固定大小集合场景推荐 | 不推荐,已被泛型集合取代 | 强烈推荐,现代应用首选 |
线性集合代码示例对比
// 创建和初始化
int[] array = new int[3] { 1, 2, 3 };
ArrayList arrayList = new ArrayList();
arrayList.Add(1);
arrayList.Add(2);
List<int> list = new List<int> { 1, 2, 3 };
// 访问元素
int a1 = array[0];
int a2 = (int)arrayList[0]; // 需要强制类型转换
int a3 = list[0]; // 不需要强制类型转换
// 添加元素
// array.Add(4); // 错误,数组无Add方法
arrayList.Add(3);
list.Add(4);
// 数组常用操作
int length = array.Length;
Array.Resize(ref array, 5); // 调整大小(创建新数组)
Array.Sort(array); // 排序
int index = Array.IndexOf(array, 2); // 查找元素
// List<T>独有操作
list.FindAll(x => x > 2); // LINQ支持
list.ForEach(x => Console.WriteLine(x)); // 内置ForEach
list.RemoveAll(x => x % 2 == 0); // 条件删除
List<T>
的高级操作
List<string> fruits = new List<string> { "apple", "banana", "cherry", "date", "elderberry" };
// 范围操作
List<string> subset = fruits.GetRange(1, 3); // 获取从索引1开始的3个元素
// 转换
string[] fruitArray = fruits.ToArray(); // 转换为数组
HashSet<string> fruitSet = new HashSet<string>(fruits); // 转换为HashSet
// 排序
fruits.Sort(); // 默认排序
fruits.Sort((a, b) => b.CompareTo(a)); // 自定义排序(降序)
// 搜索
int indexOfBanana = fruits.FindIndex(f => f.StartsWith("b")); // 查找第一个以b开头的元素
List<string> longFruits = fruits.FindAll(f => f.Length > 5); // 查找所有长度大于5的水果
// 批量操作
List<string> moreFruits = new List<string> { "fig", "grape" };
fruits.AddRange(moreFruits); // 添加多个元素
fruits.RemoveRange(2, 2); // 从索引2开始移除2个元素
// 容量管理
fruits.TrimExcess(); // 优化内存使用(减少容量到实际元素数)
fruits.Capacity = 10; // 预分配容量,减少重新分配
字典类型对比:HashTable 和 Dictionary<TKey, TValue>
在现代C#开发中,除非特别需要HashTable的线程安全特性,否则Dictionary<TKey, TValue>
几乎总是更好的选择。
特性 | HashTable | Dictionary<TKey, TValue> |
---|---|---|
命名空间 | System.Collections | System.Collections.Generic |
类型安全 | 非泛型,键和值都作为 object 存储 | 泛型实现,编译时类型检查 |
键值类型 | 任意类型 (object) | 指定的泛型类型 |
装箱/拆箱 | 存在装箱/拆箱,影响性能 | 无装箱/拆箱 (使用引用类型时) |
性能 | 较低 | 更高效 |
线程安全 | 默认线程安全 (同步的) | 默认非线程安全 (需要使用 ConcurrentDictionary) |
允许空键 | 不允许 null 作为键 | 允许 null 作为键(如果键类型可为 null) |
大小写敏感 | 默认大小写不敏感(可配置) | 默认大小写敏感(可配置) |
遍历方式 | 返回 DictionaryEntry | 返回 KeyValuePair<TKey, TValue> |
引入时间 | .NET 1.0 | .NET 2.0 |
内存效率 | 较低 | 较高 |
类型检查 | 运行时 | 编译时 |
适用场景 | 遗留代码,需要线程安全 | 现代应用,类型安全要求高 |
推荐程度 | 不推荐,除非有特殊需求 | 强烈推荐,现代应用首选 |
字典类型代码示例对比
// HashTable 示例
Hashtable hashtable = new Hashtable();
hashtable.Add("key1", 100);
hashtable.Add("key2", "value");
hashtable.Add(1, true);
// 需要类型转换(拆箱)
int value1 = (int)hashtable["key1"];
string value2 = (string)hashtable["key2"];
// 遍历
foreach (DictionaryEntry entry in hashtable)
{
Console.WriteLine($"Key: {entry.Key}, Value: {entry.Value}");
}
// Dictionary 示例
Dictionary<string, int> dictionary = new Dictionary<string, int>();
dictionary.Add("key1", 100);
dictionary.Add("key2", 200);
// dictionary.Add(1, 300); // 编译错误,键类型必须是 string
// dictionary.Add("key3", "value"); // 编译错误,值类型必须是 int
// 无需类型转换
int dictValue = dictionary["key1"];
// 遍历
foreach (KeyValuePair<string, int> pair in dictionary)
{
Console.WriteLine($"Key: {pair.Key}, Value: {pair.Value}");
}
// 安全访问
if (dictionary.TryGetValue("key3", out int result))
{
Console.WriteLine($"找到值: {result}");
}
else
{
Console.WriteLine("未找到键");
}
// 检查键是否存在
bool hasKey = dictionary.ContainsKey("key1");
// 直接赋值(如果键不存在会自动添加)
dictionary["key3"] = 300;
Dictionary<TKey, TValue>
的高级操作
Dictionary<string, int> scores = new Dictionary<string, int>()
{
["Alice"] = 95,
["Bob"] = 80,
["Charlie"] = 88
};
// 获取所有键和值
List<string> names = new List<string>(scores.Keys);
List<int> points = new List<int>(scores.Values);
// 使用自定义比较器(不区分大小写的键)
Dictionary<string, int> caseInsensitiveScores = new Dictionary<string, int>(
StringComparer.OrdinalIgnoreCase);
caseInsensitiveScores["alice"] = 95;
int aliceScore = caseInsensitiveScores["ALICE"]; // 可以用大写键访问
// 使用LINQ操作Dictionary
var highScorers = scores.Where(kv => kv.Value > 85)
.Select(kv => kv.Key)
.ToList();
// 排序Dictionary
var sortedByName = scores.OrderBy(kv => kv.Key);
var sortedByScore = scores.OrderByDescending(kv => kv.Value);
// 合并两个Dictionary
Dictionary<string, int> moreScores = new Dictionary<string, int>()
{
["Dave"] = 78,
["Alice"] = 97 // 注意:这会更新Alice的分数
};
foreach (var pair in moreScores)
{
scores[pair.Key] = pair.Value; // 添加或更新
}
集合类型:HashSet<T>
和SortedSet<T>
HashSet<T>
是一个高性能的无序集合,而SortedSet<T>
是一个有序集合,二者都不允许重复元素。
// HashSet基本用法
HashSet<int> numbers = new HashSet<int> { 1, 2, 3, 4, 5 };
numbers.Add(6); // 添加元素
numbers.Add(1); // 添加重复元素,将被忽略
// 集合操作
HashSet<int> otherNumbers = new HashSet<int> { 4, 5, 6, 7, 8 };
numbers.UnionWith(otherNumbers); // 并集
numbers.IntersectWith(otherNumbers); // 交集
numbers.ExceptWith(otherNumbers); // 差集
numbers.SymmetricExceptWith(otherNumbers); // 对称差集
// 检查包含关系
bool isSubset = numbers.IsSubsetOf(otherNumbers);
bool isProperSubset = numbers.IsProperSubsetOf(otherNumbers);
// SortedSet - 自动排序的集合
SortedSet<string> sortedNames = new SortedSet<string> { "Zack", "Alice", "Bob" };
// 自动按字母顺序排序:Alice, Bob, Zack
// 自定义排序
SortedSet<Person> people = new SortedSet<Person>(new PersonComparer())
{
new Person { Name = "Alice", Age = 30 },
new Person { Name = "Bob", Age = 25 }
};
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
public class PersonComparer : IComparer<Person>
{
public int Compare(Person x, Person y)
{
return x.Age.CompareTo(y.Age); // 按年龄排序
}
}
队列和栈:Queue<T>
和Stack<T>
这些集合类型实现了特定的数据结构,适用于特定场景。
// Queue<T> - 先进先出队列
Queue<string> processingQueue = new Queue<string>();
processingQueue.Enqueue("Task 1"); // 添加到队尾
processingQueue.Enqueue("Task 2");
processingQueue.Enqueue("Task 3");
while (processingQueue.Count > 0)
{
string currentTask = processingQueue.Dequeue(); // 从队首移除
Console.WriteLine($"处理: {currentTask}");
}
// Peek - 查看队首元素但不移除
Queue<int> numbers = new Queue<int>(new[] { 1, 2, 3 });
int firstNumber = numbers.Peek(); // 返回1,但不移除
// Stack<T> - 后进先出栈
Stack<string> browserHistory = new Stack<string>();
browserHistory.Push("https://home.page");
browserHistory.Push("https://search.page");
browserHistory.Push("https://result.page");
// 浏览器后退功能
string currentPage = browserHistory.Pop(); // 移除并返回 "https://result.page"
string previousPage = browserHistory.Peek(); // 返回 "https://search.page" 但不移除
// 检查元素是否存在
bool containsHomePage = browserHistory.Contains("https://home.page");
// 转换为数组
string[] historyArray = browserHistory.ToArray();
特殊集合类型
线程安全集合
位于 System.Collections.Concurrent
命名空间下,专为多线程应用设计。
using System.Collections.Concurrent;
// 线程安全的字典
ConcurrentDictionary<string, int> concurrentScores = new ConcurrentDictionary<string, int>();
// 线程安全的添加或更新
concurrentScores.AddOrUpdate("Alice", 95, (key, oldValue) => oldValue + 5);
// 获取或添加
int bobScore = concurrentScores.GetOrAdd("Bob", 80);
// 线程安全的队列
ConcurrentQueue<string> taskQueue = new ConcurrentQueue<string>();
taskQueue.Enqueue("Task 1");
bool success = taskQueue.TryDequeue(out string nextTask);
if (success)
{
Console.WriteLine($"处理任务: {nextTask}");
}
// 线程安全的栈
ConcurrentStack<int> stack = new ConcurrentStack<int>();
stack.Push(1);
stack.Push(2);
success = stack.TryPop(out int result);
不可变集合
位于 System.Collections.Immutable
命名空间下,创建后内容不可修改,适用于函数式编程和线程安全场景。
using System.Collections.Immutable;
// 不可变列表
ImmutableList<int> immutableList = ImmutableList.Create<int>(1, 2, 3);
// "修改"操作会返回新实例,原实例不变
ImmutableList<int> newList = immutableList.Add(4);
// immutableList 仍然是 [1,2,3]
// newList 是 [1,2,3,4]
// 不可变字典
ImmutableDictionary<string, int> scores = ImmutableDictionary.Create<string, int>()
.Add("Alice", 95)
.Add("Bob", 80);
// 不可变集合
ImmutableHashSet<string> names = ImmutableHashSet.Create<string>("Alice", "Bob");
ObservableCollection<T>
支持数据绑定和集合变更通知,常用于UI开发。
using System.Collections.ObjectModel;
using System.Collections.Specialized;
ObservableCollection<string> items = new ObservableCollection<string>
{
"Item 1", "Item 2"
};
// 注册变更事件
items.CollectionChanged += (sender, e) =>
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
Console.WriteLine($"添加了新项: {string.Join(", ", e.NewItems.Cast<string>())}");
}
};
// 触发变更通知
items.Add("Item 3"); // 输出: 添加了新项: Item 3
集合选择指南与性能考量
选择合适的集合类型
需求场景 | 推荐集合类型 |
---|---|
需要频繁添加/移除元素的序列 | List<T> |
固定大小的元素序列 | T[](数组) |
需要键值对快速查找 | Dictionary<TKey, TValue> |
确保元素唯一性 | HashSet<T> |
需要按顺序存储唯一元素 | SortedSet<T> |
需要先进先出处理 | Queue<T> |
需要后进先出处理 | Stack<T> |
多线程环境下的集合操作 | ConcurrentDictionary<TKey, TValue> 等 |
需要不可变集合 | ImmutableList<T> 等 |
需要观察集合变化 (如UI数据绑定) | ObservableCollection<T> |
链表结构,频繁在中间插入/删除元素 | LinkedList<T> |
双向优先级队列 | SortedDictionary<TKey, TValue> |
性能优化技巧
// 预分配容量以减少重新分配
List<int> list = new List<int>(10000);
// 避免在循环中调整集合大小
// 不推荐:
for (int i = 0; i < 1000; i++)
{
list.Add(i);
}
// 推荐:
list.Capacity = list.Count + 1000;
for (int i = 0; i < 1000; i++)
{
list.Add(i);
}
// 避免LINQ的过度使用
// 不推荐(多次遍历):
var result = list.Where(x => x > 10).OrderBy(x => x).Select(x => x * 2).ToList();
// 推荐(单次遍历):
List<int> result = new List<int>();
foreach (var item in list)
{
if (item > 10)
{
result.Add(item * 2);
}
}
result.Sort();
// 高效字典查找
Dictionary<string, int> dict = new Dictionary<string, int>();
// 使用TryGetValue而不是ContainsKey+索引器
// 不推荐:
if (dict.ContainsKey("key"))
{
int value = dict["key"];
}
// 推荐:
if (dict.TryGetValue("key", out int value))
{
// 使用 value
}
// 集合并行处理(适用于大数据集)
using System.Threading.Tasks;
List<int> numbers = Enumerable.Range(1, 1000000).ToList();
Parallel.ForEach(numbers, num =>
{
// 并行处理每个元素
// 注意:需要考虑线程安全问题
});
内存使用优化
- 使用
struct
而不是class
作为集合元素可以减少内存使用(但要避免过大的结构体) - 释放不再使用的集合(
list = null
)以帮助GC回收内存 - 考虑使用
ArrayPool<T>
在高性能场景中复用数组 - 使用
TrimExcess()
方法释放集合中未使用的内存
高级LINQ与集合操作
List<Product> products = GetSampleProducts();
// 基本查询
var cheapProducts = products.Where(p => p.Price < 50);
// 分组
var productsByCategory = products.GroupBy(p => p.Category);
foreach (var group in productsByCategory)
{
Console.WriteLine($"类别: {group.Key}, 数量: {group.Count()}");
}
// 联接操作
var customers = GetCustomers();
var orders = GetOrders();
var customerOrders = customers.Join(
orders,
c => c.Id,
o => o.CustomerId,
(c, o) => new { Customer = c.Name, OrderDate = o.Date, Amount = o.Amount }
);
// 聚合
double averagePrice = products.Average(p => p.Price);
decimal totalRevenue = products.Sum(p => p.Price * p.QuantitySold);
int maxStock = products.Max(p => p.StockLevel);
// 分页
int pageSize = 10;
int pageNumber = 2;
var page = products.Skip((pageNumber - 1) * pageSize).Take(pageSize);
// 去重
var uniqueCategories = products.Select(p => p.Category).Distinct();
常见集合问题解决方案
深拷贝集合
// 简单类型的深拷贝
List<int> original = new List<int> { 1, 2, 3 };
List<int> copy = new List<int>(original);
// 复杂类型的深拷贝(需要实现ICloneable)
List<CloneableObject> objects = GetObjects();
List<CloneableObject> deepCopy = objects.Select(obj => (CloneableObject)obj.Clone()).ToList();
自定义比较器
List<Product> products = GetProducts();
// 自定义排序
products.Sort(new ProductPriceComparer());
public class ProductPriceComparer : IComparer<Product>
{
public int Compare(Product x, Product y)
{
return x.Price.CompareTo(y.Price);
}
}
// 在Dictionary中使用自定义比较器
Dictionary<string, Product> productDict = new Dictionary<string, Product>(
StringComparer.OrdinalIgnoreCase); // 不区分大小写的键比较
线程安全的集合修改
List<int> list = new List<int> { 1, 2, 3, 4, 5 };
// 不安全的方式:
// foreach (var item in list)
// {
// if (item % 2 == 0)
// list.Remove(item); // 会抛出异常!
// }
// 安全的方式1:倒序遍历
for (int i = list.Count - 1; i >= 0; i--)
{
if (list[i] % 2 == 0)
list.RemoveAt(i);
}
// 安全的方式2:创建新集合
list = list.Where(item => item % 2 != 0).ToList();
// 安全的方式3:使用线程安全集合
ConcurrentBag<int> concurrentBag = new ConcurrentBag<int>(list);
选择建议:在现代C#开发中,泛型集合几乎总是比非泛型集合更佳的选择,因其类型安全、性能更好且API更丰富。根据具体场景选择最适合的集合类型,能够显著提升应用性能和代码质量。
方法与函数
方法基础
参数传递
C#中有四种主要的参数传递方式:按值传递、按引用传递(ref)、按输出传递(out)和按常量传递(in)。
按值传递
在C#中,按值传递是默认的参数传递方式,不需要特殊关键字。
主要特点
- 方法接收的是原始数据的副本
- 方法内对参数的任何修改都不会影响原始数据
- 对于值类型,复制整个数据
- 对于引用类型,复制引用(指针),但不复制引用的对象
示例代码
// 值类型的按值传递
void ModifyValue(int x)
{
x = x + 10; // 只修改副本,原始值不变
Console.WriteLine($"方法内: {x}");
}
int number = 5;
ModifyValue(number);
Console.WriteLine($"方法外: {number}"); // 输出: 方法外: 5
// 引用类型的按值传递
void ModifyArray(int[] arr)
{
arr[0] = 100; // 修改引用的对象,会影响原始数组
arr = new int[] { 200, 300 }; // 重新赋值只影响副本,原始引用不变
}
int[] myArray = { 1, 2, 3 };
ModifyArray(myArray);
Console.WriteLine($"第一个元素: {myArray[0]}"); // 输出: 第一个元素: 100
按引用传递(ref)
C#中的ref
关键字用于按引用传递参数,允许方法修改原始变量。
主要特点
- 允许方法直接修改传递给它的原始变量
- 变量在传递前必须初始化
- 适用于值类型和引用类型
- 方法签名和调用处都需要使用
ref
关键字
示例代码
void SwapNumbers(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 10;
int y = 20;
SwapNumbers(ref x, ref y);
// 现在 x 是 20,y 是 10
// ref 用于引用类型
void ReplaceList(ref List<int> list)
{
list = new List<int> { 100, 200, 300 }; // 会替换原始引用
}
List<int> myList = new List<int> { 1, 2, 3 };
ReplaceList(ref myList);
// myList 现在包含 [100, 200, 300]
ref 返回值(C# 7.0+)
C# 7.0引入了ref返回值,允许方法返回对变量的引用而不是值。
public ref int FindValue(int[] array, int value)
{
for (int i = 0; i < array.Length; i++)
{
if (array[i] == value)
return ref array[i]; // 返回引用,而不是副本
}
throw new ArgumentException("未找到值");
}
int[] numbers = { 1, 2, 3, 4, 5 };
ref int found = ref FindValue(numbers, 3);
found = 10; // 修改原始数组元素
// numbers 现在是 { 1, 2, 10, 4, 5 }
按输出传递(out)
在C#中,按输出传递参数使用out
关键字,允许方法通过参数返回多个值。
主要特点
- 方法通过参数返回多个值
- 调用方法前不需要初始化参数变量
- 在方法内部必须为out参数赋值
- 方法签名和调用处都需要使用
out
关键字
示例代码
void GetValues(out int a, out int b)
{
a = 5;
b = 10;
}
// 调用方式1:传统方式
int x, y;
GetValues(out x, out y);
Console.WriteLine($"x={x}, y={y}"); // 输出: x=5, y=10
// 调用方式2:内联声明(C# 7.0+)
GetValues(out int m, out int n);
Console.WriteLine($"m={m}, n={n}"); // 输出: m=5, n=10
// 实用示例:TryParse方法
bool success = int.TryParse("123", out int result);
if (success)
{
Console.WriteLine($"解析成功: {result}");
}
复杂示例
void Calculate(int input, out int square, out int cube, out bool isPrime)
{
square = input * input;
cube = input * input * input;
// 判断是否为质数
isPrime = true;
if (input <= 1) isPrime = false;
for (int i = 2; i <= Math.Sqrt(input); i++)
{
if (input % i == 0)
{
isPrime = false;
break;
}
}
}
Calculate(7, out int sq, out int cb, out bool prime);
Console.WriteLine($"平方: {sq}, 立方: {cb}, 是否质数: {prime}");
// 输出: 平方: 49, 立方: 343, 是否质数: True
按常量传递(in)
C# 7.2引入的in
关键字用于按常量引用传递参数,提高大型值类型参数的性能。
主要特点
- 传递的是对变量的只读引用
- 避免大型值类型(如结构体)的复制开销
- 方法内不能修改参数值
- 参数在传递前必须初始化
示例代码
void ProcessLargeStruct(in LargeStruct data)
{
// data = new LargeStruct(); // 错误!不能修改in参数
Console.WriteLine(data.Value); // 只读访问是允许的
}
struct LargeStruct
{
public long[] Data { get; }
public int Value { get; }
public LargeStruct(int size, int value)
{
Data = new long[size];
Value = value;
}
}
var large = new LargeStruct(1000, 42);
ProcessLargeStruct(in large); // 高效传递,没有复制整个结构
参数修饰符比较
修饰符 | 初始化 | 方向 | 方法内必须赋值 | 主要用途 |
---|---|---|---|---|
(无) | 必须 | 输入 | 否 | 传递信息到方法 |
ref | 必须 | 双向 | 否 | 修改现有变量值 |
out | 不需要 | 输出 | 是 | 从方法返回多个值 |
in | 必须 | 输入 | 不允许修改 | 高效传递大型值类型 |
最佳实践
- 按值传递:简单且安全,适用于大多数情况
- ref参数:谨慎使用,使代码更难理解
- out参数:适合返回多个值,但考虑使用元组或自定义类型作为替代
- in参数:用于优化大型结构体的性能,让调用者明确知道传入的值不会被修改
应用场景
- ref:交换值、修改集合、返回数组中的元素引用
- out:Parse方法、多个返回值、状态与数据同时返回
- in:大型结构体、向量/矩阵运算、3D图形计算
Math 类
常用数学常量
Math.PI
:圆周率π的值(约3.14159265358979)csharpdouble circleArea = Math.PI * radius * radius;
Math.E
:自然对数的底数e(约2.71828182845905)csharpdouble compound = principal * Math.Pow(Math.E, rate * time);
基本数学运算
Math.Abs(x)
:返回数值的绝对值csharpint absInt = Math.Abs(-10); // 返回 10 double absDouble = Math.Abs(-15.5); // 返回 15.5
Math.Sign(x)
:返回指示数字符号的整数csharpint sign1 = Math.Sign(-5); // 返回 -1 (负数) int sign2 = Math.Sign(0); // 返回 0 (零) int sign3 = Math.Sign(10); // 返回 1 (正数)
Math.Clamp(value, min, max)
:将值限制在指定范围内(.NET Core 2.0+)csharpint result = Math.Clamp(value, 0, 100); // 确保值在0到100之间
幂与对数运算
Math.Pow(x, y)
:计算x的y次幂csharpdouble squared = Math.Pow(5, 2); // 返回 25 (5²) double cubed = Math.Pow(2, 3); // 返回 8 (2³) double fraction = Math.Pow(9, 0.5); // 返回 3 (9的平方根)
Math.Sqrt(x)
:计算平方根csharpdouble root = Math.Sqrt(16); // 返回 4 double root2 = Math.Sqrt(2); // 返回 1.4142...
Math.Exp(x)
:计算e的x次幂csharpdouble result = Math.Exp(1); // 返回 Math.E (~2.718) double growth = Math.Exp(2.3); // 返回 e^2.3
Math.Log(x, [base])
:计算对数csharpdouble natural = Math.Log(10); // 返回ln(10) (~2.302) double customBase = Math.Log(8, 2); // 返回log₂(8) = 3
Math.Log10(x)
:计算以10为底的对数csharpdouble log10 = Math.Log10(100); // 返回 2 double log10_2 = Math.Log10(0.01); // 返回 -2
Math.Log2(x)
:计算以2为底的对数(.NET Core 3.0+)csharpdouble log2 = Math.Log2(8); // 返回 3 double log2_bits = Math.Log2(1024); // 返回 10
三角函数
基本三角函数
Math.Sin(x)
:正弦函数(x为弧度)csharpdouble sin90 = Math.Sin(Math.PI / 2); // 返回 1
Math.Cos(x)
:余弦函数(x为弧度)csharpdouble cos180 = Math.Cos(Math.PI); // 返回 -1
Math.Tan(x)
:正切函数(x为弧度)csharpdouble tan45 = Math.Tan(Math.PI / 4); // 返回 1
反三角函数
Math.Asin(x)
:反正弦函数(返回弧度)csharpdouble angle = Math.Asin(0.5); // 返回 π/6 (~0.524) double degrees = Math.Asin(1) * 180 / Math.PI; // 返回 90度
Math.Acos(x)
:反余弦函数(返回弧度)csharpdouble angle = Math.Acos(0); // 返回 π/2
Math.Atan(x)
:反正切函数(返回弧度)csharpdouble angle = Math.Atan(1); // 返回 π/4 (45度)
Math.Atan2(y, x)
:求点(x,y)与原点连线与x轴正方向的夹角csharpdouble angle = Math.Atan2(y, x); // 返回-π到π之间的角度
双曲函数
Math.Sinh(x)
:双曲正弦csharpdouble result = Math.Sinh(1); // 返回 1.1752...
Math.Cosh(x)
:双曲余弦csharpdouble result = Math.Cosh(1); // 返回 1.5430...
Math.Tanh(x)
:双曲正切csharpdouble result = Math.Tanh(1); // 返回 0.7615...
取整与舍入
Math.Ceiling(x)
:向上取整csharpdouble ceil1 = Math.Ceiling(3.2); // 返回 4 double ceil2 = Math.Ceiling(-3.2); // 返回 -3
Math.Floor(x)
:向下取整csharpdouble floor1 = Math.Floor(3.8); // 返回 3 double floor2 = Math.Floor(-3.2); // 返回 -4
Math.Round(x, [digits], [mode])
:舍入至最接近的值csharpdouble round1 = Math.Round(3.5); // 返回 4 double round2 = Math.Round(4.5); // 返回 4 (默认使用银行家舍入法) double round3 = Math.Round(3.456, 2); // 返回 3.46 (保留两位小数) double round4 = Math.Round(3.5, MidpointRounding.AwayFromZero); // 返回 4
Math.Truncate(x)
:截断小数部分csharpdouble trunc1 = Math.Truncate(3.8); // 返回 3 double trunc2 = Math.Truncate(-3.8); // 返回 -3
最大值与最小值
Math.Max(x, y)
:返回两个值中的较大值csharpint max = Math.Max(10, 20); // 返回 20 double maxD = Math.Max(10.5, 10.1); // 返回 10.5
Math.Min(x, y)
:返回两个值中的较小值csharpint min = Math.Min(10, 20); // 返回 10 double minD = Math.Min(-4.5, 3.7); // 返回 -4.5
获取多个值中的最大/最小值
csharp// .NET 6+ int maxOfMany = Math.Max(1, Math.Max(5, Math.Max(3, 4))); // 返回 5 // 使用LINQ (需要 using System.Linq) int[] numbers = { 5, 3, 9, 1, 7 }; int maxVal = numbers.Max(); // 返回 9 int minVal = numbers.Min(); // 返回 1
高级数学函数
Math.IEEERemainder(x, y)
:按照IEEE 754标准计算余数csharpdouble remainder = Math.IEEERemainder(10, 3); // 返回 1 // 不同于 10 % 3,后者返回 1
Math.BigMul(a, b)
:返回两个32位数相乘的完整64位结果csharplong result = Math.BigMul(70000, 70000); // 返回 4900000000
Math.DivRem(a, b, out remainder)
:计算整数除法和余数csharpint quotient = Math.DivRem(10, 3, out int remainder); // quotient=3, remainder=1
Math.FusedMultiplyAdd(x, y, z)
:计算 (x * y) + z,只进行一次舍入操作 (.NET 5+)csharpdouble result = Math.FusedMultiplyAdd(3.0, 4.0, 5.0); // 返回 17.0
实际应用示例
距离计算
double CalculateDistance(double x1, double y1, double x2, double y2)
{
double deltaX = x2 - x1;
double deltaY = y2 - y1;
return Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
}
double distance = CalculateDistance(0, 0, 3, 4); // 返回 5
角度与弧度转换
double DegreesToRadians(double degrees)
{
return degrees * Math.PI / 180.0;
}
double RadiansToDegrees(double radians)
{
return radians * 180.0 / Math.PI;
}
double rad = DegreesToRadians(90); // π/2
double deg = RadiansToDegrees(Math.PI); // 180
数值舍入到特定精度
double RoundToNearest(double value, double precision)
{
return Math.Round(value / precision) * precision;
}
double rounded = RoundToNearest(42.357, 0.05); // 返回 42.35
double rounded2 = RoundToNearest(42.357, 0.25); // 返回 42.25
兴趣复合计算
double CalculateCompoundInterest(double principal, double rate, int years)
{
return principal * Math.Pow(1 + rate, years);
}
double investment = CalculateCompoundInterest(1000, 0.05, 10); // 返回 1628.89...
注意:Math类中的所有方法都是静态的,不需要创建Math的实例就能直接调用。所有需要角度作为参数的三角函数方法都使用弧度而非角度,使用时请特别注意单位转换。
Random 类
基础用法
构造函数选项
// 默认构造函数 - 使用时间相关的种子
Random random1 = new Random();
// 指定种子构造函数 - 产生可重现的序列
Random random2 = new Random(42);
// .NET 6+ 提供的共享实例 (避免创建多个实例)
Random shared = Random.Shared;
核心方法
Random rand = new Random();
// 生成非负随机整数 (0 到 Int32.MaxValue-1)
int num1 = rand.Next();
// 生成指定上限的随机整数 (0 到 maxValue-1)
int num2 = rand.Next(100); // 0 ≤ x < 100
// 生成指定范围的随机整数 (闭开区间: minValue ≤ x < maxValue)
int num3 = rand.Next(10, 20); // 10 ≤ x < 20
// 生成 [0.0, 1.0) 范围的双精度浮点数
double num4 = rand.NextDouble();
// 填充字节数组
byte[] buffer = new byte[16];
rand.NextBytes(buffer);
进阶技巧
生成特定范围和类型的随机值
Random rand = new Random();
// 生成指定范围的浮点数
double RandomDouble(double min, double max) =>
rand.NextDouble() * (max - min) + min;
// 示例:生成 5.0 到 10.0 之间的小数
double value = RandomDouble(5.0, 10.0);
// 生成随机布尔值
bool randomBool = rand.Next(2) == 1;
// 生成闭区间 [min, max] 的随机整数
int RandomInclusive(int min, int max) =>
rand.Next(min, max + 1);
// 从数组中随机选择一项
T RandomItem<T>(T[] items) =>
items[rand.Next(items.Length)];
常用随机数分布
// 1. 正态分布随机数 (Box-Muller变换)
double NextGaussian(double mean = 0, double stdDev = 1)
{
double u1 = 1.0 - rand.NextDouble();
double u2 = 1.0 - rand.NextDouble();
double randStdNormal = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2);
return mean + stdDev * randStdNormal;
}
// 2. 指数分布随机数
double NextExponential(double lambda)
{
return -Math.Log(1.0 - rand.NextDouble()) / lambda;
}
// 3. 加权随机选择
int WeightedRandom(double[] weights)
{
double sum = weights.Sum();
double threshold = rand.NextDouble() * sum;
double cumulative = 0;
for (int i = 0; i < weights.Length; i++)
{
cumulative += weights[i];
if (cumulative >= threshold)
return i;
}
return weights.Length - 1;
}
最佳实践与常见陷阱
线程安全问题
Random
类不是线程安全的。多线程使用时有以下解决方案:
// 方案1:每个线程使用独立的Random实例
ThreadLocal<Random> threadRandom = new ThreadLocal<Random>(() => new Random());
int GetThreadSafeRandomNumber() => threadRandom.Value.Next();
// 方案2:.NET 6+ 使用线程安全的共享实例
int number = Random.Shared.Next();
// 方案3:使用锁同步
private static readonly Random _rand = new Random();
private static readonly object _lock = new object();
int GetSynchronizedRandom()
{
lock (_lock)
{
return _rand.Next();
}
}
避免连续创建Random实例
// 错误方式:在循环中创建多个实例
for (int i = 0; i < 10; i++)
{
var rand = new Random(); // 可能产生相同的种子
Console.WriteLine(rand.Next(100));
}
// 正确方式:在循环外创建实例
var random = new Random();
for (int i = 0; i < 10; i++)
{
Console.WriteLine(random.Next(100));
}
处理溢出风险
// 安全的闭区间随机整数生成(处理边界情况)
public static int NextInclusiveSafe(this Random random, int min, int max)
{
if (max == int.MaxValue)
return random.Next(min, max) == max - 1 ? max : random.Next(min, max);
else
return random.Next(min, max + 1);
}
替代方案
加密安全的随机数
当需要用于加密、安全令牌生成等场景时:
// .NET Framework/Core 传统方式
using System.Security.Cryptography;
byte[] GenerateRandomBytes(int length)
{
byte[] bytes = new byte[length];
using (var rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(bytes);
}
return bytes;
}
// 生成安全随机整数
int GetSecureRandom(int min, int max)
{
if (min >= max)
throw new ArgumentOutOfRangeException();
using (var rng = new RNGCryptoServiceProvider())
{
byte[] buffer = new byte[4];
rng.GetBytes(buffer);
int result = BitConverter.ToInt32(buffer, 0);
return Math.Abs(result % (max - min)) + min;
}
}
// .NET Core 3.0+ / .NET 5+ 现代方式
using System.Security.Cryptography;
byte[] bytes = RandomNumberGenerator.GetBytes(16);
int secureRandom = RandomNumberGenerator.GetInt32(1, 101); // 1-100之间
.NET 6+ 新特性
// .NET 6+ 提供的新方法
double GetDouble() => Random.Shared.NextDouble();
int GetInt(int max) => Random.Shared.Next(max);
int GetRange(int min, int max) => Random.Shared.Next(min, max);
// .NET 7+ 提供的新方法
int[] GetItems() => Random.Shared.GetItems(new[] {1, 2, 3, 4, 5}, 3); // 随机选择3个元素
实际应用示例
生成随机密码
string GeneratePassword(int length, bool useSpecial = true)
{
const string lowers = "abcdefghijklmnopqrstuvwxyz";
const string uppers = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const string numbers = "0123456789";
const string special = "!@#$%^&*()-_=+[]{}|;:,.<>?";
string chars = lowers + uppers + numbers + (useSpecial ? special : "");
Random rand = new Random();
return new string(Enumerable.Range(0, length)
.Select(_ => chars[rand.Next(chars.Length)])
.ToArray());
}
// 生成8位包含特殊字符的密码
string password = GeneratePassword(8);
随机洗牌算法
// Fisher-Yates 洗牌算法
void Shuffle<T>(T[] array)
{
Random rand = new Random();
for (int i = array.Length - 1; i > 0; i--)
{
int j = rand.Next(i + 1);
(array[i], array[j]) = (array[j], array[i]); // 使用C# 7+的元组交换
}
}
// 示例用法
int[] deck = Enumerable.Range(1, 52).ToArray();
Shuffle(deck);
随机颜色生成
// 生成随机RGB颜色
System.Drawing.Color GetRandomColor()
{
Random rand = new Random();
return System.Drawing.Color.FromArgb(
rand.Next(256),
rand.Next(256),
rand.Next(256)
);
}
异常类
异常处理的基本语法和工作流程
C#中的异常处理主要通过try-catch-finally结构实现:
try
{
// 可能引发异常的代码
}
catch (ExceptionType1 ex)
{
// 处理特定类型的异常
}
catch (ExceptionType2 ex)
{
// 处理另一类型的异常
}
finally
{
// 无论是否发生异常都会执行的代码
}
异常处理工作流程:
- 执行try块中的代码
- 如果发生异常,CLR创建相应的异常对象
- CLR在调用栈中寻找匹配的catch块
- 如果找到匹配的catch块,执行其中的代码
- 如果未找到匹配的catch块,异常继续向上传播
- 无论异常是否被捕获,finally块中的代码都会执行
C# 6.0引入了异常过滤器功能:
try
{
// 代码
}
catch (Exception ex) when (ex.Message.Contains("特定错误"))
{
// 仅当条件为true时捕获异常
}
异常可以重新抛出:
try
{
// 代码
}
catch (Exception ex)
{
// 处理异常
throw; // 保留原始堆栈信息重新抛出
// throw ex; // 不推荐,会丢失原始堆栈信息
}
异常类层次结构及常见异常类型
所有异常都继承自System.Exception基类,形成层次结构:
System.Exception
├── System.SystemException
│ ├── System.NullReferenceException
│ ├── System.IndexOutOfRangeException
│ ├── System.InvalidCastException
│ ├── System.IO.IOException
│ ├── System.DivideByZeroException
│ └── ...
└── System.ApplicationException (不推荐用作自定义异常的基类)
常见异常类型:
- NullReferenceException:尝试访问null对象的成员
- ArgumentException:传递给方法的参数无效
- ArgumentNullException:参数不能为null但传递了null值
- InvalidOperationException:对象状态不适合请求的操作
- IOException:I/O操作失败
- FileNotFoundException:找不到指定文件
- FormatException:字符串转换格式无效
- IndexOutOfRangeException:索引超出集合范围
- DivideByZeroException:除数为零
- OutOfMemoryException:内存不足
- StackOverflowException:递归过深或无限递归
自定义异常的创建和使用
创建自定义异常的最佳实践:
- 从Exception类(或更具体的子类)继承
- 类名以"Exception"结尾
- 实现多个构造函数
- 支持序列化
- 添加有意义的属性
自定义异常示例:
using System;
using System.Runtime.Serialization;
[Serializable]
public class OrderProcessingException : Exception
{
public string OrderId { get; }
// 默认构造函数
public OrderProcessingException() : base() { }
// 带消息的构造函数
public OrderProcessingException(string message) : base(message) { }
// 带消息和内部异常的构造函数
public OrderProcessingException(string message, Exception innerException)
: base(message, innerException) { }
// 带订单ID的构造函数
public OrderProcessingException(string message, string orderId)
: base(message)
{
OrderId = orderId;
}
// 支持序列化的构造函数
protected OrderProcessingException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
OrderId = info.GetString("OrderId");
}
// 重写GetObjectData以支持序列化
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("OrderId", OrderId);
}
}
使用自定义异常:
public void ProcessOrder(string orderId)
{
if (string.IsNullOrEmpty(orderId))
{
throw new ArgumentNullException(nameof(orderId));
}
try
{
// 处理订单逻辑
bool orderExists = CheckOrderExists(orderId);
if (!orderExists)
{
throw new OrderProcessingException("订单不存在", orderId);
}
}
catch (DatabaseException dbEx)
{
// 捕获低级异常并包装为更有意义的业务异常
throw new OrderProcessingException($"处理订单{orderId}时发生数据库错误", dbEx);
}
}
异常处理的最佳实践和性能考虑
最佳实践
- 仅捕获可以正确处理的异常
- 避免空catch块,至少记录异常信息
- 优先使用具体的异常类型而非通用Exception
- 异常应用于异常情况,不用于常规控制流
- 在适当的抽象级别处理异常
- 包装低级异常为更有意义的高级异常
- 使用finally块或using语句确保资源释放
- 异常消息应提供有用信息但不泄露敏感数据
- 异常过滤器可实现更精细的异常处理
性能考虑
- 异常处理有性能开销,尤其是在抛出异常时
- 避免在性能关键循环中使用异常处理
- 使用预检查避免可预见的异常(例如检查null)
- 考虑使用TryParse模式而非捕获 FormatException
代码示例 - 好的做法vs不好的做法:
// 不好的做法 - 使用异常控制流程
public int Parse(string input)
{
try
{
return int.Parse(input);
}
catch (FormatException)
{
return 0;
}
}
// 好的做法 - 使用TryParse避免异常
public int Parse(string input)
{
if (int.TryParse(input, out int result))
{
return result;
}
return 0;
}
面试常问 异常处理相关问题
问:try/catch/finally块的执行顺序是什么?
答:先执行try块代码。如果发生异常,执行匹配的catch块。无论是否发生异常,finally块都会执行。即使try或catch块中有return语句,finally块仍会在方法返回前执行。
问:throw和throw ex有什么区别?
答:throw保留原始堆栈跟踪信息;throw ex会重置堆栈跟踪,丢失原始异常位置信息,通常应避免使用。
问:如何正确创建自定义异常?
答:自定义异常应继承Exception类,类名以Exception结尾,实现序列化支持,提供多个构造函数(默认、带消息、带内部异常等)。
问:什么是异常过滤器?
答:异常过滤器是C# 6引入的功能,语法为
catch (ExceptionType ex) when (condition)
,允许根据条件捕获异常。问:什么是AggregateException?
答:AggregateException用于封装多个异常,主要在并行编程和任务编程中使用,例如Task.WhenAll或Parallel.ForEach操作引发多个异常时。
问:如何确保资源正确释放?
答:使用finally块或using语句确保资源释放。using语句自动扩展为try-finally块,确保IDisposable对象的Dispose方法被调用。
问:如何处理未捕获的异常?
答:可以通过注册AppDomain.UnhandledException、Application.ThreadException或TaskScheduler.UnobservedTaskException事件来处理未捕获的异常。
问:异常处理对性能有什么影响?
答:异常处理有性能开销,特别是抛出异常时。创建异常对象、收集堆栈跟踪和查找匹配的catch块都需要资源。应避免在性能关键路径中使用异常进行流程控制。
实际开发中的异常处理示例代码
基本文件操作异常处理:
public string ReadFileContent(string path)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
}
try
{
// using语句确保StreamReader会被正确释放
using (var reader = new StreamReader(path))
{
return reader.ReadToEnd();
}
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"文件不存在: {path}");
Logger.LogWarning(ex, $"尝试读取不存在的文件: {path}");
return string.Empty;
}
catch (IOException ex)
{
Console.WriteLine($"IO错误: {ex.Message}");
Logger.LogError(ex, $"读取文件时发生IO错误: {path}");
throw new FileProcessingException($"无法读取文件: {path}", ex);
}
catch (Exception ex)
{
Logger.LogError(ex, $"读取文件时发生未预期错误: {path}");
throw; // 重新抛出未处理的异常
}
}
异常过滤器的使用:
public async Task<string> DownloadDataAsync(string url)
{
try
{
using (var client = new HttpClient())
{
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
catch (HttpRequestException ex) when (ex.Message.Contains("404"))
{
Logger.LogWarning($"资源不存在: {url}");
return string.Empty;
}
catch (HttpRequestException ex) when (ex.Message.Contains("500"))
{
Logger.LogError(ex, $"服务器错误: {url}");
throw new ServiceUnavailableException("远程服务器发生错误", ex);
}
catch (Exception ex) when (LogError(ex, url))
{
// LogError方法会记录异常并返回false,
// 这样异常会继续传播但我们确保它被记录
throw;
}
}
private bool LogError(Exception ex, string url)
{
Logger.LogError(ex, $"下载数据时发生错误: {url}");
return false; // 返回false确保异常继续传播
}
多层架构中的异常处理:
// 数据访问层
public class UserRepository
{
public User GetUserById(int userId)
{
try
{
// 数据库操作
return FetchUserFromDatabase(userId);
}
catch (SqlException ex)
{
// 记录原始异常详情
Logger.LogError(ex, $"查询用户ID={userId}时发生数据库错误");
// 转换为域异常
throw new DatabaseAccessException($"无法获取用户(ID={userId})", ex);
}
}
}
// 业务逻辑层
public class UserService
{
private readonly UserRepository _repository;
public UserService(UserRepository repository)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
}
public User GetActiveUser(int userId)
{
try
{
var user = _repository.GetUserById(userId);
if (user == null)
{
throw new UserNotFoundException(userId);
}
if (!user.IsActive)
{
throw new InactiveUserException(userId);
}
return user;
}
catch (DatabaseAccessException ex)
{
// 添加额外的业务上下文
throw new UserOperationException($"获取用户数据时发生系统错误", ex);
}
}
}
// API控制器
[ApiController]
public class UsersController : ControllerBase
{
private readonly UserService _userService;
private readonly ILogger<UsersController> _logger;
public UsersController(UserService userService, ILogger<UsersController> logger)
{
_userService = userService;
_logger = logger;
}
[HttpGet("{id}")]
public ActionResult<UserDto> GetUser(int id)
{
try
{
var user = _userService.GetActiveUser(id);
return Ok(MapToDto(user));
}
catch (UserNotFoundException)
{
return NotFound();
}
catch (InactiveUserException)
{
return BadRequest("用户账户未激活");
}
catch (UserOperationException ex)
{
_logger.LogError(ex, $"获取用户ID={id}时发生错误");
return StatusCode(500, "处理请求时发生错误");
}
}
}
异步方法中的异常处理:
public async Task ProcessMultipleUrlsAsync(string[] urls)
{
List<Task<string>> downloadTasks = new List<Task<string>>();
foreach (var url in urls)
{
downloadTasks.Add(DownloadDataAsync(url));
}
try
{
string[] results = await Task.WhenAll(downloadTasks);
// 处理所有成功下载的结果
ProcessResults(results);
}
catch (AggregateException ae)
{
// 处理多个任务中的异常
foreach (var ex in ae.InnerExceptions)
{
if (ex is HttpRequestException httpEx)
{
_logger.LogWarning(httpEx, "HTTP请求失败");
}
else
{
_logger.LogError(ex, "下载过程中发生未知错误");
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "处理URLs时发生错误");
throw new DataProcessingException("处理数据失败", ex);
}
}
全局异常处理(ASP.NET Core):
// Program.cs或Startup.cs中配置中间件
app.UseExceptionHandler("/error");
// 错误控制器
[ApiController]
public class ErrorController : ControllerBase
{
private readonly ILogger<ErrorController> _logger;
public ErrorController(ILogger<ErrorController> logger)
{
_logger = logger;
}
[Route("/error")]
[ApiExplorerSettings(IgnoreApi = true)]
public IActionResult Error()
{
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
var exception = context?.Error;
if (exception is UserNotFoundException)
{
return NotFound(new { message = "请求的用户不存在" });
}
if (exception is ValidationException validationEx)
{
return BadRequest(new { message = validationEx.Message, errors = validationEx.Errors });
}
// 记录未预期的异常
_logger.LogError(exception, "未处理的异常");
return StatusCode(500, new { message = "处理请求时发生错误" });
}
}
通过这些详细信息和代码示例,您应该能够全面了解C#中的异常处理机制,并在实际开发中合理使用异常处理提高代码的健壮性和可维护性。
枚举
枚举的定义和语法
枚举(Enumeration)是一种特殊的值类型,用于定义一组命名的常量值。基本语法如下:
[访问修饰符] enum 枚举名称 [: 基础类型]
{
常量1 [= 值1],
常量2 [= 值2],
常量3 [= 值3],
// ...
}
简单示例:
public enum Season
{
Spring, // 0
Summer, // 1
Autumn, // 2
Winter // 3
}
枚举的底层存储类型及默认值规则
- 默认情况下,枚举使用
int
作为底层存储类型 - 可以指定其他整型作为基础类型:
byte
、sbyte
、short
、ushort
、int
、uint
、long
、ulong
- 如果不显式指定值,第一个成员默认为0,后续成员值自动递增1
指定不同的底层类型示例:
public enum SmallEnum : byte
{
A, B, C // 占用1字节空间
}
public enum LargeValues : long
{
BigValue1 = 2147483648, // 超过int范围
BigValue2 = 2147483649
}
枚举成员的命名规范和最佳实践
- 枚举类型名称应使用PascalCase命名法(如
OrderStatus
) - 枚举成员也应使用PascalCase(如
Pending
,而非pending
) - 枚举名称通常使用单数形式,除非它表示位标志集合
- 不要使用"Enum"作为枚举类型名称的后缀
- 避免在枚举成员名称中使用数字(如
Color1
、Color2
) - 为枚举成员赋予有意义的名称,增强代码可读性
进阶用法
显式赋值与隐式赋值机制
C#允许显式赋值和隐式赋值混合使用:
public enum ErrorCode
{
None = 0,
Unknown = 1,
ConnectionLost = 100, // 显式赋值为100
TimeOut, // 隐式赋值为101
InvalidInput = 200, // 显式赋值为200
PermissionDenied // 隐式赋值为201
}
Flags特性的使用场景和实现方式
[Flags]
特性用于标记一个枚举的值可以按位组合使用:
[Flags]
public enum FilePermission
{
None = 0, // 0000
Read = 1, // 0001
Write = 2, // 0010
Execute = 4, // 0100
Delete = 8, // 1000
FullControl = Read | Write | Execute | Delete // 值为15 (1111)
}
// 使用示例
FilePermission permission = FilePermission.Read | FilePermission.Write;
// 检查权限
bool canRead = permission.HasFlag(FilePermission.Read); // true
// 添加权限
permission |= FilePermission.Execute;
// 移除权限
permission &= ~FilePermission.Write;
使用Flags枚举的关键点:
- 值应设计为2的幂(1, 2, 4, 8, 16...)
- 将
[Flags]
特性应用于枚举类型 - 通常包含一个表示"无"的值(通常为0)
- 可以预定义常用的组合值
如何进行枚举类型转换
- 枚举与整型转换:
// 枚举转整型
Season season = Season.Winter;
int seasonValue = (int)season; // 结果为3
// 整型转枚举
int value = 1;
Season convertedSeason = (Season)value; // 结果为Summer
- 枚举与字符串转换:
// 枚举转字符串
Season season = Season.Autumn;
string name = season.ToString(); // "Autumn"
// 字符串转枚举
string value = "Winter";
Season parsedSeason = (Season)Enum.Parse(typeof(Season), value);
// 更安全的方式
if (Enum.TryParse<Season>(value, out Season result))
{
// 使用result变量
}
// 忽略大小写
Season season = (Season)Enum.Parse(typeof(Season), "winter", true);
枚举的扩展方法设计
通过扩展方法可以增强枚举功能:
public static class EnumExtensions
{
// 获取枚举描述特性
public static string GetDescription<T>(this T enumValue) where T : Enum
{
var field = enumValue.GetType().GetField(enumValue.ToString());
if (field.GetCustomAttributes(typeof(DescriptionAttribute), false)
is DescriptionAttribute[] attributes && attributes.Length > 0)
{
return attributes[0].Description;
}
return enumValue.ToString();
}
// 检查Flags枚举是否包含任何指定的标志
public static bool HasAnyFlag<T>(this T value, T flags) where T : Enum
{
long valueInt = Convert.ToInt64(value);
long flagsInt = Convert.ToInt64(flags);
return (valueInt & flagsInt) != 0;
}
}
实际应用
枚举在switch语句中的使用技巧
枚举与switch语句配合使用非常强大:
public enum PaymentStatus
{
Pending,
Processing,
Completed,
Failed,
Refunded
}
public void ProcessPayment(PaymentStatus status)
{
switch (status)
{
case PaymentStatus.Pending:
Console.WriteLine("Payment is pending.");
break;
case PaymentStatus.Processing:
Console.WriteLine("Payment is being processed.");
break;
case PaymentStatus.Completed:
Console.WriteLine("Payment has been completed.");
break;
case PaymentStatus.Failed:
Console.WriteLine("Payment failed.");
break;
case PaymentStatus.Refunded:
Console.WriteLine("Payment has been refunded.");
break;
default:
throw new ArgumentOutOfRangeException(nameof(status));
}
}
在C# 8.0+中,可以使用switch表达式简化代码:
public string GetPaymentStatusMessage(PaymentStatus status) => status switch
{
PaymentStatus.Pending => "Payment is pending.",
PaymentStatus.Processing => "Payment is being processed.",
PaymentStatus.Completed => "Payment has been completed.",
PaymentStatus.Failed => "Payment failed.",
PaymentStatus.Refunded => "Payment has been refunded.",
_ => throw new ArgumentOutOfRangeException(nameof(status))
};
提高代码可读性的枚举使用模式
- 使用枚举替代魔法数字:
// 不好的做法
if (userType == 2) // 2表示什么?
{
// 执行管理员操作
}
// 好的做法
public enum UserType
{
Guest = 0,
Regular = 1,
Admin = 2
}
if (userType == UserType.Admin)
{
// 执行管理员操作 - 代码更加自解释
}
- 使用[Description]特性增强可读性:
public enum OrderStatus
{
[Description("等待付款")]
AwaitingPayment = 0,
[Description("处理中")]
Processing = 1,
[Description("已发货")]
Shipped = 2,
[Description("已完成")]
Completed = 3
}
与泛型结合的高级用法
- 泛型约束中使用枚举:
public class EnumProcessor<T> where T : Enum
{
public Dictionary<T, string> GetAllValues()
{
var result = new Dictionary<T, string>();
foreach (T value in Enum.GetValues(typeof(T)))
{
result.Add(value, value.ToString());
}
return result;
}
public T Parse(string value)
{
return (T)Enum.Parse(typeof(T), value, true);
}
}
// 使用
var processor = new EnumProcessor<DayOfWeek>();
var allDays = processor.GetAllValues();
- 枚举与泛型字典结合:
public enum LogLevel
{
Debug,
Info,
Warning,
Error,
Critical
}
public class Logger
{
private readonly Dictionary<LogLevel, Action<string>> _logHandlers;
public Logger()
{
_logHandlers = new Dictionary<LogLevel, Action<string>>
{
[LogLevel.Debug] = message => Console.WriteLine($"DEBUG: {message}"),
[LogLevel.Info] = message => Console.WriteLine($"INFO: {message}"),
[LogLevel.Warning] = message => Console.WriteLine($"WARNING: {message}"),
[LogLevel.Error] = message => Console.WriteLine($"ERROR: {message}"),
[LogLevel.Critical] = message => Console.WriteLine($"CRITICAL: {message}")
};
}
public void Log(LogLevel level, string message)
{
if (_logHandlers.TryGetValue(level, out var handler))
{
handler(message);
}
}
}
面试重点
常见的枚举相关面试题及答案
问题:枚举的底层类型是什么?可以更改吗? 答案:枚举的默认底层类型是
int
。可以通过在枚举声明中指定其他整型类型来更改,如:public enum MyEnum : byte { ... }
。可选类型包括:byte
、sbyte
、short
、ushort
、int
、uint
、long
、ulong
。问题:枚举与常量(const)的区别是什么? 答案:
- 枚举是一种类型,常量只是一个不可变的值
- 枚举可以在IDE中获得IntelliSense支持
- 枚举在调试时显示友好名称
- 枚举可以使用特性,如[Flags]和[Description]
- 枚举保留类型信息,常量在编译时直接替换为值
问题:[Flags]特性的作用是什么? 答案:[Flags]特性用于指示枚举是按位标志,可以组合使用多个值。它主要影响ToString()方法,使组合值显示为逗号分隔的名称列表,而不是数字。
问题:如何遍历枚举的所有可能值? 答案:
csharpforeach (Season season in Enum.GetValues(typeof(Season))) { Console.WriteLine($"{season}: {(int)season}"); } // C# 7.3+可以使用泛型版本 foreach (Season season in Enum.GetValues<Season>()) { Console.WriteLine(season); }
问题:枚举是值类型还是引用类型?这意味着什么? 答案:枚举是值类型。这意味着:
- 枚举在栈上分配而不是堆上
- 传递枚举作为参数时会进行值拷贝
- 枚举不能为null(除非使用
Enum?
可空类型) - 枚举的相等比较是值比较
性能考量和局限性
性能优势:
- 枚举作为值类型,不会产生堆分配和垃圾回收压力
- 枚举比较操作非常快速(整数比较)
- 在switch语句中使用枚举可以被编译器高度优化
性能考量:
Enum.Parse
和Enum.ToString
涉及反射,相对较慢- 获取枚举的自定义特性需要反射,应考虑缓存结果
- 在旧版.NET中,
HasFlag
方法性能较差(涉及装箱),现代.NET已优化
局限性:
- 枚举成员只能是整数常量,不能包含字符串或其他类型
- 枚举是静态的,不能在运行时添加新成员
- 无法定义抽象枚举或扩展现有枚举
- 枚举不能有方法(需要使用扩展方法)
- 枚举值在编译时确定,不能从配置或数据库动态加载
与其他类型(如常量)的对比分析
枚举 vs 常量:
特性 | 枚举 | 常量 |
---|---|---|
类型安全 | 是 | 否 |
可读性 | 好(有名称) | 差(仅值) |
调试友好 | 是(显示名称) | 否(仅显示值) |
组合多个值 | 可以(Flags) | 不直观 |
反射支持 | 可以获取所有值 | 不可以 |
特性支持 | 支持 | 不支持 |
枚举 vs 静态类常量:
// 使用枚举
public enum Color
{
Red,
Green,
Blue
}
// 使用静态类常量
public static class ColorConstants
{
public const int Red = 0;
public const int Green = 1;
public const int Blue = 2;
}
实际项目中的最佳实践案例
- 使用[Flags]处理权限系统:
[Flags]
public enum UserPermissions
{
None = 0,
View = 1 << 0, // 1
Create = 1 << 1, // 2
Edit = 1 << 2, // 4
Delete = 1 << 3, // 8
Approve = 1 << 4, // 16
// 预定义的权限组
BasicUser = View | Create,
Editor = BasicUser | Edit,
Manager = Editor | Delete | Approve
}
// 使用
public bool CanPerformAction(UserPermissions userPermissions, UserPermissions requiredPermission)
{
return userPermissions.HasFlag(requiredPermission);
}
- 使用枚举创建状态机:
public enum OrderState
{
Created,
PaymentPending,
PaymentReceived,
Preparing,
Shipped,
Delivered,
Canceled,
Returned
}
public class OrderStateMachine
{
private static readonly Dictionary<OrderState, HashSet<OrderState>> _allowedTransitions =
new Dictionary<OrderState, HashSet<OrderState>>
{
[OrderState.Created] = new HashSet<OrderState> { OrderState.PaymentPending, OrderState.Canceled },
[OrderState.PaymentPending] = new HashSet<OrderState> { OrderState.PaymentReceived, OrderState.Canceled },
[OrderState.PaymentReceived] = new HashSet<OrderState> { OrderState.Preparing, OrderState.Canceled },
[OrderState.Preparing] = new HashSet<OrderState> { OrderState.Shipped, OrderState.Canceled },
[OrderState.Shipped] = new HashSet<OrderState> { OrderState.Delivered, OrderState.Returned },
[OrderState.Delivered] = new HashSet<OrderState> { OrderState.Returned },
[OrderState.Canceled] = new HashSet<OrderState> { },
[OrderState.Returned] = new HashSet<OrderState> { }
};
public bool CanTransitionTo(OrderState currentState, OrderState newState)
{
return _allowedTransitions[currentState].Contains(newState);
}
public void TransitionTo(Order order, OrderState newState)
{
if (!CanTransitionTo(order.State, newState))
{
throw new InvalidOperationException($"Cannot transition from {order.State} to {newState}");
}
order.State = newState;
// 执行状态变更的相关逻辑
}
}
- 使用枚举与策略模式结合:
public enum ShippingMethod
{
Standard,
Express,
Overnight,
International
}
public interface IShippingCalculator
{
decimal CalculateShippingCost(Order order);
}
public class ShippingStrategyFactory
{
private readonly Dictionary<ShippingMethod, IShippingCalculator> _calculators =
new Dictionary<ShippingMethod, IShippingCalculator>();
public ShippingStrategyFactory()
{
_calculators[ShippingMethod.Standard] = new StandardShippingCalculator();
_calculators[ShippingMethod.Express] = new ExpressShippingCalculator();
_calculators[ShippingMethod.Overnight] = new OvernightShippingCalculator();
_calculators[ShippingMethod.International] = new InternationalShippingCalculator();
}
public IShippingCalculator GetCalculator(ShippingMethod method)
{
if (!_calculators.TryGetValue(method, out var calculator))
{
throw new ArgumentException($"No calculator found for shipping method: {method}");
}
return calculator;
}
}
以上涵盖了C#枚举的核心知识点和实际应用场景,掌握这些内容将有助于在面试和实际开发中灵活运用枚举类型。
partial 关键字
partial
关键字在 C# 中用于定义部分类、结构或方法。它允许将一个类、结构或方法的定义分散到多个文件中。这样做的主要目的是为了更好地组织代码,特别是在大型项目中,或者在自动生成代码的工具中。
在你的代码中,LoginForm
类被定义为 partial
,这意味着它的定义可能分布在多个文件中。在 WinForms 应用程序中,通常会有一个 .Designer.cs
文件,其中包含由设计器生成的代码,而主逻辑代码则放在另一个文件中。
例如,你的项目中可能有一个 LoginForm.Designer.cs
文件,其中包含 LoginForm
类的部分定义:
namespace WinFrom
{
partial class LoginForm
{
private void InitializeComponent()
{
// 设计器生成的代码
}
}
}
这样做的好处是将设计器生成的代码与手写的逻辑代码分开,便于维护和管理。
总结:
partial
关键字允许将类、结构或方法的定义分散到多个文件中。- 在 WinForms 应用程序中,通常用于将设计器生成的代码与手写的逻辑代码分开。
面向对象编程
类与对象
类定义
类是面向对象编程的基本单元,是创建对象的模板或蓝图。在C#中,类使用class
关键字定义。
// 基本类定义语法
[访问修饰符] class 类名
{
// 字段(成员变量)
// 属性
// 构造函数
// 方法
// 事件
// 索引器
// 嵌套类型
}
示例:
public class Person
{
// 字段
private string name;
private int age;
// 属性
public string Name
{
get { return name; }
set { name = value; }
}
// 自动属性(简化写法)
public int Age { get; set; }
// 构造函数
public Person(string name, int age)
{
this.name = name;
this.Age = age;
}
// 方法
public void Introduce()
{
Console.WriteLine($"我叫{name},今年{Age}岁");
}
}
嵌套类
在C#中,嵌套类(nested class,即类中定义的类)的默认访问修饰符是private,而不是internal。这与顶级类(非嵌套类)默认为internal不同。
静态方法
基本概念
- 静态方法是属于类本身而非实例的方法
- 使用static关键字声明
- 不需要创建类的实例即可调用
- 通过**类名.方法名()**的方式访问
特性与限制
- 不能访问实例成员(字段、属性、方法)
- 只能访问其他静态成员
- 不能使用this关键字(因无实例上下文)
- 不能被重写(不支持virtual、override或abstract)
- 不能被继承,尽管可通过子类名访问父类静态方法
- 静态方法的绑定在编译时确定,不支持多态
基类的静态方法可以被子类重写吗
不可以!
解释:
- 静态方法不能被重写,它们不参与多态
- 静态方法属于类本身而非实例,不能用virtual、abstract或override修饰
- 静态方法不能通过继承关系被覆盖
- 子类可以定义同名的静态方法,但这只是方法隐藏,不是重写
- 调用的是哪个版本完全取决于引用变量的静态类型,而非运行时类型
静态成员变量
静态成员变量(static field)是由static
关键字修饰的类成员变量。其核心特性是:所有实例化对象共享同一个值。
特性与原理
基本特性
- 类级别而非实例级别:静态成员变量属于类本身,而不是类的实例
- 共享单一内存位置:所有实例共享同一个变量值
- 无需实例即可访问:可以不创建对象直接通过类名访问
内存与生命周期
- 单一存储:无论创建多少实例,静态变量只在内存中存储一次
- 生命周期:静态变量的生命周期与应用程序域相同,不随实例创建或销毁
- 初始化时机:在类第一次被加载时初始化,先于任何实例创建
代码示例
基本用法示例
class Counter {
public static int sharedCount = 0; // 静态成员变量
public int instanceCount = 0; // 实例成员变量
public void Increment() {
sharedCount++; // 所有实例共享此值
instanceCount++; // 只影响当前实例
}
}
// 使用示例
Counter c1 = new Counter();
Counter c2 = new Counter();
c1.Increment();
// 此时 c1.instanceCount = 1, c2.instanceCount = 0
// 但 Counter.sharedCount = 1, c1.sharedCount = 1, c2.sharedCount = 1
c2.Increment();
// 此时 c1.instanceCount 仍然是1, c2.instanceCount = 1
// 但 Counter.sharedCount = 2, c1.sharedCount = 2, c2.sharedCount = 2
实际应用场景
class DatabaseConnection {
// 限制最大连接数
public static int MaxConnections = 100;
// 跟踪当前活跃连接数
public static int ActiveConnections = 0;
private string connectionString;
public DatabaseConnection(string connString) {
connectionString = connString;
}
public bool Connect() {
if (ActiveConnections >= MaxConnections) {
Console.WriteLine("已达到最大连接数限制!");
return false;
}
// 连接逻辑...
ActiveConnections++;
return true;
}
public void Disconnect() {
// 断开连接逻辑...
ActiveConnections--;
}
}
最佳实践与注意事项
使用建议
- 访问方式:始终通过类名访问静态成员(
Counter.sharedCount
),而不是通过实例 - 线程安全:在多线程环境中操作静态变量需考虑线程安全问题
- 适用场景:适用于需要在所有实例间共享信息的场景
- 避免过度使用:静态变量会增加类之间的耦合度,降低代码可测试性
常见应用场景
- 计数器:记录类被实例化的次数
- 缓存数据:存储需要被所有实例访问的数据
- 配置信息:应用程序全局配置
- 工具类:不需要实例化的工具方法和数据
静态成员变量与实例成员变量对比
特性 | 静态成员变量 | 实例成员变量 |
---|---|---|
声明方式 | static 关键字 | 无需特殊关键字 |
内存分配 | 所有实例共享一个 | 每个实例独立 |
访问方式 | 类名或实例名 | 只能通过实例 |
生命周期 | 与应用程序域相同 | 与实例对象相同 |
初始化时机 | 类加载时 | 实例创建时 |
访问修饰符
C#中的访问修饰符决定了类及其成员的可访问性:
修饰符 | 同一类 | 派生类(同程序集) | 派生类(不同程序集) | 同程序集 | 不同程序集 |
---|---|---|---|---|---|
public | ✓ | ✓ | ✓ | ✓ | ✓ |
private | ✓ | ✗ | ✗ | ✗ | ✗ |
protected | ✓ | ✓ | ✓ | ✗ | ✗ |
internal | ✓ | ✓ | ✗ | ✓ | ✗ |
protected internal | ✓ | ✓ | ✓ | ✓ | ✗ |
private protected | ✓ | ✓ | ✗ | ✗ | ✗ |
类访问修饰符和子类成员访问修饰符对比
关于类的访问修饰符继承规则:
- 子类的访问修饰符可以等于或比父类更严格
- 子类的访问级别不能比父类更宽松
具体规则:
- 如果父类是public,子类可以是public或internal
- 如果父类是internal,子类必须是internal
- 对于嵌套类,规则更复杂,但基本原则相同
关于继承类(子类)成员的访问修饰符规则:
- 对于重写(override)方法或属性,子类的访问修饰符不能比父类更严格
- 子类的重写成员只能:
- 与父类成员保持相同的访问级别
- 使用更宽松的访问级别
具体规则:
- 如果父类方法是public,子类重写必须是public
- 如果父类方法是protected,子类重写可以是protected或public
- 如果父类方法是internal,子类重写可以是internal或public
- 如果父类方法是protected internal,子类重写可以是protected internal或public
这一规则背后的原理是里氏替换原则:子类对象应该能够在任何期望父类对象的地方使用,因此子类重写方法的可见性不能比父类更低。
注意:如果使用的是new关键字(方法隐藏,非重写),则可以使用任意访问修饰符。
原因
类的访问修饰符继承规则
- 子类的访问修饰符可以比父类更严格
- 例如:父类是public,子类可以是internal
类成员重写的访问修饰符规则
- 子类重写成员的访问修饰符不能比父类更严格
- 必须相同或更宽松
为什么看似矛盾?
这两条规则服务于不同的设计目标:
类访问级别规则(可以更严格):
- 关注类型的可见性范围
- 子类更严格不会破坏程序行为
- 只是限制了子类自身的可见范围
- 符合"设计收缩"原则,子类可以选择减小自身的应用范围
成员重写规则(不能更严格):
- 关注多态行为的一致性
- 基于里氏替换原则:子类必须能替代父类使用
- 如果子类重写方法比父类更严格,会导致某些能调用父类方法的地方无法调用子类版本
- 会破坏多态的基本保证
创建对象
使用new
关键字创建类的实例:
Person person1 = new Person("张三", 25);
var person2 = new Person("李四", 30); // 使用var关键字(类型推断)
构造函数
构造函数是类的特殊方法,用于初始化新创建的对象。
构造函数特性
- 与类同名
- 没有返回类型(甚至不是void)
- 可以重载(定义多个不同参数的构造函数)
- 如果没有显式定义,编译器会提供默认无参构造函数
public class Student
{
public string Name { get; set; }
public int Id { get; set; }
// 无参构造函数
public Student()
{
Name = "未命名";
Id = 0;
}
// 带参构造函数
public Student(string name, int id)
{
Name = name;
Id = id;
}
}
构造函数不能被继承
- 子类不能继承父类的构造函数
- 每个类必须定义自己的构造函数
- 子类构造函数可以(通常需要)调用父类构造函数
- 如果不显式调用,编译器会尝试调用父类的无参构造函数
构造函数不能继承的原因:
- 命名规则冲突:构造函数必须与类同名
- 目的不同:构造函数负责初始化类的特定状态
- 执行顺序:创建对象时需要特定的初始化顺序
- 类型安全:继承构造函数可能导致不一致状态
public class Animal
{
public string Species { get; set; }
public Animal(string species)
{
Species = species;
}
}
public class Dog : Animal
{
public string Breed { get; set; }
// 调用父类构造函数
public Dog(string breed) : base("Canine")
{
Breed = breed;
}
// 另一种调用父类构造函数的方式
public Dog(string species, string breed) : base(species)
{
Breed = breed;
}
}
构造函数为什么不能加 static void
构造函数是一个特殊的方法,用于初始化类的实例:
- 构造函数的名称必须与类名相同
- 不能有返回类型(包括
void
) - 不能是
static
,因为它们是用于创建类的实例
错误示例与正确示例对比:
// 错误的构造函数定义
class Good
{
string name = "";
double price = 0;
static void Good(string name, double price) // 错误!
{
this.name = name;
this.price = price;
}
}
// 正确的构造函数定义
class Good
{
string name = "";
double price = 0;
public Good(string name, double price) // 正确
{
this.name = name;
this.price = price;
}
}
面向对象的关键特性
封装
封装是隐藏对象内部状态和实现细节的机制,对外只暴露必要的操作接口。
public class BankAccount
{
// 私有字段 - 外部无法直接访问
private decimal balance;
// 公共属性 - 受控制的访问方式
public decimal Balance
{
get { return balance; }
private set { balance = value; } // 只能在类内部修改
}
// 公共方法 - 提供操作接口
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("存款金额必须大于零");
balance += amount;
}
public bool Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("取款金额必须大于零");
if (balance >= amount)
{
balance -= amount;
return true;
}
return false;
}
}
this 关键字
this
关键字表示当前类的实例。主要用途:
- 区分同名的局部变量和成员变量
- 在构造函数中调用同一个类的其他构造函数
- 将当前对象作为参数传递给其他方法
- 返回当前对象的引用(链式方法)
public class Person
{
private string name; // 成员变量
private int age; // 成员变量
// 使用 this 区分成员变量和参数
public Person(string name, int age)
{
this.name = name; // this.name 指向成员变量,name 指向参数
this.age = age; // this.age 指向成员变量,age 指向参数
}
// 构造函数之间的调用
public Person(string name) : this(name, 0)
{
// 调用另一个构造函数初始化
}
// 链式方法示例
public Person SetName(string name)
{
this.name = name;
return this; // 返回当前对象实例
}
public Person SetAge(int age)
{
this.age = age;
return this; // 返回当前对象实例
}
// 使用链式方法的例子:
// var person = new Person().SetName("张三").SetAge(25);
}
readonly 关键字
readonly关键字用于创建只读字段,特点:
- 只能在声明时或构造函数中赋值
- 赋值后不能再被修改
- 适用于需要在运行时确定且之后不应更改的值
public class ImmutablePoint
{
// 声明时初始化
public readonly int X = 0;
// 在构造函数中初始化
public readonly int Y;
// 只读引用类型
public readonly List<int> Coordinates;
public ImmutablePoint(int y, List<int> coordinates)
{
Y = y; // 合法,在构造函数中初始化
Coordinates = coordinates; // 引用不可改变,但内容可以
}
public void ChangeValues()
{
// X = 5; // 编译错误!构造函数外不能修改readonly字段
// Y = 10; // 编译错误!构造函数外不能修改readonly字段
// 虽然引用不能改变,但引用的对象内容可以修改
Coordinates.Add(100); // 合法
// Coordinates = new List<int>(); // 编译错误!不能更改引用
}
}
readonly vs const
特性 | readonly | const |
---|---|---|
赋值时机 | 声明时或构造函数中 | 只能在声明时 |
值类型 | 运行时确定 | 编译时确定 |
静态性 | 可以是静态或实例成员 | 隐式静态 |
应用范围 | 只能用于字段 | 可用于字段和局部变量 |
public class Constants
{
// const必须在声明时初始化,值在编译时确定
public const double PI = 3.14159265359;
// readonly可在运行时计算
public readonly double CircleArea;
public Constants(double radius)
{
CircleArea = PI * radius * radius; // 运行时计算
}
}
属性与字段
字段(Field)
字段是类中直接存储数据的变量,通常用于存储对象的状态。
public class Person
{
// 这是一个字段
private int age;
}
属性(Property)
属性是通过访问器方法(get/set)来控制对数据的访问和修改的成员,提供了数据封装机制。
public class Person
{
private int age; // 后台字段
// 这是一个属性
public int Age
{
get { return age; }
set { age = value; }
}
}
传统属性
传统属性显式定义后台字段和访问器逻辑,适用于需要添加验证、转换或触发事件的场景。
public class Person
{
private string _name; // 显式定义后台字段
public string Name
{
get { return _name; }
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException("姓名不能为空");
_name = value;
}
}
}
自动实现的属性 { get; set; }
这是C#的简化属性语法,编译器会自动生成后台字段。
// 简化写法(自动实现的属性)
public string Name { get; set; }
// 等同于以下完整写法
private string name; // 编译器自动生成的后台字段
public string Name // 属性
{
get { return name; } // 获取值
set { name = value; } // 设置值
}
自动实现属性的优点:
- 代码更简洁,减少样板代码
- 编译器自动创建并管理后台字段
- 保持封装性原则
- 可以随时扩展为完整属性以添加验证逻辑
何时使用自动属性
当属性不需要特殊逻辑(如验证或计算)时,应优先使用自动属性。需要添加逻辑时可以轻松转换为传统属性:
public class ExchangeItem
{
// 从自动属性转换为传统属性
private double minPrice;
public double MinPrice
{
get { return minPrice; }
set
{
if (value < 0)
throw new ArgumentException("最低消费金额不能为负数");
minPrice = value;
}
}
}
属性访问修饰符
可以为属性的访问器设置不同的访问级别:
public class User
{
// 公开的get,但只有类内部可以set
public string Password { get; private set; }
// 只读属性 - 只有构造函数可以设置
public DateTime CreatedDate { get; }
public User(string password)
{
Password = password;
CreatedDate = DateTime.Now;
}
public void ChangePassword(string newPassword)
{
// 类内部可以访问private set
Password = newPassword;
}
}
C# 6.0+ 的属性新特性
自动属性初始化器
// 直接在声明时初始化
public string Name { get; set; } = "默认名称";
public List<string> Tags { get; } = new List<string>();
表达式体属性
private string firstName;
private string lastName;
// 使用表达式体简化只读计算属性
public string FullName => $"{firstName} {lastName}";
三种方式对比
特性 | 公共字段 | 自动属性 | 传统属性 |
---|---|---|---|
语法 | public int Age; | public int Age { get; set; } | public int Age { get { return _age; } set { _age = value; } } |
封装性 | ❌ 无 | ✅ 有(隐式封装) | ✅ 有(显式控制) |
数据验证 | ❌ 无法实现 | ❌ 无法直接添加 | ✅ 可通过setter实现 |
序列化支持 | ⚠️ 部分框架不支持 | ✅ 广泛支持 | ✅ 支持 |
内存占用 | 稍低 | 与传统属性相同 | 与自动属性相同 |
扩展性 | ❌ 修改会破坏兼容性 | ✅ 可随时改为传统属性 | ✅ 已具备扩展能力 |
主要区别
封装性
- 字段直接存储数据,没有封装
- 属性提供对数据的受控访问,实现了封装
访问控制
- 属性可以为读写操作设置不同的访问级别
- 可以创建只读属性(只有get)或只写属性(只有set)
数据验证
- 属性可以在set访问器中执行数据验证
- 字段不提供内置的验证机制
其他区别
- 属性可以是虚拟的(virtual)并在派生类中重写
- 属性可以在接口中定义,字段不能
- 属性可以在对象初始化器中使用
继承
基本概念
- 基类/父类:被继承的类,提供可被复用的属性和方法
- 派生类/子类:继承自基类的类,获得基类所有非私有成员
- 继承特性:C#支持单一继承(一个类只能有一个直接父类),但可以多级继承(A继承B,B继承C)
- 接口实现:虽然只支持单一继承,但一个类可以实现多个接口
继承语法
基本语法
// 基类
public class Animal
{
public string Name { get; set; }
public virtual void MakeSound()
{
Console.WriteLine("动物发出声音");
}
}
// 派生类
public class Dog : Animal
{
public string Breed { get; set; }
public override void MakeSound()
{
Console.WriteLine("汪汪汪");
}
}
// 使用示例
Dog myDog = new Dog();
myDog.Name = "旺财"; // 继承自基类的属性
myDog.Breed = "金毛"; // 派生类自己的属性
myDog.MakeSound(); // 输出: 汪汪汪 (重写的方法)
方法重写和隐藏
public class BaseClass
{
// 可被重写的方法
public virtual void VirtualMethod()
{
Console.WriteLine("基类虚方法");
}
// 普通方法
public void NormalMethod()
{
Console.WriteLine("基类普通方法");
}
}
public class DerivedClass : BaseClass
{
// 重写方法 - 多态行为
public override void VirtualMethod()
{
Console.WriteLine("派生类重写方法");
}
// 隐藏方法 - 非多态
public new void NormalMethod()
{
Console.WriteLine("派生类隐藏方法");
}
}
// 使用示例
BaseClass bc = new DerivedClass();
bc.VirtualMethod(); // 输出: 派生类重写方法 (多态)
bc.NormalMethod(); // 输出: 基类普通方法 (非多态)
访问修饰符与继承
访问修饰符决定了成员在继承链中的可见性和可访问性:
修饰符 | 可访问范围 | 继承情况 |
---|---|---|
public | 任何地方 | 被继承并对所有人可见 |
protected | 本类和派生类内部 | 被继承且只在继承链中可见 |
internal | 同一程序集内 | 同一程序集的派生类可访问 |
protected internal | 同一程序集或派生类 | 同一程序集内部或任何派生类可访问 |
private | 仅在声明类内部 | 不被继承 |
private protected | 同一程序集内的派生类 | 只有同一程序集中的派生类可访问 |
public class Base
{
public string PublicField = "公共";
protected string ProtectedField = "保护";
private string PrivateField = "私有"; // 不会被继承
internal string InternalField = "内部";
}
public class Derived : Base
{
public void AccessTest()
{
Console.WriteLine(PublicField); // 可访问
Console.WriteLine(ProtectedField); // 可访问
// Console.WriteLine(PrivateField); // 编译错误:不可访问
Console.WriteLine(InternalField); // 可访问(同程序集)
}
}
构造函数与继承
派生类实例化时,基类构造函数总是先执行:
public class Person
{
public string Name { get; }
// 基类构造函数
public Person(string name)
{
Console.WriteLine("Person 构造函数执行");
Name = name;
}
}
public class Employee : Person
{
public string JobTitle { get; }
// 派生类构造函数,使用base显式调用基类构造函数
public Employee(string name, string jobTitle) : base(name)
{
Console.WriteLine("Employee 构造函数执行");
JobTitle = jobTitle;
}
}
// 使用示例
Employee emp = new Employee("张三", "工程师");
// 输出顺序:
// Person 构造函数执行
// Employee 构造函数执行
构造函数执行顺序
- 基类的静态成员初始化
- 派生类的静态成员初始化
- 基类的实例成员初始化
- 基类的构造函数
- 派生类的实例成员初始化
- 派生类的构造函数
高级继承概念
抽象类和方法
抽象类提供不完整实现,必须被继承并实现其抽象成员:
// 抽象类
public abstract class Shape
{
// 普通方法
public void Display()
{
Console.WriteLine($"这是一个{GetName()},面积为{CalculateArea():F2}");
}
// 抽象方法 - 必须由派生类实现
public abstract double CalculateArea();
// 虚方法 - 可以被重写,但有默认实现
public virtual string GetName()
{
return "形状";
}
}
// 具体派生类
public class Circle : Shape
{
public double Radius { get; set; }
public Circle(double radius)
{
Radius = radius;
}
// 实现抽象方法
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
// 重写虚方法
public override string GetName()
{
return "圆形";
}
}
// 使用示例
Circle circle = new Circle(5);
circle.Display(); // 输出: 这是一个圆形,面积为78.54
密封类与密封方法
使用sealed
关键字可以防止类被继承或方法被进一步重写:
// 密封类 - 不能被继承
public sealed class FinalClass
{
public void DoSomething() { }
}
public class Base
{
public virtual void Method() { }
}
public class Derived : Base
{
// 密封方法 - 阻止进一步派生类重写该方法
public sealed override void Method() { }
}
public class FurtherDerived : Derived
{
// 编译错误:无法重写已密封的方法
// public override void Method() { }
}
多态性
多态是继承最强大的特性之一:
public class Animal
{
public string Name { get; set; }
public virtual void MakeSound()
{
Console.WriteLine("一般动物声音");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("汪汪汪");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("喵喵喵");
}
}
// 多态示例
public void AnimalSounds(Animal[] animals)
{
foreach (var animal in animals)
{
Console.Write($"{animal.Name}说: ");
animal.MakeSound(); // 调用各自的实现
}
}
// 使用
Animal[] animals = new Animal[]
{
new Dog { Name = "小狗" },
new Cat { Name = "小猫" },
new Animal { Name = "未知动物" }
};
AnimalSounds(animals);
// 输出:
// 小狗说: 汪汪汪
// 小猫说: 喵喵喵
// 未知动物说: 一般动物声音
为什么不能缩小继承成员的访问权限
C#强制要求:重写方法不能缩小访问范围,只能保持相同或扩大。
里氏替换原则(LSP)
这一限制基于面向对象设计的核心原则之一:里氏替换原则
如果S是T的子类型,那么T类型的对象可以被S类型的对象替换,而不会改变程序的行为。
技术原因
多态行为的一致性
- 当通过基类引用访问派生类对象时,必须能够访问所有基类定义的可访问成员
- 如果派生类缩小了方法的访问权限,会破坏多态的一致性
合约保证
- 基类定义了一个"公开合约",指定哪些方法是公开可用的
- 派生类必须遵守并实现这个合约,不能减少或限制这些承诺
public class Parent {
public virtual void Method() {
Console.WriteLine("Parent method");
}
}
public class Child : Parent {
// 编译错误:'Child.Method()' 不能将继承的成员 'Parent.Method()' 的可访问性更改为 'private'
// private override void Method() {
// Console.WriteLine("Child method");
// }
// 正确:可以扩大访问权限
protected virtual void AnotherMethod() { }
}
public class Grandchild : Child {
// 正确:可以从protected扩大到public
public override void AnotherMethod() { }
}
访问修饰符的层次关系(从高到低):
public → protected internal → internal → protected → private protected → private
重写时只能向左移动(扩大权限),不能向右移动(缩小权限)。
继承的最佳实践
适度使用继承
- 继承建立了强耦合关系,过度使用会导致系统难以维护
- 优先考虑组合/聚合,当确实需要"是一个"关系时再使用继承
组合优于继承
- 例如,不要让
ElectricCar
继承Engine
,而是让ElectricCar
包含一个ElectricEngine
对象
- 例如,不要让
设计稳定的基类
- 基类的变化会影响所有派生类,应尽量稳定
- 慎重决定哪些方法应该是virtual的
遵循LSP原则
- 确保派生类可以完全替代基类使用
- 不要在派生类中违反基类的约定或前置条件
使用抽象类定义模板
- 抽象类适合定义通用算法框架(模板方法模式)
- 具体实现由派生类提供
// 一个更复杂的实际示例 - 模板方法模式
public abstract class ReportGenerator
{
// 模板方法 - 定义算法骨架
public void GenerateReport()
{
CollectData();
FormatData();
if (NeedsValidation())
{
ValidateData();
}
ExportReport();
SendNotification();
}
// 抽象方法 - 子类必须实现
protected abstract void CollectData();
protected abstract void FormatData();
// 钩子方法 - 子类可选择性重写
protected virtual bool NeedsValidation() => true;
protected virtual void ValidateData() { }
// 具体方法 - 所有子类共享
protected void ExportReport()
{
Console.WriteLine("导出报表到标准格式");
}
protected virtual void SendNotification()
{
Console.WriteLine("报表生成完成通知");
}
}
// 具体报表实现
public class SalesReport : ReportGenerator
{
protected override void CollectData()
{
Console.WriteLine("从销售数据库收集数据");
}
protected override void FormatData()
{
Console.WriteLine("格式化销售数据为图表");
}
protected override void SendNotification()
{
Console.WriteLine("向销售团队发送报表通知");
}
}
接口与继承对比
C#支持接口实现,这与继承密切相关:
特性 | 继承 | 接口实现 |
---|---|---|
语法 | class Child : Parent | class Child : IInterface |
多重性 | 单一继承 | 可实现多个接口 |
实现 | 继承实现和定义 | 只继承定义,必须实现所有成员 |
用途 | 表达"是一个"关系 | 表达"能做什么"关系 |
访问修饰符 | 支持各种访问修饰符 | 所有成员隐式public |
// 接口定义
public interface IDrawable
{
void Draw();
}
public interface ISavable
{
void Save(string filename);
}
// 同时继承基类并实现多个接口
public class Diagram : Shape, IDrawable, ISavable
{
public override double CalculateArea() => 100;
// 实现IDrawable接口
public void Draw()
{
Console.WriteLine("绘制图表");
}
// 实现ISavable接口
public void Save(string filename)
{
Console.WriteLine($"保存图表到 {filename}");
}
}
总结
C#的继承是一种强大的机制,正确使用可以显著提高代码复用性和扩展性。理解继承的核心概念、访问修饰符规则、构造函数行为以及多态性,对于编写出良好设计的C#程序至关重要。在实际应用中,应当结合接口和组合等其他技术,根据"是一个"还是"有一个"关系,选择最合适的代码组织方式。
多态
多态性概述
多态性是面向对象编程的核心概念之一,允许我们以统一的方式处理不同类型的对象。在C#中,多态性主要通过以下几种方式实现:
- 编译时多态:方法重载
- 运行时多态:虚方法、抽象方法和接口实现
- 其他形式:方法隐藏
静态(编译时)多态 和 动态(运行时)多态
- 静态多态是在编译期确定的多态形式
- 方法重载(overload)是静态多态的典型实现:
- 相同方法名
- 不同参数列表(类型、个数、顺序)
- 编译器根据调用时的参数决定执行哪个版本
- 相对的,动态多态通过继承和方法重写(override)实现,在运行时确定
运行时多态核心机制
虚方法与抽象方法对比
虚方法提供默认行为但允许修改,抽象方法强制子类必须提供实现。
特性 | 虚方法(Virtual Method) | 抽象方法(Abstract Method) |
---|---|---|
关键字 | virtual | abstract |
所属类 | 普通类或抽象类 | 只能在抽象类中 |
实现 | 有方法体/默认实现 | 无方法体/无实现 |
继承要求 | 子类可选重写 | 子类必须重写(除非子类也是抽象类) |
语法与实现示例
虚方法:
public class Base
{
public virtual void Method()
{
Console.WriteLine("Base实现");
}
}
public class Derived : Base
{
// 可以选择重写,也可以不重写
public override void Method()
{
Console.WriteLine("Derived实现");
}
}
抽象方法:
public abstract class AbstractBase
{
// 没有方法体,只有声明
public abstract void Method();
}
public class Concrete : AbstractBase
{
// 必须重写实现
public override void Method()
{
Console.WriteLine("Concrete实现");
}
}
调用行为
// 虚方法示例
Base b1 = new Base();
b1.Method(); // 输出:"Base实现"
Base b2 = new Derived();
b2.Method(); // 输出:"Derived实现" - 体现了多态性
// 抽象方法示例
// AbstractBase ab = new AbstractBase(); // 错误!抽象类不能实例化
AbstractBase ab = new Concrete();
ab.Method(); // 输出:"Concrete实现" - 同样体现了多态性
主要区别
实现要求:
- 虚方法:有方法体,提供默认实现
- 抽象方法:无方法体,仅提供方法签名
继承处理:
- 虚方法:子类可以选择是否重写
- 抽象方法:子类必须重写,否则编译错误
类型限制:
- 虚方法:可以在任何非静态类中声明
- 抽象方法:只能在抽象类中声明
使用场景:
- 虚方法:当基类有默认行为,但允许子类特化时使用
- 抽象方法:当基类无法提供默认实现,强制子类实现时使用
进阶对比
特性 | 虚方法 | 抽象方法 |
---|---|---|
方法调用 | 可直接在基类实例上调用 | 只能通过子类实例调用 |
密封选项 | 可以被sealed override封闭 | 可以被sealed override封闭 |
默认行为 | 有默认行为,如不重写则使用基类行为 | 无默认行为,必须在子类中提供具体实现 |
性能 | 运行时确定调用哪个方法 | 与虚方法相同 |
实际应用选择
使用虚方法当:
- 基类能够提供合理的默认实现
- 希望给子类重写的自由,但不强制
- 有些子类可能不需要特殊实现
使用抽象方法当:
- 基类无法提供有意义的默认实现
- 需要强制所有子类都实现该方法
- 方法在基类层面上只是一个"契约"
方法重写
virtual和abstract在子类重写时都需要override关键字
public class Shape
{
public virtual double CalculateArea()
{
return 0; // 默认实现
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
public class Square : Shape
{
public double Side { get; set; }
public override double CalculateArea()
{
return Side * Side;
}
}
方法隐藏与重写的区别
方法隐藏 (Method Hiding)
当子类中定义了与父类同名的方法但不使用override
关键字时,会发生方法隐藏而非方法重写。
public class Parent
{
public void Display()
{
Console.WriteLine("父类的Display方法");
}
}
public class Child : Parent
{
// 方法隐藏,使用new关键字明确表示意图
public new void Display()
{
Console.WriteLine("子类的Display方法");
}
}
使用new关键字的原因
- 明确表明开发者意图是隐藏而非重写
- 避免编译器警告
- 提高代码可读性
方法隐藏的核心特征
Parent parent = new Child();
Child child = new Child();
parent.Display(); // 输出: "父类的Display方法"
child.Display(); // 输出: "子类的Display方法"
隐藏的关键点:基于引用类型调用方法,而不是基于对象的实际类型
方法重写 vs 方法隐藏
class Parent {
public virtual void VirtualMethod() {
Console.WriteLine("Parent's virtual method");
}
public void RegularMethod() {
Console.WriteLine("Parent's regular method");
}
}
class Child : Parent {
// 方法重写 - 多态行为
public override void VirtualMethod() {
Console.WriteLine("Child's overridden method");
}
// 方法隐藏 - 非多态行为
public new void RegularMethod() {
Console.WriteLine("Child's hiding method");
}
}
// 行为对比
Parent p = new Child();
p.VirtualMethod(); // 输出: "Child's overridden method" - 多态
p.RegularMethod(); // 输出: "Parent's regular method" - 非多态
方法隐藏与方法重写的对比表
特性 | 方法隐藏(Method Hiding) | 方法重写(Method Override) |
---|---|---|
关键字 | new (可省略但会产生警告) | override |
父类要求 | 无特殊要求 | 父类方法必须标记为virtual、abstract或override |
多态行为 | 不支持多态 | 支持多态 |
方法调用 | 基于引用类型决定 | 基于对象实际类型决定 |
适用场景 | 不推荐,通常表示设计问题 | 实现多态性的标准方式 |
编译时多态 - 方法重载
方法重载允许在同一个类中定义多个同名但参数不同的方法。
public class Calculator
{
// 整数加法
public int Add(int a, int b)
{
return a + b;
}
// 浮点数加法
public double Add(double a, double b)
{
return a + b;
}
// 三个参数的加法
public int Add(int a, int b, int c)
{
return a + b + c;
}
}
方法重载与方法重写的对比
特性 | 方法重载(Method Overloading) | 方法重写(Method Override) |
---|---|---|
本质 | 静态多态性(编译时) | 动态多态性(运行时) |
位置 | 同一个类中 | 继承关系的类之间 |
参数 | 必须不同 | 必须相同 |
返回类型 | 可以不同(但仅返回类型不同不足以构成重载) | 必须相同或协变类型 |
关键字 | 不需要特殊关键字 | 需要override关键字 |
使用场景 | 同一个类中处理不同类型输入 | 子类重新实现父类功能 |
C#方法调用解析机制
虚方法调用解析过程
当执行以下代码时:
Parent p = new Child();
p.VirtualMethod();
解析过程是:
- 编译器检查 Parent 类是否有 VirtualMethod()方法
- 运行时,系统识别 p 实际指向的是 Child 对象
- 系统检查 VirtualMethod() 是否是虚方法(是)
- 系统查找对象实际类型(Child)的方法实现
- 如果子类未重写该虚方法,系统会向上查找继承链
方法解析顺序
核心规则:如果子类没有重写虚方法,系统会使用最近的父类实现,通常是声明该虚方法的父类。
class GrandParent {
public virtual void Show() {
Console.WriteLine("GrandParent's implementation");
}
}
class Parent : GrandParent {
public override void Show() {
Console.WriteLine("Parent's implementation");
}
}
class Child : Parent {
// 没有重写Show()
}
// 测试
GrandParent gp = new Child();
gp.Show(); // 输出: "Parent's implementation"
多态性实际应用示例
图形绘制系统
public abstract class Shape
{
// 抽象方法 - 强制子类实现
public abstract double CalculateArea();
// 虚方法 - 提供默认实现但允许重写
public virtual void Draw()
{
Console.WriteLine("绘制基本形状");
}
// 普通方法 - 所有子类共享相同实现
public void DisplayInfo()
{
Console.WriteLine($"这个形状的面积是: {CalculateArea()}");
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public Circle(double radius)
{
Radius = radius;
}
// 必须实现抽象方法
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
// 选择重写虚方法
public override void Draw()
{
Console.WriteLine($"绘制半径为{Radius}的圆");
}
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
// 必须实现抽象方法
public override double CalculateArea()
{
return Width * Height;
}
// 选择重写虚方法
public override void Draw()
{
Console.WriteLine($"绘制{Width}×{Height}的矩形");
}
// 方法隐藏示例 - 不推荐这种做法
public new void DisplayInfo()
{
Console.WriteLine($"这是一个{Width}×{Height}的矩形,面积为{CalculateArea()}");
}
}
接口(Interface)
基本概念
接口定义了一组相关功能的契约,只包含成员的签名,不包含实现代码。
接口支持多重继承,一个接口可以继承多个其他接口。不能直接实例化。
特性
- 接口只能包含方法、属性、事件、索引器的签名
- 不能包含字段、构造函数、析构函数或方法实现
- 自C# 8.0起,接口可以包含默认实现
- 类和结构可以实现多个接口(弥补C#单继承的限制)
- 所有接口成员默认为public
接口的基本语法
// 定义接口
public interface ILogger
{
void LogInfo(string message);
void LogError(string message, Exception ex);
string LogLevel { get; set; }
}
// 实现接口
public class FileLogger : ILogger
{
public string LogLevel { get; set; } = "Info";
public void LogInfo(string message)
{
Console.WriteLine($"[{LogLevel}] {message}");
}
public void LogError(string message, Exception ex)
{
Console.WriteLine($"[ERROR] {message}. Exception: {ex.Message}");
}
}
接口和抽象类对比
特性 | 接口 (Interface) | 抽象类 (Abstract Class) |
---|---|---|
实现方式 | 类"实现"(implement)接口 | 类"继承"(inherit)抽象类 |
多重继承 | 一个类可以实现多个接口 | 一个类只能继承一个抽象类 |
方法实现 | C# 8.0前:只能包含方法签名 C# 8.0后:可有默认实现 | 可同时包含抽象方法和具体实现 |
状态 | 不能包含字段(实例变量) | 可以包含字段和状态 |
构造函数 | 不能有构造函数 | 可以有构造函数 |
访问修饰符 | 成员隐式为public (C# 8.0后有例外) | 可使用任何访问修饰符 |
静态成员 | C# 8.0前:不能有 C# 8.0后:可以有 | 可以有静态成员 |
C# 8.0 接口的新特性
public interface IModernInterface
{
// 默认实现
void Method() => Console.WriteLine("默认实现");
// 静态成员
static string CommonValue = "共享值";
// 私有成员
private void HelperMethod() => Console.WriteLine("辅助方法");
}
接口使用场景
使用接口的情况
- 当类需要多重继承能力时
- 定义不相关类之间的共同行为
- 关注能力而非身份("可以做什么"而非"是什么")
- 需要组件化设计,使系统更模块化
- 定义松散耦合的系统组件
常用内置接口示例
// IComparable - 为类型定义比较行为
public class Student : IComparable<Student>
{
public string Name { get; set; }
public int Score { get; set; }
public int CompareTo(Student other)
{
return this.Score.CompareTo(other.Score);
}
}
// IDisposable - 管理非托管资源
public class ResourceManager : IDisposable
{
private bool disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// 释放托管资源
}
// 释放非托管资源
disposed = true;
}
}
}
使用抽象类的情况
- 表示"是一个"的关系(继承关系)
- 需要在相关类之间共享代码和状态
- 需要非public的成员
- 需要定义共同的基础行为,同时允许特定行为
- 想控制继承版本和层次结构
sealed关键字
sealed是一个限制继承的特殊修饰符,用于控制类和方法的可扩展性。它是面向对象编程中实现封装和安全性的重要工具。
密封类 (sealed class)
当applied到类上时:
- 完全禁止继承:该类不能作为任何其他类的基类
- 终止继承链:标志着继承层次的终点
- 整体封闭:类中所有成员自动不可被重写(因为没有派生类)
// 密封类示例
public sealed class CreditCardProcessor
{
private string securityKey;
public CreditCardProcessor(string key)
{
securityKey = key;
}
public bool ProcessPayment(decimal amount, string cardNumber)
{
// 包含敏感算法的支付处理逻辑
return VerifyCard(cardNumber) && DeductAmount(amount);
}
private bool VerifyCard(string cardNumber)
{
// 验证卡号的安全逻辑
return true; // 简化示例
}
private bool DeductAmount(decimal amount)
{
// 扣款逻辑
return true; // 简化示例
}
}
密封方法 (sealed method)
当applied到方法上时:
- 必须与override同时使用:只能用于已经重写基类虚方法的方法
- 选择性限制:只阻止特定方法被进一步重写,不影响类的继承
- 精确控制:允许类被继承,但锁定关键功能的实现
public class BaseClass
{
public virtual void SecurityCheck() { }
}
public class MiddleClass : BaseClass
{
public sealed override void SecurityCheck()
{
// 此安全验证实现不能被子类进一步修改
Console.WriteLine("执行标准安全检查");
ValidateUser();
CheckPermissions();
}
private void ValidateUser() { /* 验证用户 */ }
private void CheckPermissions() { /* 检查权限 */ }
}
public class DerivedClass : MiddleClass
{
// 尝试重写会导致编译错误
// public override void SecurityCheck() { }
}
使用sealed的原因
- 安全考虑:防止关键算法或安全敏感操作被意外或恶意重写
- 设计完整性:确保某些组件行为的一致性和可预测性
- 性能优化:编译器可以对密封成员进行更多优化
- 明确设计意图:显式表达"此处不应被扩展"的意图
sealed使用最佳实践
- 适度使用:过度使用sealed会降低代码的灵活性
- 粒度控制:优先考虑方法级别的密封而不是整个类
- 文档化决策:说明使用sealed的理由,帮助其他开发者理解
sealed的限制和注意事项
- sealed是永久性限制,后期难以更改设计
- 在框架设计中尤其需要慎重考虑是否使用sealed
- 密封类可能会使单元测试变得更加困难(因为无法通过继承进行模拟)
接口和sealed的组合使用
// 定义接口
public interface IDataProcessor
{
void ProcessData(string data);
}
// 基础实现
public class StandardProcessor : IDataProcessor
{
public virtual void ProcessData(string data)
{
// 标准处理逻辑
}
}
// 安全实现,不允许进一步修改处理逻辑
public sealed class SecureProcessor : IDataProcessor
{
public void ProcessData(string data)
{
// 安全处理实现
}
}
// 可扩展实现,但核心安全检查不可修改
public class ExtendableProcessor : StandardProcessor
{
public sealed override void ProcessData(string data)
{
// 不可被重写的安全处理逻辑
}
}
实际应用示例
使用接口实现策略模式
public interface ITaxCalculator
{
decimal CalculateTax(decimal amount);
}
public class StandardTaxCalculator : ITaxCalculator
{
public decimal CalculateTax(decimal amount)
{
return amount * 0.2m; // 20% 税率
}
}
public class ReducedTaxCalculator : ITaxCalculator
{
public decimal CalculateTax(decimal amount)
{
return amount * 0.05m; // 5% 优惠税率
}
}
public class Order
{
private readonly ITaxCalculator _taxCalculator;
public Order(ITaxCalculator taxCalculator)
{
_taxCalculator = taxCalculator;
}
public decimal CalculateTotal(decimal subtotal)
{
decimal tax = _taxCalculator.CalculateTax(subtotal);
return subtotal + tax;
}
}
使用sealed保护关键业务逻辑
public abstract class PaymentGateway
{
public void ProcessPayment(decimal amount)
{
// 公共流程
ValidateAmount(amount);
decimal fee = CalculateFee(amount);
decimal total = amount + fee;
// 核心支付处理
bool success = ExecutePayment(total);
// 后处理
if (success)
{
NotifySuccess(amount);
}
}
// 允许子类自定义验证逻辑
protected virtual void ValidateAmount(decimal amount) { }
// 允许子类定义手续费计算方式
protected abstract decimal CalculateFee(decimal amount);
// 核心支付逻辑,不允许重写
protected virtual bool ExecutePayment(decimal total)
{
// 安全支付处理逻辑
return true;
}
// 允许自定义通知方式
protected virtual void NotifySuccess(decimal amount) { }
}
public sealed class BankPaymentGateway : PaymentGateway
{
protected sealed override bool ExecutePayment(decimal total)
{
// 银行支付的安全实现,不允许进一步重写
return true;
}
protected override decimal CalculateFee(decimal amount)
{
return amount * 0.01m; // 1% 手续费
}
}
结构体
基本概念
结构体(struct)是C#中的一种值类型数据结构,它可以包含数据成员和方法成员。结构体使用struct关键字定义,是轻量级的数据结构,主要用于表示那些简单且不需要继承的小型数据对象。
public struct Point
{
public int X;
public int Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
public void Display()
{
Console.WriteLine($"X: {X}, Y: {Y}");
}
}
结构体和类有什么区别?
答案:
- 结构体是值类型,而类是引用类型
- 结构体存储在栈上,类存储在堆上
- 结构体不支持继承,但类支持
- 结构体有隐式无参构造函数,而类没有
- 结构体在赋值时会复制整个值,类仅复制引用
- 结构体不能有显式无参构造函数或析构函数
什么情况下应该使用结构体而不是类?
答案:当满足以下条件时应考虑使用结构体:
- 表示的是轻量级数据结构(通常小于16字节)
- 主要用于封装少量相关变量
- 不需要继承功能
- 实例不会频繁修改
- 需要减少内存碎片和提高性能
结构体能否有构造函数?有什么限制?
答案:
- 结构体可以有构造函数,但必须是带参数的
- 所有构造函数必须初始化所有字段
- 不能显式定义无参构造函数(系统已提供默认的)
- 可以有静态构造函数
结构体如何实现接口?
答案:结构体可以实现接口,实现方式与类相同。
public interface IDisplayable
{
void Display();
}
public struct Customer : IDisplayable
{
public string Name;
public void Display()
{
Console.WriteLine($"Customer: {Name}");
}
}
结构体作为参数传递时会发生什么?
答案:默认情况下,结构体作为参数传递时会创建完整副本。如果要避免复制,可以使用ref或out关键字。
// 传递副本
void UpdatePoint(Point p) { p.X = 100; } // 原Point不变
// 传递引用
void UpdatePoint(ref Point p) { p.X = 100; } // 原Point会变
什么是结构体的装箱和拆箱?
答案:
- 装箱是将值类型转换为引用类型的过程(如转为object)
- 拆箱是将已装箱的对象转回值类型
- 这些操作会影响性能,频繁装箱拆箱应当避免
结构体和类的详细对比
特性 | 结构体(Struct) | 类(Class) |
---|---|---|
类型性质 | 值类型 | 引用类型 |
存储位置 | 栈 | 堆 |
继承 | 不支持 | 支持 |
默认构造函数 | 隐式提供且不可重写 | 需要显式定义 |
析构函数 | 不支持 | 支持 |
初始化 | 所有字段初始化为默认值 | 引用为null |
赋值行为 | 复制整个值 | 复制引用 |
内存效率 | 小型数据结构效率高 | 大型数据结构效率高 |
装箱/拆箱 | 转为引用类型时需要 | 不需要 |
可空性 | C# 8.0前天然不可空 | 可空 |
性能影响 | 小结构体传递快 | 大对象传递引用快 |
最佳实践
- 结构体大小不应超过16字节
- 结构体应该是不可变的以避免副本问题
- 结构体用于表示值而非对象,如坐标点、货币金额等
- 需要大量创建的小型数据结构适合用结构体
文件编程
File类与FileInfo类
File类介绍
File类是C#中System.IO命名空间下的一个静态类,它提供了一系列用于文件操作的静态方法,无需实例化即可使用。
主要功能包括:
- 创建文件:
File.Create()
- 删除文件:
File.Delete()
- 复制文件:
File.Copy()
- 移动文件:
File.Move()
- 读取文件内容:
File.ReadAllText()
、File.ReadAllLines()
- 写入文件内容:
File.WriteAllText()
、File.WriteAllLines()
- 检查文件是否存在:
File.Exists()
- 获取文件属性:
File.GetAttributes()
示例代码:
// 检查文件是否存在
bool exists = File.Exists("test.txt");
// 读取文件所有内容
string content = File.ReadAllText("test.txt");
// 创建并写入文件
File.WriteAllText("new.txt", "Hello World");
FileInfo类介绍
FileInfo类同样位于System.IO命名空间下,但它是一个实例类,需要先创建对象才能使用其功能。
主要功能和属性:
- 创建文件:
fileInfo.Create()
- 删除文件:
fileInfo.Delete()
- 复制文件:
fileInfo.CopyTo()
- 移动文件:
fileInfo.MoveTo()
- 文件属性:
Length
、Name
、FullName
、Extension
等 - 时间信息:
CreationTime
、LastAccessTime
、LastWriteTime
示例代码:
// 创建FileInfo对象
FileInfo fileInfo = new FileInfo("test.txt");
// 检查文件是否存在
bool exists = fileInfo.Exists;
// 获取文件大小
long size = fileInfo.Length;
// 创建文件并写入内容
using (StreamWriter writer = fileInfo.CreateText())
{
writer.WriteLine("Hello World");
}
File类与FileInfo类的对比
特性 | File类 | FileInfo类 |
---|---|---|
类型 | 静态类 | 实例类 |
安全检查 | 每次方法调用都进行安全检查 | 只在创建实例时进行一次安全检查 |
性能 | 单次操作较好 | 多次操作同一文件较好 |
内存占用 | 较低 | 较高(需要实例化) |
功能范围 | 仅提供方法 | 提供方法和属性 |
适用场景 | 简单的一次性文件操作 | 需要频繁操作同一文件或获取文件详细信息 |
跨磁盘移动
无论使用哪个类,都可以采用以下代码确保跨驱动器移动文件:
try {
// 使用File静态类
File.Move(@"D:\source.txt", @"E:\destination.txt");
// 或使用FileInfo实例类
FileInfo file = new FileInfo(@"D:\source.txt");
file.MoveTo(@"E:\destination.txt");
}
catch (IOException ex) {
// 捕获潜在的IO异常
Console.WriteLine("移动失败: " + ex.Message);
// 备选方案:手动执行复制后删除
File.Copy(@"D:\source.txt", @"E:\destination.txt", true);
File.Delete(@"D:\source.txt");
}
在面试中,关于这个问题可能是个常见陷阱。正确的回答应该包括:
- 说明**.NET版本的差异**
- 解释新版本中两者都支持跨驱动器移动
- 描述实现原理(复制后删除)
- 提及可能影响移动操作的其他因素(文件锁定、权限等)
现代.NET开发中,您可以放心使用File.Move()或FileInfo.MoveTo()进行跨驱动器的文件移动,但应当加入适当的错误处理机制。
.NET版本 | File.Move() | FileInfo.MoveTo() |
---|---|---|
<4.0 | ❌ 不支持跨驱动器 | ❌ 不支持跨驱动器 |
≥4.0 | ✅ 支持跨驱动器 | ✅ 支持跨驱动器 |
常见面试知识点与答案
问:File类和FileInfo类的主要区别是什么?
答:File是静态类,直接通过类名调用方法;FileInfo是实例类,需要先创建对象。File每次操作都会执行安全检查,而FileInfo只在实例化时检查一次,所以对同一文件进行多次操作时,FileInfo性能更好。
问:什么情况下应该选择使用File类?
答:当只需要对文件进行一次性操作,或者需要处理不同文件的简单操作时,使用File类更便捷。
问:什么情况下应该选择使用FileInfo类?
答:当需要对同一个文件进行多次操作,或需要获取文件的详细属性信息时,使用FileInfo更高效。
问:如何处理File和FileInfo可能抛出的异常?
答:两者可能抛出相似的异常,如IOException、FileNotFoundException、SecurityException等。应使用try-catch块进行适当的异常处理:
csharptry { string content = File.ReadAllText("test.txt"); } catch (FileNotFoundException ex) { Console.WriteLine("文件不存在:" + ex.Message); } catch (IOException ex) { Console.WriteLine("IO错误:" + ex.Message); }
问:如何高效地处理大文件?
答:对于大文件,应避免使用
ReadAllText()
等一次性读取全部内容的方法,而是使用流式处理:csharpusing (StreamReader reader = File.OpenText("largefile.txt")) { string line; while ((line = reader.ReadLine()) != null) { // 处理每一行 } }
问:File和FileInfo类是线程安全的吗?
答:这两个类本身不是线程安全的。如果多个线程需要同时访问同一文件,需要实现适当的同步机制。
问:如何使用File和FileInfo实现文件复制?
答:
csharp// 使用File类 File.Copy("source.txt", "destination.txt", true); // 参数true表示覆盖已存在的文件 // 使用FileInfo类 FileInfo sourceFile = new FileInfo("source.txt"); sourceFile.CopyTo("destination.txt", true);
Directory类与DirectoryInfo类
Directory类介绍
Directory类是C#中System.IO命名空间下的一个静态类,提供了用于目录操作的静态方法,无需实例化即可使用。
主要功能包括:
- 创建目录:
Directory.CreateDirectory()
- 删除目录:
Directory.Delete()
- 移动目录:
Directory.Move()
- 判断目录是否存在:
Directory.Exists()
- 获取文件列表:
Directory.GetFiles()
- 获取子目录列表:
Directory.GetDirectories()
- 获取当前目录:
Directory.GetCurrentDirectory()
- 设置当前目录:
Directory.SetCurrentDirectory()
示例代码:
// 检查目录是否存在
bool exists = Directory.Exists("C:\\Temp");
// 创建目录
Directory.CreateDirectory("C:\\Temp\\NewFolder");
// 获取指定目录中的所有文件
string[] files = Directory.GetFiles("C:\\Temp", "*.txt");
// 获取指定目录中的所有子目录
string[] subdirectories = Directory.GetDirectories("C:\\Temp");
DirectoryInfo类介绍
DirectoryInfo类同样位于System.IO命名空间下,是一个实例类,需要先创建对象才能使用其功能。
主要功能和属性:
- 创建目录:
directoryInfo.Create()
- 删除目录:
directoryInfo.Delete()
- 移动目录:
directoryInfo.MoveTo()
- 目录属性:
Name
、FullName
、Parent
、Root
等 - 时间信息:
CreationTime
、LastAccessTime
、LastWriteTime
- 获取文件列表:
directoryInfo.GetFiles()
DirectoryInfo.GetFiles
方法不支持正则表达式。它只支持简单的通配符模式(如 * 和 ?)- 获取子目录列表:
directoryInfo.GetDirectories()
示例代码:
// 创建DirectoryInfo对象
DirectoryInfo dirInfo = new DirectoryInfo("C:\\Temp");
// 检查目录是否存在
bool exists = dirInfo.Exists;
// 创建子目录
DirectoryInfo newDir = dirInfo.CreateSubdirectory("NewFolder");
// 获取目录中的所有.txt文件
FileInfo[] files = dirInfo.GetFiles("*.txt");
// 获取所有子目录
DirectoryInfo[] subdirs = dirInfo.GetDirectories();
Directory类与DirectoryInfo类的对比
特性 | Directory类 | DirectoryInfo类 |
---|---|---|
类型 | 静态类 | 实例类 |
安全检查 | 每次方法调用都进行安全检查 | 只在创建实例时进行一次安全检查 |
性能 | 单次操作较好 | 多次操作同一目录较好 |
内存占用 | 较低 | 较高(需要实例化) |
返回类型 | 通常返回字符串数组 | 返回强类型对象(FileInfo、DirectoryInfo) |
适用场景 | 简单的一次性目录操作 | 需要频繁操作同一目录或获取目录详细信息 |
常见面试知识点与答案
问:Directory类和DirectoryInfo类的主要区别是什么?
答:Directory是静态类,直接通过类名调用方法;DirectoryInfo是实例类,需要先创建对象。Directory每次操作都会执行安全检查,而DirectoryInfo只在实例化时检查一次,所以对同一目录进行多次操作时,DirectoryInfo性能更好。Directory方法通常返回字符串,而DirectoryInfo方法返回强类型对象。
问:什么情况下应该选择使用Directory类?
答:当只需要对目录进行一次性操作,或者需要处理不同目录的简单操作时,使用Directory类更便捷。
问:什么情况下应该选择使用DirectoryInfo类?
答:当需要对同一个目录进行多次操作,或需要获取目录的详细属性信息,或需要利用返回的强类型对象时,使用DirectoryInfo更高效。
问:如何递归获取目录中的所有文件?
答:两种方式都可以实现:
csharp// 使用Directory类 public static List<string> GetAllFiles(string path) { List<string> allFiles = new List<string>(); // 添加当前目录下的所有文件 allFiles.AddRange(Directory.GetFiles(path)); // 递归处理子目录 foreach (string dir in Directory.GetDirectories(path)) { allFiles.AddRange(GetAllFiles(dir)); } return allFiles; } // 使用DirectoryInfo类 public static List<FileInfo> GetAllFiles(DirectoryInfo dir) { List<FileInfo> allFiles = new List<FileInfo>(); // 添加当前目录下的所有文件 allFiles.AddRange(dir.GetFiles()); // 递归处理子目录 foreach (DirectoryInfo subDir in dir.GetDirectories()) { allFiles.AddRange(GetAllFiles(subDir)); } return allFiles; }
问:如何处理目录操作中可能出现的异常?
答:目录操作可能会抛出IOException、UnauthorizedAccessException、DirectoryNotFoundException等异常,应使用try-catch块处理:
csharptry { Directory.Delete("C:\\Temp", true); // 递归删除目录及其内容 } catch (DirectoryNotFoundException ex) { Console.WriteLine("目录不存在:" + ex.Message); } catch (UnauthorizedAccessException ex) { Console.WriteLine("权限不足:" + ex.Message); } catch (IOException ex) { Console.WriteLine("IO错误:" + ex.Message); }
问:如何判断一个路径是文件还是目录?
答:可以使用File.Exists和Directory.Exists方法组合判断:
csharpstring path = "C:\\SomePath"; if (File.Exists(path)) { Console.WriteLine("这是一个文件"); } else if (Directory.Exists(path)) { Console.WriteLine("这是一个目录"); } else { Console.WriteLine("路径不存在"); }
问:DirectoryInfo如何与FileInfo配合使用?
答:DirectoryInfo可以获取目录中的FileInfo对象,便于进一步操作:
csharpDirectoryInfo dir = new DirectoryInfo("C:\\Temp"); // 获取所有.txt文件 FileInfo[] txtFiles = dir.GetFiles("*.txt"); // 操作这些文件 foreach (FileInfo file in txtFiles) { Console.WriteLine($"文件名: {file.Name}, 大小: {file.Length} 字节"); // 复制文件到备份目录 file.CopyTo(Path.Combine("C:\\Backup", file.Name), true); }
问:Directory和DirectoryInfo类如何监控目录变化?
答:这两个类本身不提供目录监控功能,但可以结合使用System.IO命名空间下的FileSystemWatcher类来监控目录变化:
csharpusing (FileSystemWatcher watcher = new FileSystemWatcher("C:\\Temp")) { watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite; watcher.Created += OnChanged; watcher.Changed += OnChanged; watcher.Deleted += OnChanged; watcher.Renamed += OnRenamed; watcher.EnableRaisingEvents = true; Console.WriteLine("按任意键退出..."); Console.ReadKey(); } static void OnChanged(object sender, FileSystemEventArgs e) { Console.WriteLine($"文件 {e.FullPath} {e.ChangeType}"); } static void OnRenamed(object sender, RenamedEventArgs e) { Console.WriteLine($"文件 {e.OldFullPath} 重命名为 {e.FullPath}"); }
Stream类 数据流
Stream是C#中所有流的抽象基类,它提供了一组标准方法和属性,用于对字节序列进行读写操作。
实现了 IDisposable 接口,因此可以使用 using 语句来确保它们在使用完毕后被正确释放。
用 using 语句时,如果需要给 StreamWriter 重新赋值,应先显式关闭或释放原有对象,然后再创建并赋值新的 StreamWriter,以避免资源泄漏或文件被占用。
主要特性
- 抽象类:不能直接实例化,必须使用其派生类
- 支持同步和异步操作
- 基于字节的数据传输
重要属性和方法
- CanRead/CanWrite/CanSeek:指示流是否支持读取/写入/查找
- Length:获取流的长度
- Position:获取或设置当前位置
- Read()/ReadAsync():从流中读取字节序列
- Write()/WriteAsync():向流中写入字节序列
- Flush()/FlushAsync():将缓冲区数据写入基础设备
- Seek():设置流中的当前位置
- Close()/Dispose():关闭流并释放资源
FileStream类
FileStream是Stream的具体实现,专门用于文件操作。
特点
- 直接继承自Stream
- 提供对文件的字节级访问
- 支持同步和异步文件操作
主要构造函数
FileStream(string path, FileMode mode)
FileStream(string path, FileMode mode, FileAccess access)
FileStream(string path, FileMode mode, FileAccess access, FileShare share)
- FileAccess.Read: FileAccess 枚举定义了对文件的访问权限。 FileAccess.Read 表示只读访问权限。使用此选项时,您只能读取文件内容,而不能对文件进行写入或修改操作。
- FileShare.ReadWrite: FileShare 枚举定义了其他进程对文件的访问权限。 FileShare.ReadWrite 允许其他进程同时读取和写入文件。使用此选项时,即使当前进程正在读取文件,其他进程仍然可以读取和写入该文件。
使用示例
using (FileStream fs = new FileStream("test.txt", FileMode.Create))
{
byte[] data = Encoding.UTF8.GetBytes("Hello World");
fs.Write(data, 0, data.Length);
}
StreamWriter类
StreamWriter提供了一种便捷的方式将字符串写入文件,它在FileStream的基础上增加了文本处理功能。
特点
- 基于TextWriter抽象类
- 处理字符而非字节
- 支持不同编码
- 提供缓冲区提高性能
主要方法
- Write()/WriteLine():写入数据/写入并换行
- Flush():清空缓冲区
- Close():关闭流
使用示例
using (StreamWriter writer = new StreamWriter("text.txt"))
{
writer.WriteLine("第一行文本");
writer.WriteLine("第二行文本");
}
StreamReader类
StreamReader用于从流中读取字符数据,是读取文本文件的理想选择。
特点
- 基于TextReader抽象类
- 按字符而非字节读取
- 支持不同编码
- 自动检测Unicode编码
主要方法
- Read():读取单个字符
- ReadLine():读取一行文本
- ReadToEnd():读取所有剩余文本
- Peek():查看下一个字符而不消费它
使用示例
using (StreamReader reader = new StreamReader("text.txt"))
{
string content = reader.ReadToEnd();
// 或按行读取
// string line;
// while ((line = reader.ReadLine()) != null)
// {
// Console.WriteLine(line);
// }
}
常见面试知识点
类层次结构
- Stream:所有流的抽象基类
- FileStream:文件流
- MemoryStream:内存流
- NetworkStream:网络流
关键区别
Stream与其派生类:
- Stream是抽象基类,不能直接实例化
- FileStream等是具体实现,针对特定数据源
FileStream与StreamReader/Writer:
- FileStream是基于字节的
- StreamReader/Writer是基于字符的
缓冲区使用:
- StreamWriter有内置缓冲区,性能更好
- 使用BufferedStream可以为任何流添加缓冲区
性能考虑
- 使用缓冲区能显著提高性能
- 正确关闭流至关重要,推荐使用using语句
- 对于大文件,考虑分块读取而非一次性加载
- 适当使用异步方法提高响应性
常见面试问题
如何确保资源正确释放? 答:使用using语句或try-finally块显式调用Dispose()方法
FileStream的主要模式有哪些? 答:FileMode.Create、FileMode.Open、FileMode.OpenOrCreate、FileMode.Append等
StreamReader与BinaryReader的区别? 答:StreamReader处理文本数据,BinaryReader处理二进制数据
如何处理不同编码的文本文件? 答:在创建StreamReader/Writer时指定Encoding参数
异步读写与同步读写的选择? 答:UI应用通常选择异步方法避免阻塞,控制台程序可以使用同步方法
代码示例:完整的文件复制操作
public static void CopyFile(string sourcePath, string targetPath)
{
using (FileStream sourceStream = new FileStream(sourcePath, FileMode.Open))
{
using (FileStream targetStream = new FileStream(targetPath, FileMode.Create))
{
byte[] buffer = new byte[4096]; // 4KB缓冲区
int bytesRead;
while ((bytesRead = sourceStream.Read(buffer, 0, buffer.Length)) > 0)
{
targetStream.Write(buffer, 0, bytesRead);
}
}
}
}
BinaryWriter类
BinaryWriter类提供了一种将原始数据类型以二进制形式写入流的方法,是处理二进制文件的理想选择。
主要特性:
- 直接操作二进制数据,而非文本
- 保存数据的精确表示,避免格式转换
- 高效存储各种原始数据类型
- 可以写入任何Stream派生类
重要方法:
- Write():重载方法,可写入各种数据类型(bool, byte, char, decimal, double等)
- Seek():改变流中的位置
- Flush():清空缓冲区
- Close()/Dispose():关闭写入器和基础流
使用示例:
using (FileStream fs = new FileStream("data.bin", FileMode.Create))
using (BinaryWriter writer = new BinaryWriter(fs))
{
writer.Write(42); // 写入整数
writer.Write(3.14159); // 写入双精度浮点数
writer.Write("Hello"); // 写入字符串
writer.Write(true); // 写入布尔值
}
BinaryReader类
BinaryReader类用于从二进制流中读取原始数据类型,通常与BinaryWriter配合使用。读取的顺序要于写入的顺序相同。
主要特性:
- 直接读取二进制数据
- 维护数据类型的完整性
- 支持所有基本数据类型的读取
- 可以从任何Stream派生类读取
重要方法:
- ReadBoolean(), ReadByte(), ReadInt32()等:读取特定类型数据
- ReadString():读取字符串
- ReadBytes():读取指定数量的字节
- PeekChar():查看下一个字符而不前进位置
- Close()/Dispose():关闭读取器和基础流
使用示例:
using (FileStream fs = new FileStream("data.bin", FileMode.Open))
using (BinaryReader reader = new BinaryReader(fs))
{
int intValue = reader.ReadInt32(); // 读取整数
double doubleValue = reader.ReadDouble(); // 读取双精度浮点数
string text = reader.ReadString(); // 读取字符串
bool boolValue = reader.ReadBoolean(); // 读取布尔值
Console.WriteLine($"整数: {intValue}");
Console.WriteLine($"浮点数: {doubleValue}");
Console.WriteLine($"字符串: {text}");
Console.WriteLine($"布尔值: {boolValue}");
}
复杂数据结构的二进制存储
下面是一个更复杂的示例,展示如何存储和读取自定义数据结构:
// 定义一个简单的数据结构
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public DateTime CreatedDate { get; set; }
}
// 写入多个产品记录
public static void SaveProducts(List<Product> products, string filePath)
{
using (FileStream fs = new FileStream(filePath, FileMode.Create))
using (BinaryWriter writer = new BinaryWriter(fs))
{
// 首先写入产品数量
writer.Write(products.Count);
// 然后写入每个产品的详细信息
foreach (var product in products)
{
writer.Write(product.Id);
writer.Write(product.Name);
writer.Write(product.Price);
writer.Write(product.CreatedDate.Ticks);
}
}
}
// 读取产品记录
public static List<Product> LoadProducts(string filePath)
{
List<Product> products = new List<Product>();
using (FileStream fs = new FileStream(filePath, FileMode.Open))
using (BinaryReader reader = new BinaryReader(fs))
{
// 读取产品数量
int count = reader.ReadInt32();
// 读取每个产品
for (int i = 0; i < count; i++)
{
Product product = new Product
{
Id = reader.ReadInt32(),
Name = reader.ReadString(),
Price = reader.ReadDecimal(),
CreatedDate = new DateTime(reader.ReadInt64())
};
products.Add(product);
}
}
return products;
}
二进制与文本文件操作的比较
特性 | 二进制文件 | 文本文件 |
---|---|---|
可读性 | 人类不可直接读取 | 可直接阅读 |
存储效率 | 高,数据以原始格式存储 | 较低,需要文本转换 |
精度 | 精确保留原始数据 | 可能有精度损失 |
文件大小 | 通常更小 | 通常更大 |
处理速度 | 更快,无需转换 | 较慢,需要解析 |
跨平台 | 可能有字节序问题 | 较好,但有编码问题 |
常见面试知识点
BinaryWriter与StreamWriter的主要区别
- BinaryWriter处理二进制数据,StreamWriter处理文本数据
- BinaryWriter保存原始数据类型的准确表示,StreamWriter转换为文本格式
- BinaryWriter创建的文件人类不可读,StreamWriter创建人类可读文件
字节序(Endianness)问题
- 不同系统可能使用不同的字节序(大端序/小端序)
- 二进制文件在跨平台使用时可能需要考虑字节序转换
- .NET通常使用**小端序(Little-Endian)**存储多字节数据
二进制I/O的性能优势
- 减少格式转换开销
- 更小的文件大小,减少I/O操作
- 数据类型的精确表示,避免精度损失
资源管理的最佳实践
- 始终使用using语句确保资源释放
- 处理异常可能发生的情况
- 考虑使用缓冲区提高性能
常见面试问题
Q: 什么情况下应选择二进制文件而非文本文件?
A: 当需要精确保存数据类型、存储效率是关键因素、文件不需要人类直接阅读或需要创建自定义文件格式时。
Q: BinaryReader如何处理不同数据类型?
A: BinaryReader提供了特定类型的读取方法(如ReadInt32(), ReadDouble()等),根据原始数据的二进制表示直接解析。
Q: 如何处理二进制文件的版本兼容性?
A: 可以在文件开头添加版本标识,实现向后兼容的读取逻辑,或使用更灵活的序列化框架。
Q: 大文件的二进制读写有什么注意事项?
A: 使用适当大小的缓冲区,考虑分块处理,利用异步I/O操作,并注意内存管理。
实用技巧
- 组合使用缓冲流提高性能:
using (FileStream fs = new FileStream("largefile.bin", FileMode.Create))
using (BufferedStream bs = new BufferedStream(fs, 65536)) // 64KB缓冲区
using (BinaryWriter writer = new BinaryWriter(bs))
{
// 写入大量数据
}
- 处理不同编码的二进制文本:
using (FileStream fs = new FileStream("data.bin", FileMode.Create))
using (BinaryWriter writer = new BinaryWriter(fs, Encoding.UTF8))
{
writer.Write("多语言文本");
}
- 异步二进制操作:
public static async Task SaveBinaryDataAsync(byte[] data, string filePath)
{
using (FileStream fs = new FileStream(filePath, FileMode.Create))
{
await fs.WriteAsync(data, 0, data.Length);
}
}
Json 操作
public class JSONHelper {
public class Student {
public string Name { get; set; }
public string Gender { get; set; }
public int Age { get; set; }
public int Height { get; set; }
public string StudentId { get; set; }
}
public static List<Student> students = new List<Student>();
//文本存放地址
public static string currentPath = Directory.GetCurrentDirectory();
public static DirectoryInfo parentPath = Directory.GetParent(currentPath);
public static DirectoryInfo solutionPath = Directory.GetParent(parentPath.FullName);
public static string _studentsInfoFilePath = solutionPath.ToString() + "\\studentsInfo.json";
//新建JSON文件
public static void createSmsJson() {
if (!File.Exists(_studentsInfoFilePath)) {
File.Create(_studentsInfoFilePath).Close();
} else {
// 读取文件中的学生信息
string[] lines = File.ReadAllLines(_studentsInfoFilePath);
foreach (string line in lines) {
string[] studentInfo = line.Split(' ');
if (studentInfo.Length == 5) {
Student newStudent = new Student();
newStudent.Name = studentInfo[0];
newStudent.Gender = studentInfo[1];
// 使用 int.TryParse 来更安全地解析整数
if (int.TryParse(studentInfo[2], out int age)) {
newStudent.Age = age;
} else {
// 处理解析失败的情况,可以记录日志、给出提示等
// 在这里,你可以设置默认值或者采取其他措施
newStudent.Age = 0; // 默认值为 0,你可以根据需要修改
}
if (int.TryParse(studentInfo[3], out int height)) {
newStudent.Height = height;
} else {
// 处理解析失败的情况,可以记录日志、给出提示等
// 在这里,你可以设置默认值或者采取其他措施
newStudent.Height = 0; // 默认值为 0,你可以根据需要修改
}
newStudent.StudentId = studentInfo[4];
AddStudent(newStudent);
}
}
}
}
//将学生信息转为JSON格式,并将JSON写入指定文件中
private static void SaveStudentsToJSON() {
string studentsJson = JsonConvert.SerializeObject(students);
File.WriteAllText(_studentsInfoFilePath, studentsJson);
}
//获取JSON文件中的数据
public static List<Student> LoadStudentsFromJson() {
if (File.Exists(_studentsInfoFilePath)) {
string jsonContent = File.ReadAllText(_studentsInfoFilePath);
//将 JSON 格式的字符串 jsonContent 反序列化为 List<Student> 对象。
List<Student> loadedStudents = JsonConvert.DeserializeObject<List<Student>>(jsonContent);
// 提取学生ID并添加到usedIds中
foreach (Student student in students) {
usedIds.Add(student.StudentId);
}
return loadedStudents ?? new List<Student>(); // 返回整个学生列表,若为空则返回空列表
}
return new List<Student>();
}
}
窗体
WinForm 架构
基本组件
- Form 类:所有窗体的基类,代表一个窗口
- Control 类:所有控件的基类
- Application 类:管理应用程序执行流程
事件驱动模型
WinForm 应用程序基于事件驱动模型工作:
// 为按钮添加点击事件处理程序
button1.Click += new EventHandler(button1_Click);
// 事件处理程序
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("按钮被点击了");
}
常用控件及其特性
基础控件
- Button:触发操作的按钮
- Label:显示不可编辑的文本
- TextBox:允许用户输入文本
- CheckBox:选择或取消选择选项
- RadioButton:从一组选项中选择一个
- ComboBox:下拉列表
- ListView:以不同视图显示项目集合
- TreeView:以层次结构显示节点
- DataGridView:以表格形式显示数据
容器控件
- Panel:简单的容器,可包含其他控件
- GroupBox:带标题的容器
- TabControl:创建选项卡式界面
- SplitContainer:创建可调整大小的分割面板
布局与设计
控件布局
- Dock:控件停靠到父容器的边缘
- Anchor:控件相对于父容器的锚定点
- FlowLayoutPanel:流式布局容器
- TableLayoutPanel:表格式布局容器
// 设置控件停靠
button1.Dock = DockStyle.Bottom;
// 设置控件锚点
textBox1.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
数据绑定
基础数据绑定
// 简单绑定
textBox1.DataBindings.Add("Text", customer, "Name");
// 绑定到列表
dataGridView1.DataSource = customerList;
高级数据绑定
- BindingSource:中间数据层,简化数据管理
- BindingNavigator:导航和修改绑定数据
多线程处理
UI 线程与工作线程
WinForm 应用程序是单线程的,所有 UI 操作必须在 UI 线程上执行。
// 在工作线程中执行长时间操作
Task.Run(() => {
// 长时间操作
// 更新 UI(必须回到 UI 线程)
this.Invoke((MethodInvoker)delegate {
label1.Text = "操作完成";
});
});
BackgroundWorker
backgroundWorker1.DoWork += (s, e) => {
// 长时间操作
};
backgroundWorker1.RunWorkerCompleted += (s, e) => {
// 操作完成,可以安全地更新 UI
};
backgroundWorker1.RunWorkerAsync();
窗体快捷键
在菜单项(如 MenuStrip
、ContextMenuStrip
)或按钮(如 Button
、ToolStripMenuItem
)的文字中显示快捷键或设置快捷键提示,可以使用 &
符号 来表示快捷键。
在控件的
Text
属性中添加&
,后面的字母就是快捷键。比如:
csharpfileToolStripMenuItem.Text = "&File";
这样在界面中会显示为
File
,但字母 F 会被下划线标记,并可通过 Alt+F 激活。如果想在文字中真正显示
&
字符(比如 "Save & Exit"),需要用两个&&
,例如:csharpexitToolStripMenuItem.Text = "Save && Exit";
设置组合快捷键(Ctrl+S、Ctrl+O 等):
对于
ToolStripMenuItem
,你还可以设置ShortcutKeys
属性:csharpsaveToolStripMenuItem.ShortcutKeys = Keys.Control | Keys.S;
这样可以直接在菜单项右边显示快捷键信息,用户也可以直接使用快捷键触发。
常见面试问题
基础概念
WinForm 和 WPF 的区别是什么?
- WinForm 基于旧的 GDI+,而 WPF 基于 DirectX
- WinForm 布局较简单,WPF 使用 XAML 和更强大的布局系统
- WPF 支持更高级的图形、动画和绑定能力
WinForm 的事件生命周期有哪些重要阶段?
- Load:窗体首次加载
- Shown:窗体显示后
- Activated:窗体激活
- FormClosing:窗体正在关闭
- FormClosed:窗体已关闭
- Disposed:窗体资源释放
控件与布局
MDI (多文档界面) 是什么,如何实现?
csharp// 设置父窗体为 MDI 容器 parentForm.IsMdiContainer = true; // 创建子窗体 Form childForm = new Form(); childForm.MdiParent = parentForm; childForm.Show();
如何处理控件的拖放功能?
csharp// 设置允许拖放 textBox1.AllowDrop = true; // 拖动事件 textBox1.DragEnter += (s, e) => { if (e.Data.GetDataPresent(DataFormats.Text)) e.Effect = DragDropEffects.Copy; }; // 放置事件 textBox1.DragDrop += (s, e) => { textBox1.Text = (string)e.Data.GetData(DataFormats.Text); };
性能与优化
如何提高 WinForm 应用程序的性能?
- 使用双缓冲减少闪烁
- 延迟加载非关键组件
- 异步处理耗时操作
- 优化数据绑定,减少不必要的更新
如何处理 WinForm 应用程序中的异常?
csharpstatic void Main() { Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException); AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); Application.Run(new Form1()); } static void Application_ThreadException(object sender, ThreadExceptionEventArgs e) { // 处理 UI 线程异常 } static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { // 处理非 UI 线程异常 }
高级主题
如何实现自定义控件?
csharppublic class MyCustomButton : Button { public MyCustomButton() { this.FlatStyle = FlatStyle.Flat; this.BackColor = Color.Blue; this.ForeColor = Color.White; } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); // 自定义绘制逻辑 } }
WinForm 中如何实现 MVP 或 MVVM 模式?
- 使用接口定义视图功能
- 创建 Presenter 类处理业务逻辑
- 实现数据绑定连接模型和视图
C# 控件命名规范
C#中控件的命名非常重要,它直接影响代码的可读性和可维护性。一个好的命名规范可以让开发团队更容易理解代码,减少错误。
基本原则
- 使用有意义的名称:控件名称应当清晰表达其用途和功能
- 保持一致性:在整个项目中遵循相同的命名模式
- 避免使用单个字母或数字作为控件名称
- 使用Pascal命名法(每个单词首字母大写)
C# 控件命名规范扩展列表
以下是更全面的C#控件前缀命名规范,按不同类型分类整理:
基础控件前缀
前缀 | 控件类型 |
---|---|
btn | Button (按钮) |
txt | TextBox (文本框) |
rtxt | RichTextBox (富文本框) |
lbl | Label (标签) |
chk | CheckBox (复选框) |
rb | RadioButton (单选按钮) |
pic | PictureBox (图片框) |
img | Image (图像) |
lnk | LinkLabel (链接标签) |
num | NumericUpDown (数字调节器) |
容器类控件前缀
前缀 | 控件类型 |
---|---|
pnl | Panel (面板) |
grp | GroupBox (组合框) |
tab | TabControl (选项卡) |
pgc | Page (页面) |
frm | Form (窗体) |
dlg | Dialog (对话框) |
flp | FlowLayoutPanel (流式布局面板) |
tlp | TableLayoutPanel (表格布局面板) |
spl | SplitContainer (分隔容器) |
scv | ScrollViewer (滚动视图) |
列表和数据显示控件前缀
前缀 | 控件类型 |
---|---|
cmb | ComboBox (下拉列表) |
ddl | DropDownList (下拉列表) |
lst | ListBox (列表框) |
lv | ListView (列表视图) |
trv | TreeView (树视图) |
dgv | DataGridView (数据网格) |
gv | GridView (网格视图) |
rpt | Repeater (重复器) |
dv | DataView/DetailsView (数据视图) |
cbl | CheckBoxList (复选框列表) |
rbl | RadioButtonList (单选按钮列表) |
菜单和导航控件前缀
前缀 | 控件类型 |
---|---|
mnu | Menu/MainMenu (菜单) |
ctx | ContextMenu (上下文菜单) |
mstp | MenuStrip (菜单条) |
tstp | ToolStrip (工具条) |
sstp | StatusStrip (状态条) |
nav | Navigator (导航器) |
bc | BreadCrumb (面包屑导航) |
pnv | PageNavigator (页面导航) |
日期和时间控件前缀
前缀 | 控件类型 |
---|---|
dtp | DateTimePicker (日期时间选择器) |
dp | DatePicker (日期选择器) |
tp | TimePicker (时间选择器) |
cal | Calendar (日历) |
mc | MonthCalendar (月历) |
进度和度量控件前缀
前缀 | 控件类型 |
---|---|
pgb | ProgressBar (进度条) |
sldr | Slider (滑块) |
tbar | TrackBar (轨迹条) |
spin | Spinner (微调器) |
gauge | Gauge (仪表) |
mtr | Meter (计量器) |
WPF特有控件前缀
前缀 | 控件类型 |
---|---|
cnv | Canvas (画布) |
dp | DockPanel (停靠面板) |
sp | StackPanel (堆叠面板) |
wp | WrapPanel (自动换行面板) |
grd | Grid (网格) |
tb | ToggleButton (切换按钮) |
exp | Expander (展开器) |
ink | InkCanvas (墨迹画布) |
vm | ViewBox (视图框) |
Web特有控件前缀
前缀 | 控件类型 |
---|---|
hpl | HyperLink (超链接) |
upl | FileUpload (文件上传) |
lit | Literal (文本) |
wiz | Wizard (向导) |
mv | MultiView (多视图) |
vw | View (视图) |
wuc | WebUserControl (Web用户控件) |
其他辅助控件前缀
前缀 | 控件类型 |
---|---|
err | ErrorProvider (错误提供器) |
tip | ToolTip (工具提示) |
nfy | NotifyIcon (通知图标) |
tmr | Timer (定时器) |
bw | BackgroundWorker (后台工作器) |
hlp | Helper (辅助控件) |
web | WebBrowser (网页浏览器) |
uc | UserControl (用户控件) |
cc | CustomControl (自定义控件) |
popw | PopupWindow (弹出窗口) |
数据相关组件前缀
前缀 | 控件类型 |
---|---|
bs | BindingSource (绑定源) |
dta | DataAdapter (数据适配器) |
ds | DataSet (数据集) |
dt | DataTable (数据表) |
cmd | Command (命令) |
con | Connection (连接) |
bnd | Binding (绑定) |
val | Validator (验证器) |
命名示例
// 好的命名示例
btnSubmit // 提交按钮
txtUserName // 用户名文本框
lblErrorMessage // 错误信息标签
chkAgreeTerms // 同意条款复选框
cmbCountry // 国家下拉列表
// 不好的命名示例
button1 // 不描述功能
t // 过于简短
TextBox_1 // 使用数字作为标识
进阶建议
- 避免在名称中重复控件类型:例如 btnSubmitButton (重复了Button)
- 控件名称应反映业务含义而非UI外观
- 为相关控件使用一致的命名模式,例如:btnSave, btnCancel, btnEdit
- 避免使用匈牙利命名法(除了控件类型前缀)
遵循这些命名规范能让代码更加规范和专业,对团队协作和长期维护都有很大帮助。
数据库
SQLite
在 SQLiteHelper 类中,ExecuteReader
、ExecuteQuery
和 ExecuteNonQuery
这三个方法有不同的用途和返回类型:
SQLiteHelper.ExecuteNonQuery
public static int ExecuteNonQuery(string sqlStr, params SQLiteParameter[] p)
- 用途:用于执行不返回结果集的 SQL 命令(如 INSERT、UPDATE、DELETE)
- 返回值:返回受影响的行数(整数)
- 典型场景:数据修改操作,如用户修改密码、新增记录、删除数据等
- 示例:在
User.ChangePassword
方法中使用此方法更新用户密码
SQLiteHelper.ExecuteQuery
public static DataSet ExecuteQuery(string sqlStr, params SQLiteParameter[] p)
- 用途:用于执行返回多行数据的 SQL 查询(通常是 SELECT 语句)
- 返回值:返回包含查询结果的 DataSet 对象
- 典型场景:需要处理多行数据的查询操作,如用户登录验证、列出数据等
- 示例:在
FrmLogin
类中用于用户登录验证
SQLiteHelper.ExecuteReader
public static SQLiteDataReader ExecuteReader(string sqlStr, params SQLiteParameter[] p)
- 用途:用于执行需要逐行读取数据的 SQL 查询
- 返回值:返回 SQLiteDataReader 对象,允许逐行读取数据
- 典型场景:需要高效处理大量数据时,不需要一次性加载全部数据到内存
- 特点:使用快速前向只读游标,内存占用更少,但使用完毕需要手动关闭
总结区别
数据处理方式:
ExecuteNonQuery
:不返回数据,只执行命令ExecuteQuery
:一次性加载全部数据到 DataSetExecuteReader
:提供流式访问,逐行读取数据
返回类型:
ExecuteNonQuery
:int(影响的行数)ExecuteQuery
:DataSetExecuteReader
:SQLiteDataReader
适用场景:
ExecuteNonQuery
:数据修改操作ExecuteQuery
:需要全部数据并进行复杂操作ExecuteReader
:大数据量或逐行处理场景
选择哪种方法取决于具体的业务需求和数据处理方式。
断开式(Disconnected)
优点
- 减少数据库连接时间:数据读取后,连接立即关闭,减少了数据库连接的占用时间。
- 提高应用程序性能:由于不需要持续连接数据库,减少了数据库服务器的负载。
- 适合批量操作:可以在内存中对数据进行批量操作,然后一次性提交到数据库。
- 离线操作:可以在没有数据库连接的情况下对数据进行操作,适合需要离线处理的场景。
应用场景
- 数据展示:如在 DataGridView 中显示数据,用户可以浏览和编辑数据,然后一次性提交更改。
- 报表生成:从数据库中读取数据后,生成报表或导出数据。
- 缓存数据:在内存中缓存数据,减少频繁的数据库访问。
示例
在 SQLiteHelper
类中,ExecuteQuery
方法就是一个典型的断开式数据访问方法:
public static DataSet ExecuteQuery(string sqlStr, params SQLiteParameter[] p)
{
using (SQLiteConnection conn = new SQLiteConnection(ConnectionString))
{
using (SQLiteCommand command = new SQLiteCommand())
{
DataSet ds = new DataSet();
try
{
PrepareCommand(command, conn, sqlStr, p);
SQLiteDataAdapter da = new SQLiteDataAdapter(command);
da.Fill(ds);
return ds;
}
catch (Exception ex)
{
return ds;
}
}
}
}
链接式(Connected)
优点
- 实时数据访问:可以实时读取和更新数据库中的数据,确保数据的最新状态。
- 适合大数据量操作:可以逐行读取数据,减少内存占用,适合处理大数据量的场景。
- 事务支持:可以在一个事务中执行多个数据库操作,确保数据的一致性和完整性。
应用场景
- 实时数据处理:如实时监控系统,需要不断从数据库中读取最新数据。
- 大数据量处理:如逐行读取和处理大量数据,避免一次性加载全部数据到内存。
- 事务操作:如银行转账,需要确保多个操作在一个事务中完成。
示例
在 SQLiteHelper
类中,ExecuteReader
方法就是一个典型的链接式数据访问方法:
public static SQLiteDataReader ExecuteReader(string sqlStr, params SQLiteParameter[] p)
{
using (SQLiteConnection conn = new SQLiteConnection(ConnectionString))
{
using (SQLiteCommand command = new SQLiteCommand())
{
try
{
PrepareCommand(command, conn, sqlStr, p);
return command.ExecuteReader(CommandBehavior.CloseConnection);
}
catch (Exception ex)
{
return null;
}
}
}
}
总结
- 断开式数据访问:适合需要减少数据库连接时间、提高性能、支持离线操作的场景。
- 链接式数据访问:适合需要实时数据访问、处理大数据量、支持事务操作的场景。
选择哪种数据访问方式取决于具体的业务需求和应用场景。
委托和事件
委托(Delegate)
基础知识
委托在C#中是一个非常重要的概念。委托是一种引用类型,它表示对具有特定参数列表和返回类型的方法的引用。简单来说,委托是一个可以存储方法引用的类型。
核心特点:
- 类型安全:委托在编译时就确定了可以引用的方法签名
- 面向对象:委托是一个类,继承自System.Delegate
- 封装方法调用:委托允许将方法作为参数传递
委托语法与使用
基本语法:
using System;
namespace DelegateDemo
{
// 1. 声明委托
delegate int Calculator(int x, int y);
class Program
{
// 2. 定义与委托匹配的方法
static int Add(int a, int b) => a + b;
static int Subtract(int a, int b) => a - b;
static void Main()
{
// 3. 实例化委托
Calculator calc1 = new Calculator(Add);
Calculator calc2 = Subtract; // 简写
// 4. 调用委托
Console.WriteLine($"Add: {calc1(5, 3)}"); // 输出: 8
Console.WriteLine($"Subtract: {calc2(5, 3)}"); // 输出: 2
// 5. 多播委托(订阅)
Calculator multiCalc = calc1; // 第一个方法
multiCalc += calc2; // 订阅第二个方法
Console.WriteLine("\nMulticast Delegate:");
foreach (Calculator del in multiCalc.GetInvocationList())
{
Console.WriteLine($"Result: {del(10, 4)}"); // 分别调用 Add 和 Subtract
}
}
}
}
使用步骤:
- 声明委托类型
- 创建委托实例,将方法与委托关联
- 通过委托调用方法
委托的特性和应用场景
委托在C#中有多种应用场景:
- 事件处理:Windows Forms和WPF中的事件处理就是基于委托实现的
- 回调方法:方法完成后执行的回调操作
- 异步编程:配合异步编程模式使用
- LINQ:许多LINQ操作接受委托作为参数
- 策略模式:在运行时更改算法的行为
预定义委托类型
C#提供了几种常用的预定义委托类型:
Action
:表示无返回值的方法(void)Action<T1, T2, ...>
:带参数的无返回值方法Func<TResult>
:有返回值的无参数方法Func<T1, T2, ..., TResult>
:有参数有返回值的方法Predicate<T>
:返回bool类型的方法,通常用于条件判断
多播委托
多播委托是C#中的一个强大特性:
- 可以让一个委托引用多个方法
- 使用
+=
添加方法,-=
移除方法 - 调用委托时会按顺序执行所有关联的方法
- 对于有返回值的委托,最终返回值为最后一个方法的返回值
委托与事件
事件(Event)是基于委托模式的一种特殊实现:
- 事件是委托的一个实例,但有额外的安全限制
- 事件只能在声明它的类中被触发(invoke)
- 外部类只能订阅/取消订阅事件,不能直接调用
注意事项与最佳实践
- 避免频繁创建委托实例,可能影响性能
- 使用泛型委托类型而非自定义委托,如Action和Func
- 注意多播委托的执行顺序和异常处理
- 委托与匿名方法和Lambda表达式配合使用更简洁
- 防止委托为null,避免NullReferenceException
代码示例
基本示例:
// 声明委托类型
delegate int MathOperation(int x, int y);
class Program
{
// 符合委托签名的方法
static int Add(int a, int b) { return a + b; }
static int Subtract(int a, int b) { return a - b; }
static void Main()
{
// 实例化委托
MathOperation operation = Add;
// 通过委托调用方法
int result = operation(10, 5); // 结果为15
// 改变引用的方法
operation = Subtract;
result = operation(10, 5); // 结果为5
// 使用匿名方法
operation = delegate(int a, int b) { return a * b; };
result = operation(10, 5); // 结果为50
// 使用Lambda表达式
operation = (a, b) => a / b;
result = operation(10, 5); // 结果为2
}
}
多播委托示例:
delegate void Notifier(string message);
class Program
{
static void EmailNotify(string message)
{
Console.WriteLine($"发送邮件通知: {message}");
}
static void SMSNotify(string message)
{
Console.WriteLine($"发送短信通知: {message}");
}
static void Main()
{
// 创建多播委托
Notifier notifier = EmailNotify;
notifier += SMSNotify;
// 调用多播委托
notifier("系统更新完成");
// 输出:
// 发送邮件通知: 系统更新完成
// 发送短信通知: 系统更新完成
// 移除一个方法
notifier -= EmailNotify;
// 再次调用
notifier("任务已完成");
// 输出:
// 发送短信通知: 任务已完成
}
}
泛型委托示例:
class Program
{
static void Main()
{
// Action - 无返回值
Action<string> logger = message => Console.WriteLine($"日志: {message}");
logger("操作开始");
// Func - 有返回值
Func<int, int, double> divide = (x, y) => (double)x / y;
double result = divide(10, 3); // 结果为3.3333...
// Predicate - 返回布尔值
Predicate<int> isEven = x => x % 2 == 0;
bool check = isEven(4); // 结果为true
}
}
面试常见问题及回答
1. 什么是委托?为什么要使用委托?
回答:委托是一种引用类型,用于引用具有特定参数列表和返回类型的方法。使用委托的主要原因是实现回调机制,提高代码的灵活性,以及支持事件处理。委托使方法可以作为参数传递,从而实现更加松耦合的设计。
2. 委托和接口有什么区别?
回答:
- 委托引用单个方法,而接口可以定义多个方法
- 委托主要用于回调和事件处理,接口主要用于定义类的行为契约
- 委托可以动态改变引用的方法,接口实现在类定义时就确定了
- 委托允许使用匿名方法和Lambda表达式,接口不支持这种方式
3. 解释多播委托的工作原理
回答:多播委托允许一个委托引用多个方法。它使用委托组合(+=
和-=
)来添加和移除方法。当调用多播委托时,它会按照添加顺序依次执行所有引用的方法。对于有返回值的多播委托,最终返回的是最后一个方法的返回值。内部实现上,多播委托维护了一个调用列表(invocation list)。
4. Action、Func和Predicate有什么区别?
回答:
- Action:表示无返回值的委托(返回void)
- Func:表示有返回值的委托,最后一个泛型参数是返回类型
- Predicate:特殊的Func,只接受一个参数并返回bool值
5. 委托和事件有什么区别?
回答:事件是基于委托的一种封装,主要区别在于:
- 事件只能由声明它的类触发,委托可以在任何地方调用
- 事件只提供
+=
和-=
操作,不能直接赋值(防止清空其他订阅者) - 事件通常遵循发布-订阅模式,而委托使用场景更广泛
- 事件隐式为null,需要判断后才能触发,而委托可能导致空引用异常
6. 如何处理委托中的异常?
回答:在多播委托中,如果某个方法抛出异常,后续方法将不会执行。处理委托异常的常用方法是:
- 使用try-catch块包围委托调用
- 对于多播委托,可以手动遍历调用列表并单独处理每个方法的异常
- 使用安全调用模式,确保委托不为null
// 安全调用模式
delegateInstance?.Invoke(parameters);
// 手动遍历处理异常
foreach (Delegate handler in delegateInstance.GetInvocationList())
{
try
{
handler.DynamicInvoke(parameters);
}
catch (Exception ex)
{
// 处理异常
}
}
7. 委托是值类型还是引用类型?
回答:委托是引用类型。它们继承自System.Delegate类,遵循引用类型的行为规则。
8. 委托与匿名方法和Lambda表达式的关系?
回答:匿名方法和Lambda表达式都是创建委托实例的简化语法:
- 匿名方法(C# 2.0引入):
delegate(parameters) { code; }
- Lambda表达式(C# 3.0引入):
(parameters) => expression
或(parameters) => { statements; }
这些都会被编译器转换为委托实例。Lambda表达式是最简洁的语法,也是现代C#中最常用的形式。
9. 委托的性能影响如何?
回答:委托在性能上有一定开销:
- 创建委托实例会分配堆内存
- 委托调用比直接方法调用稍慢
- 多播委托的调用列表需要维护
但在大多数应用场景中,这种性能差异通常可以忽略。只有在性能关键的场景,如高频调用的循环中,才需要特别注意委托的使用。
10. 什么是协变和逆变?委托如何支持?
回答:
- 协变(Covariance):允许使用比原始指定的类型"更具体"的类型
- 逆变(Contravariance):允许使用比原始指定的类型"更一般"的类型
在委托中:
- 返回类型支持协变(可以返回派生类型)
- 参数类型支持逆变(可以接受基类类型)
C# 2.0开始支持这种特性,但从C# 4.0才引入显式语法(in
和out
关键字)。
事件(Event)
基础知识
事件(Event) 是C#中实现观察者模式的一种机制,它允许一个类(发布者)在特定情况发生时通知其他类(订阅者)。事件是基于**委托(Delegate)**的特殊成员。
事件的组成部分
- 事件发布者 - 定义和触发事件的类
- 事件订阅者 - 响应事件的类
- 委托 - 定义事件处理方法的签名
- 事件处理器 - 响应事件的方法
事件的声明和使用
声明事件
// 1. 定义委托类型
public delegate void TemperatureChangedEventHandler(object sender, TemperatureEventArgs e);
// 2. 定义事件参数
public class TemperatureEventArgs : EventArgs
{
public float NewTemperature { get; }
public TemperatureEventArgs(float newTemperature)
{
NewTemperature = newTemperature;
}
}
// 3. 声明事件
public class Thermostat
{
private float _currentTemperature;
// 事件声明
public event TemperatureChangedEventHandler TemperatureChanged;
// 或使用EventHandler<T>泛型委托
// public event EventHandler<TemperatureEventArgs> TemperatureChanged;
}
触发事件
// 触发事件的方法
protected virtual void OnTemperatureChanged(TemperatureEventArgs e)
{
// 线程安全的调用事件
TemperatureChanged?.Invoke(this, e);
}
public float CurrentTemperature
{
get { return _currentTemperature; }
set
{
if (_currentTemperature != value)
{
_currentTemperature = value;
OnTemperatureChanged(new TemperatureEventArgs(_currentTemperature));
}
}
}
订阅和取消订阅事件
// 订阅事件
thermostat.TemperatureChanged += Thermostat_TemperatureChanged;
// 事件处理方法
private void Thermostat_TemperatureChanged(object sender, TemperatureEventArgs e)
{
Console.WriteLine($"温度已变化为: {e.NewTemperature}°C");
}
// 取消订阅
thermostat.TemperatureChanged -= Thermostat_TemperatureChanged;
事件的注意事项
- 内存泄漏风险 - 订阅者未正确取消订阅可能导致内存泄漏
- 线程安全问题 - 在多线程环境中需要特别注意事件的触发方式
- 空引用检查 - 触发事件前应检查事件是否为null(使用空条件运算符
?.
) - 事件访问修饰符 - 事件通常是公共的(public),但触发事件的方法通常是保护的(protected)
完整示例
using System;
namespace EventExample
{
// 事件参数
public class TemperatureEventArgs : EventArgs
{
public float NewTemperature { get; }
public TemperatureEventArgs(float newTemperature)
{
NewTemperature = newTemperature;
}
}
// 发布者
public class Thermostat
{
private float _currentTemperature;
// 使用.NET内置的EventHandler<T>泛型委托
public event EventHandler<TemperatureEventArgs> TemperatureChanged;
protected virtual void OnTemperatureChanged(TemperatureEventArgs e)
{
// 检查是否有订阅者
TemperatureChanged?.Invoke(this, e);
}
public float CurrentTemperature
{
get { return _currentTemperature; }
set
{
if (_currentTemperature != value)
{
_currentTemperature = value;
OnTemperatureChanged(new TemperatureEventArgs(_currentTemperature));
}
}
}
}
// 订阅者
public class TemperatureMonitor
{
public string MonitorName { get; }
public TemperatureMonitor(string name, Thermostat thermostat)
{
MonitorName = name;
// 订阅事件
thermostat.TemperatureChanged += HandleTemperatureChanged;
}
private void HandleTemperatureChanged(object sender, TemperatureEventArgs e)
{
Console.WriteLine($"{MonitorName}: 温度已变化为 {e.NewTemperature}°C");
}
// 解除订阅方法
public void Detach(Thermostat thermostat)
{
thermostat.TemperatureChanged -= HandleTemperatureChanged;
}
}
// 测试代码
class Program
{
static void Main()
{
Thermostat thermostat = new Thermostat();
TemperatureMonitor monitor1 = new TemperatureMonitor("客厅监视器", thermostat);
TemperatureMonitor monitor2 = new TemperatureMonitor("厨房监视器", thermostat);
// 改变温度将触发事件
thermostat.CurrentTemperature = 25.5f;
// 解除一个订阅
monitor2.Detach(thermostat);
// 此时只有monitor1会收到通知
thermostat.CurrentTemperature = 26.0f;
}
}
}
面试常见问题及解答
1. 事件和委托的区别是什么?
答:
- 委托是一种类型,定义了方法的签名,可以持有对一个或多个方法的引用
- 事件是基于委托的类成员,它提供了一种封装机制,限制了外部代码对委托的访问。外部类只能订阅/取消订阅事件,不能直接调用或替换它
2. 为什么在C#中使用事件而不是直接使用委托?
答: 事件提供了封装性和安全性。如果使用公共委托,外部代码可以清空所有订阅者或直接调用委托,而事件只允许订阅和取消订阅操作,保护了发布者对事件触发的控制权。
3. 如何避免事件导致的内存泄漏?
答:
- 确保在不再需要订阅时显式取消订阅
- 使用弱引用委托
- 考虑使用事件聚合器模式,集中管理事件订阅
- 避免在静态事件中使用实例方法作为处理程序
4. 什么是事件的线程安全问题,如何解决?
答:
- 问题:多线程环境下,一个线程可能在触发事件时,另一个线程正在修改订阅者列表
- 解决方案:
- 使用线程安全的事件触发模式:
TemperatureChanged?.Invoke(this, e)
- 考虑使用
lock
语句保护订阅操作 - 在高并发场景,可考虑使用线程安全集合存储订阅者
- 使用线程安全的事件触发模式:
5. C# 中的事件访问修饰符有什么限制?
答:
- 事件声明通常是public的,允许外部类订阅
- 触发事件的方法(通常命名为On+事件名称)通常是protected virtual的,允许子类重写事件触发行为
- C#不允许在类外部直接触发事件,这提供了对事件触发的保护
6. 解释一下事件的底层实现机制?
答: 在C#中,事件是通过添加两个特殊的访问器方法来实现的:add
和remove
。这两个方法分别在使用+=
和-=
操作符时被调用。默认情况下,事件使用内部的委托字段来存储所有订阅者,但也可以通过自定义事件访问器来改变这一行为。
7. 什么是自定义事件访问器,何时使用?
答: 自定义事件访问器允许开发者完全控制事件的订阅和取消订阅逻辑,形式如下:
private EventHandler<EventArgs> _myEvent;
public event EventHandler<EventArgs> MyEvent
{
add { /* 自定义添加订阅者逻辑 */ }
remove { /* 自定义移除订阅者逻辑 */ }
}
在需要特殊的线程同步、调试、日志记录或使用不同的存储机制时使用。
8. EventHandler和自定义委托类型的区别?
答:
EventHandler
和EventHandler<T>
是.NET框架提供的通用事件委托类型- 优点是标准化、简化代码,符合.NET设计规范
- 自定义委托类型则可以提供更明确的方法签名,增强代码可读性,但增加了额外的类型定义
事件 vs 委托的区别示例代码
在 C# 中,如果使用 公共委托,外部代码可以:
- 直接调用委托(绕过类控制)
- 清空所有订阅者(赋值为
null
)
而使用 事件 时:
- 外部代码只能订阅或取消订阅
- 无法触发事件或清空订阅者,确保了发布者对事件的控制权
🔥 示例代码
using System;
class Publisher
{
// ✅ 使用事件
public event Action OnEvent;
// ⚠️ 使用公共委托
public Action OnDelegate;
public void Trigger()
{
Console.WriteLine("\n[发布者触发]");
OnEvent?.Invoke();
OnDelegate?.Invoke();
}
}
class Program
{
static void Main()
{
var publisher = new Publisher();
// 订阅事件与委托
publisher.OnEvent += () => Console.WriteLine("事件:收到通知");
publisher.OnDelegate += () => Console.WriteLine("委托:收到通知");
Console.WriteLine("\n[第一次触发]");
publisher.Trigger();
// 外部代码可以操作委托,但无法直接操作事件
Console.WriteLine("\n[外部代码操作]");
// ✅ 事件:只能添加或移除订阅者,无法清空或直接调用
// publisher.OnEvent.Invoke(); // ❌ 无法在外部调用
// publisher.OnEvent = null; // ❌ 编译错误,事件不可直接赋值
// ⚠️ 委托:外部代码可清空或调用
publisher.OnDelegate = null; // 清空所有订阅者
// publisher.OnDelegate?.Invoke(); // 可在外部调用
Console.WriteLine("\n[第二次触发]");
publisher.Trigger();
}
}
💡 运行结果
css复制编辑[第一次触发]
事件:收到通知
委托:收到通知
[外部代码操作]
[第二次触发]
事件:收到通知
✅ 第二次触发时,委托订阅者已被外部代码清空,只有事件仍然触发!
🛠️ 区别解析
- 事件保护机制
- 外部代码 无法清空订阅者或直接触发,只能通过
+=
和-=
进行订阅/取消订阅。 - 提供了更好的封装性和安全性。
- 外部代码 无法清空订阅者或直接触发,只能通过
- 委托暴露问题
- 外部代码可以:
- 直接赋值为
null
清空所有订阅者 - 直接调用,绕过类内部控制
- 直接赋值为
- 容易导致错误或安全问题。
- 外部代码可以:
- 最佳实践
- 如果不希望外部类直接触发或清空事件,应使用
event
而非delegate
,以确保发布者拥有事件的触发控制权。
- 如果不希望外部类直接触发或清空事件,应使用
✅ 总结:事件 = 委托 + 封装保护
特性 | 委托 (Delegate) | 事件 (Event) |
---|---|---|
本质 | 类型安全的函数指针。定义方法签名。 | 基于委托的、用于通知的成员。 |
公开访问 | 如果是 public 字段,外部可直接调用、赋值 (=)、+=、-=。 | 外部只能 += (订阅) 和 -= (取消订阅)。 |
调用/触发 | 任何能访问委托实例的代码都可以调用它。 | 只能在声明该事件的类内部触发。 |
赋值 (=) | 外部可以对公共委托字段使用 = 重新赋值,会清除所有已有引用。 | 外部不能对事件使用 = 赋值。 |
封装性 | 若作为 public 字段,封装性较差。 | 提供更好的封装,保护委托实例不被外部随意修改或调用。 |
主要目的 | 作为方法参数、回调、定义可执行代码的“形状”。 | 实现发布-订阅模式,对象间通信。 |
接口中 | 接口中可以声明委托类型的成员。 | 接口中可以声明事件。 |
线程
C#线程详解
线程基础知识
线程是程序执行的最小单位,是轻量级的进程。在C#中,线程由System.Threading命名空间提供支持。
进程与线程的区别
- 进程:是操作系统分配资源的基本单位,包含独立的内存空间
- 线程:是进程中的执行单元,共享进程的内存空间
线程状态
C#中线程有以下几种状态:
- Unstarted:线程已创建但未启动
- Running:线程正在执行
- WaitSleepJoin:线程被阻塞
- Suspended:线程被挂起
- Stopped:线程已终止
- AbortRequested:请求终止线程
- Background:后台线程
线程的创建和使用
创建线程
// 方法1:使用ThreadStart委托
Thread thread1 = new Thread(new ThreadStart(ThreadMethod));
thread1.Start();
// 方法2:使用Lambda表达式
Thread thread2 = new Thread(() => Console.WriteLine("线程2执行中"));
thread2.Start();
// 方法3:使用ParameterizedThreadStart委托传递参数
Thread thread3 = new Thread(new ParameterizedThreadStart(ParameterizedMethod));
thread3.Start("参数");
public void ThreadMethod()
{
Console.WriteLine("线程1执行中");
}
public void ParameterizedMethod(object obj)
{
Console.WriteLine($"接收参数:{obj}");
}
线程属性和方法
- IsAlive:判断线程是否处于活动状态
- IsBackground:设置或获取线程是否为后台线程
- ManagedThreadId:获取线程的唯一标识符
- Priority:设置或获取线程的优先级
- ThreadState:获取线程的状态
Thread thread = new Thread(() => Console.WriteLine("线程执行中"));
thread.IsBackground = true; // 设为后台线程
thread.Priority = ThreadPriority.Highest; // 设置优先级
thread.Start();
Console.WriteLine($"线程ID: {thread.ManagedThreadId}");
Console.WriteLine($"线程状态: {thread.ThreadState}");
线程控制
- Start():启动线程
- Join():等待线程完成
- Sleep():使当前线程暂停指定时间
- Abort():终止线程(不推荐使用)
- Suspend() 和 Resume():挂起和恢复线程(.NET Core中已弃用)
Thread thread = new Thread(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"线程执行: {i}");
Thread.Sleep(1000); // 暂停1秒
}
});
thread.Start();
thread.Join(); // 等待线程完成
Console.WriteLine("主线程继续执行");
线程同步机制
锁定(lock)
lock关键字是最常用的同步机制,它确保一次只有一个线程可以访问代码块。
private static readonly object _lockObject = new object();
private static int _counter = 0;
public void IncrementCounter()
{
lock (_lockObject)
{
_counter++;
Console.WriteLine($"计数器值: {_counter}");
}
}
Monitor类
Monitor类提供了比lock更灵活的锁定机制。
private static readonly object _lockObject = new object();
private static int _counter = 0;
public void IncrementCounter()
{
bool lockTaken = false;
try
{
Monitor.Enter(_lockObject, ref lockTaken);
_counter++;
Console.WriteLine($"计数器值: {_counter}");
}
finally
{
if (lockTaken)
{
Monitor.Exit(_lockObject);
}
}
}
互斥锁(Mutex)
Mutex允许跨进程同步。
private static Mutex _mutex = new Mutex(false, "MyMutexName");
public void SafeOperation()
{
try
{
_mutex.WaitOne();
// 执行需要同步的代码
}
finally
{
_mutex.ReleaseMutex();
}
}
信号量(Semaphore)
Semaphore限制可同时访问资源的线程数。
private static Semaphore _semaphore = new Semaphore(2, 2); // 允许2个并发访问
public void AccessResource()
{
try
{
_semaphore.WaitOne();
Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 正在访问资源");
Thread.Sleep(2000);
}
finally
{
Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 释放资源");
_semaphore.Release();
}
}
读写锁(ReaderWriterLockSlim)
允许多个线程同时读取但仅允许一个线程写入。
private static ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
private static List<int> _data = new List<int>();
public void ReadData()
{
try
{
_rwLock.EnterReadLock();
foreach (var item in _data)
{
Console.WriteLine(item);
}
}
finally
{
_rwLock.ExitReadLock();
}
}
public void WriteData(int value)
{
try
{
_rwLock.EnterWriteLock();
_data.Add(value);
}
finally
{
_rwLock.ExitWriteLock();
}
}
Interlocked类
用于执行线程安全的原子操作。
private static int _counter = 0;
public void IncrementCounter()
{
Interlocked.Increment(ref _counter);
Console.WriteLine($"计数器值: {_counter}");
}
public void DecrementCounter()
{
Interlocked.Decrement(ref _counter);
Console.WriteLine($"计数器值: {_counter}");
}
线程池(ThreadPool)
线程池维护一组可以重用的线程,减少线程创建和销毁的开销。
public void UsingThreadPool()
{
// 获取线程池信息
ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxCompletionPortThreads);
Console.WriteLine($"最大工作线程: {maxWorkerThreads}, 最大IO线程: {maxCompletionPortThreads}");
// 使用线程池执行工作
for (int i = 0; i < 10; i++)
{
int taskNum = i;
ThreadPool.QueueUserWorkItem(state =>
{
Console.WriteLine($"任务 {taskNum} 在线程 {Thread.CurrentThread.ManagedThreadId} 上执行");
Thread.Sleep(1000);
});
}
}
线程池的优势
- 减少系统开销:避免频繁创建和销毁线程
- 提高响应速度:复用已创建的线程
- 管理线程数量:避免创建过多线程导致系统资源紧张
线程池的限制
- 无法手动控制线程优先级
- 所有线程池线程都是后台线程
- 不适合长时间运行的任务
异步编程模式
基于任务的异步模式(TAP)
使用async
和await
关键字,是现代C#推荐的异步编程方式。
public async Task<string> DownloadDataAsync(string url)
{
using (HttpClient client = new HttpClient())
{
Console.WriteLine($"开始下载,线程ID: {Thread.CurrentThread.ManagedThreadId}");
string result = await client.GetStringAsync(url);
Console.WriteLine($"下载完成,线程ID: {Thread.CurrentThread.ManagedThreadId}");
return result;
}
}
// 调用异步方法
public async Task ProcessDataAsync()
{
string data = await DownloadDataAsync("https://example.com");
Console.WriteLine($"数据长度: {data.Length}");
}
异步编程最佳实践
- 命名规范:异步方法名以Async结尾
- 避免阻塞:不要在异步方法中使用同步等待(如
.Result
或.Wait()
) - 传播异步:尽量保持完整的异步调用链
- 合理使用ConfigureAwait:在库代码中使用
ConfigureAwait(false)
避免上下文切换
// 库代码中的推荐写法
public async Task LibraryMethodAsync()
{
await SomeAsyncOperation().ConfigureAwait(false);
// 继续处理...
}
Task和 Task<T>
Task是现代C#中表示异步操作的主要方式。
创建Task
// 方法1:使用Task.Run
Task task1 = Task.Run(() => Console.WriteLine("Task.Run执行"));
// 方法2:使用TaskFactory
Task task2 = Task.Factory.StartNew(() => Console.WriteLine("TaskFactory执行"));
// 方法3:创建返回值的Task
Task<int> task3 = Task.Run(() => {
Console.WriteLine("计算中...");
return 42;
});
// 方法4:手动创建和启动Task
Task task4 = new Task(() => Console.WriteLine("手动创建的Task"));
task4.Start();
等待Task
// 等待单个Task
await task1;
// 等待多个Task
await Task.WhenAll(task1, task2, task4);
// 等待任一Task完成
Task completedTask = await Task.WhenAny(task1, task2, task4);
// 获取Task<T>的结果
int result = await task3; // 异步等待
Console.WriteLine($"结果: {result}");
// 同步等待(不推荐)
task1.Wait();
int syncResult = task3.Result;
Task延续
Task<int> task = Task.Run(() => 42);
// 使用ContinueWith
Task<string> continuation = task.ContinueWith(t =>
{
return $"结果是: {t.Result}";
});
// 使用async/await更清晰
async Task<string> GetResultAsync()
{
int number = await task;
return $"结果是: {number}";
}
并行编程
Parallel类
用于并行执行工作负载。
// 并行执行For循环
Parallel.For(0, 10, i =>
{
Console.WriteLine($"并行处理项 {i},线程ID: {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(100);
});
// 并行执行ForEach
List<string> items = new List<string> { "项目1", "项目2", "项目3", "项目4", "项目5" };
Parallel.ForEach(items, item =>
{
Console.WriteLine($"处理 {item},线程ID: {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(100);
});
// 并行执行多个操作
Parallel.Invoke(
() => Console.WriteLine("操作1执行"),
() => Console.WriteLine("操作2执行"),
() => Console.WriteLine("操作3执行")
);
PLINQ (并行LINQ)
// 使用AsParallel启用并行处理
var numbers = Enumerable.Range(1, 1000000);
var evenNumbers = numbers.AsParallel()
.Where(n => n % 2 == 0)
.ToList();
// 控制并行度
var result = numbers.AsParallel()
.WithDegreeOfParallelism(4)
.Where(n => n % 2 == 0)
.ToList();
注意事项和最佳实践
避免常见问题
- 死锁:两个或多个线程互相等待对方持有的资源
- 竞态条件:多个线程访问共享资源的顺序不确定导致的错误
- 线程饥饿:某些线程长时间无法获取所需资源
线程安全的设计原则
- 最小化共享状态:尽量减少线程间共享的数据
- 不可变对象:使用只读数据结构和不可变对象
- 适当的同步:确保对共享资源的访问是同步的
- 避免过度同步:过多的同步会导致性能下降
线程安全集合
使用System.Collections.Concurrent
命名空间中的线程安全集合:
// 线程安全的字典
ConcurrentDictionary<string, int> concurrentDict = new ConcurrentDictionary<string, int>();
concurrentDict.TryAdd("key1", 100);
concurrentDict.AddOrUpdate("key1", 200, (key, oldValue) => oldValue + 1);
// 线程安全的队列
ConcurrentQueue<int> concurrentQueue = new ConcurrentQueue<int>();
concurrentQueue.Enqueue(1);
concurrentQueue.Enqueue(2);
if (concurrentQueue.TryDequeue(out int result))
{
Console.WriteLine($"出队: {result}");
}
// 线程安全的栈
ConcurrentStack<string> concurrentStack = new ConcurrentStack<string>();
concurrentStack.Push("项目1");
concurrentStack.Push("项目2");
if (concurrentStack.TryPop(out string item))
{
Console.WriteLine($"出栈: {item}");
}
// 线程安全的包
ConcurrentBag<double> concurrentBag = new ConcurrentBag<double>();
concurrentBag.Add(1.1);
concurrentBag.Add(2.2);
if (concurrentBag.TryTake(out double value))
{
Console.WriteLine($"取出: {value}");
}
使用ThreadLocal<T>
当每个线程需要自己的数据副本时使用:
ThreadLocal<int> threadLocalValue = new ThreadLocal<int>(() => Thread.CurrentThread.ManagedThreadId);
void DisplayThreadLocalValue()
{
Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 的值: {threadLocalValue.Value}");
}
// 在多个线程上测试
for (int i = 0; i < 3; i++)
{
new Thread(DisplayThreadLocalValue).Start();
}
常见面试问题及解答
什么是线程安全?如何确保线程安全?
回答:线程安全是指在多线程环境下,代码能够正确地处理共享资源,不会因为多线程的并发访问而产生错误结果。
确保线程安全的方法:
- 使用同步机制(如lock、Monitor、Mutex等)
- 使用线程安全集合(如ConcurrentDictionary)
- 使用原子操作(如Interlocked类提供的方法)
- 采用不可变设计,避免共享可变状态
- 使用线程本地存储(
ThreadLocal<T>
)
解释Thread和Task的区别
回答:
- Thread是较底层的线程抽象,直接表示操作系统线程
- Task是高级抽象,表示一个异步操作,可能使用线程池线程或IO完成端口
- Task支持返回值、异常传播和延续
- Task可以通过
async/await
实现更简洁的异步代码 - Task更加轻量级,一个Task不一定对应一个专用线程
- Task适合基于I/O的操作,Thread适合CPU密集型长期运行的操作
解释线程饥饿和死锁的区别,如何避免?
回答:
- 线程饥饿:某些线程长时间无法获得所需资源,可能是因为其他线程占用资源时间过长
- 死锁:两个或多个线程相互等待对方持有的资源,导致所有相关线程都无法继续执行
避免死锁的方法:
- 总是按照相同的顺序获取锁
- 使用超时机制获取锁
- 使用
Monitor.TryEnter
而非Monitor.Enter
- 避免在持有锁的情况下调用外部代码
- 使用更高级的同步机制如
SemaphoreSlim
等
避免线程饥饿的方法:
- 确保线程不会长时间持有锁
- 使用公平的锁策略
- 设置适当的线程优先级
async/await的工作原理是什么?
回答:
async
标记一个方法可以包含await
表达式- 当执行遇到
await
时,如果等待的任务未完成,方法会返回到调用者 - 方法的状态(局部变量、执行位置等)会被保存在状态机中
- 当被等待的任务完成时,执行会在之前暂停的地方继续
await
表达式会将异步操作的结果提取出来- 整个过程中,编译器会生成状态机代码来管理异步操作和回调
什么是上下文切换?为什么它很重要?
回答:
- 上下文切换是指CPU从一个线程切换到另一个线程的过程
- 切换时需要保存当前线程的状态,并恢复另一个线程的状态
- 上下文切换有一定的性能开销
- 频繁的上下文切换会导致CPU花更多时间在切换上而非执行实际工作
- 在异步编程中,通过
ConfigureAwait(false)
可以避免不必要的上下文切换 - 理解上下文切换有助于优化多线程应用程序性能
如何处理线程中未捕获的异常?
回答:
- 在普通线程中,未捕获的异常会导致进程终止
- 可以通过
AppDomain.CurrentDomain.UnhandledException
事件捕获这些异常 - Task未观察到的异常会在垃圾回收时引发
TaskScheduler.UnobservedTaskException
事件 - 在UI线程中,可以通过
Application.ThreadException
捕获异常(WinForms) - 最佳实践是在线程内部使用try-catch处理所有可能的异常
// 注册未捕获的异常处理
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
Exception ex = (Exception)e.ExceptionObject;
Console.WriteLine($"未处理的异常: {ex.Message}");
// 记录日志等操作
};
// 注册Task未观察到的异常
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
Console.WriteLine($"未观察到的任务异常: {e.Exception.Message}");
e.SetObserved(); // 标记为已观察,防止进程崩溃
};
解释一下线程池的工作原理和使用场景
回答:
- 线程池维护一组可重用的工作线程
- 当请求工作时,线程池会分配空闲线程或创建新线程(有上限)
- 工作完成后,线程不会销毁,而是返回池中等待下一个任务
- 线程池会根据系统负载自动调整线程数量
- 适用于短期、CPU绑定的任务
- 不适合长时间运行的任务或需要特定优先级的任务
- .NET中
Task.Run()
和ThreadPool.QueueUserWorkItem()
都使用线程池
SynchronizationContext的作用是什么?
回答:
- SynchronizationContext提供执行上下文的抽象
- 它允许不同线程模型(UI线程、ASP.NET请求上下文等)提供自己的线程调度行为
- 在UI应用程序中,它确保代码在UI线程上执行
await
默认使用捕获的SynchronizationContext来继续后续代码- 使用
ConfigureAwait(false)
可以忽略捕获的SynchronizationContext - 库代码通常应使用
ConfigureAwait(false)
以避免潜在的死锁
什么是ReaderWriterLockSlim,它与普通锁有何不同?
回答:
- ReaderWriterLockSlim是一种允许多个读取者但只允许一个写入者的锁
- 适用于读多写少的场景
- 与普通锁(lock)的区别:
- 普通锁对所有访问排他锁定
- ReaderWriterLockSlim允许多个读取线程并发访问
- 提供三种锁模式:读取锁、可升级的读取锁和写入锁
- 性能上,频繁读取时ReaderWriterLockSlim更高效
解释ValueTask和Task的区别和使用场景
回答:
- ValueTask是一个值类型,而Task是引用类型
- ValueTask主要用于减少内存分配,提高性能
- 适用场景:
- 异步操作可能同步完成时(避免不必要的Task分配)
- 高性能、频繁调用的异步API
- 注意事项:
- ValueTask不能多次等待
- 适合"使用一次后丢弃"的场景
- 在需要多次等待结果的场景,应使用
.AsTask()
转换
实用示例
生产者-消费者模式
使用BlockingCollection实现:
public class ProducerConsumerExample
{
private BlockingCollection<int> _queue = new BlockingCollection<int>(boundedCapacity: 100);
public void Start()
{
// 创建生产者线程
Thread producerThread = new Thread(Producer);
producerThread.Start();
// 创建消费者线程
for (int i = 0; i < 3; i++)
{
Thread consumerThread = new Thread(Consumer);
consumerThread.Start(i);
}
}
private void Producer()
{
for (int i = 0; i < 500; i++)
{
_queue.Add(i);
Console.WriteLine($"生产: {i}");
Thread.Sleep(10);
}
_queue.CompleteAdding(); // 标记为完成添加
}
private void Consumer(object id)
{
int consumerId = (int)id;
foreach (var item in _queue.GetConsumingEnumerable())
{
Console.WriteLine($"消费者 {consumerId} 消费: {item}");
Thread.Sleep(50);
}
Console.WriteLine($"消费者 {consumerId} 完成");
}
}
异步Web请求处理
public class WebRequestExample
{
public async Task DownloadMultipleSitesAsync(List<string> urls)
{
List<Task<string>> downloadTasks = new List<Task<string>>();
foreach (string url in urls)
{
downloadTasks.Add(DownloadSiteAsync(url));
}
// 等待所有下载完成
await Task.WhenAll(downloadTasks);
// 处理结果
for (int i = 0; i < urls.Count; i++)
{
string content = downloadTasks[i].Result;
Console.WriteLine($"网站 {urls[i]} 内容长度: {content.Length}");
}
}
private async Task<string> DownloadSiteAsync(string url)
{
using (HttpClient client = new HttpClient())
{
client.Timeout = TimeSpan.FromSeconds(10);
try
{
Console.WriteLine($"开始下载 {url}");
string content = await client.GetStringAsync(url).ConfigureAwait(false);
Console.WriteLine($"完成下载 {url}");
return content;
}
catch (Exception ex)
{
Console.WriteLine($"下载 {url} 时出错: {ex.Message}");
return string.Empty;
}
}
}
}
并行数据处理
public class ParallelProcessingExample
{
public void ProcessLargeDataSet(List<int> data)
{
Console.WriteLine("开始并行处理数据...");
// 配置并行选项
ParallelOptions options = new ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount
};
ConcurrentBag<int> results = new ConcurrentBag<int>();
Parallel.ForEach(data, options, (item) =>
{
int processedValue = ExpensiveOperation(item);
results.Add(processedValue);
});
Console.WriteLine($"处理完成,结果数量: {results.Count}");
Console.WriteLine($"结果总和: {results.Sum()}");
}
private int ExpensiveOperation(int value)
{
// 模拟耗时操作
Console.WriteLine($"处理 {value},线程ID: {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(100);
return value * value;
}
}
使用线程池的自定义任务队列
public class CustomTaskQueue
{
private readonly Queue<Action> _taskQueue = new Queue<Action>();
private readonly object _lockObject = new object();
private bool _isProcessing = false;
public void EnqueueTask(Action task)
{
lock (_lockObject)
{
_taskQueue.Enqueue(task);
if (!_isProcessing)
{
_isProcessing = true;
ThreadPool.QueueUserWorkItem(ProcessQueue);
}
}
}
private void ProcessQueue(object state)
{
while (true)
{
Action nextTask = null;
lock (_lockObject)
{
if (_taskQueue.Count == 0)
{
_isProcessing = false;
break;
}
nextTask = _taskQueue.Dequeue();
}
try
{
nextTask();
}
catch (Exception ex)
{
Console.WriteLine($"任务执行出错: {ex.Message}");
}
}
}
}
使用异步/并行编程的文件处理
public class FileProcessingExample
{
public async Task ProcessDirectoryAsync(string directory)
{
// 异步获取所有文件
string[] files = await Task.Run(() => Directory.GetFiles(directory, "*.txt", SearchOption.AllDirectories));
Console.WriteLine($"找到 {files.Length} 个文件");
// 并行处理文件内容
ConcurrentDictionary<string, int> wordCounts = new ConcurrentDictionary<string, int>();
await Task.Run(() => {
Parallel.ForEach(files, file => {
ProcessFile(file, wordCounts);
});
});
// 显示结果
List<KeyValuePair<string, int>> sortedWords = wordCounts
.OrderByDescending(pair => pair.Value)
.Take(10)
.ToList();
Console.WriteLine("出现频率最高的10个单词:");
foreach (var pair in sortedWords)
{
Console.WriteLine($"{pair.Key}: {pair.Value}次");
}
}
private void ProcessFile(string filePath, ConcurrentDictionary<string, int> wordCounts)
{
try
{
string content = File.ReadAllText(filePath);
string[] words = content.Split(new[] { ' ', '\t', '\r', '\n', '.', ',', ';', ':', '!', '?' },
StringSplitOptions.RemoveEmptyEntries);
foreach (string word in words)
{
string lowerWord = word.ToLower();
wordCounts.AddOrUpdate(lowerWord, 1, (key, count) => count + 1);
}
Console.WriteLine($"处理完成: {filePath}");
}
catch (Exception ex)
{
Console.WriteLine($"处理文件 {filePath} 出错: {ex.Message}");
}
}
}
总结
C#中的线程编程是一个广泛且深入的主题,从基本的Thread类到现代的Task和async/await模式,提供了丰富的工具和API用于构建高性能、可靠的多线程应用程序。关键点是理解各种同步机制、避免常见的多线程问题(如死锁和竞态条件),并根据应用场景选择合适的并发模型。现代C#开发更倾向于使用Task和async/await而非直接操作Thread,以获得更好的性能和更简洁的代码。对于特定场景,各种并发集合和同步原语也是不可或缺的工具。
希望这份详细指南对您的线程编程学习和面试准备有所帮助!
UI线程中的Invoke方法
在C#中处理UI应用程序(如WinForms和WPF)的多线程编程时,Invoke方法是一个非常重要的概念。让我详细补充这部分内容。
UI线程模型
单线程亲和性(Single-Threaded Apartment)
UI框架(包括Windows Forms和WPF)都遵循单线程亲和性模型,这意味着:
- UI控件只能由创建它们的线程(通常是主线程)访问和修改
- 从其他线程直接访问UI控件会导致InvalidOperationException异常
- 需要使用特殊机制在后台线程和UI线程之间进行通信
为什么需要Invoke
当后台线程需要更新UI时,必须将操作"转发"到UI线程上执行,这就是Invoke方法的作用。
委托的执行时机是:
- 立即排队,随后执行:当调用this.Invoke方法时,该Action委托会被立即排入UI线程的消息队列中。
- UI线程空闲时执行:UI线程会在处理完当前任务后,按照消息队列中的顺序执行这个Action委托。
- 同步执行:Invoke方法是同步的,意味着调用线程(在这里是SerialPort的事件线程)会等待,直到UI线程完成对委托的执行才会继续。
Windows Forms中的Invoke
Windows Forms中的Control类提供了以下方法用于线程间通信:
Invoke
同步将操作委托给UI线程执行,阻塞调用线程直到操作完成。
private void UpdateUI(string message)
{
// 检查是否需要调用Invoke
if (this.InvokeRequired)
{
// 同步调用
this.Invoke(new Action<string>(UpdateUI), message);
return;
}
// 直接在UI线程上执行的代码
lblStatus.Text = message;
progressBar1.Value += 10;
}
BeginInvoke
异步将操作委托给UI线程执行,不阻塞调用线程。
private void UpdateUIAsync(string message)
{
if (this.InvokeRequired)
{
// 异步调用,立即返回
this.BeginInvoke(new Action<string>(UpdateUIAsync), message);
return;
}
lblStatus.Text = message;
progressBar1.Value += 10;
}
完整示例
public partial class FormExample : Form
{
public FormExample()
{
InitializeComponent();
}
private void btnStartProcess_Click(object sender, EventArgs e)
{
// 禁用按钮,防止多次点击
btnStartProcess.Enabled = false;
// 启动后台线程执行耗时操作
Thread workerThread = new Thread(DoWork);
workerThread.Start();
}
private void DoWork()
{
// 在后台线程更新UI - 开始处理
UpdateUI("开始处理...");
// 模拟耗时操作
for (int i = 1; i <= 10; i++)
{
// 执行一些工作
Thread.Sleep(500);
// 更新进度
UpdateUI($"处理中: {i * 10}% 完成");
}
// 完成
UpdateUI("处理完成!");
// 重新启用按钮
EnableButton();
}
private void UpdateUI(string statusText)
{
if (InvokeRequired)
{
// 我们在非UI线程,需要Invoke
this.Invoke(new Action<string>(UpdateUI), statusText);
return;
}
// 在UI线程运行的代码
lblStatus.Text = statusText;
progressBar1.Value = int.TryParse(statusText.Replace("%", "")
.Replace("处理中:", "")
.Trim(), out int value) ? value : 0;
if (statusText == "处理完成!")
progressBar1.Value = 100;
}
private void EnableButton()
{
if (InvokeRequired)
{
this.Invoke(new Action(EnableButton));
return;
}
btnStartProcess.Enabled = true;
}
}
WPF中的Dispatcher
WPF使用Dispatcher模式而非Invoke。原理类似,但API略有不同。
Dispatcher.Invoke
private void UpdateWpfUI(string message)
{
// 检查是否在UI线程
if (!Dispatcher.CheckAccess())
{
// 我们在非UI线程,需要通过Dispatcher调用
Dispatcher.Invoke(() => UpdateWpfUI(message));
return;
}
// UI线程代码
txtStatus.Text = message;
progressBar.Value += 10;
}
Dispatcher.BeginInvoke
private void UpdateWpfUIAsync(string message)
{
if (!Dispatcher.CheckAccess())
{
// 异步调用
Dispatcher.BeginInvoke(new Action(() => UpdateWpfUIAsync(message)));
return;
}
txtStatus.Text = message;
progressBar.Value += 10;
}
WPF完整示例
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void BtnStartProcess_Click(object sender, RoutedEventArgs e)
{
// 禁用按钮
btnStartProcess.IsEnabled = false;
// 启动后台线程
Task.Run(() => DoWork());
}
private void DoWork()
{
// 更新UI - 开始
UpdateUI("开始处理...");
// 模拟耗时任务
for (int i = 1; i <= 10; i++)
{
Thread.Sleep(500);
UpdateUI($"处理中: {i * 10}%");
}
// 完成
UpdateUI("处理完成!");
// 启用按钮
EnableButton();
}
private void UpdateUI(string status)
{
if (!Dispatcher.CheckAccess())
{
// 非UI线程
Dispatcher.Invoke(() => UpdateUI(status));
return;
}
// UI线程代码
txtStatus.Text = status;
// 更新进度条
if (status.Contains("%"))
{
string percentStr = status.Replace("处理中:", "").Replace("%", "").Trim();
if (int.TryParse(percentStr, out int percent))
progressBar.Value = percent;
}
else if (status == "处理完成!")
{
progressBar.Value = 100;
}
}
private void EnableButton()
{
if (!Dispatcher.CheckAccess())
{
Dispatcher.Invoke(EnableButton);
return;
}
btnStartProcess.IsEnabled = true;
}
}
Invoke的优缺点
优点
- 线程安全更新UI元素
- 提供了同步和异步两种方式
- 简单的递归模式易于实现
缺点
- 同步Invoke可能导致死锁
- 过度使用会影响应用程序性能
- 代码结构可能变得复杂
现代替代方法
使用async/await更新UI
在现代C#中,可以使用Task和async/await模式简化UI更新:
// WinForms示例
private async void btnStartProcess_Click(object sender, EventArgs e)
{
btnStartProcess.Enabled = false;
lblStatus.Text = "开始处理...";
await Task.Run(() =>
{
// 执行耗时操作
for (int i = 1; i <= 10; i++)
{
Thread.Sleep(500);
// 使用UI线程安全的方式更新UI
this.Invoke((MethodInvoker)delegate
{
lblStatus.Text = $"处理中: {i * 10}%";
progressBar1.Value = i * 10;
});
}
});
// 回到UI线程
lblStatus.Text = "处理完成!";
progressBar1.Value = 100;
btnStartProcess.Enabled = true;
}
WPF中的Task和Dispatcher结合
private async void BtnStartProcess_Click(object sender, RoutedEventArgs e)
{
btnStartProcess.IsEnabled = false;
txtStatus.Text = "开始处理...";
await Task.Run(() =>
{
// 执行耗时操作
for (int i = 1; i <= 10; i++)
{
Thread.Sleep(500);
// 使用Dispatcher更新UI
Dispatcher.Invoke(() =>
{
txtStatus.Text = $"处理中: {i * 10}%";
progressBar.Value = i * 10;
});
}
});
// 回到UI线程
txtStatus.Text = "处理完成!";
progressBar.Value = 100;
btnStartProcess.IsEnabled = true;
}
Progress<T>
模式
.NET提供了IProgress<T>
和Progress<T>
用于报告进度,这是一种更现代的做法:
private async void btnStartProcess_Click(object sender, EventArgs e)
{
btnStartProcess.Enabled = false;
lblStatus.Text = "开始处理...";
progressBar1.Value = 0;
// 创建进度报告对象
var progress = new Progress<int>(percent =>
{
// 这个代码会在UI线程上执行
lblStatus.Text = $"处理中: {percent}%";
progressBar1.Value = percent;
});
// 执行耗时任务并报告进度
await ProcessDataAsync(progress);
lblStatus.Text = "处理完成!";
btnStartProcess.Enabled = true;
}
private async Task ProcessDataAsync(IProgress<int> progress)
{
return await Task.Run(() =>
{
for (int i = 1; i <= 10; i++)
{
// 执行任务
Thread.Sleep(500);
// 报告进度
progress?.Report(i * 10);
}
});
}
最佳实践
总是检查InvokeRequired
// WinForms
if (this.InvokeRequired)
{
this.Invoke(new Action(() => UpdateLabel("更新文本")));
return;
}
// WPF
if (!Dispatcher.CheckAccess())
{
Dispatcher.Invoke(() => UpdateLabel("更新文本"));
return;
}
优先使用BeginInvoke而非Invoke
使用BeginInvoke可避免潜在的死锁情况,特别是在复杂的应用程序中。
注意Form/Window关闭时的Invoke
在窗体已关闭或正在关闭时调用Invoke会导致异常。应该检查IsDisposed或IsHandleCreated:
// WinForms
if (!this.IsDisposed && this.IsHandleCreated && this.InvokeRequired)
{
try
{
this.Invoke(new Action(() => UpdateUI()));
}
catch (ObjectDisposedException)
{
// 处理窗体已关闭的情况
}
}
// WPF
if (Dispatcher.CheckAccess() == false && !this.IsLoaded)
{
try
{
Dispatcher.Invoke(() => UpdateUI());
}
catch (TaskCanceledException)
{
// 处理窗体已关闭的情况
}
}
使用扩展方法简化Invoke
// WinForms扩展方法
public static class ControlExtensions
{
public static void InvokeIfRequired(this Control control, Action action)
{
if (control.InvokeRequired)
{
control.Invoke(action);
}
else
{
action();
}
}
}
// 使用
textBox1.InvokeIfRequired(() => textBox1.Text = "更新的文本");
常见面试问题
为什么在WinForms/WPF中需要使用Invoke?
回答:UI框架遵循单线程亲和性模型,所有UI控件必须由创建它们的线程(通常是主线程)访问。当后台线程需要更新UI时,必须使用Invoke/BeginInvoke将代码"转发"到UI线程上执行,否则会抛出InvalidOperationException异常。
Invoke和BeginInvoke的区别是什么?
回答:
- Invoke 是同步调用,会阻塞当前线程直到UI线程执行完委托的代码
- BeginInvoke 是异步调用,不会阻塞当前线程,UI线程会在合适的时机执行委托的代码
如何判断当前是否需要使用Invoke?
回答:
- 在WinForms中,使用
Control.InvokeRequired
属性检查 - 在WPF中,使用
Dispatcher.CheckAccess()
方法检查
使用Invoke可能导致哪些问题,如何避免?
回答:
- 死锁:如果UI线程正在等待当前线程的操作,而当前线程又通过Invoke等待UI线程,就会发生死锁
- 性能问题:频繁的线程切换会影响性能
- 窗体关闭异常:在窗体关闭后调用Invoke会抛出异常
避免方法:
- 优先使用
BeginInvoke
而非Invoke - 使用现代的
async/await
模式和Progress<T>
- 在调用Invoke前检查控件的
IsDisposed
和*IsHandleCreated
属性
如何简化WinForms/WPF中的多线程UI更新代码?
回答:
- 使用
扩展方法
封装Invoke逻辑 - 采用
async/await
模式简化代码 - 使用
BackgroundWorker
组件(较旧的方式) - 使用
Progress<T>
报告进度 - 使用
ReactiveUI
等响应式框架
示例:线程安全的BackgroundWorker替代实现
public class SafeBackgroundOperation
{
private readonly Control _uiControl;
public SafeBackgroundOperation(Control uiControl)
{
_uiControl = uiControl;
}
public void RunAsync(Action<IProgress<int>> workAction, Action<Exception> errorHandler = null, Action completedAction = null)
{
// 创建进度报告对象
var progress = new Progress<int>(percent =>
{
UpdateUI(() => ProgressChanged?.Invoke(this, percent));
});
// 在线程池上执行操作
Task.Run(() =>
{
try
{
workAction(progress);
UpdateUI(() => completedAction?.Invoke());
}
catch (Exception ex)
{
UpdateUI(() => errorHandler?.Invoke(ex));
}
});
}
private void UpdateUI(Action action)
{
if (_uiControl.InvokeRequired)
{
try
{
_uiControl.BeginInvoke(action);
}
catch (ObjectDisposedException)
{
// 控件已销毁,忽略
}
}
else
{
action();
}
}
public event EventHandler<int> ProgressChanged;
}
// 使用方式
private void btnProcess_Click(object sender, EventArgs e)
{
var operation = new SafeBackgroundOperation(this);
operation.ProgressChanged += (s, progressPercent) =>
{
progressBar1.Value = progressPercent;
lblStatus.Text = $"处理中: {progressPercent}%";
};
btnProcess.Enabled = false;
operation.RunAsync(
// 工作函数
progress =>
{
for (int i = 0; i <= 100; i += 10)
{
Thread.Sleep(500);
progress.Report(i);
}
},
// 错误处理
ex => MessageBox.Show($"发生错误: {ex.Message}"),
// 完成处理
() =>
{
progressBar1.Value = 100;
lblStatus.Text = "处理完成!";
btnProcess.Enabled = true;
}
);
}
总结
在UI应用程序中,Invoke方法
是连接后台线程和UI线程的关键桥梁。理解并正确使用Invoke是开发响应式UI应用的基础。随着现代C#的发展,我们有更多优雅的方式来处理线程间通信,如async/await
和Progress<T>
,但底层原理仍然是相同的。无论使用哪种方式,都需要遵循UI线程的单线程亲和性原则,确保UI元素的更新在正确的线程上进行。
网络编程
串口
手写版本
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
// 串口通信,手写代码
namespace WindowsFormsApp._04_11
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private SerialPort serialPort;
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
serialPort.Close();
}
private void Form1_Load(object sender, EventArgs e)
{
serialPort = new SerialPort();
serialPort.PortName = "COM2"; // 设置串口名称
serialPort.BaudRate = 9600; // 设置波特率
serialPort.DataBits = 8; // 设置数据位
serialPort.Parity = Parity.None; // 设置校验位
serialPort.StopBits = StopBits.One; // 设置停止位
serialPort.DataReceived += SerialPort_DataReceived;
serialPort.Open();
}
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
int size = serialPort.BytesToRead;
byte[] buffer = new byte[size];
serialPort.Read(buffer, 0, size);
string receivedData = Encoding.Default.GetString(buffer);
this.BeginInvoke(new Action(() =>
{
textBox1.AppendText(receivedData + "\r\n");
}));
}
private void btnSend_Click(object sender, EventArgs e)
{
if (serialPort.IsOpen)
{
serialPort.WriteLine(textBox2.Text);
}
}
}
}
控件版本
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp._04_11
{
public partial class Form2: Form
{
public Form2()
{
InitializeComponent();
}
private void btnSend_Click(object sender, EventArgs e)
{
if (serialPort1.IsOpen)
{
serialPort1.WriteLine(textBox2.Text);
}
}
private void Form2_Load(object sender, EventArgs e)
{
serialPort1.Open();
}
private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
int size = serialPort1.BytesToRead;
byte[] buffer = new byte[size];
serialPort1.Read(buffer, 0, size);
string receivedData = Encoding.Default.GetString(buffer);
this.BeginInvoke(new Action(() =>
{
textBox1.AppendText(receivedData + "\r\n");
}));
}
}
}
套接字
服务端
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace WindowsFormsApp._04_11
{
public partial class Form4 : Form
{
private Socket serverSocket;
private List<Socket> clientSockets = new List<Socket>();
private readonly object syncLock = new object();
private const int BufferSize = 4096;
public Form4()
{
InitializeComponent();
this.FormClosing += Form4_FormClosing;
}
private void AddMsg(string msg)
{
if (textBox3.InvokeRequired)
{
textBox3.BeginInvoke(new Action(() => textBox3.AppendText($"{msg}\r\n")));
}
else
{
textBox3.AppendText($"{msg}\r\n");
}
}
private void StartListening()
{
try
{
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(textBox1.Text), int.Parse(textBox2.Text));
serverSocket.Bind(endPoint);
serverSocket.Listen(10);
AddMsg($"{DateTime.Now:HH:mm:ss} 服务器已启动,开始监听...");
serverSocket.BeginAccept(AcceptCallback, null);
}
catch (Exception ex)
{
AddMsg($"启动错误: {ex.Message}");
button1.BeginInvoke(new Action(() => button1.Enabled = true));
}
}
private void AcceptCallback(IAsyncResult ar)
{
try
{
Socket client = serverSocket.EndAccept(ar);
lock (syncLock)
{
clientSockets.Add(client);
}
AddMsg($"{DateTime.Now:HH:mm:ss} {client.RemoteEndPoint} 已连接");
BeginReceive(client);
// 继续接受新的连接
serverSocket.BeginAccept(AcceptCallback, null);
}
catch (ObjectDisposedException) { /* 服务器已关闭 */ }
catch (Exception ex)
{
AddMsg($"接受连接错误: {ex.Message}");
}
}
private void BeginReceive(Socket client)
{
try
{
StateObject state = new StateObject { Socket = client };
client.BeginReceive(state.Buffer, 0, BufferSize, 0, ReceiveCallback, state);
}
catch (Exception ex)
{
AddMsg($"接收初始化错误: {ex.Message}");
RemoveClient(client);
}
}
private void ReceiveCallback(IAsyncResult ar)
{
StateObject state = (StateObject)ar.AsyncState;
Socket client = state.Socket;
try
{
int bytesRead = client.EndReceive(ar);
if (bytesRead > 0)
{
string received = Encoding.UTF8.GetString(state.Buffer, 0, bytesRead);
AddMsg($"{DateTime.Now:HH:mm:ss} 来自 {client.RemoteEndPoint}: {received}");
// 继续接收数据
client.BeginReceive(state.Buffer, 0, BufferSize, 0, ReceiveCallback, state);
}
else
{
RemoveClient(client);
}
}
catch (Exception ex)
{
AddMsg($"接收错误 ({client.RemoteEndPoint}): {ex.Message}");
RemoveClient(client);
}
}
private void RemoveClient(Socket client)
{
lock (syncLock)
{
if (clientSockets.Contains(client))
{
clientSockets.Remove(client);
AddMsg($"{DateTime.Now:HH:mm:ss} {client.RemoteEndPoint} 已断开");
client.Close();
}
}
}
private void Form4_FormClosing(object sender, FormClosingEventArgs e)
{
CleanupResources();
}
private void CleanupResources()
{
lock (syncLock)
{
foreach (var client in clientSockets)
{
client.Shutdown(SocketShutdown.Both);
client.Close();
}
clientSockets.Clear();
}
serverSocket?.Close();
}
private void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
new Thread(StartListening) { IsBackground = true }.Start();
}
private void button2_Click(object sender, EventArgs e)
{
CleanupResources();
button1.Enabled = true;
AddMsg($"{DateTime.Now:HH:mm:ss} 服务器已停止");
}
private void button3_Click(object sender, EventArgs e)
{
if (clientSockets.Count == 0)
{
MessageBox.Show("没有连接的客户端");
return;
}
string message = textBox4.Text;
byte[] buffer = Encoding.UTF8.GetBytes(message);
lock (syncLock)
{
foreach (var client in clientSockets)
{
try
{
client.Send(buffer);
AddMsg($"{DateTime.Now:HH:mm:ss} 发送至 {client.RemoteEndPoint}: {message}");
}
catch (Exception ex)
{
AddMsg($"发送错误 ({client.RemoteEndPoint}): {ex.Message}");
}
}
}
}
private class StateObject
{
public Socket Socket { get; set; }
public byte[] Buffer { get; } = new byte[BufferSize];
}
}
}
客户端
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Windows.Forms;
namespace WindowsFormsApp._04_11
{
public partial class Form5 : Form
{
private Socket _clientSocket;
private byte[] _receiveBuffer = new byte[1024]; // 复用接收缓冲区
public Form5()
{
InitializeComponent();
// 确保窗体关闭时释放资源
this.FormClosing += (s, e) => CloseConnection();
}
private void AddMsg(string msg)
{
if (this.InvokeRequired)
{
this.BeginInvoke(new Action(() => AddMsg(msg)));
return;
}
textBox3.AppendText($"{msg}\r\n");
}
private void button1_Click(object sender, EventArgs e)
{
try
{
button1.Enabled = false;
_clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
var ipAddress = IPAddress.Parse(textBox1.Text);
var port = int.Parse(textBox2.Text);
var endPoint = new IPEndPoint(ipAddress, port);
_clientSocket.BeginConnect(endPoint, ar =>
{
try
{
_clientSocket.EndConnect(ar);
AddMsg($"{DateTime.Now:HH:mm:ss} 连接成功");
BeginReceiveData(); // 开始异步接收
}
catch (Exception ex)
{
AddMsg($"{DateTime.Now:HH:mm:ss} 连接失败: {ex.Message}");
ResetConnection();
}
}, null);
}
catch (Exception ex)
{
AddMsg($"{DateTime.Now:HH:mm:ss} 连接异常: {ex.Message}");
ResetConnection();
}
}
private void BeginReceiveData()
{
try
{
_clientSocket.BeginReceive(_receiveBuffer, 0, _receiveBuffer.Length, SocketFlags.None,
ReceiveCallback, null);
}
catch (Exception ex)
{
AddMsg($"{DateTime.Now:HH:mm:ss} 接收启动失败: {ex.Message}");
CloseConnection();
}
}
private void ReceiveCallback(IAsyncResult ar)
{
try
{
int bytesReceived = _clientSocket.EndReceive(ar);
if (bytesReceived > 0)
{
string msg = Encoding.UTF8.GetString(_receiveBuffer, 0, bytesReceived);
AddMsg($"{DateTime.Now:HH:mm:ss} 接收: {msg}");
BeginReceiveData(); // 继续接收下一条消息
}
else
{
AddMsg($"{DateTime.Now:HH:mm:ss} 连接已正常关闭");
CloseConnection();
}
}
catch (ObjectDisposedException)
{
// Socket已关闭,无需处理
}
catch (Exception ex)
{
AddMsg($"{DateTime.Now:HH:mm:ss} 接收异常: {ex.Message}");
CloseConnection();
}
}
private void button3_Click(object sender, EventArgs e)
{
try
{
if (_clientSocket?.Connected != true)
{
MessageBox.Show("未建立连接");
return;
}
byte[] sendData = Encoding.UTF8.GetBytes(textBox4.Text);
_clientSocket.BeginSend(sendData, 0, sendData.Length, SocketFlags.None, ar =>
{
try
{
int bytesSent = _clientSocket.EndSend(ar);
AddMsg($"{DateTime.Now:HH:mm:ss} 发送成功 ({bytesSent} bytes)");
}
catch (Exception ex)
{
AddMsg($"{DateTime.Now:HH:mm:ss} 发送失败: {ex.Message}");
}
}, null);
textBox4.Clear();
}
catch (Exception ex)
{
AddMsg($"{DateTime.Now:HH:mm:ss} 发送异常: {ex.Message}");
}
}
private void button2_Click(object sender, EventArgs e)
{
CloseConnection();
}
private void CloseConnection()
{
try
{
if (_clientSocket != null)
{
if (_clientSocket.Connected)
{
_clientSocket.Shutdown(SocketShutdown.Both);
_clientSocket.Close();
}
_clientSocket.Dispose();
}
}
catch (Exception ex)
{
AddMsg($"{DateTime.Now:HH:mm:ss} 关闭异常: {ex.Message}");
}
finally
{
ResetConnection();
}
}
private void ResetConnection()
{
if (button1.InvokeRequired)
{
button1.BeginInvoke(new Action(ResetConnection));
return;
}
button1.Enabled = true;
AddMsg($"{DateTime.Now:HH:mm:ss} 连接已重置");
}
}
}
更方便简洁的写法 Task
.NET Framework 4.8.1 不支持,仅支持 .NET Core/.NET 5+
TCP 客户端
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
class TcpAsyncClient
{
public static async Task Main()
{
string serverIp = "127.0.0.1"; // 服务器IP
int serverPort = 8000; // 服务器端口
string message = "Hello, Server!";
// 创建Socket对象(TCP)
using var clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
var endPoint = new IPEndPoint(IPAddress.Parse(serverIp), serverPort);
Console.WriteLine("正在连接服务器...");
await clientSocket.ConnectAsync(endPoint); // 异步连接
Console.WriteLine("连接成功!");
// 发送消息
byte[] sendBuffer = Encoding.UTF8.GetBytes(message);
await clientSocket.SendAsync(sendBuffer, SocketFlags.None);
Console.WriteLine("发送消息: " + message);
// 接收服务器响应
var recvBuffer = new byte[1024];
int recvCount = await clientSocket.ReceiveAsync(recvBuffer, SocketFlags.None);
string serverReply = Encoding.UTF8.GetString(recvBuffer, 0, recvCount);
Console.WriteLine("收到服务器回应: " + serverReply);
clientSocket.Shutdown(SocketShutdown.Both); // 断开连接
}
catch (Exception ex)
{
Console.WriteLine("异常: " + ex.Message);
}
Console.WriteLine("客户端已关闭.");
}
}
TCP 服务器
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class TcpServer
{
public static void Main()
{
int port = 8000;
var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
listener.Bind(new IPEndPoint(IPAddress.Any, port));
listener.Listen(10);
Console.WriteLine("服务器正在等待连接...");
while (true)
{
var handler = listener.Accept();
Console.WriteLine("客户端已连接。");
// 接收数据
var buffer = new byte[1024];
int recLen = handler.Receive(buffer);
string clientMsg = Encoding.UTF8.GetString(buffer, 0, recLen);
Console.WriteLine("收到客户端消息:" + clientMsg);
// 回复消息
string reply = "服务器已收到: " + clientMsg;
handler.Send(Encoding.UTF8.GetBytes(reply));
handler.Shutdown(SocketShutdown.Both);
handler.Close();
Console.WriteLine("一次会话结束。\n");
}
}
}