一、类
1.1、面向对象(Object-Oriented Programming,简称OOP)
1.1.1、面向对象的理解
面向对象编程(OOP)是一种编程范式,它使用“对象”来设计软件和应用程序,当然,此“对象”非彼“对象”。大家都知道,编程的目的归根结底是为现实世界服务,而面向对象就是将现实世界中的事物抽象成一个个对象,通过对象模拟现实世界中事物的种种行为和客观规律来组成我们的软件。C#语言是一种面向对象的语言,C语言是一种面向过程的语言。除此之外,面向对象的语言还有C++、Java等。(*觉得理解不了的朋友可以去看看刘铁猛老师的视频,讲得很通俗易懂)
1.1.2、面向对象编程的优点
- 提高代码的可重用性:通过将对象抽象为类和接口,可以在不同的项目中重复使用这些类和接口,减少代码的重复编写。
- 提高代码的可维护性:面向对象编程使得代码结构更加清晰,易于理解和维护。通过封装和抽象,可以隐藏对象的内部实现细节,只暴露必要的接口,使得代码更加模块化。
- 提高开发效率:通过模拟现实世界中的行为和规律,可以快速地设计和构建软件应用程序,缩短开发周期。同时,通过使用现有的类库和框架,可以快速地构建功能强大的应用程序。
- 提高软件质量:面向对象编程提供了一种更加符合人类思维模式的编程范式,使得软件更加易于测试、调试和使用。同时,通过使用设计模式等最佳实践,可以提高软件的可扩展性和可维护性。
1.2、类的概述
类是一个能够存储数据并执行代码的数据结构,例如,人类可以看成是一个类,人有身高体重名字等(存储代码),人能说话、写字、走路(执行代码)。而刘德华是人类中的一个人,则可以看作是类的一个实例。所以说,类的方方面面在现实生活中都是有迹可循的。
*一个运行中的C#程序可以看成是一组相互作用的类型对象,他们大部分都是类的实例
1.3、类的声明
// 定义一个名为“Person”的类
public class Person
{
// 定义类的属性
public string Name { get; set; }
public int Age { get; set; }
// 定义类的构造函数
public Person(string name, int age)
{
Name = name;
Age = age;
}
// 定义类的方法
public void Greet()
{
Console.WriteLine("Hello, my name is " + Name);
}
}
这个示例中,我们定义了一个名为“Person”的类,它具有两个属性(Name和Age),一个构造函数和一个方法(Greet)。构造函数用于初始化对象的属性值,方法用于实现特定的功能。
要使用这个类,可以创建它的实例并调用其方法:
// 创建Person类的实例
Person person = new Person("Alice", 30);
// 调用Greet方法输出问候信息
person.Greet();//输出结果:Hello, my name is Alice
1.4、类的成员
类的成员包括数据成员和函数成员。数据成员可以是字段、常量或事件。函数成员包括方法、属性、构造函数、析构函数、运算符和索引器。
1.4.1、字段(Fields)
字段是类的数据成员,用于存储类实例的状态。根据作用域,字段可以分为两种类型:实例字段和静态字段。
实例字段:
每个类的对象实例都有其自己的实例字段副本。当我们创建类的实例时,这些实例字段会随着对象的创建而初始化。实例字段只能在其所属的类内部被访问,并且通过对象实例来访问。
public class Person
{
public string Name; // 公共实例字段
private int Age; // 私有实例字段
}
静态字段:
静态字段属于类本身,而不是类的任何特定实例。这意味着所有实例共享同一个静态字段。它们通常用于存储与类相关而不是与对象实例相关的数据。静态字段可以在类的外部直接通过类名来访问。
public class Person
{
public static int TotalPersons; // 公共静态字段
}
自动实现的属性:
从C# 3.0开始,可以使用自动实现的属性来简化对字段的封装和访问。这种方法不需要显式地声明私有字段,编译器会自动为我们处理。当一个属性被自动实现时,编译器会在后台为该属性创建一个私有字段。属性的访问器(get和set)会自动生成,用于访问或修改该字段的值。
public class Person
{
public int AgeInYears { get; set; } // 自动实现的属性
}
注意事项:
- 字段应该谨慎使用,尤其是在面向对象编程中。封装是面向对象编程的重要原则之一,而字段的封装通常是通过属性来实现的。直接使用字段可能会破坏对象的封装性,因此应优先考虑使用属性。
- 静态字段常用于计数、记录类实例的数量等场景,因为它们不属于任何特定实例,而是属于整个类。
- 实例字段通常用于存储与对象状态相关的数据,如人的姓名、年龄等。
1.4.2、常量
用于存储不会改变的值。常量是类的一种特殊字段,其值在编译时被设定并且不能修改。这意味着常量一旦在程序中被定义,它的值就永远保持不变。常量主要用于表示在程序的生命周期内不会更改的值,如数学常数π、e,或者是一些固定配置的参数等。
定义常量:
常量的声明与字段的声明非常相似,但需要使用const
关键字。同时,必须在声明时给常量赋值。
public class MathConstants
{
public const double Pi = 3.141592653589793; // 定义π的常量值
public const double E = 2.718281828459045; // 定义e的常量值
}
使用常量:
常量是静态的,这意味着它们属于类本身而不是类的任何特定实例。因此,可以直接通过类名来访问常量,而无需创建类的实例。
double radius = 5.0;
double circumference = 2 * MathConstants.Pi * radius; // 使用π常量计算圆的周长
常量的特性:
- 常量必须在声明时初始化。你不能在声明之后为它赋值,因为它是只读的。
- 常量的名称通常采用大写字母来表示,多个单词之间用下划线分隔(即采用驼峰命名法)。
- 常量只能是内置值类型(如int、double、char等),不能是引用类型(如string)。但C#中的string有一个特殊性,它虽然是引用类型,但因为字符串的不变性,所以允许作为常量使用。
- 由于常量在编译时就确定了其值,所以它们的值不能依赖于运行时才能确定的信息。
- 常量可以参与表达式的计算,但它们的值不能被修改。
使用场景:
- 常量通常用于定义那些程序逻辑中不会改变的值,比如物理常数、数学常数、或者程序中需要固定的值(比如缓冲区的大小)。
- 常量还可以用于定义枚举类型的底层值。
- 在跨多个类或程序集中共享值时,可以使用常量来确保所有引用点都使用相同的值。
注意事项:
- 过度使用常量可能会使代码难以维护。如果预计某个值可能会在未来发生变化,考虑使用变量或配置文件来管理这些值。
- 常量应该谨慎使用,确保它们真正代表“不变”的值。如果某个值有可能改变,即使这种改变很少发生,也不应该使用常量来表示。
1.4.3、属性(Properties)
属性是用于封装字段的一种机制,它提供了一种安全的方式来读取、写入或计算字段的值。通过属性,我们可以控制对字段的访问,确保数据完整性,并可以在访问字段时添加额外的逻辑。可以说,属性是对字段的扩展。
定义属性:
属性由get
和set
访问器组成。get
访问器用于读取字段的值,而set
访问器用于写入字段的值。可以使用自动实现的属性来简化属性的定义,编译器会自动为我们生成私有字段和访问器。
public class Person
{
private string name;
public string Name
{
get { return name; } // get访问器
set { name = value; } // set访问器
}
}
自动实现的属性:
从C# 3.0开始,可以使用自动实现的属性来简化属性的定义。编译器会自动为我们生成私有字段,并且属性的访问器会自动实现。
public class Person
{
public string Name { get; set; } // 自动实现的属性 //vs生成属性快捷键:输入prop后按下tab键
}
属性的特性:
- 属性实际上是一种特殊的成员方法,但它们具有特殊的语法,使我们能够更容易地访问或修改类的内部数据。
- 属性不具有显式的访问修饰符,因为它们会根据是否只读或可写而自动决定其访问修饰符。默认情况下,如果只有
get
访问器,则属性是只读的;如果只有set
访问器,则属性是只写的;如果两者都有,则属性是可读写的。 - 属性可以具有与它们关联的私有字段,但并非必需的。如果没有显式定义私有字段,编译器会自动为我们生成一个。这是自动实现的属性的工作方式。
- 属性可以包含复杂的逻辑,例如验证、触发事件等。这使得属性不仅仅是简单的数据存储器。
- 属性可以用于接口中,作为接口成员的契约的一部分。接口中的属性必须是公共的,因为接口不包含实现细节。
- 属性可以用于抽象类中,作为抽象成员的实现细节的一部分。抽象类中的属性可以是公共的或受保护的,因为抽象类可以有具体的实现细节。
- 属性可以在其他程序集中使用相同的名称和签名来共享数据。这使得跨程序集的数据共享变得更容易。
- 属性可以用于在运行时动态地改变对象的行为或状态。例如,可以根据属性的值来改变对象的渲染效果或行为模式等。
属性的作用
对外:暴露数据,数据可以是存储在字段里的,也可以是动态计算出来的。
对外:保护字段不被非法值 污染。
1.4.4、方法(Methods)
定义类可以执行的操作。用于表示类的行为与功能。是类中用于执行特定操作或实现特定功能的成员函数。
1.4.4.1 方法的参数传递
**值传递:**默认情况下,参数是通过值传递的。这意味着当我们将一个值类型的变量作为参数传递给方法时,实际上是传递了该变量的一个副本,而不是原始变量本身。对副本的任何修改都不会影响原始变量。
public void MyMethods(int value)
{
value = value * 2; // 修改副本的值,原始变量不受影响
}
**引用传递(ref):**对于引用类型的变量,例如类实例、数组和用户定义的类型等,参数是通过引用传递的。这意味着当我们将引用类型的变量作为参数传递给方法时,实际上是传递了该变量的地址或引用,而不是变量的副本。对引用的任何修改都会影响原始变量。
public void ModifyObject(MyClass obj)
{
obj.Property = "New Value"; // 修改原始对象的属性
}
**输出参数(out):**输出参数是一种特殊类型的参数,用于将方法的返回值通过引用传递。通过使用out
关键字声明参数,我们可以将输出参数初始化为方法内部的某个值,并将其返回给调用者。
public void GetSquareRoot(double number, out double result)
{
result = Math.Sqrt(number); // 将结果赋值给输出参数
}
可选参数(Optional Parameters)
可选参数允许为方法的某个参数提供默认值,这样调用者就可以选择是否提供该参数的值。通过在参数名后面添加等号和默认值来定义可选参数。
public void MyMethod(int a, int b = 0) // b 是一个可选参数,默认值为0
{
// 方法体...
}
1.4.4.2 方法的返回值
方法的返回值是方法执行后返回给调用者的数据。在C#中,方法的返回值可以是任何数据类型,包括基本数据类型、引用类型和自定义类型。方法的返回值通过return关键字进行返回。一个方法可以有多个返回语句,但只会执行其中的一个。
public int Add(int a, int b) // 返回整数类型的值
{
return a + b; // 返回两个整数的和作为返回值
}
1.4.5、构造函数(Constructors)
构造函数是一种特殊的方法,用于在创建对象时初始化对象的内部状态。它与类同名,并且没有返回类型。当创建一个类的新实例时,构造函数会自动调用。
定义构造函数:
构造函数可以有不同的访问修饰符,包括public
、protected
、private
和internal
。默认情况下,如果没有指定访问修饰符,构造函数是public
的。
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
// 公共构造函数
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
构造函数的特性:
- 构造函数与类同名,并且没有返回类型。
- 构造函数可以有一个或多个参数,用于初始化对象的属性。这些参数被称为构造函数参数。
- 一个类可以有多个构造函数,称为重载的构造函数。它们通过参数的数量、类型或顺序来区分彼此。
- 构造函数在创建类的对象时自动调用。
- 如果没有提供构造函数,编译器会自动提供一个默认的无参构造函数。但是,如果类中定义了任何构造函数,编译器就不会提供默认的构造函数。
- 构造函数不能被直接调用,它们在创建对象时自动执行。
- 构造函数不能被继承或重写,因为它们不是虚拟的或抽象的。
- 构造函数可以在其他程序集中使用相同的名称和签名来共享数据。这有助于促进代码的重用和模块化。
- 构造函数可以在运行时根据需要调用其他方法来执行额外的初始化逻辑。例如,一个构造函数可能会调用另一个构造函数来共享初始化逻辑。这称为委托构造函数或构造器的链式调用。
- 构造函数可以用于实现依赖注入和工厂模式等设计模式,以控制对象的创建和初始化过程。
- 构造函数可以用于实现单例模式或原型模式等设计模式,以控制对象的实例化过程。
- 构造函数可以用于在对象创建时执行一些额外的验证逻辑,例如检查参数的有效性或确保对象的状态是有效的。这有助于提高代码的健壮性和安全性。
1.4.6、析构函数(Destructors)
析构函数(也称为终结器)是在.NET环境中,对象生命周期结束时调用的特殊方法。它的主要用途是释放类所持有的非托管资源,执行清理操作,或者执行任何其他必要的后处理工作。
定义析构函数:
析构函数使用~
符号进行标记,并且没有返回类型和参数。它与类的构造函数类似,但析构函数不能被直接调用,它们在对象的生命周期结束时自动执行。
public class ResourceHolder
{
private IntPtr handle; // 非托管资源
public ResourceHolder()
{
handle = Marshal.AllocHGlobal(100); // 分配非托管内存
}
// 析构函数,释放非托管资源
~ResourceHolder()
{
Dispose();
}
// 显式实现Dispose方法以释放资源
public void Dispose()
{
if (handle != IntPtr.Zero)
{
Marshal.FreeHGlobal(handle); // 释放非托管内存
handle = IntPtr.Zero;
}
}
}
析构函数的特性:
- 析构函数是以类的名称前加上波浪线(
~
)来标记的。 - 析构函数不能被直接调用,它们在对象的生命周期结束时自动执行。当垃圾回收器确定没有任何引用指向对象时,就会调用析构函数。
- 析构函数主要用于释放非托管资源,如文件句柄、数据库连接和内存块等。对于托管资源,.NET运行时会负责垃圾回收和资源释放。
- 除了使用析构函数,还可以通过实现
IDisposable
接口来提供Dispose
方法,以便显式地释放资源。这种做法更加灵活,因为它允许更细粒度的控制和资源的多次释放。在上面的示例中,我们显式地实现了Dispose
方法来释放非托管资源。 - 析构函数的执行顺序是按照它们在代码中出现的顺序。如果有多个析构函数需要执行,它们会按照顺序一个接一个地调用。在上面的示例中,
Dispose
方法被调用时,会触发自动调用析构函数。 - 如果类中定义了析构函数,那么在创建类的实例时会自动调用构造函数来初始化对象。如果类中没有定义析构函数,则会自动创建一个默认的构造函数(如果有定义任何其他构造函数)。如果没有显式定义构造函数或析构函数,编译器会提供默认的构造函数和析构函数。
- 在使用完对象后显式调用
Dispose
方法可以帮助尽早释放资源,而不是等待垃圾回收器自动释放它们。这有助于提高应用程序的性能和响应性。
1.4.7、索引器(Indexers)
索引器是C#中类的一个特殊成员,它使我们能够为集合类型(如数组、列表、字典等)提供类似数组的索引访问方式。它允许我们为类定义一个类似于数组的索引方式,使得我们可以使用类似于obj[index]
的方式来访问和修改对象的集合或数组类型的成员。
定义:
在C#中,索引器是通过在类定义中添加一个名为this
的特殊属性来定义的。该属性后面跟随着参数列表,以指定如何访问集合中的元素。
public class MyCollection
{
private int[] items = new int[10];
// 定义一个索引器,使用int类型的键访问items数组中的元素
public int this[int index]
{
get { return items[index]; } // 读取元素的值
set { items[index] = value; } // 修改元素的值
}
}
特征:
- 语法简洁:使用索引器可以简化代码,使集合的访问和操作更加直观和简洁。
- 参数化:索引器可以接受一个或多个参数,以实现更灵活的访问方式。例如,可以定义一个接受键和值的参数的索引器,以实现类似
obj[key, value]
的访问方式。 - 类型安全:由于索引器可以定义参数的类型,因此它具有类型安全性,可以避免运行时错误。
- 数据绑定:索引器可以在UI组件中用于数据绑定,以实现动态更新和双向绑定。
- 可重载:索引器可以重载,允许定义多个具有不同参数的索引器,以适应不同的访问需求。
- 封装性:通过索引器,我们可以隐藏集合的内部实现细节,提供一种更符合面向对象设计原则的方式来操作集合。
使用:
- 访问元素:使用索引器可以像操作数组一样访问集合中的元素。例如,假设我们有一个名为
myCollection
的MyCollection类实例,可以使用myCollection[0]
来访问第一个元素的值。 - 修改元素:除了读取元素之外,索引器还可以用于修改集合中的元素。例如,
myCollection[1] = newValue
将修改第二个元素的值。 - 链式调用:通过返回集合中的元素,我们可以实现链式调用。例如,假设MyCollection类中的索引器返回一个对象,可以使用以下代码:
myCollection[0].AnotherProperty = newValue
。 - 参数化访问:如果索引器具有多个参数,可以使用这些参数来访问特定位置的元素。例如,假设有一个接受两个参数的索引器,可以使用以下代码:
myCollection[index1, index2]
来访问特定位置的元素。
1.4.8、运算符重载
运算符重载是一种强大的特性,它允许我们为自定义类型定义现有的运算符的行为。这意味着我们可以为自定义类型提供类似于内置类型的操作方式。通过运算符重载,我们可以使代码更加简洁、直观,并提高开发效率。
定义:
运算符重载是通过在类中定义特殊的方法来实现的。这些方法必须使用static
关键字和operator
关键字进行声明,并且必须遵循特定的签名约定。
public class ComplexNumber
{
public double Real { get; set; }
public double Imaginary { get; set; }
// 运算符重载示例:加法运算符
public static ComplexNumber operator +(ComplexNumber a, ComplexNumber b)
{
return new ComplexNumber { Real = a.Real + b.Real, Imaginary = a.Imaginary + b.Imaginary };
}
}
特征:
- 静态方法:运算符重载是通过静态方法实现的,这意味着该方法不能访问类的非静态成员。
- 限定符:运算符重载的方法必须使用
static
和operator
关键字来声明。 - 返回类型:运算符重载方法的返回类型取决于被重载的运算符。例如,加法运算符应该返回一个新的对象或一个适当的值类型。
- 参数类型:运算符重载方法的参数类型取决于被重载的运算符的操作数。例如,对于二元运算符,方法应该接受两个参数。
- 优先级和结合性:运算符重载方法的优先级和结合性应该与被重载的运算符保持一致。
- 重载限制:某些运算符不能被重载,如赋值运算符、点运算符和索引器等。
- 行为一致性:运算符重载应该遵循其原始语义,以保持一致的行为。
- 遵循语法规则:运算符重载必须遵循C#语法规则和规范,以确保正确的使用和语义。
适用场景:
- 自定义类型操作:当我们需要为自定义类型提供类似于内置类型的操作时,可以使用运算符重载。例如,在数学库中为复数类型重载加法、减法、乘法和除法等运算符。
- 简化代码:通过使用运算符重载,我们可以使代码更加简洁和直观。例如,使用重载的加法运算符可以避免显式的函数调用或方法调用,使代码更易于阅读和理解。
- 扩展性:通过为现有类添加新的运算符重载,我们可以扩展类的功能而无需修改类的实现。这有助于提高代码的灵活性和可扩展性。
- 提高性能:在某些情况下,通过重载运算符可以在运行时直接调用高效的底层操作,从而提高性能。这在某些低级领域如科学计算和游戏开发中可能非常重要。
1.4.9、静态成员(Static Members)
属于类本身而不是类的实例的成员,可以直接通过类名访问。使用 static 修饰符可声明属于类型本身而不是属于特定对象的静态成员。
*static修饰符不能用于索引器或终结器
1.4.9.1 静态类
使用静态类的意义
(1)防止程序员写代码时实例化该静态类。
(2)防止在类的内部声明任何实例字段或方法。
使用静态类的要求
(1)静态类的成员都是静态的。
(2)静态类不能实例化
(3)C#编译器会自动把静态类标记为sealed。
1.4.9.2 静态成员
若使用static关键字修饰类成员,则该成员为静态成员。(索引器和终结器不能被static修饰)
使用静态成员的要求
(1)无论对一个类创建多少个实例,它的静态成员都只有一个副本。
(2)静态方法只能被重载,而不能被重写,因为静态方法不属于类的实例成员。
(3)静态方法和属性不能访问其包含类型中的非静态字段和事件,并且不能访问任何对象的实例变量。
(4)在字段中,static不能与const同用,因为 const 字段的行为在本质上是静态的。这样的字段属于类。
1.4.9.3 静态构造函数
用于初始化类中的静态成员
(1)静态类可以有静态构造函数,静态构造函数不可继承。
(2)静态构造函数可以用于静态类,也可用于非静态类。
(3)静态构造函数无访问修饰符、无参数,只有一个 static 标志。
(4)静态构造函数不可被直接调用,在创建第一个实例或引用任何静态成员之前,CLR(公共语言运行时)都将自动调用静态构造函数,静态构造函数将被自动执行,并且只执行一次。
1.4.10、事件(Events)
允许类通知其他对象某些事情发生了。详见第十点。
1.5、类的实例化
这几种方法可以了解一下,本人之前面试有问过实例化类有几种方式。
1.5.1、new
关键字
这是最直接的方法,通过new
关键字和类名来创建类的实例。
MyClass myObject = new MyClass();
1.5.2、反射(Reflection)
反射允许在运行时动态地创建和操作类型,通过反射API可以实例化一个类。
Type type = typeof(MyClass);
object instance = Activator.CreateInstance(type);
1.5.3、工厂模式
在工厂模式中,通常会有一个工厂类来负责创建其他类的实例,这可以隐藏对象的创建逻辑,并使代码更加模块化。
object factory = new MyFactory();
MyClass myObject = factory.CreateMyClass();
1.5.4、依赖注入(Dependency Injection)
在依赖注入中,对象的依赖关系通常由外部容器或框架来管理。对象的实例化不由直接调用new
来完成,而是由依赖注入框架来处理。
// 假设有一个IDependency interface 和它的实现类 MyDependency
IDependency myDependency = new MyDependency();
1.5.5、序列化与反序列化
通过将对象序列化为字节流(例如JSON或XML)或将字节流反序列化为对象,可以在不同的应用程序或系统中共享对象状态。
// 序列化对象为JSON字符串
string json = JsonConvert.SerializeObject(myObject);
// 从JSON字符串反序列化为对象
MyClass myDeserializedObject = JsonConvert.DeserializeObject<MyClass>(json);
1.5.6、克隆或复制构造函数
可以通过定义额外的构造函数或克隆方法来创建现有对象的一个副本。
public class MyClass
{
public MyClass Clone()
{
return new MyClass(this); // 假设有一个复制构造函数的实现
}
}
二、封装
封装是指将数据(属性)与操作数据的函数(方法)捆绑到一个称为“对象”的单一实体中。通过封装,对象的内部状态变得不可见,只能通过定义好的公开方法来访问或修改其状态。
2.1、目的:
- 隐藏实现细节:通过封装,类的使用者无需关心类内部如何实现细节,只需要知道提供的公共接口即可。
- 安全性:隐藏内部状态使得外部代码不能随意修改对象的内部数据,从而增加了数据的安全性。
- 代码重用:封装有助于将相关的数据和操作组织在一起,使得代码更易于理解和重用。
2.2、重要性:
- 信息隐藏和数据保密:封装有助于限制对对象内部状态的访问,防止外部代码随意修改或破坏对象的状态。
- 提高软件的可维护性和可扩展性:由于封装限制了对对象内部细节的直接访问,当对象内部实现发生改变时,对外部代码的影响最小化。这有助于降低软件维护的难度,提高软件的可维护性。同时,通过封装可以将功能模块化,方便添加新的功能或行为,提高软件的可扩展性。
- 支持继承和多态:封装是支持面向对象编程的其他特性如继承和多态的基础。
2.3、好处:
- 提高代码的安全性和可靠性:由于封装限制了对内部状态的直接访问,可以减少错误操作导致的数据损坏或程序崩溃。
- 简化编程模型:封装使得开发者可以专注于使用对象提供的公开接口,而不必关心其内部实现的细节,降低了编程的复杂性。
- 促进代码重用:封装使得数据和操作数据的逻辑紧密结合在一起,方便创建可重用的组件。
- 增强软件的可维护性和可扩展性:如上文所述,由于封装降低了对内部细节的依赖,当软件需要修改或扩展时,工作量得以降低。
- 支持更好的模块化:通过封装,可以更好地将相关的数据和操作组织在一起,形成独立的、可复用的模块。
2.4、访问修饰符
访问修饰符用于控制对类、属性、方法、事件等的可见性。通过访问修饰符,我们可以定义哪些代码可以访问某个成员,以及从哪个作用域可以访问它。
2.4.1、public
公共访问修饰符表示成员可以从任何其他类或方法中访问。这是最宽泛的访问级别。
public class MyClass
{
public int PublicProperty { get; set; }
public void PublicMethod() { }
}
2.4.2、private
私有访问修饰符表示成员只能在包含它的类内部访问。这是默认的访问级别,如果没有指定访问修饰符,则默认为私有访问。
class MyClass
{
private int PrivateProperty { get; set; }
private void PrivateMethod() { }
}
2.4.3、protected
保护访问修饰符表示成员可以在包含它的类以及派生自该类的任何子类中访问。
public class MyBaseClass
{
protected int ProtectedProperty { get; set; }
protected virtual void ProtectedMethod() { }
}
2.4.4、internal
内部访问修饰符表示成员只能在包含它的程序集内部访问。这通常用于那些不应该从其他程序集中公开的类或成员。
internal class InternalClass
{
internal int InternalProperty { get; set; }
internal void InternalMethod() { }
}
2.4.5、protected internal
保护内部访问修饰符表示成员可以在包含它的程序集或派生自该类的子类中访问。这相当于同时使用protected
和internal
修饰符。
public class MyBaseClass
{
protected internal int ProtectedInternalProperty { get; set; }
protected internal virtual void ProtectedInternalMethod() { }
}
2.4.6、private protected
私有保护访问修饰符表示成员可以在包含它的类以及任何从那个类派生的外部子类中访问。这相当于同时使用private
和protected
修饰符。
public class MyBaseClass
{
private protected int PrivateProtectedProperty { get; set; }
private protected virtual void PrivateProtectedMethod() { }
}
2.4.7、override
当一个成员在基类中定义为protected
或public
,并且被标记为virtual
或abstract
时,子类可以通过使用override
关键字来提供自己的实现。这是面向对象编程中的多态性的一种表现。
public class MyBaseClass
{
protected virtual void MyMethod() { }
}
public class MyDerivedClass : MyBaseClass
{
public override void MyMethod() { }
}
三、继承
继承是面向对象编程中的一个核心概念,它允许一个类继承另一个类的属性和方法。继承在C#中为代码重用、多态和实现软件开发生命周期中的代码复用提供了强大的支持。通过合理地使用继承,可以提高代码的可维护性、可读性和可扩展性,有助于构建健壮、灵活和可重用的软件系统。
3.1、概念
继承是一个类(派生类或子类)获取另一个类(基类或父类)的成员(属性、方法、事件等)的过程。通过继承,子类可以拥有父类的所有非私有成员,并且可以添加自己的新成员或覆盖父类的成员。
3.2、优势
- 代码重用:通过继承,子类可以复用父类的代码,避免了重复编写相同的功能代码,提高了代码的复用性和维护性。
- 多态性支持:继承是实现多态的基础。通过覆盖父类的方法,子类可以提供不同的实现,使得在运行时能够根据对象的实际类型来调用相应的方法。
- 逻辑组织:通过将具有共同特性的类组织在一起,形成一个层次结构,有助于理解和维护代码。
- 扩展性:通过继承,可以很容易地扩展现有类的功能而无需修改原始类。这为软件开发生命周期中的扩展性设计提供了可能。
3.3、应用场景
- 层次结构定义:当需要定义一个类与其他类之间的层次关系时,可以使用继承。例如,动物是一个基类,哺乳动物和鸟类是动物的子类,它们继承了动物的属性和方法。
- 分类与子分类关系:当一个类别包含另一个类别时,可以使用继承来表示这种关系。例如,汽车是一个类别,轿车和卡车是汽车的不同子类别。轿车和卡车可以继承汽车类的属性和方法。
- 模板方法实现:当需要实现一个通用的算法或流程,但某些步骤需要根据子类的特性进行定制时,可以使用继承。通过在父类中定义算法的框架,子类可以提供定制的实现部分。
- 组件化开发:在大型软件项目中,使用继承可以将复杂的系统分解为更小、更易于管理的组件。每个组件可以定义为一个类,并使用继承来构建更高级别的组件或系统。
- 接口实现与抽象类的使用:当需要定义一组方法签名但不提供具体实现时,可以使用抽象类或接口。其他类可以继承这些抽象类或实现这些接口来提供具体的实现。
- 设计模式中的结构与行为复用:在设计模式中,如工厂模式、策略模式、观察者模式等,继承常常被用来复用已有的结构或行为,同时提供定制化的扩展点。
四、多态
多态是面向对象编程的三大特性之一,它允许一个接口以多种形式出现。多态的存在增加了程序的灵活性和扩展性,是构建强大软件系统的关键。
4.1、概念
多态是指一个接口在多种情况下有不同的表现形式。在C#中,多态允许子类对象以父类引用的形式存在,从而在运行时根据对象的实际类型来调用相应的方法。
4.2、特点
- 动态绑定:多态允许在运行时根据对象的实际类型来确定要调用哪个方法,而不是在编译时确定。
- 接口一致性:通过多态,可以使用父类引用或接口引用来调用方法,而不需要关心实际对象的类型。
- 方法覆盖与重载:多态通过方法覆盖(也称为重写)和方法重载来实现。
4.3、方法重写(Method Overriding)
当子类继承了父类的一个方法,并且想要提供一个新的实现时,可以使用方法重写。在子类中定义一个与父类方法签名完全相同的方法,并为其提供新的实现。这样,当通过父类引用调用该方法时,将执行子类中的实现。
class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("The animal makes a sound");
}
}
class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Meow");
}
}
4.4、方法重载(Method Overloading)
方法重载是指在同一个类中定义多个同名方法,但它们的参数列表(参数类型、数量或顺序)不同。通过方法重载,可以实现同一个方法名在不同情况下执行不同的操作。
class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b)
{
return a + b;
}
}
五、抽象类(abstract)
5.1、概念
抽象类是一种特殊的类,通常用于定义一个通用的、共享的基类,它可以包含抽象方法,具体方法,字段和属性等成员,而抽象方法的具体实现则由它的派生类来实现,使用Abstract修饰符修饰的类是抽象类,它不能被实例化。
5.2、特点
- 抽象类不能被直接实例化。也就是说,你不能直接创建抽象类的对象。如果你尝试这样做,编译器会报错。
- 抽象类可以包含抽象成员。抽象成员是没有实现的成员,必须由派生类提供具体实现。抽象成员可以有访问修饰符,但通常它们都是公开的,因为派生类需要能够访问它们。
- 抽象类可以包含具体成员。这些成员在抽象类中提供了具体的实现,派生类可以选择覆盖这些实现或者使用基类的实现。
- 抽象类可以包含非公开的成员(例如私有字段、私有方法等)。这是与接口的一个主要区别,因为接口中的所有成员都是公共的。
- 抽象类可以包含事件、索引器、属性、方法等成员。这些成员必须在派生类中被实现。
- 派生类必须实现所有的抽象成员。如果一个派生类不实现所有的抽象成员,那么它也必须声明为抽象类。
- 抽象类可以继承自其他类或接口。这允许将多个继承层次结构合并到一个单独的继承层次结构中。
- 抽象类和普通类一样可以有字段、属性、事件和方法。
- 抽象类可以有构造函数,允许在创建派生类对象时进行一些初始化操作。
5.3、定义
public abstract class Vehicle
{
public int MyProperty { get; set; }
public abstract void Drive();
public void Run()
{
Console.WriteLine("交通工具在运动!");
}
}
public class Car : Vehicle
{
public override void Drive()
{
Run();
Console.WriteLine("我开着汽车!");
}
}
5.4、应用场景
- 代码复用:抽象类可以提供一组通用的属性和方法,这些属性和方法将被派生类继承并实现。通过这种方式,抽象类可以减少代码重复,提高代码复用性。
- 行为规范:抽象类可以定义一组公共的成员(例如方法、属性、事件和索引器),强制要求派生类实现这些成员。通过这种方式,抽象类可以强制要求派生类遵循特定的行为规范。
- 组件设计:在设计组件时,抽象类可以充当接口的角色。与接口不同的是,抽象类可以包含非公共的成员(例如私有字段和私有方法),而接口中的所有成员都是公共的。通过抽象类,可以更好地控制组件的行为和实现细节。
- 大型应用开发:在开发大型应用程序时,往往需要定义一个类,随着功能的增加,可能需要添加一个新的类。如果发现新添加的类与原先定义的类有很多相似之处,就可以考虑使用抽象类来泛化这些相似之处,让子类继承抽象类,复用代码的同时也提升了应用后期的可维护性。
- 设计模式:抽象类在许多设计模式中都有应用,例如模板方法模式、策略模式等。在这些模式中,抽象类通常用于定义一个通用的算法框架或行为规范,具体的实现细节由派生类来完成。
- 兼容性和扩展性:抽象类可以为派生类提供兼容性和扩展性。通过在抽象类中定义一组公共的属性和方法,可以确保派生类在实现这些属性和方法时保持兼容性。同时,如果需要添加新的功能或行为,可以通过添加新的抽象成员来实现,而不需要修改现有的派生类代码。
六、接口(interface)
6.1、概念
接口用于定义一组方法的契约,但不包含这些方法的实现。接口可以包含方法、属性、事件和索引器。其派生类必须提供接口中所有成员的具体实现。
6.2、特点
- 接口不能被实例化。接口只是定义了一组行为规范,具体的实现由实现接口的类或结构提供。
- 接口可以继承自其他接口,实现多重继承。一个类或结构可以实现多个接口,通过这些实现的接口被索引。
- 接口中的成员默认是public的,不能包含字段、构造函数和析构函数。
- 实现接口的类或结构必须严格遵守接口中定义的行为规范,即必须实现接口中的所有方法。
6.3、定义
- 接口名后接一对大括号,大括号内包含方法、属性、事件和索引器的定义。
- 方法、属性、事件和索引器的修饰符必须为public。
- 接口名称一般以I为开头
public interface IMyInterface
{
string MyProperty { get; set; }
void MyMethod();
}
6.4、应用场景
- 解耦:通过定义接口,可以分离具体的实现细节和抽象的行为规范,使得程序的结构更加清晰和灵活。使用接口可以使得程序更容易应对变化,因为具体的实现可以随时替换而不影响使用接口的其他代码。
- 多重继承:通过实现多个接口,类可以实现多重继承的效果。这使得一个类可以同时拥有多个基类的行为,提高了代码的复用性和扩展性。
- 设计模式:接口在许多设计模式中都有应用,例如工厂模式、观察者模式等。在这些模式中,接口用于定义一组通用的行为规范,具体的实现由实现接口的类来完成。这使得代码更加模块化和可维护。
七、五大基本原则
7.1、单一职责原则(SRP: Single Resposibility Principle)
一个类只做好一件事情。它是面向对象编程中的一个重要原则,它要求一个类应该只有一个职责,或者说一个类只有一个改变的原因。这个原则的目的是提高代码的可维护性和可读性,降低类的复杂度,减少类之间的耦合度。
7.1.1、单一职责原则的使用
- 职责明确:一个类应该只负责一项功能或业务逻辑,它的职责应该是单一的。如果一个类承担了过多的职责,就可能导致代码的耦合度过高,增加维护的难度。
- 分离关注点:将不同的职责分散到不同的类中,每个类只关注自己的职责。这样可以让代码更加清晰,更容易理解和维护。
- 降低耦合度:通过将不同的职责分散到不同的类中,可以降低类之间的耦合度,使得代码更加模块化,提高了代码的可重用性和可维护性。
- 提高可读性:单一职责原则使得代码更加简洁明了,每个类都有明确的职责和功能,提高了代码的可读性。
7.1.2、注意事项
使用单一职责原则时,需要注意不要过度分解类。将一个大型类拆分成多个小型类并不一定就是好的设计,需要综合考虑类的职责、功能和耦合度等因素。同时,也需要考虑类的命名和注释,使得代码更加易于理解和维护。
7.2、开放封闭原则(OCP: Open Closed Principle)
对扩展开放,对修改封闭。它是面向对象编程中的一个重要原则,它要求软件实体(类、模块、函数等)对于扩展是开放的,但对于修改是封闭的。这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。
7.2.1、开放封闭原则的使用
- 对扩展开放:当软件需要添加新的功能或行为时,可以通过添加新的类或模块来实现,而不是修改现有的代码。这样可以使得现有代码保持不变,降低了修改的风险和维护的成本。
- 对修改封闭:一旦设计完成并经过测试的类或模块,不应该被修改。如果需要修改,应该通过扩展来添加新的行为,而不是直接修改现有代码。这样可以保证现有代码的稳定性和可维护性。
使用开放封闭原则时,可以通过抽象和继承来实现。通过定义抽象类或接口,可以定义一组通用的属性和方法,然后通过继承和实现来添加新的行为。这样可以使得代码更加模块化,提高了代码的可重用性和可维护性。
7.3、里氏替换原则(LSP: Liskov Substituion Principle)
子类可以替换父类并出现在父类能够出现的任何地方。里氏替换原则是由美国计算机科学家Barbara Liskov提出的。这个原则是说,如果一个程序使用了一个基类的对象,那么使用该基类的任何子类的对象来替换它,程序的行为应该保持不变。
7.3.1、里氏替换原则的使用
- 子类必须能够替换其基类:这意味着子类必须实现与基类相同的功能和行为,并且子类的对象可以在任何使用基类对象的地方使用,而不会破坏程序的原有逻辑。
- 子类可以对基类进行扩展,但不能对基类进行限制:这意味着子类可以添加新的行为和属性,但不能改变基类的原有行为或属性。
- 子类方法的异常应该和基类方法的异常一致或者更少:在方法抛出的异常方面,子类应该至少与基类一样安全,或者更安全。
7.4、依赖倒置原则(DIP: Dependecy Inversion Principle)
依赖于抽象,即高层模块不依赖于底层模块,二者都依赖于抽象。它要求高层模块不应该依赖于低层模块,而是应该依赖于抽象。换句话说,具体实现应该依赖于抽象,而不是抽象依赖于具体实现。这样可以降低模块间的耦合度,提高系统的可维护性和可扩展性。
7.4.1、依赖反转原则的使用
- 依赖抽象而非具体实现:高层模块不应该依赖于低层模块的具体实现,而是应该依赖于抽象。这样,当低层模块的实现发生改变时,高层模块不需要随之改变。
- 模块间的解耦:通过将依赖关系反转,高层模块和低层模块之间的耦合度降低,使得模块间的解耦成为可能。这样,当某个模块发生变化时,其他模块不会受到影响,降低了系统的复杂性。
- 提高可维护性和可扩展性:由于模块间的耦合度降低,系统的可维护性和可扩展性得到了提高。当需要添加新功能或修改现有功能时,只需要修改某个模块,而不会影响到其他模块。
7.5、接口分离原则(ISP: Interface Segregation Principle)
使用多个小的专门接口,而不要使用一个大的总接口。它要求将大接口拆分成小接口,每个接口负责单一职责。这样可以降低类之间的耦合度,提高系统的可维护性和可扩展性。
7.5.1、接口分离原则的使用
- 单一职责原则:每个接口应该只负责一个功能或业务逻辑,这样可以使接口更加清晰、职责单一。
- 高内聚、低耦合:将大接口拆分成小接口,可以降低类之间的耦合度,提高系统的内聚性。这样可以使每个接口更加独立,便于维护和扩展。
- 松耦合:通过将大接口拆分成小接口,可以使系统中的模块或组件之间的依赖关系变得更加松散,从而降低模块之间的耦合度。
- 灵活性:接口分离原则可以提高系统的灵活性,使系统更加易于扩展和维护。通过将大接口拆分成小接口,可以方便地添加新功能或修改现有功能,而不会对整个系统造成影响。
八、异常处理
8.1、System.Exception
是所有异常类的基类,它包含了异常的基本信息,如消息、堆栈跟踪等。它有两个主要的派生类:
8.1.1、 System.SystemException
它是系统定义的异常类的基类,通常由系统抛出。
8.1.2 、System.ApplicationException
它是用户定义的异常类的基类,通常由应用程序抛出。
8.2、派生自System.SystemException的一些常见异常类
8.2.1 、System.NullReferenceException
当尝试访问空对象的成员时抛出的异常。
8.2.2、 System.IndexOutOfRangeException
当尝试访问数组或集合中不存在的索引时抛出的异常。
8.2.3、 System.DividedByZeroException
当除数为零时抛出的异常。
8.2.4 、System.ArithmeticException
算术运算异常的基类。
8.3、派生自System.ApplicationException的用户自定义异常类
用户可以根据自己的需求,派生自System.ApplicationException类来定义自己的异常类型,以便更好地区分不同类型的异常情况。
8.4、try-catch 块
try
块:包含可能引发异常的代码。
catch
块:用于捕获 try
块中抛出的异常,并执行相应的错误处理代码。
8.5、throw语句
throw
语句用于引发(即重新抛出)异常。通常,当你在catch
块中处理一个异常时,你可能需要将该异常传递给更高级别的代码,以便进一步处理或记录。使用throw
语句可以重新引发该异常,使异常的处理流程得以继续。
try
{
// 一些可能抛出异常的代码
}
catch (Exception ex)
{
// 处理异常的代码
// ...
throw; // 重新引发异常
}
8.6、finally块
无论是否发生异常,都会执行此块中的代码。通常用于资源清理操作。
try
{
// 可能抛出异常的代码
int result = 10 / 0; // 除以零的异常
}
catch (DivideByZeroException ex)
{
// 处理除以零异常的代码
Console.WriteLine("发生除以零异常: " + ex.Message);
}
catch (Exception ex)
{
// 处理其他异常的代码
Console.WriteLine("发生其他异常: " + ex.Message);
}
finally
{
// 清理资源的代码,无论是否发生异常都会执行
Console.WriteLine("执行finally块中的代码。");
}
8.7、基本异常类型
8.7.1 、System.Exception
这是所有异常类型的基类。通常情况下,我们不会直接捕获该异常,而是使用它的子类来捕获特定类型的异常。
8.7.2、 System.ArithmeticException
表示算术运算异常,例如除以零。
using System;
class Program
{
static void Main()
{
try
{
int dividend = 10;
int divisor = 0;
int quotient = dividend / divisor; // 这将引发ArithmeticException
}
catch (System.ArithmeticException ex)
{
Console.WriteLine("发生算术异常: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("发生未知异常: " + ex.Message);
}
finally
{
Console.WriteLine("finally块被执行。");
}
}
}
8.7.3 、System.IndexOutOfRangeException
表示数组索引超出范围异常。
using System;
class Program
{
static void Main()
{
try
{
int[] array = new int[5]; // 创建一个长度为5的数组
int index = 10; // 设置一个超出数组范围的索引值
array[index] = 100; // 这将引发IndexOutOfRangeException
}
catch (System.IndexOutOfRangeException ex)
{
Console.WriteLine("发生索引越界异常: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("发生未知异常: " + ex.Message);
}
finally
{
Console.WriteLine("finally块被执行。");
}
}
}
8.7.4 、System.NullReferenceException
表示空引用异常,当尝试访问空引用对象的成员时抛出。
using System;
class Program
{
static void Main()
{
try
{
var obj = null; // 创建一个空引用对象
var result = obj.ToString(); // 这将引发NullReferenceException
}
catch (System.NullReferenceException ex)
{
Console.WriteLine("发生空引用异常: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("发生未知异常: " + ex.Message);
}
finally
{
Console.WriteLine("finally块被执行。");
}
}
}
8.7.5 、System.OutOfMemoryException
表示内存不足异常,当无法分配所需内存时抛出。
using System;
class Program
{
static void Main()
{
try
{
var array = new byte[int.MaxValue]; // 尝试分配超出可用内存的字节数组
}
catch (System.OutOfMemoryException ex)
{
Console.WriteLine("发生内存溢出异常: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("发生未知异常: " + ex.Message);
}
finally
{
Console.WriteLine("finally块被执行。");
}
}
}
8.7.6 、System.DivideByZeroException
表示除以零异常,当除法或模运算的分母为零时抛出。
using System;
class Program
{
static void Main()
{
try
{
int dividend = 10;
int divisor = 0; // 定义一个零除数
int quotient = dividend / divisor; // 这将引发DivideByZeroException
}
catch (System.DivideByZeroException ex)
{
Console.WriteLine("发生除以零异常: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("发生未知异常: " + ex.Message);
}
finally
{
Console.WriteLine("finally块被执行。");
}
}
}
8.7.7、 System.StackOverflowException
表示堆栈溢出异常,通常发生在递归调用过程中。
using System;
class Program
{
static void Main()
{
try
{
Recursion(); // 无限递归调用,导致栈溢出
}
catch (System.StackOverflowException ex)
{
Console.WriteLine("发生栈溢出异常: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("发生未知异常: " + ex.Message);
}
finally
{
Console.WriteLine("finally块被执行。");
}
}
static void Recursion()
{
Recursion(); // 无限递归调用
}
}
8.7.8 、System.IO.IOException
表示输入输出异常,用于处理文件和流的读写操作中的错误。
using System;
using System.IO;
class Program
{
static void Main()
{
try
{
string path = "nonexistentfile.txt"; // 指定一个不存在的文件路径
string content = File.ReadAllText(path); // 这将引发IOException
}
catch (System.IO.IOException ex)
{
Console.WriteLine("发生输入/输出异常: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("发生未知异常: " + ex.Message);
}
finally
{
Console.WriteLine("finally块被执行。");
}
}
}
8.7.9、 System.FormatException
表示格式化异常,通常在字符串转换为其他类型时发生。
using System;
class Program
{
static void Main()
{
try
{
string numberString = "invalidNumber"; // 一个格式不正确的数字字符串
int number = int.Parse(numberString); // 这将引发FormatException
}
catch (System.FormatException ex)
{
Console.WriteLine("发生格式异常: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("发生未知异常: " + ex.Message);
}
finally
{
Console.WriteLine("finally块被执行。");
}
}
}
8.7.10、 System.ArgumentException
表示参数异常,通常在传递无效的参数值时抛出。
using System;
class Program
{
static void Main()
{
try
{
string emptyString = ""; // 创建一个空字符串
int length = int.Parse(emptyString); // 这将引发ArgumentException,因为空字符串不能转换为整数
}
catch (System.ArgumentException ex)
{
Console.WriteLine("发生参数异常: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("发生未知异常: " + ex.Message);
}
finally
{
Console.WriteLine("finally块被执行。");
}
}
}
8.7.11 、System.NotSupportedException
表示不支持的操作异常,当调用不支持的方法或功能时抛出。
using System;
class Program
{
static void Main()
{
try
{
// 假设我们有一个不支持旋转的图形对象
Graphics graphics = new Graphics();
graphics.RotateTransform(45); // 这将引发NotSupportedException
}
catch (System.NotSupportedException ex)
{
Console.WriteLine("发生不受支持的异常: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("发生未知异常: " + ex.Message);
}
finally
{
Console.WriteLine("finally块被执行。");
}
}
}
九、委托
9.1、委托的概念和基本使用
委托是一种特殊的类型,它代表了一个具有特定参数列表和返回类型的可调用方法。委托可以被视为一个类型安全的函数指针,允许将方法作为参数传递给其他方法,或者赋值给变量。委托可以链接一系列方法调用,形成一个链式调用。委托的声明需要指定委托的返回类型和参数列表。委托的定义可以包含在类中,也可以作为独立类型定义。一旦定义了委托,就可以将任何具有匹配签名的方法赋值给该委托变量。简单来说,委托就是C#中的“函数指针”,它实现了对方法的间接调用。
9.1.1 、委托的定义
委托的定义使用delegate
关键字,后跟返回类型和委托的名称,以及方法的参数列表。以下是委托定义的基本语法:
public delegate void MyDelegate(int param1, string param2);
在这个例子中,MyDelegate
是委托类型的名称,它表示一个具有一个整数参数和一个字符串参数,并且没有返回值的方法。
一旦定义了委托类型,就可以创建委托实例并将其指向一个或多个具有匹配签名的方法。委托实例可以使用+=
运算符链接多个方法,形成多播委托(Multicast Delegate),也可以使用-=
运算符从委托链中移除方法。
using System;
// 定义一个具有两个整数参数和返回值类型为void的委托
public delegate void MyDelegate(int a, int b);
class Program
{
static void Main()
{
// 创建委托实例并指向一个方法
MyDelegate myDel = new MyDelegate(AddNumbers);
myDel(10, 20); // 调用AddNumbers方法并输出结果30
}
// 与MyDelegate签名匹配的方法
public static void AddNumbers(int a, int b)
{
Console.WriteLine(a + b);
}
}
需要注意的是,委托类型是引用类型,因此当委托作为参数传递或赋值时,实际上传递的是对方法的引用,而不是方法的副本。这使得委托在事件处理、回调函数和异步编程等方面非常有用。
9.1.2、 委托的特点
- 类型安全:委托是类型安全的,类似于C++函数指针,但它们是类型安全的。委托允许将方法作为参数进行传递,但方法不必与委托签名完全匹配(委托中的协变和逆变)。
- 间接调用:委托提供了一种间接调用方法的方式。通过委托变量,可以将一个方法作为参数传递给另一个方法,或者保存起来稍后再调用。
- 链接方法调用:委托可以链接在一起,对一个事件调用多个方法。调用委托的时候,委托包含的所有方法将被执行。
- 多播:委托支持多播功能,可以在一个委托中注册多个方法。使用加号运算符或减号运算符可以实现添加或撤销方法。
- 可取消:通过使用
-=
运算符,可以从委托链中移除方法,从而实现委托调用的取消。 - 匿名方法与Lambda表达式:C# 2.0后引入了匿名方法,允许将代码块作为参数传递,以代替单独定义的方法。Lambda表达式进一步简化了委托的定义和使用。
- 事件:委托在C#中常用于定义事件,事件可以被视为委托类型的成员,当事件发生时,所有注册到该事件的方法都会被调用。
- 回调:委托可用于回调机制,允许一个对象将其内部状态或数据更新通知给其他对象。
- 异步编程:委托在异步编程中也非常有用,可以用来处理异步操作的结果。
- 扩展性:由于委托是类型安全的,并且可以与扩展方法一起使用,因此可以扩展现有类型的方法集。
9.1.3、多播委托和委托链
多播委托(MulticastDelegate)是包含多个方法的委托类型。当通过委托调用一个方法时,所有注册到该委托的方法都会被调用。多播委托允许将多个方法链接在一起,形成一个委托链。
委托链实际上是指将多个委托链接在一起,形成一个委托链。委托链的作用是将多个委托链接在一起,当调用委托链时,会依次调用委托链中的每个委托。每个委托可以是一个方法,当调用委托链时,会依次调用这些方法。
委托链的创建可以通过使用 +
运算符将多个委托分配到一个委托实例中来实现。一旦创建了委托链,就可以使用 +
或 -
运算符向链中添加或删除委托。
using System;
class Program
{
static void Main()
{
// 定义一个委托类型
public delegate void MyDelegate(int a, int b);
// 创建委托实例
MyDelegate myDel = null;
myDel += Method1; // 将Method1添加到委托链中
myDel += Method2; // 将Method2添加到委托链中
myDel -= Method1; // 从委托链中移除Method1
// 调用委托链中的方法
myDel(10, 20); // 输出 "30"(Method2的结果)
}
// 定义一个匹配委托签名的静态方法
public static void Method1(int a, int b)
{
Console.WriteLine(a + b); // 输出 "15"
}
// 定义一个匹配委托签名的实例方法
public void Method2(int a, int b)
{
Console.WriteLine(a + b); // 输出 "30"
}
}
9.1.4、常用委托
9.1.4.1、Fun委托
Func
委托用于封装返回值的方法。它定义了一个返回值类型,并可以接受0到16个输入参数。
Func<TResult>
Func<TResult, TArg1>
Func<TResult, TArg1, TArg2>
...
Func<TResult, TArg1, TArg2, ..., TArgN>
Func<int, int, int> add = (x, y) => x + y;
int result = add(5, 3); // result = 8
9.1.4.2、Action委托
Action
委托用于封装没有返回值的方法。它也可以接受0到16个输入参数。
Action
Action<TArg1>
Action<TArg1, TArg2>
...
Action<TArg1, TArg2, ..., TArgN>
Action sayHello = () => Console.WriteLine("Hello!");
sayHello(); // 输出 "Hello!"
9.2、委托的应用场景
9.2.1 、回调函数
回调函数是指一个由方法签名的指针,它可以在运行时被调用。通过将一个方法作为参数传递给另一个方法,并在适当的时候调用该方法,可以实现回调。通过委托的机制,可以将一个函数作为参数传递给另一个函数,使得后者在适当的时机调用传入的函数。这种机制在需要异步操作、事件处理、用户交互等情况下非常有用。
using System;
class Program
{
static void Main()
{
// 定义一个委托类型,表示具有两个整数参数且没有返回值的方法
public delegate void MyDelegate(int a, int b);
// 创建委托实例并将其赋值为一个方法
MyDelegate myDel = null;
myDel += Method1; // 将Method1添加到委托链中
myDel += Method2; // 将Method2添加到委托链中
// 调用回调函数,传入委托实例和参数
PerformCallback(myDel, 10, 20);
}
// 定义一个匹配委托签名的静态方法作为回调函数
public static void Method1(int a, int b)
{
Console.WriteLine($"Method1: {a} + {b} = {a + b}"); // 输出 "Method1: 10 + 20 = 30"
}
// 定义另一个匹配委托签名的实例方法作为回调函数
public void Method2(int a, int b)
{
Console.WriteLine($"Method2: {a} + {b} = {a + b}"); // 输出 "Method2: 10 + 20 = 30"
}
// 定义一个接受委托和参数的方法,用于执行回调函数
public static void PerformCallback(MyDelegate del, int x, int y)
{
// 检查委托是否为空,防止空引用异常
if (del != null)
{
del(x, y); // 调用委托链中的方法
}
}
}
9.2.2 、事件处理
事件处理是响应特定事件的过程,比如用户点击按钮、文本框输入等。通过使用委托,可以将多个方法与同一事件关联起来,以便在事件发生时调用这些方法。
using System;
class Program
{
// 定义一个委托类型,表示具有一个字符串参数的方法
public delegate void MyEventHandler(string message);
static void Main()
{
// 创建委托实例
MyEventHandler handler = null;
handler += HandleMessage1; // 将HandleMessage1方法添加到委托链中
handler += HandleMessage2; // 将HandleMessage2方法添加到委托链中
// 触发事件,传递字符串参数
handler("Hello, World!");
}
// 定义一个匹配委托签名的静态方法作为事件处理程序
public static void HandleMessage1(string message)
{
Console.WriteLine($"Message1: {message}"); // 输出 "Message1: Hello, World!"
}
// 定义另一个匹配委托签名的实例方法作为事件处理程序
public void HandleMessage2(string message)
{
Console.WriteLine($"Message2: {message}"); // 输出 "Message2: Hello, World!"
}
}
通过使用委托,可以将多个方法与同一事件关联起来,并在事件发生时自动调用这些方法。这种机制使得事件处理更加灵活和可扩展。
9.2.3 、异步编程
委托在C#中也可以用于异步编程,以方便地实现异步操作。异步编程是一种编程模式,允许程序在等待某些操作完成时继续执行其他代码。通过使用委托,可以将异步操作封装为一个方法,并在需要时调用该方法。
以下是使用委托实现异步编程的示例:
using System;
using System.Threading;
class Program
{
// 定义一个委托类型,表示具有无返回值的方法
public delegate void AsyncDelegate();
static void Main()
{
// 创建委托实例
AsyncDelegate asyncDel = null;
asyncDel += PerformAsyncOperation; // 将PerformAsyncOperation方法添加到委托链中
// 启动异步操作
asyncDel.BeginInvoke(ar =>
{
// 异步操作完成后的回调函数
Console.WriteLine("Async operation completed.");
Console.ReadLine(); // 暂停程序执行,以便查看输出结果
}, null);
// 主线程继续执行其他任务
Console.WriteLine("Main thread is running.");
}
// 定义一个匹配委托签名的异步方法
public static void PerformAsyncOperation()
{
// 模拟异步操作,例如网络请求或文件I/O等
Thread.Sleep(5000); // 等待5秒钟
}
}
在这个例子中,我们定义了一个名为 AsyncDelegate
的委托类型,表示一个没有返回值的方法。然后我们创建了一个名为 asyncDel
的 AsyncDelegate
类型的实例,并将 PerformAsyncOperation
方法添加到委托链中。接下来,我们使用 BeginInvoke
方法启动异步操作。该方法接受一个回调函数作为参数,该回调函数将在异步操作完成后被调用。在回调函数中,我们输出一条消息表示异步操作已完成。最后,我们继续执行主线程的其他任务。
通过使用委托和异步编程模式,可以方便地实现异步操作,并在操作完成后执行特定的回调函数。这使得程序更加高效和响应,并允许主线程继续执行其他任务。
十、事件
10.1、初步了解事件
10.1.1、 事件的概念
事件是一种特殊的成员,用于在类或结构的实例上通知外部代码发生了某些情况。事件可以看作是一种观察者模式,其中类可以通知其他类或对象它所发生的更改或状态变化。
事件的概念基于委托,委托是一种类型安全的函数指针,可以指向具有相同签名的方法。事件就是一种特殊的委托,用于在特定情况下触发一系列方法。
事件通过使用 event
关键字进行定义,并且必须与一个委托类型匹配。事件可以有零个或多个订阅者,这些订阅者是在类外部定义的方法,它们与事件关联并被调用当事件被触发。
事件的使用允许类与类之间以松耦合的方式进行通信,即类之间不需要知道彼此的内部实现细节。当事件发生时,订阅了该事件的任何方法都会被自动调用,从而实现响应机制。
使用事件可以使代码更加灵活和可扩展,因为类可以添加或删除事件处理器而无需修改源代码。这有助于构建响应不同事件的自定义行为和插件系统。
10.1.2 、事件的角色
事件是指能够使对象或类具备通知能力的成员。如把手机看成一个类,响铃使手机具备了通知的功能,则响铃就可以看成是事件。
10.1.3 、事件是干什么的
事件主要用于实现对象之间的松耦合通信。当一个对象的状态发生变化或发生某些动作时,它可以触发一个事件,而其他对象可以订阅这个事件,以便在事件发生时得到通知并作出响应。
通过使用事件,可以实现以下目的:
- 通知机制:一个类可以在内部发生某些动作或状态变化时触发一个事件,通知其他类或对象。这样,外部代码可以响应这些动作或变化,而不需要直接与内部逻辑交互。
- 解耦:事件允许类或对象之间保持低耦合度。这意味着类或对象的实现细节可以自由变化,只要事件签名保持不变,外部代码就不需要修改。这有助于提高代码的可维护性和可扩展性。
- 响应机制:事件可以用于构建响应各种外部事件的机制。例如,在GUI应用程序中,按钮的点击事件被触发时,相关的处理方法被调用,从而响应用户的操作。
- 插件架构:通过定义一组事件,主程序可以构建一个插件架构,允许第三方开发者为这些事件编写自定义的处理逻辑。这种架构允许程序在运行时动态加载和卸载插件,而不需要修改主程序的源代码。
- 异步编程:事件也常用于异步编程模型中。例如,在文件I/O或网络通信操作中,触发一个事件可以表示操作已完成或出现错误。通过异步编程模式和事件,应用程序可以在等待操作完成时继续执行其他任务,从而提高性能和响应能力。
简而言之,事件就是用来通知的,现实世界中的闹铃,车站广播等都可以抽象成事件。
10.1.4 、事件的原理(事件响应机制的两个五)
10.1.4.1 、五个部分:
事件拥有者(Event Source,对象):事件不会自己发生,它必须由一个对象触发。这个对象通常被称为事件源或事件拥有者。
事件(Event,成员):事件是一种特殊的成员,用于让类或对象具备通知能力。它是事件源的一部分,但不会主动发生,而是由事件源的某些内部逻辑触发。
事件响应者(Event Subscriber,对象):事件响应者是订阅了特定事件的类或对象。当事件被触发时,这些订阅者会收到通知并执行相应的操作。
事件处理器(Event Handler,方法成员或者委托):事件处理器本质上是一个回调方法,用于处理事件的逻辑。当事件被触发时,处理器方法会被调用。
事件订阅(Event Subscription):通过事件订阅,事件处理器与事件关联在一起。订阅过程本质上是将一个方法与特定事件关联起来,这样当事件发生时,该方法会被自动调用。
例如:闹钟响了我起床,他的五个部分分别为:闹钟-事件的拥有者,响了-事件,我-事件响应者,起床-事件处理器,我订的闹钟-事件的订阅。那么,别人的闹钟响了我为什么不起床呢?因为我没有订阅这个闹钟。
10.1.4.2、 五个动作:
- 拥有事件:事件拥有者拥有一个事件。
- 订阅事件:事件响应者订阅了这个事件。
- 触发事件:事件拥有者触发了事件。
- 通知响应者:事件响应者会被依次通知到(按照订阅的顺序)。
- 处理事件:事件响应者根据拿到的事件参数对事件进行处理。
10.2、 事件的应用
10.2.1 、事件模型的五个组成部分
事件拥有者(Event Source,对象):事件不会自己发生,它必须由一个对象触发。这个对象通常被称为事件源或事件拥有者。
事件(Event,成员):事件是一种特殊的成员,用于让类或对象具备通知能力。它是事件源的一部分,但不会主动发生,而是由事件源的某些内部逻辑触发。
事件响应者(Event Subscriber,对象):事件响应者是订阅了特定事件的类或对象。当事件被触发时,这些订阅者会收到通知并执行相应的操作。
事件处理器(Event Handler,方法成员或者委托):事件处理器本质上是一个回调方法,用于处理事件的逻辑。当事件被触发时,处理器方法会被调用。
事件订阅(Event Subscription):通过事件订阅,事件处理器与事件关联在一起。订阅过程本质上是将一个方法与特定事件关联起来,这样当事件发生时,该方法会被自动调用。
例如:闹钟响了我起床,他的五个部分分别为:闹钟-事件的拥有者,响了-事件,我-事件响应者,起床-事件处理器,我订的闹钟-事件的订阅。那么,别人的闹钟响了我为什么不起床呢?因为我没有订阅这个闹钟。
10.2.2 、事件模型的四种关联模式
- 事件拥有者与事件响应者的关联:事件拥有者拥有一个或多个事件,而事件响应者通过订阅这些事件来接收通知。
- 事件与事件处理器的关联:事件处理器是与特定事件相关联的方法。通过订阅事件,可以将事件处理器与事件关联起来。
- 事件处理器与事件响应者的关联:事件处理器是事件响应者中的方法成员,用于处理事件的逻辑。通过订阅事件,将事件处理器与事件关联,同时确定了事件的响应者。
- 事件响应者与事件的关联:通过订阅操作,事件响应者与特定事件建立关联,从而成为事件的接收者和处理者。
10.2.3、事件的使用场景
- GUI应用程序:在GUI应用程序中,事件用于响应用户的交互操作,如按钮点击、鼠标移动和键盘输入等。事件驱动机制使得程序能够响应用户的实时操作,提供良好的用户体验。
- 异步编程:在异步编程中,事件用于处理异步任务,例如文件I/O、网络通信和定时器等。通过事件,可以方便地处理异步任务的结果,避免阻塞主线程和提高程序的响应性能。
- 发布-订阅模式:发布-订阅模式是一种常用的消息传递模式,其中事件扮演着重要的角色。在这种模式中,发布者发布事件,而订阅者订阅感兴趣的事件。当事件发生时,发布者通知所有订阅了该事件的订阅者,从而实现消息的传递和共享。
- 自定义控件:在自定义控件的开发中,事件用于实现控件的交互功能。例如,在Windows窗体应用程序中,可以定义控件的事件来响应用户的操作,如按钮点击、文本框内容变化等。
- 多线程编程:在多线程编程中,事件用于线程间的通信和协调。通过事件,一个线程可以通知另一个线程某个动作或状态的变化,从而实现线程间的协作和同步。
- 日志记录和监控:事件可以用于实现日志记录和系统监控功能。当某个特定事件发生时,可以触发一个事件处理器来记录日志或执行相应的操作,例如发送警报或触发某个流程。
- 游戏开发:在游戏开发中,事件用于处理用户的输入和游戏逻辑的更新。通过事件,游戏可以响应用户的操作并实时更新游戏状态。
10.3、事件的声明
10.3.1、 完整声明
using System;
public class Bus
{
// 声明一个名为"BusAlarm"的事件
public event EventHandler BusAlarm;
// 一个模拟公交广播的方法
public void AnnounceBusArrival()
{
Console.WriteLine("乘车广播响起: 公交车即将到达!");
OnBusAlarm(); // 触发事件
}
// 触发事件的私有方法,通常用于内部逻辑
private void OnBusAlarm()
{
// 检查是否有订阅者(事件处理器)
if (BusAlarm != null)
{
// 如果有订阅者,则触发事件
BusAlarm(this, EventArgs.Empty);
}
}
}
public class Passenger
{
public void QueueUp()
{
Console.WriteLine("听到广播后,我去排队等候上车。");
}
}
class Program
{
static void Main(string[] args)
{
Bus bus = new Bus(); // 创建一个公交车对象
Passenger passenger = new Passenger(); // 创建一个乘客对象
// 订阅公交车的事件,当"乘车广播响起"时,执行乘客的QueueUp方法
bus.BusAlarm += passenger.QueueUp;
// 模拟公交车广播响起的情况
bus.AnnounceBusArrival();
}
}
在上面的代码中,我们创建了一个Bus
类和一个Passenger
类。Bus
类有一个名为BusAlarm
的事件,当调用AnnounceBusArrival
方法时,它会触发这个事件。Passenger
类有一个QueueUp
方法,当它被添加为BusAlarm
事件的订阅者时,这个方法会在事件被触发时自动执行。在Main
方法中,我们创建了一个公交车对象和一个乘客对象,并让乘客对象订阅公交车的事件。最后,我们模拟公交车广播响起的情况,这将触发事件并执行乘客的QueueUp
方法。
10.3.2、 简略声明
public class Car
{
// 简略声明事件
public event EventHandler<string> Alarm;
// 触发事件的方法
public void TriggerAlarm()
{
// 触发事件,传递一个字符串参数
Alarm?.Invoke(this, "汽车警报响起!");
}
}
class Program
{
static void Main(string[] args)
{
Car car = new Car(); // 创建一个汽车对象
// 订阅汽车的事件,当"汽车警报响起"时,执行Console.WriteLine方法
car.Alarm += (sender, e) => Console.WriteLine(e); ;
// 触发汽车警报事件
car.TriggerAlarm();
}
}
在上面的代码中,我们创建了一个Car
类,它有一个简略声明的事件Alarm
。当调用TriggerAlarm
方法时,它会触发这个事件。我们使用?
操作符来检查是否有订阅者(事件处理器)订阅了该事件,如果有,则通过Invoke
方法触发事件并传递一个字符串参数。在Main
方法中,我们创建了一个汽车对象,并订阅了汽车的事件。当事件被触发时,它会执行一个Lambda表达式,将事件参数打印到控制台。
10.3.3、 为什么需要事件
- 实现松耦合通信:事件允许程序组件以松耦合的方式进行通信。这意味着一个组件(发布者)可以发布事件,而另一个组件(订阅者)可以订阅这些事件,而不必知道发布者是如何实现的。这样可以提高代码的可维护性和可重用性。
- 提供实时响应:事件在需要实时响应的情况下非常有用,例如在GUI应用程序中响应用户的点击或移动操作,或在网络应用程序中处理接收到的数据包。通过事件,程序可以及时地响应用户输入或其他外部动作。
- 通知机制:事件是一种通知机制,允许一个组件通知其他组件某个动作或状态已经发生或改变。例如,当文件被成功保存时,可以触发一个事件来通知其他组件。
- 消息传递:事件可以用于实现消息传递系统,其中组件可以发布和订阅消息。这与发布-订阅模式类似,其中发布者发布事件,而订阅者订阅感兴趣的事件。
- 异步编程:在异步编程中,事件用于处理异步任务的结果。例如,在文件I/O或网络通信中,可以使用事件来处理异步操作完成后的回调。
- 跨线程通信:在多线程编程中,事件可以用于线程间的通信和同步。通过事件,一个线程可以通知另一个线程某个动作或状态的变化。
- 自定义控件开发:在Windows窗体应用程序或WPF应用程序中,自定义控件常常使用事件来响应用户的交互操作,如按钮点击、文本框内容变化等。
10.3.4、 事件的命名规范
- 使用动词或动词短语:事件名称通常以动词或动词短语开始,因为事件表示一个动作或状态的变化。例如,
Click
、Load
、MouseDown
等。 - 使用PascalCasing:事件名称应使用PascalCasing(每个单词首字母大写)的命名约定。这是C#的常规命名规范。
- 避免使用下划线:在C#中,下划线在事件命名中不是常见的约定。相反,通常使用驼峰式命名法(camelCase)来拼接多个单词。
- 前缀或后缀:避免在事件名称前添加“On”前缀或后缀,除非存在历史或重载原因。现在推荐使用动词或动词短语来明确表示事件的触发。
- 避免使用“Before”和“After”:避免使用“Before”和“After”前缀和后缀来表示事件之前或之后发生的情况。相反,应使用动词时态来表示事件的时间顺序,如
Loading
、Loaded
等。 - 复合词:当需要描述一个事件的更多详细信息时,可以使用复合词来形成新的名称。例如,
DocumentOpen
、UserLoginFailed
等。 - 保持一致性:在同一个项目中,应保持事件命名的统一性。遵循相同的命名规范有助于代码的可读性和维护性。
- 避免使用数字和特殊字符:避免在事件名称中使用数字和特殊字符,除非有特定的技术需求或API要求。
- 考虑缩写:在某些情况下,为了简洁性,可以考虑使用事件的缩写名称。但要确保缩写是众所周知的,并且不会引起混淆。
- 遵循类型命名规范:如果事件属于某个类或接口的一部分,应遵循该类或接口的命名规范。例如,如果类名以名词开头,则事件名称也应当以动词开头。
10.4、事件与委托的关系
- 委托是事件的基础:事件本质上是委托类型的一个成员,用于封装一个或多个方法作为事件处理器。委托是一种类型安全的变量,可以用来存储方法的引用,并且可以调用这些方法。事件成员本质上是一个私有委托字段和两个用于添加和移除事件处理器的公有方法。
- 事件的内部实现:事件的内部实现使用了委托。当一个事件被触发时,与该事件关联的所有委托(即事件处理器)会被依次调用。通过使用委托,事件能够实现松耦合的通信,使得事件的发送者和接收者之间不需要显式地相互引用。
- 订阅关系:事件通过订阅关系将事件的拥有者和事件的响应者关联起来。订阅关系通过将一个方法(即事件处理器)与特定事件关联起来实现。通过使用
+=
运算符来订阅事件,将事件处理器添加到事件的调用列表中。同样地,使用-=
运算符来取消订阅事件,从事件的调用列表中移除事件处理器。 - 多播委托:委托是C#中的多播委托类型,这意味着一个委托可以代表多个方法。事件作为委托的成员,也支持多播特性。这意味着一个事件可以有多个关联的事件处理器,当事件被触发时,所有注册的事件处理器都会被依次调用。
十一、泛型
11.1、泛型概述
泛型是C#中一个强大的特性,它允许开发者在类、结构、接口和方法中定义类型参数化,以便创建更加灵活和可重用的代码。泛型的主要目的是提高代码的重用性、类型安全性和性能。
11.1.1、泛型的定义
泛型是一种编程技术,它允许在定义类、接口或方时使用类型参数。这些类型参数在实例化泛型类型时被具体的类型替换。通过这种方式,可以创建适用于任何数据类型的可重用代码。
11.1.2、泛型的目的
- 提高代码重用性:通过使用泛型,你可以编写适用于多种数据类型的代码,而无需为每种数据类型编写特定的实现。这大大减少了重复的代码,并提高了开发效率。
- 增加类型安全性:使用泛型可以避免运行时错误,因为编译器可以在编译时捕获许多常见的类型不匹配问题。此外,由于泛型在编译时检查类型,因此可以提供更好的性能和内存使用。
- 减少冗余代码:通过编写泛型代码,你可以避免编写重复的逻辑,只需编写一次代码就可以应用于多种类型。这使得代码更简洁、易于维护。
- 提供更好的性能:泛型可以通过在编译时生成特定类型的代码来提高性能。这避免了运行时的装箱和拆箱操作,从而提高了性能。
- 支持协变和逆变:通过使用泛型,你可以定义协变和逆变关系,这意味着你可以使用子类型的实例替换泛型参数,这有助于实现更灵活的集合操作。
11.1.3、泛型与Object类型的对比
11.1.3.1、类型安全性
泛型:泛型提供了编译时的类型安全。当你使用泛型类或方法时,你必须在编译时指定具体的类型参数。这允许编译器捕获可能的类型错误,并提供强类型检查。
Object 类型:Object 是所有类型的基类,它可以存储任何类型的值。但是,当你使用 Object 类型时,你会失去编译时的类型信息,这可能导致运行时的类型错误。此外,当你需要对 Object 类型的值执行操作时,通常需要进行显式的类型转换或使用反射,这可能会增加出错的可能性。
11.1.3.1、性能
泛型:泛型可以提供更好的性能,因为它允许编译器生成针对特定类型的优化代码。这避免了装箱和拆箱操作,减少了类型转换的开销,并允许值类型保持其值语义。
Object 类型:使用 Object 类型可能会导致性能下降,特别是当涉及值类型时。因为值类型在存储为 Object 时需要进行装箱操作,从 Object 提取值类型时需要进行拆箱操作。这些操作涉及额外的内存分配和类型转换开销。
11.1.3.3、代码重用和可读性
泛型:泛型允许你编写可重用的代码逻辑,该逻辑可以适用于多种类型,而无需为每种类型编写特定的代码。这提高了代码的重用性,并减少了维护工作量。此外,泛型代码更具可读性,因为类型参数化使得代码的意图更加明确。
Object 类型:虽然使用 Object 类型可以实现一定程度的代码重用,但它通常会导致更多的运行时类型检查和类型转换代码。这可能会降低代码的可读性和可维护性。
11.1.4、泛型的优势
类型安全:泛型的主要优势之一是提供类型安全。在非泛型编程中,许多操作都需要进行类型转换,这增加了出错的可能性。使用泛型,你可以明确指定类型参数,编译器可以在编译时检查类型错误,从而提高安全性。
代码重用:泛型通过创建独立于特定类型的类和方法,显著提高了代码的重用性。这意味着你可以编写一次代码,并使用不同的类型参数多次调用该代码。这减少了重复的代码,并提高了开发效率。
性能优化:泛型通过减少类型转换和避免装箱和拆箱操作,可以提高性能。在非泛型编程中,将简单类型作为 Object 传递会引起装箱和拆箱操作,这些操作具有较大的开销。使用泛型可以避免这些开销,从而提高性能。
灵活性:泛型提供了很大的灵活性,允许你编写适用于多种数据类型的代码。这使得你可以在不牺牲类型安全性的情况下,更加灵活地处理数据。
可扩展性:泛型使得代码更容易扩展。你可以定义泛型接口、泛型类、泛型方法等,并在需要时添加新的实现。这使得代码更容易适应未来的需求变化。
二进制代码重用:由于泛型在编译时实例化,生成的二进制代码可以重用。这减少了程序的大小,并提高了加载性能。
11.2、泛型基础
11.2.1、泛型类
11.2.1.1、泛型类的声明
声明一个泛型类使用<T>
语法,其中T
是一个类型参数。类型参数用于指定泛型类中使用的数据类型。
public class GenericClass<T>
{
// 泛型类的成员
}
11.2.1.2、创建泛型类的实例
要创建泛型类的实例,需要指定类型参数的值。
var instance = new GenericClass<int>(); // 使用 int 作为类型参数
var otherInstance = new GenericClass<string>(); // 使用 string 作为类型参数
11.2.1.3、泛型类的约束
在某些情况下,可能希望对泛型类使用的类型参数施加一些限制。这可以通过使用约束来实现。约束告诉编译器类型参数必须满足某些条件。
引用类型约束 (where T : class
):该约束指定类型参数必须是引用类型。
public class GenericClass<T> where T : class
{
// 泛型类的成员
}
使用此约束,可以确保泛型类中的某些成员(如非静态方法)仅适用于引用类型。
值类型约束 (where T : struct
):该约束指定类型参数必须是值类型。
public class GenericClass<T> where T : struct
{
// 泛型类的成员
}
使用此约束,可以确保泛型类中的某些成员(如非静态方法)仅适用于值类型。
基类约束 (where T : BaseType
):该约束指定类型参数必须是指定的基类或其派生类。
public class GenericClass<T> where T : BaseType
{
// 泛型类的成员
}
使用此约束,可以确保泛型类中的某些成员仅适用于继承自特定基类的类型。
接口约束 (where T : IInterface
):该约束指定类型参数必须实现指定的接口或其派生接口。
public class GenericClass<T> where T : IInterface
{
// 泛型类的成员
}
使用此约束,可以确保泛型类中的某些成员仅适用于实现了特定接口的类型。
自定义约束:还可以定义自己的约束,通过在泛型类中定义一个静态方法,并在约束表达式中调用它。
public class GenericClass<T> where T : new() // 自定义约束,表示T必须有一个无参构造函数
{
// 泛型类的成员和构造函数调用逻辑等。
}
11.2.2、泛型接口
11.2.2.1、泛型接口的声明
声明一个泛型接口使用相同的<T>
语法,其中T
是一个类型参数。类型参数用于指定泛型接口中使用的数据类型。
public interface GenericInterface<T>
{
// 泛型接口的成员
}
11.2.2.2、实现泛型接口
要实现一个泛型接口,需要创建一个类或结构体,并实现接口中定义的所有成员。在实现泛型接口时,需要指定与接口相同的类型参数。
public class MyClass : GenericInterface<int> // 实现具有 int 类型参数的泛型接口
{
public int GetValue() // 实现接口中的方法
{
return 42; // 返回一个整数值
}
}
需要注意的是,也可以通过定义泛型方法来间接实现泛型接口。泛型方法是类的成员,它返回一个值或者没有返回值,并可以接受类型参数。通过定义泛型方法,可以编写适用于多种数据类型的通用逻辑,并在需要时将其应用于不同的类型。
11.2.3、泛型方法
11.2.3.1、泛型方法的声明
声明一个泛型方法使用相同的<T>
语法,其中T
是一个类型参数。类型参数用于指定泛型方法中使用的数据类型。
public static int GenericMethod<T>(T arg)
{
// 泛型方法的实现
return (int)arg; // 假设T是可转换为int的类型
}
11.2.3.2、调用泛型方法
要调用一个泛型方法,需要指定类型参数的值。类型参数的值可以是任何有效的C#类型。
int result = GenericMethod<int>(42); // 调用具有 int 类型参数的泛型方法,并传递一个 int 类型的参数
Console.WriteLine(result); // 输出 42
11.2.3.3、类型推断
编译器可以自动推断泛型方法的类型参数值。当在调用泛型方法时,如果没有显式指定类型参数的值,编译器将根据传递给方法的实际参数类型来推断类型参数的值。
string str = "Hello";
int result = GenericMethod(str); // 调用泛型方法时没有指定类型参数的值,编译器将自动推断为 string 类型
Console.WriteLine(result); // 输出 0,因为 string 不能直接转换为 int(这里仅为示例)
11.2.4、泛型委托
11.2.4.1、泛型委托的声明
泛型委托允许定义一个委托,该委托可以引用任何返回类型和任何参数类型的任何方法。声明泛型委托时,使用delegate
关键字后跟泛型标记<T>
。
public delegate T GenericDelegate<T>(T arg); // 泛型委托,T 是返回类型,T 是参数类型
11.2.4.2、使用泛型委托
要使用泛型委托,需要创建一个委托实例,并将其指向一个具有匹配签名的方法。然后可以通过该委托调用该方法。
public class Program
{
public static void Main()
{
// 创建泛型委托实例并指向一个方法
GenericDelegate<string> del = new GenericDelegate<string>(PrintString);
// 通过委托调用方法
del("Hello, World!"); // 输出 "Hello, World!"
}
// 与泛型委托签名匹配的方法
public static string PrintString(string str)
{
Console.WriteLine(str);
return str; // 返回字符串作为泛型委托的返回类型
}
}
在此示例中,我们创建了一个名为del
的GenericDelegate<string>
实例,并将其指向名为PrintString
的方法。然后我们通过委托del
调用PrintString
方法,并传递字符串参数"Hello, World!"
。由于PrintString
方法的签名与委托签名匹配(返回类型为string
,参数类型为string
),因此该方法可以通过委托进行调用。最后,我们打印出传递给方法的字符串参数,并返回它作为委托的返回值。
11.3、泛型高级特性
11.3.1、泛型约束
where子句的使用:可以使用where
子句来指定泛型参数的类型约束。
public class Box<T> where T : IEquatable<T>
{
// 类的实现
}
在上面的例子中,T
必须实现IEquatable<T>
接口。这样的约束确保了可以安全地对泛型类型进行比较操作。
主约束和次要约束:可以使用多种约束来限制泛型参数的类型。主要的约束包括:
- class:表示
T
必须是引用类型。 - struct:表示
T
必须是值类型。 - new():表示
T
必须有一个无参数的构造函数。 - :表示
T
必须是一个类型参数。这是默认的约束,如果没有指定任何其他约束,那么T
必须是类型参数。
这些约束可以组合使用,以提供更具体的类型限制
public class MyGenericClass<T> where T : class, IMyInterface, new()
{
// 类的实现
}
在上面的例子中,T
必须是一个引用类型、实现IMyInterface
接口,并且必须有一个无参数的构造函数。
构造函数约束:通过使用new()
约束,可以要求泛型类型具有一个无参数的公共构造函数。这在创建泛型对象的实例时非常有用。
public class MyGenericClass<T> where T : new()
{
public T CreateInstance()
{
return new T(); // 调用无参数构造函数创建新实例
}
}
值类型和引用类型约束:通过使用struct
和class
约束,可以限制泛型参数是值类型还是引用类型。
public class MyGenericClass<T> where T : struct // T 必须是值类型
{
// 类的实现
}
public class MyGenericClass<T> where T : class // T 必须是引用类型
{
// 类的实现
}
继承约束和接口约束:通过指定一个或多个类或接口约束,可以限制泛型参数的类型必须是某个类或必须实现某个接口。
public class MyGenericClass<T> where T : MyBaseClass, IMyInterface // T 必须是 MyBaseClass 的派生类并且实现 IMyInterface 接口
{
// 类的实现
}
或者使用多个接口约束:
public class MyGenericClass<T> where T : IMyInterface1, IMyInterface2 // T 必须实现 IMyInterface1 和 IMyInterface2 接口
{
// 类的实现
}
11.3.2、协变与逆变
11.3.2.1、协变(Covariance)
协变允许将派生类的实例赋值给基类的引用。在泛型接口或委托中,可以使用out
关键字来声明协变参数。这意味着,如果一个泛型接口或委托有一个协变参数,那么可以传递一个更具体的类型(派生自原始类型的类型)给这个参数。
interface ICovariant<out T>
{
T Get();
}
在上面的例子中,T
被声明为协变参数,这意味着你可以有一个ICovariant<string>
的实例,或者有一个更具体的派生自string
的类型。
11.3.2.2、逆变(Contravariance)
逆变允许将基类的实例赋值给派生类的引用。在泛型接口或委托中,可以使用in
关键字来声明逆变参数。这意味着,如果一个泛型接口或委托有一个逆变参数,那么可以传递一个更具体的类型(派生自原始类型的类型)给这个参数。
interface IContravariant<in T>
{
void Set(T value);
}
在上面的例子中,T
被声明为逆变参数,这意味着你可以有一个IContravariant<object>
的实例,或者有一个更具体的派生自object
的类型。
11.3.2.3、协变与逆变的限制
- 只对接口和委托有效:协变和逆变只能应用于接口和委托的参数列表中,而不能直接应用于类或方法。
- 不能同时使用:一个类型参数不能同时声明为协变和逆变。
- 不能用于值类型:值类型(如struct)不能用作协变或逆变的参数。
- 不能用于默认值参数:协变和逆变参数不能有默认值。
- 不能与
struct
约束一起使用:不能在同时声明struct
约束和协变或逆变的类型参数上使用struct
约束。 - 不能与
out
和in
同时使用:在同一个泛型接口或委托中,一个类型参数不能同时声明为协变和逆变。 - 继承与协变/逆变的兼容性:如果一个类或结构体继承自协变或逆变的泛型接口,则该类或结构体必须实现继承自该接口的所有协变或逆变的方法。这是因为协变和逆变方法是通过重载(overloading)来处理的,而不是通过重写(overriding)。这意味着协变方法必须在子类中被重载,而不是被重写。同样,逆变方法也必须被重载,而不是被重写。
- 使用上的限制:协变和逆变主要应用于委托和接口的泛型参数上,以便允许更灵活的类型组合和使用场景。在实际编程中,你应该谨慎地使用协变和逆变,确保它们不会破坏类型安全性。
11.3.3、泛型缓存
泛型缓存通常是一个技术概念,指的是为了提升性能和避免不必要的重复工作,将已经创建过的泛型类型实例存储起来以便重用。由于.NET中的泛型类型在每次使用不同的类型参数时都会生成新的类型,这可能导致大量的重复工作,尤其是在涉及到反射和动态类型生成时。
11.3.3.1、泛型类型的唯一性
在.NET中,每个唯一的泛型类型参数组合都会产生一个新的、唯一的运行时类型。例如,List<int>
和List<string>
是两个完全不同的类型,尽管它们都来自于List<T>
的泛型定义。这种唯一性确保了类型安全,但也可能导致性能开销,因为每个泛型实例化都需要单独的JIT编译和类型元数据。
11.3.3.2、泛型缓存的概念和作用
由于每次使用不同的类型参数都会创建新的泛型类型实例,这可能会导致性能问题,尤其是在需要频繁创建和销毁这些实例的情况下。泛型缓存是一种技术,它存储了已经创建过的泛型实例,以便在后续需要相同类型的实例时可以重用它们,而不是重新创建。这可以显著减少内存分配和垃圾回收的开销,以及减少JIT编译器的负担。
11.3.3.3、如何使用泛型缓存提高性能
要使用泛型缓存提高性能,需要设计一个能够存储和检索泛型实例的机制。这通常涉及到使用字典或其他键值存储结构来保存泛型实例,其中键是类型参数的组合,值是对应的泛型实例。以下是一个简单的示例来说明这个概念:
public static class GenericCache<T>
{
private static readonly Dictionary<Type, object> Cache = new Dictionary<Type, object>();
public static T GetOrCreate<TKey>(Func<T> creator) where TKey : class
{
Type key = typeof(TKey);
lock (Cache)
{
if (!Cache.TryGetValue(key, out var value))
{
value = creator();
Cache[key] = value;
}
return (T)value;
}
}
}
然而,上面的代码示例并不是真正的“泛型缓存”,因为它没有利用到泛型的类型参数T
。实际上,实现一个真正的泛型缓存可能更为复杂,因为你需要处理如何唯一地标识和存储每个不同的泛型类型实例。在实践中,你可能需要使用到反射和表达式树来动态地创建和缓存泛型类型。
实际上,更常见的做法是使用现有的框架和库提供的缓存机制,比如MemoryCache类,或者是针对特定场景的缓存库如CacheManager。这些机制可以透明地处理许多与缓存相关的复杂问题,如过期策略、并发控制和内存管理。
如果你确实需要实现一个针对泛型的缓存机制,你可能需要设计一个更为复杂的系统,它能够根据传入的类型参数动态地生成键,并在内部维护这些键与对应泛型实例之间的映射关系。这样的系统可能还需要处理如何安全地在多线程环境中访问和更新缓存的问题。
11.4、泛型应用场景与实例
11.4.1、集合类库中的泛型应用(如List, Dictionary<TKey, TValue>等)
11.4.1.1、 List
List<T>
是 .NET Framework 中最常用的泛型集合之一,它表示一个动态数组,可以存储任何类型的元素。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 创建一个存储整数的 List<T>
List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
// 遍历 List<T> 中的元素
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}
}
11.4.1.2、Dictionary<TKey, TValue>
Dictionary<TKey, TValue>
是一个关联数组,它存储键值对集合,其中每个键都是唯一的。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 创建一个存储字符串到整数的 Dictionary<TKey, TValue>
Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Alice"] = 25;
ages["Bob"] = 30;
ages["Charlie"] = 35;
// 遍历 Dictionary<TKey, TValue> 中的键值对
foreach (KeyValuePair<string, int> entry in ages)
{
Console.WriteLine($"{entry.Key}: {entry.Value}");
}
}
}
11.4.1.3、Queue
这是一个先进先出(FIFO)的队列,可以通过指定T来存储任何类型的元素。
Queue<int> numbers = new Queue<int>(); // 存储整数的队列
numbers.Enqueue(1);
numbers.Enqueue(2);
11.4.1.4、Stack
这是一个后进先出(LIFO)的栈,可以通过指定T来存储任何类型的元素。
Stack<int> stack = new Stack<int>(); // 存储整数的栈
stack.Push(1);
stack.Push(2);
11.4.1.5、HashSet
这是一个不包含重复元素的集合。可以通过指定T来存储任何类型的元素。
HashSet<string> uniqueNames = new HashSet<string>(); // 存储唯一字符串的集合
uniqueNames.Add("Alice");
uniqueNames.Add("Bob");
11.4.1.6、LinkedList
这是一个双向链表,可以通过指定T来存储任何类型的元素。
LinkedList<string> linkedList = new LinkedList<string>(); // 存储字符串的链表
linkedList.AddLast("Alice");
linkedList.AddLast("Bob");
11.4.2、自定义泛型类和方法的实例演示
11.4.2.1、自定义泛型类
下面是一个简单的泛型类示例,它使用泛型来存储并操作任何类型的列表
public class GenericList<T>
{
private List<T> list = new List<T>();
public void Add(T item)
{
list.Add(item);
}
public T this[int index]
{
get { return list[index]; }
set { list[index] = value; }
}
public int Count
{
get { return list.Count; }
}
}
可以这样使用这个泛型类:
GenericList<int> intList = new GenericList<int>();
intList.Add(10);
int value = intList[0]; // value 是 int 类型,值为 10
或者对于字符串类型:
GenericList<string> stringList = new GenericList<string>();
stringList.Add("Hello");
string str = stringList[0]; // str 是 string 类型,值为 "Hello"
11.4.2.2、自定义泛型方法
下面是一个简单的泛型方法示例,该方法接受一个泛型参数并返回其类型:
public static T ReturnType<T>() where T : new() // 约束T必须有一个无参数的构造函数,因为我们将创建一个新的T实例。
{
return new T(); // 创建一个新的T实例并返回。
}
可以这样使用这个泛型方法:
var instanceInt = ReturnType<int>(); // instanceInt 是 int 类型,值为 0(因为 int 的默认构造函数会返回0)
var instanceString = ReturnType<string>(); // instanceString 是 string 类型,值为空字符串("")(因为 string 的默认构造函数会返回空字符串)
11.4.3、使用泛型解决常见问题(如交换两个变量的值、查找数组中的最大值等)
11.4.3.1、交换两个变量的值
假设有两个变量,我们想要交换它们的值,可以使用泛型方法来做到这一点,而不必担心具体的类型。
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
使用示例:
int x = 5;
int y = 10;
Swap(ref x, ref y);
Console.WriteLine($"After swap: x = {x}, y = {y}"); // 输出: After swap: x = 10, y = 5
11.4.3.2、查找数组中的最大值
使用泛型,可以编写一个查找数组中最大值的通用方法,无论数组包含何种类型的数据。
public static T FindMax<T>(T[] array) where T : IComparable<T>
{
return array.Max();
}
使用示例:
int[] intArray = { 1, 5, 3, 7, 9 };
int maxInt = FindMax(intArray); // maxInt 是 int 类型,值为 9
string[] stringArray = { "apple", "banana", "cherry" };
string maxString = FindMax(stringArray); // maxString 是 string 类型,值为 "banana" (或 "cherry",取决于字母顺序)
11.4.3.3、插入元素到集合
我们想要一个泛型方法,它可以接受任何类型的集合,并将元素插入到该集合中。我们可以利用IEnumerable<T>
和IAddable<T>
接口来实现这一点。
首先,为泛型添加一个约束,确保它实现了IAddable<T>
接口:
public interface IAddable<out T> { }
然后,实现一个泛型方法来添加元素:
public static void AddToCollection<TCollection, T>(TCollection collection, T item) where TCollection : IEnumerable<IAddable<T>>
{
foreach (var addable in collection)
{
addable.Add(item); // 因为IAddable<T>接口假设有一个Add方法。
}
}
注意:这个例子中的IAddable<T>
接口和AddToCollection
方法都是假设的,因为C#标准库中并没有这样的接口和实现。这只是一个理论上的示例来说明如何使用泛型约束来操作集合。在实际应用中,可能需要根据具体的集合类型来实现这些操作。
11.4.4、泛型在LINQ中的应用
1.4.4.1、LINQ to Objects
使用LINQ查询内存中的集合时,例如List<T>
,可以使用泛型来定义查询。
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = from num in numbers where num > 3 select num;
foreach (var number in query)
{
Console.WriteLine(number); // 输出: 4, 5
}
1.4.4.2、LINQ to XML
在处理XML文档时,可以使用泛型来定义查询。
XDocument doc = XDocument.Parse("<root><item>1</item><item>2</item><item>3</item></root>");
var query = from el in doc.Descendants("item") where (int)el > 2 select el;
foreach (var element in query)
{
Console.WriteLine(element); // 输出: <item>3</item>
}
1.4.4.3、LINQ to SQL
在处理数据库时,可以使用LINQ to SQL来编写类型安全的查询。
using (var context = new MyDataContext())
{
var query = from customer in context.Customers where customer.Age > 30 select customer;
foreach (var customer in query)
{
Console.WriteLine(customer); // 输出: Customer对象集合,年龄大于30的客户信息。
}
}
1.4.4.4、扩展方法与LINQ
使用泛型扩展方法来扩展LINQ查询。例如,可以编写一个泛型的方法来获取集合中的第一个元素。
public static T First<T>(this IEnumerable<T> source) => source.FirstOrDefault<T>(); // 使用默认值,如果集合为空则返回默认值。
然后可以这样使用它:
var firstNumber = numbers.First(); // 返回1,因为它是第一个元素。
11.4.5、泛型在设计模式中的应用(如工厂模式、单例模式等)
11.4.5.1、工厂模式
在工厂模式中,泛型可以用来创建泛型工厂,以生成指定类型的对象。
public interface IProduct<T> { }
public class ConcreteProductA : IProduct<string> { }
public class ConcreteProductB : IProduct<int> { }
public class GenericFactory<T>
{
public IProduct<T> CreateProduct()
{
// 这里可以根据某种逻辑来创建IProduct<T>的实例。
// 例如,根据配置文件、数据库或其他方式来动态决定创建哪个具体产品。
return new ConcreteProductA(); // 示例: 返回一个字符串类型的具体产品。
}
}
11.4.5.2、单例模式
使用泛型可以在单例模式中创建一个泛型单例类,该类可以处理任何类型的单例。
public class Singleton<T> where T : new()
{
private static readonly Lazy<T> _instance = new Lazy<T>(() => new T());
public static T Instance { get { return _instance.Value; } }
}
11.4.5.3、策略模式
在策略模式中,可以使用泛型来定义一个策略接口,然后创建实现该接口的策略类。
public interface IStrategy<T> { void Execute(T input); }
public class ConcreteStrategyA : IStrategy<string> { public void Execute(string input) { /*...*/ } }
public class ConcreteStrategyB : IStrategy<int> { public void Execute(int input) { /*...*/ } }
11.4.5.5、装饰器模式
在装饰器模式中,可以使用泛型来定义一个通用的装饰器类。
public interface IComponent<T> { T Operation(); }
public class ComponentA : IComponent<string> { public string Operation() { /*...*/ } }
public class ComponentB : IComponent<int> { public int Operation() { /*...*/ } }
public class Decorator<T> : IComponent<T>
{
private readonly IComponent<T> _component;
public Decorator(IComponent<T> component)
{
_component = component;
}
public T Operation()
{
// 在这里添加装饰逻辑。
return _component.Operation();
}
}
11.5、泛型的性能考虑与优化建议
11.5.1、装箱与拆箱的性能影响及避免方法
泛型在运行时可能会涉及装箱和拆箱操作,这可能会影响性能。例如,当值类型用作泛型参数时,可能会发生装箱(将值类型转换为对象引用)和拆箱(将对象引用转换回值类型)操作。
避免方法:
- 使用值类型(如结构体)作为泛型参数,而不是引用类型。
- 避免频繁的装箱和拆箱操作,例如通过缓存已装箱的实例或重用对象。
11.5.2、类型安全带来的性能提升与优化建议
泛型通过类型参数化增加了类型安全性,这有助于减少运行时错误和潜在的性能损失。
优化建议:
- 利用泛型提供的类型安全来减少运行时类型检查和异常处理。
- 利用约束来限制泛型参数的类型,以减少可能的运行时错误。
11.5.3、合理使用泛型缓存以提高程序性能
对于频繁使用的泛型实例,可以考虑使用缓存来避免重复创建相同的实例。
public static class Cache<T>
{
private static readonly Dictionary<T, object> cache = new Dictionary<T, object>();
public static object Get(T key) => cache[key];
public static void Set(T key, object value) => cache[key] = value;
}
11.5.4、对比使用Object类型与泛型的性能差异及原因分析
使用Object
类型会丧失类型安全性,可能导致运行时异常和性能下降。相比之下,泛型提供了更好的类型控制和编译器级别的检查。
差异分析:
- 泛型能够提供更好的类型安全性和复用性。
- 使用
Object
类型可能会导致运行时类型转换、装箱和拆箱操作,从而影响性能。 - 泛型通过消除运行时类型转换和增加编译器检查来提高性能和减少潜在错误。
11.5.5、根据实际场景选择合适的类型参数以减少性能损失和提高代码可读性
选择合适的类型参数可以平衡代码的可读性与性能。例如,选择常用的值类型作为泛型参数可以减少装箱和拆箱操作。同时,根据实际需求选择合适的约束可以进一步优化性能。
示例:
- 使用
where T : struct
约束来限制泛型参数为值类型,以减少装箱操作。 - 使用
where T : new()
约束来确保泛型实例化时可以调用默认构造函数。 - 使用
where T : IEquatable<T>
约束来提供自定义的相等性比较逻辑。
十二、集合
12.1、集合的基本概念
12.1.1、集合的定义
集合是一种数据结构,它可以包含多个元素,并且这些元素可以是相同类型或不同类型。在C#中,集合通常用于存储具有共同特征的对象集合。
12.1.2、集合的常用操作
添加元素:向集合中添加一个或多个元素。
List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
删除元素:从集合中移除一个或多个元素。
List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
numbers.Remove(2); // 移除元素2
查找元素:在集合中查找是否存在某个元素。
List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
bool containsTwo = numbers.Contains(2); // 返回true,因为集合中包含元素2
计数:获取集合中元素的数量。
List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
int count = numbers.Count; // 返回3,因为集合中有三个元素
遍历:按顺序访问集合中的每个元素。
List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
foreach (int number in numbers) {
Console.WriteLine(number); // 输出1、2、3三个数字。
}
排序:对集合中的元素进行排序
List<int> numbers = new List<int>() { 3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5 };
numbers.Sort(); // 使用Sort方法对集合进行排序,结果是升序排列
foreach (int number in numbers) {
Console.WriteLine(number); // 输出排序后的数字:1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9
}
聚合操作:对集合中的元素进行分组、过滤、投影等操作
List<Student> students = new List<Student>() {
new Student { Name = "张三", Age = 20, Grade = "一年级" },
new Student { Name = "李四", Age = 21, Grade = "二年级" },
new Student { Name = "王五", Age = 19, Grade = "一年级" },
new Student { Name = "赵六", Age = 22, Grade = "三年级" }
};
// 分组操作
var groupedByGrade = students.GroupBy(s => s.Grade); // 根据年级对学生进行分组
foreach (var gradeGroup in groupedByGrade) {
Console.WriteLine($"年级: {gradeGroup.Key}");
foreach (var student in gradeGroup) {
Console.WriteLine($"姓名: {student.Name}, 年龄: {student.Age}");
}
}
// 过滤操作
var filteredStudents = students.Where(s => s.Age > 20); // 过滤出年龄大于20岁的学生
foreach (var student in filteredStudents) {
Console.WriteLine($"姓名: {student.Name}, 年龄: {student.Age}");
}
// 投影操作(改变对象的属性值)
var projectedStudents = students.Select(s => new Student { Name = s.Name, Age = s.Age + 1 }); // 将所有学生的年龄加1
foreach (var student in projectedStudents) {
Console.WriteLine($"姓名: {student.Name}, 年龄: {student.Age}");
}
12.1.3、集合的用途
数据存储:集合可以用来存储和管理大量数据,提供了灵活的数据结构来组织数据。
数据操作:通过集合提供的操作,可以对数据进行添加、删除、查找、排序等操作,方便对数据的处理和操作。
数据访问:通过集合,可以方便地访问和遍历其中的元素,支持各种数据检索和访问需求。
提高性能:使用集合可以有效地管理内存和避免重复创建相同对象,从而提高性能和效率。
代码复用:通过泛型集合,可以实现代码的复用,减少重复的代码编写工作量。
12.2、数组集合
12.2.1、数组的概念
数组是一个有序的数据集合,它包含固定数量的元素,每个元素都分配一个唯一的索引。数组中的每个元素都具有相同的类型,因此数组是一种类型安全的数据结构。
12.2.2、数组的创建与初始化
// 声明并初始化一个整型数组
int[] intArray = new int[] { 1, 2, 3, 4, 5 };
// 声明并初始化一个二维整型数组
int[,] int2DArray = new int[3, 3] { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };
12.2.3、数组的常见操作
访问元素:通过索引访问数组中的元素。
int[] intArray = new int[] { 1, 2, 3, 4, 5 };
int firstElement = intArray[0]; // 获取第一个元素,值为1
修改元素:通过索引修改数组中的元素。
int[] intArray = new int[] { 1, 2, 3, 4, 5 };
intArray[1] = 10; // 将第二个元素修改为10
遍历元素:通过循环遍历数组中的每个元素。
int[] intArray = new int[] { 1, 2, 3, 4, 5 };
foreach (int value in intArray) {
Console.WriteLine(value); // 输出每个元素的值:1、2、3、4、5
}
排序:可以使用Array.Sort()
方法对数组进行排序。
查找:可以使用Array.IndexOf()
方法查找特定元素在数组中的位置。
复制:可以使用Array.Copy()
方法将一个数组的元素复制到另一个数组中。
多维数组:可以声明和操作二维或更高维度的数组。
动态数组:使用List<T>
泛型集合类可以创建动态增长的数组。
12.2.4、数组与集合的关系
类型安全:与集合一样,数组是类型安全的,因为它们只能存储特定类型的元素。
有序性:与集合不同,数组中的元素具有有序性,可以通过索引访问和修改元素。
固定大小:与集合相比,数组的大小是固定的,一旦创建就不能改变大小。不过,可以使用List<T>
等集合类来模拟动态增长的行为。
12.3、泛型集合
12.3.1、泛型集合的概念
泛型集合是通过在集合类定义中包含类型参数来创建的。这个类型参数在实例化集合时被具体的类型替换。通过使用泛型集合,可以编写适用于任何数据类型的代码,而不是为每种数据类型编写特定代码。
12.3.2、泛型集合的常用接口
- IEnumerable:这是最常用的泛型接口,它表示一个可枚举的集合。通过实现这个接口,集合类提供了遍历元素的方法,如
GetEnumerator()
或foreach
循环。 - ICollection:这个接口扩展了
IEnumerable<T>
,并添加了一些用于操作集合中元素的方法,比如Add()
,Remove()
,Count
等。 - IList:这个接口也是
ICollection<T>
的扩展,它提供了一系列用于访问和修改列表中特定位置元素的方法。
12.3.3、泛型集合的使用
List<int> intList = new List<int>(); // 创建一个整数类型的List
intList.Add(1); // 添加元素
int firstElement = intList[0]; // 访问第一个元素
intList.RemoveAt(0); // 移除第一个元素
foreach (int value in intList) { // 遍历列表中的元素
Console.WriteLine(value); // 输出每个元素的值
}
12.3.4、泛型集合的优势
类型安全:使用泛型集合时,类型在编译时确定,减少了运行时的类型转换错误的可能性。
代码重用:通过编写泛型代码,你可以一次编写并在多种数据类型上重用代码,提高了代码的复用性。
性能优化:由于泛型集合在编译时知道具体的类型,因此它们可以生成特定于该类型的优化的代码。
更好的设计:使用泛型可以减少类、接口和方法之间的耦合度,使代码更加模块化和可维护。
可读性和可维护性:通过使用泛型,可以避免编写冗余的代码来处理不同数据类型的集合,从而提高代码的可读性和可维护性。
12.4、常用泛型集合类
12.4.1、ArrayList
12.4.1.1、概念
ArrayList
类提供了一个动态数组,该数组在运行时可以重新分配大小。
12.4.1.2、创建与使用
ArrayList<T> list = new ArrayList<T>();
list.Add(item); // 添加元素
T item = list[index]; // 通过索引访问元素
12.4.1.3、特点
ArrayList
提供了类似数组的功能,但动态地调整大小。适用于不经常修改大小且需要动态增长的集合。
12.4.2、LinkedList
12.4.2.1、概念
LinkedList<T>
类实现了双向链表数据结构。每个节点包含一个指向下一个节点的引用和一个指向前一个节点的引用。
12.4.2.2、创建与使用
LinkedList<T> list = new LinkedList<T>();
list.AddLast(item); // 在链表末尾添加元素
T item = list.First.Value; // 获取链表中的第一个元素
12.4.2.3、特点
适用于需要频繁地在集合的开头和结尾进行插入和删除操作的场景。
12.4.3、HashSet
12.4.3.1、概念
HashSet<T>
类实现了一个不包含重复元素的集合。它通过散列函数来存储元素,使得添加、删除和查找操作具有高效性能。
12.4.3.2、创建与使用
HashSet<T> set = new HashSet<T>();
set.Add(item); // 添加元素
bool contains = set.Contains(item); // 检查集合是否包含特定元素
12.4.3.3、特点
适用于需要快速检查元素是否存在、且不关心元素顺序的场景。
12.4.4、Queue
12.4.4.1、概念
Queue<T>
类实现了一个先进先出 (FIFO) 的集合。元素被添加到队列的末尾,并从队列的开头移除。
12.4.4.2、创建与使用
Queue<T> queue = new Queue<T>();
queue.Enqueue(item); // 将元素添加到队列末尾
T item = queue.Dequeue(); // 从队列头部移除并返回元素
12.4.4.3、特点
适用于需要按顺序处理元素的场景,比如任务调度或缓冲数据流。
12.4.5、Stack
12.4.5.1、概念
Stack<T>
类实现了一个后进先出 (LIFO) 的集合。最后一个添加到堆栈的元素总是第一个被移除的。
12.4.5.2、创建与使用
Stack<T> stack = new Stack<T>();
stack.Push(item); // 将元素添加到堆栈顶部
T item = stack.Pop(); // 从堆栈顶部移除并返回元素
12.4.5.3、特点
适用于需要按照相反顺序处理元素的场景,比如括号匹配或表达式求值。
12.5、自定义泛型集合类
可以通过创建一个泛型类来实现自定义的泛型集合。泛型集合类可以继承自IEnumerable<T>
接口或实现ICollection<T>
接口,或者可以自定义一个泛型接口,并让集合类实现这个接口。
public class MyGenericCollection<T> : IEnumerable<T>
{
private List<T> items = new List<T>();
public void Add(T item)
{
items.Add(item);
}
public void Remove(T item)
{
items.Remove(item);
}
public T this[int index]
{
get { return items[index]; }
set { items[index] = value; }
}
public int Count
{
get { return items.Count; }
}
public IEnumerator<T> GetEnumerator()
{
return items.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return items.GetEnumerator();
}
}
在这个例子中,MyGenericCollection<T>
类继承自IEnumerable<T>
接口,这意味着它自动实现了IEnumerable<T>
接口中的所有方法,包括GetEnumerator()
方法。这个类还实现了ICollection<T>
接口的Count
属性,并添加了自己的Add
和Remove
方法以及索引器。最后,我们覆盖了两个GetEnumerator
方法,一个是IEnumerable<T>
的版本,另一个是System.Collections.IEnumerable
的版本,这是为了确保所有实现都正确地工作。
要使用这个自定义的泛型集合类,可以像下面这样创建一个实例并添加元素:
MyGenericCollection<string> myCollection = new MyGenericCollection<string>();
myCollection.Add("Apple");
myCollection.Add("Banana");
myCollection.Add("Cherry");
foreach (string fruit in myCollection)
{
Console.WriteLine(fruit);
}
如果想自定义一个泛型接口,然后让集合类实现这个接口,可以这样做:
public interface IMyGenericCollection<T>
{
void Add(T item);
void Remove(T item);
T this[int index] { get; set; } // 假设你想让你的集合支持索引操作,但不需要展示删除功能。
int Count { get; } // 仅提供计数功能。实际使用中你可能想加入更多的方法或属性。
}
然后,可以让集合类实现这个接口:
public class MyGenericCollection<T> : IMyGenericCollection<T> // 实现自定义的泛型接口。
{
private List<T> items = new List<T>(); // 存储元素的私有成员变量。
// 实现接口中的所有方法。例如:
public void Add(T item) { items.Add(item); } // 添加元素的方法。
public void Remove(T item) { items.Remove(item); } // 移除元素的方法。注意:List<T>没有提供直接移除元素的方法,所以这里可能需要使用其他方式来实现。例如使用RemoveAt(int index)或者遍历列表来移除元素。
public T this[int index] { get { return items[index]; } set { items[index] = value; } } // 索引器实现。支持通过索引访问元素。注意:这里的索引是从0开始的整数,而且对于List<T>来说,索引越界会导致异常。你可能需要添加一些错误检查机制来处理越界问题。
public int Count { get { return items.Count; } } // 返回元素数量。注意:这里的Count属性仅仅返回存储在List<T>中的元素数量,如果你的集合还包含其他来源的元素或者有特殊计数逻辑,你可能需要修改这个属性的实现。
}
12.6、性能优化
减少内存分配:
使用List<T>
的Capacity
属性预先分配足够的空间,以减少元素添加时频繁的内存重新分配。
考虑使用Array
或Span<T>
来避免在处理大量数据时频繁的内存分配。
避免装箱和拆箱:
装箱是将值类型转换为引用类型的过程,拆箱则是相反的过程。尽量避免装箱和拆箱,尤其是在循环中。
使用值类型(如int
, float
, struct
等)而不是引用类型(如class
)。
使用缓存:
如果集合中的对象是可重用的,使用缓存来存储它们,避免重复创建相同的实例。
MemoryCache
类可用于创建内存中的缓存。
避免频繁的集合创建:
避免在循环中频繁创建新的集合实例。这可以通过重用现有集合或使用可变集合(如List<T>
)来完成。
使用适当的集合类型:
根据需求选择最合适的集合类型。例如,HashSet<T>
对于检查元素是否存在非常高效,而List<T>
则适用于需要顺序访问元素的场景。
使用并行处理:
对于可以并行处理的数据,使用Parallel
类来提高性能。例如,使用Parallel.ForEach
或Parallel.Invoke
。
减少锁的使用:
如果集合需要在多线程环境中使用,确保正确地管理锁以避免死锁和性能下降。考虑使用ConcurrentBag<T>
或ConcurrentQueue<T>
等线程安全的集合。
避免异常处理:
在循环或其他性能敏感代码中避免使用异常处理,因为异常可能会导致性能下降。如果可能,使用错误代码或其他机制来处理错误情况。
优化查询:
对于大型集合,优化查询操作可以提高性能。例如,对于需要频繁查找的场景,可以考虑使用字典结构(如Dictionary<TKey, TValue>
)来存储数据。
使用LINQ:
LINQ表达式通常会编译成高效的代码,但要确保你的LINQ查询不会产生大量的中间对象或导致其他性能问题。如果可能,尽量减少LINQ查询的复杂性或将其与循环结合使用。
利用值类型和引用类型的差异:
根据需要选择正确的数据类型。值类型通常更快,因为它们存储在栈上而不是堆上,但它们的大小可能更大。另一方面,引用类型通常占用更少的空间,但它们需要额外的内存管理开销。
优化字符串操作:
如果你在集合中处理大量字符串,考虑使用字符串池(String Interning)来重用字符串实例,以减少内存分配和垃圾收集的开销。
减少不必要的装箱和拆箱操作:
如果你的集合包含值类型,但你经常将它们转换为引用类型(例如传递给需要引用类型的API),这可能会导致装箱和拆箱操作。考虑重新设计API或数据结构以避免这些操作。
避免不必要的复制操作:
在某些情况下,集合的复制操作可能是昂贵的。如果可能,尽量在原始集合上进行操作而不是复制它。例如,如果你需要对集合进行排序或筛选操作,而不需要修改原始集合,你可以使用LINQ的OrderBy
或Where
方法而不是复制整个集合。
合理使用异步编程模型:
对于I/O密集型操作(如文件读写、数据库查询等),异步编程可以提高性能并减少线程阻塞。使用C#的异步方法(如async
和await
关键字)来编写异步代码,并确保正确地管理异步操作的取消、异常处理和资源释放。
12.7、异常处理
12.7.1、ArgumentNullException
当集合的参数为null时,可能会抛出此异常。例如,调用一个需要非null参数的集合方法时传入null。
处理方法:在调用集合方法之前,检查参数是否为null,如果是,则抛出异常或采取其他适当的措施。
12.7.2、ArgumentOutOfRangeException
当传递给集合方法的参数超出有效范围时,可能会抛出此异常。例如,尝试访问数组中不存在的索引。
处理方法:验证参数的有效性,确保它们在预期的范围内。
12.7.3、InvalidOperationException
当集合处于无效状态,无法执行某个操作时,可能会抛出此异常。例如,尝试对只读集合进行修改操作。
处理方法:检查集合的状态,确保它处于可以执行所需操作的状态。
12.7.4、KeyNotFoundException
当尝试访问字典或其他键值对集合中不存在的键时,可能会抛出此异常。
处理方法:在访问键之前检查键是否存在于集合中,或者使用TryGetValue等不会抛出异常的方法。
12.7.5、ConcurrentModificationException
当多个线程同时修改集合时,可能会抛出此异常。
处理方法:使用线程安全的集合类型(如ConcurrentBag<T>
),或在使用集合时使用锁来同步对集合的访问。
12.7.6、NotSupportedException
当集合不支持某个操作时,可能会抛出此异常。例如,尝试对只读集合进行修改操作。
处理方法:检查集合是否支持所需的操作,如果不支持,则抛出异常或采取其他适当的措施。
12.7.7、NullReferenceException
当引用类型为null时,访问其成员或调用其方法时可能会抛出此异常。
处理方法:在访问引用类型的成员或方法之前,检查引用是否为null。
12.8、集合的高级特性
12.8.1、LINQ 查询操作符
LINQ 是一组在 C# 中用于查询和操作数据的语言结构,它允许你使用类似于 SQL 的语法来查询各种数据源,如集合、数据库等。LINQ 提供了一组查询操作符,可以用于筛选、排序、投影等操作。
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = from num in numbers
where num % 2 == 0
orderby num descending
select num;
foreach (var result in query)
{
Console.WriteLine(result);
}
12.8.2、Lambda 表达式
Lambda 表达式是 C# 中的一种简洁声明匿名函数的方式,它可以在代码中定义一个小型的匿名函数,并直接在代码中使用。Lambda 表达式可以用于 LINQ 查询中,作为查询操作符的参数。
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = numbers.Where(num => num % 2 == 0);
foreach (var result in query)
{
Console.WriteLine(result);
}
12.8.3、延迟执行和立即执行
延迟执行和立即执行是 LINQ 中的两个重要概念。延迟执行意味着 LINQ 查询在执行时才执行,而不是在声明时执行。这意味着你可以创建一个 LINQ 查询,并在需要时多次执行它。而立即执行则意味着 LINQ 查询在声明时立即执行,并且只会执行一次。
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = numbers.Where(num => num % 2 == 0); // 延迟执行,此时没有执行筛选操作
query.ToList(); // 立即执行,将筛选结果转换为列表并返回结果
十三、Lambda表达式
13.1、Lambda表达式的基本概念
Lambda表达式是一种简洁的匿名函数表示方式,可以用于编写简洁、可读性强的代码。它常用于LINQ查询操作和委托回调。
13.2、Lambda表达式的语法规则
Lambda表达式由Lambda运算符(=>)和参数列表以及返回值或语句块组成。Lambda表达式的语法形式为“参数 => 返回值”或“参数 => { 语句块 }”。
一个没有参数的 Lambda 表达式:
() => Console.WriteLine("Hello, World!")
一个有一个参数的 Lambda 表达式:
x => x * x
一个有两个参数的 Lambda 表达式:
(x, y) => x + y
一个包含多个语句的 Lambda 表达式:
x => { Console.WriteLine("Statement 1: " + x); return x * x; }
一个捕获变量并使用该变量的 Lambda 表达式:
int x = 10;
a => a + x
13.3、Lambda表达式的应用场景
- 函数式编程:在函数式编程范式中,函数被当作一等公民,可以作为参数传递给其他函数,或者从函数中返回。Lambda 表达式可以让我们更加方便地定义简单的函数。例如在 Python 中使用 map、filter 等函数进行数据处理时,可以传入 lambda 表达式作为参数来定义数据处理逻辑。
- 排序:在很多编程场景中,我们需要对数据进行排序。Lambda 表达式可以与排序函数结合,快速地定义排序规则。例如在 Python 中,使用 sorted 函数对列表进行排序时,可以传入 lambda 表达式作为 key 参数,定义排序的规则。
- 列表迭代:如果只需要调用单个函数对列表元素进行处理,可以使用 lambda 表达式。例如在 Java 的 Predicate 接口中,可以很方便地用于过滤。如果需要对多个对象进行过滤并执行相同的处理逻辑,可以将这些相同的操作封装到 filter 方法中,由调用者提供过滤条件,以便重复使用。
- 事件监听:在一些事件驱动的编程模型中,可以使用 lambda 表达式来简化事件处理器的编写。
- 在 LINQ 中使用:Lambda 表达式在 LINQ(Language Integrated Query)查询中扮演着重要角色,可以用于筛选、排序、投影等操作。
- 回调函数和高阶函数:Lambda 表达式可以作为回调函数使用,例如在异步编程或定时任务中。
- 匿名方法:在没有必要创建具名方法的场景中,可以使用 lambda 表达式代替。
- 组合逻辑:当需要组合多个逻辑条件时,可以使用 lambda 表达式来简洁地表达复杂的逻辑关系。
- 并行计算:在并行计算中,lambda 表达式可以用于定义任务和分发工作负载。
13.4、Lambda表达式与委托的关系
Lambda表达式可以隐式转换为委托类型,如果Lambda表达式没有返回值,则转换为Action委托类型;如果有返回值,则转换为Func委托类型。
13.5、Lambda表达式与匿名函数的关系
Lambda表达式实际上是一种匿名函数,可以用于代替传统的显式声明函数的方式,使代码更加简洁。
13.6、Lambda表达式的优缺点
Lambda表达式可以使代码更加简洁、可读性更强,但也可能会使代码难以理解,特别是对于初学者。因此,在使用Lambda表达式时需要注意适度。
13.7、Lambda表达式的扩展方法
除了基本语法外,C# 还提供了多种扩展方法来进一步简化 Lambda 表达式的编写,如使用 Lamda 捕捉变量、使用 out/ref 参数、使用默认参数等。
13.8、LINQ与Lambda表达式的关系
- 互补性:LINQ 提供了一种统一的查询语法和编程模型,用于查询和操作各种数据源,如对象集合、数据库、XML 文档等。Lambda 表达式是一种简洁而强大的编程语法,可以用于创建匿名函数或委托。在实际开发中,通过查询结果或数据源进行方法调用,从而进行更多的查询操作。由于 Lambda 表达式是匿名函数,它可以赋值到一个委托,而在 IEnumerable 接口的方法中很多通过函数委托来实现自定义运算、条件等操作,所以 Lambda 表达式在 LINQ 中被广泛使用。
- 易于使用:Lambda 表达式提供了简洁的语法,使得内联的、即时执行的函数编写变得简单。它使得编写类似于 SQL 语句的查询表达式成为可能,实现数据查询的功能。在 LINQ 中使用 Lambda 表达式可以方便地构建查询表达式,而无需显式定义命名函数或委托。
- 运行机制:LINQ 是一种在 .NET 平台上集成查询功能的技术,它不仅仅用于查询数据库,还可以用于查询集合、XML 文档、对象等。Lambda 表达式则是一种表达式,用于创建匿名函数或委托。在运行时,表达式树可以解析和操作,允许动态地生成、转换和执行代码。这在一些需要在运行时生成查询或转换逻辑的场景中非常有用。
十四、数据库操作
以下所有例子均使用SQL server,因为内容比较多,这里只简单介绍,后面新开一篇详细介绍C#和数据库的交互
14.1、ADO.NET
14.1.1、ADO.NET基础
14.1.1.1、ADO.NET的基本概念和组成部分
1.数据提供程序(Data Providers)
数据提供程序是一组类库,用于连接到数据库、执行命令和检索数据。ADO.NET提供了多种数据提供程序,例如SqlClient用于连接和操作SQL Server数据库,OleDb用于连接和操作其他数据库,如Access、Oracle等。
2.对象模型
ADO.NET的对象模型包括几个核心的类,这些类提供用于连接到数据库、执行SQL命令以及读取结果的方法和属性。主要包括以下部分:
Connection
类:负责建立和管理数据库连接。Command
类:用于执行SQL命令或存储过程。DataReader
类:用于读取从数据库返回的结果集。DataAdapter
类:用于在数据源和DataSet之间进行桥接。
3.DataSet
DataSet是一个内存中的数据表示,它可以包含多个DataTable对象,这些对象表示从数据库检索出来的表。DataSet还可以包含数据列、数据行、主键、外键等关系信息。DataSet是独立于任何特定数据源的,可以与多种数据源进行交互。
4.XML
ADO.NET与XML紧密集成,可以通过XML格式在服务器和客户端之间传输数据,也可以将数据转换为XML格式进行存储或传输。
14.1.1.2、如何使用Connection对象建立与数据库的连接
使用Connection对象建立与数据库的连接通常包括以下几个步骤:
1.引入相应的命名空间:根据所使用的数据库类型,需要引入对应的命名空间。例如,对于SqlClient数据提供程序,需要引入System.Data.SqlClient
。
using System.Data.SqlClient; // 如果是SqlClient数据提供程序
2.创建Connection对象并指定连接字符串:连接字符串包含了连接到数据库所需的所有信息,如服务器名称、数据库名称、用户名和密码等。
string connectionString = "Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;";
using (SqlConnection connection = new SqlConnection(connectionString))
{
// 连接建立后可以进行数据库操作...
} // 这里会自动关闭连接
3.打开Connection对象:使用Open()
方法打开数据库连接。
connection.Open(); // 打开连接以进行数据库操作
4.执行操作并关闭连接:完成数据库操作后,应关闭Connection对象以释放资源。如果使用using
语句创建Connection对象,则在代码块结束时会自动关闭连接。
connection.Close(); // 关闭连接释放资源
5.异常处理:在实际应用中,应该添加异常处理逻辑来捕获和处理可能发生的异常情况。例如,可以使用try-catch语句来捕获和处理异常。
try
{
// 尝试打开连接和执行操作...
}
catch (SqlException ex)
{
// 处理异常...
}
14.1.2、使用实例
using System;
using System.Data;
using System.Data.SqlClient;
class Program
{
static void Main()
{
string connectionString = "Data Source=YourServerName;Initial Catalog=YourDatabaseName;Integrated Security=True";
using (SqlConnection connection = new SqlConnection(connectionString))
{
// 打开连接
connection.Open();
// 查询示例
string query = "SELECT * FROM YourTable";
using (SqlCommand command = new SqlCommand(query, connection))
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine($"{reader["ColumnName1"]}, {reader["ColumnName2"]}");
}
}
// 插入示例
string insertQuery = "INSERT INTO YourTable (Column1, Column2) VALUES (@Value1, @Value2)";
using (SqlCommand insertCommand = new SqlCommand(insertQuery, connection))
{
insertCommand.Parameters.AddWithValue("@Value1", "Value1");
insertCommand.Parameters.AddWithValue("@Value2", "Value2");
insertCommand.ExecuteNonQuery();
}
// 更新示例
string updateQuery = "UPDATE YourTable SET Column1 = @Value1 WHERE Column2 = @Value2";
using (SqlCommand updateCommand = new SqlCommand(updateQuery, connection))
{
updateCommand.Parameters.AddWithValue("@Value1", "NewValue1");
updateCommand.Parameters.AddWithValue("@Value2", "ConditionValue");
updateCommand.ExecuteNonQuery();
}
// 删除示例
string deleteQuery = "DELETE FROM YourTable WHERE Column1 = @Value1";
using (SqlCommand deleteCommand = new SqlCommand(deleteQuery, connection))
{
deleteCommand.Parameters.AddWithValue("@Value1", "ValueToDelete");
deleteCommand.ExecuteNonQuery();
}
// 执行存储过程示例
string storedProcedureName = "YourStoredProcedureName";
using (SqlCommand storedProcedureCommand = new SqlCommand(storedProcedureName, connection))
{
storedProcedureCommand.CommandType = CommandType.StoredProcedure; // 表示这是一个存储过程
storedProcedureCommand.Parameters.AddWithValue("@Parameter1", "ParameterValue"); // 添加参数,如果存储过程不需要参数则不添加
storedProcedureCommand.ExecuteNonQuery(); // 执行存储过程,如果存储过程返回结果集,则使用 ExecuteReader() 方法代替
}
// 执行函数示例(返回单一值)
string functionQuery = "SELECT dbo.YourFunctionName(@Parameter)"; // 使用正确的函数名和参数格式替换这里的内容
using (SqlCommand functionCommand = new SqlCommand(functionQuery, connection))
{
functionCommand.Parameters.AddWithValue("@Parameter", "ParameterValue"); // 添加参数,如果函数不需要参数则不添加
object result = functionCommand.ExecuteScalar(); // 执行函数,获取返回的单一值(例如,自增计数器)
Console.WriteLine(result); // 输出结果,根据函数返回类型进行相应处理(例如,转换为int类型)
}
}
}
}
14.2、EF(Entity Framework)
14.2.1、ORM和EF的基本概念
ORM(Object-Relational Mapping): 对象关系映射,是一种将关系型数据库的数据映射成对象的方法。通俗地说,就是用操作对象的方式来操作数据库。
Entity Framework: 微软官方的ORM框架,基于ADO.NET,支持多种数据库。
14.2.2、EF的三种模式
数据库优先(Database First): 通过设计数据库,然后生成实体类和数据上下文。
模型优先(Model First): 在Visual Studio中通过设计界面来创建实体类和数据库关系。
代码优先(Code First): 通过编写C#代码来定义实体类和数据上下文,然后根据这些代码来创建数据库。
14.2.3、EF的架构
EDM(Entity Data Model): 包括概念模型、映射和存储模型。概念模型定义了实体类及其之间的关系。
Entity Client Data Provider: 负责将L2E或Entity SQL转换为数据库可以识别的SQL查询语句。
ADO.NET Data Provider: 使用标准的ADO.NET与数据库进行通信。
14.2.4、EF常用表达式语句
基础查询:
var query = dbContext.Set<TEntity>().Where(condition);
选择(Select):用于将查询结果转换为新的对象或匿名类型。
var query = dbContext.Set<TEntity>().Select(item => new { Property1 = item.Property1, Property2 = item.Property2 });
排序(OrderBy, OrderByDescending, ThenBy, ThenByDescending):对结果进行排序。
var query = dbContext.Set<TEntity>().OrderBy(item => item.Property);
var queryDescending = dbContext.Set<TEntity>().OrderByDescending(item => item.Property);
var queryThenBy = dbContext.Set<TEntity>().ThenBy(item => item.OtherProperty);
var queryThenByDescending = dbContext.Set<TEntity>().ThenByDescending(item => item.OtherProperty);
分页(Skip, Take):跳过指定数量的元素并获取指定数量的元素。
var query = dbContext.Set<TEntity>().Skip(number).Take(number);
分组(GroupBy):根据一个或多个键将结果分组。
var query = dbContext.Set<TEntity>().GroupBy(item => item.Key);
连接(Join):将两个集合基于某个键进行连接。
var query = dbContext.Set1.Join(dbContext.Set2, item1 => item1.Key, item2 => item2.Key, (item1, item2) => new { Item1 = item1, Item2 = item2 });
元素值检查(Any, All, FirstOrDefault, SingleOrDefault):检查集合中是否存在满足条件的元素。
var hasAny = dbContext.Set<TEntity>().Any(item => condition); // 检查集合是否为空。
var allMatch = dbContext.Set<TEntity>().All(item => condition); // 检查所有元素是否满足条件。
var firstItem = dbContext.Set<TEntity>().FirstOrDefault(); // 获取第一个元素,如果没有则返回默认值。
var singleItem = dbContext.Set<TEntity>().SingleOrDefault(); // 获取唯一元素,如果没有则返回默认值。
聚合函数(Sum, Average, Min, Max):对集合中的元素进行聚合计算。
var sum = dbContext.Set<TEntity>().Sum(item => item.Value); // 计算集合中所有元素的和。
var average = dbContext.Set<TEntity>().Average(item => item.Value); // 计算集合中所有元素的平均值。
var minValue = dbContext.Set<TEntity>().Min(item => item.Value); // 找到集合中的最小值。
var maxValue = dbContext.Set<TEntity>().Max(item => item.Value); // 找到集合中的最大值。
14.2.5、EF的使用
设置数据库上下文和实体类
假设我们有一个简单的Users
表:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
public class AppDbContext : DbContext
{
public DbSet<User> Users { get; set; }
// 其他的DbSets和配置可以按需添加
}
执行CRUD操作
using (var context = new AppDbContext())
{
// 创建实体对象
var user = new User { Name = "张三", Email = "zhangsan@example.com" };
// 添加到数据库
context.Users.Add(user);
context.SaveChanges(); // 保存更改到数据库
// 查询数据
var allUsers = context.Users.ToList(); // 获取所有用户数据
var userById = context.Users.Find(1); // 通过ID查找用户数据
var userByName = context.Users.FirstOrDefault(u => u.Name == "张三"); // 通过名称查找用户数据
// 更新数据
user.Email = "zhangsan@example.com"; // 修改用户数据
context.SaveChanges(); // 保存更改到数据库
// 删除数据
context.Users.Remove(user); // 删除用户数据
context.SaveChanges(); // 保存更改到数据库
}
执行存储过程
首先,在数据库中创建一个简单的存储过程:
CREATE PROCEDURE GetUserCount @Count INT OUTPUT AS
BEGIN
SELECT @Count = COUNT(*) FROM Users
END;
然后在C#中调用它:
using (var context = new AppDbContext())
{
var outputParameter = new SqlParameter();
outputParameter.ParameterName = "@Count";
outputParameter.SqlDbType = SqlDbType.Int;
outputParameter.Direction = ParameterDirection.Output;
var result = context.Database.SqlQuery<int>("GetUserCount @Count", outputParameter).ToList();
}
14.3、LINQ
14.3.1、LINQ的概念
LINQ(Language Integrated Query)是一种强大的查询语言,它允许开发人员在C#代码中使用类似于SQL的语法来查询各种数据源,包括内存中的集合、数据库、XML文档等。
LINQ提供了一种统一的方法来查询和操作数据,无论数据存储在何处。它基于表达式编程,允许使用声明性语法来表示查询,从而使代码更易于阅读和理解。
14.3.2、常见查询表达式语法
基础查询(From):
var query = from item in collection
where condition
select item;
选择(Select):用于投影或转换查询结果中的每个元素。
var query = from item in collection
select item.Property;
排序(OrderBy, OrderByDescending, ThenBy, ThenByDescending):根据一个或多个键对结果进行排序。
var query = from item in collection
orderby item.Property ascending/descending
select item;
分页(Skip, Take):跳过指定数量的元素并获取指定数量的元素。
var query = from item in collection
skip number
take number
select item;
分组(GroupBy):根据一个或多个键将结果分组。
var query = from item in collection
groupby property into group
select new { Group = group, Count = group.Count() };
连接(Join):将两个集合基于某个键进行连接。
var query = from item1 in collection1
join item2 in collection2 on item1.Key equals item2.Key
select new { Item1 = item1, Item2 = item2 };
包含与排除(Contains, NotContains):检查某个元素是否存在于集合中。
var query = from item in collection where someItem.Contains(item) select item;
元素值检查(Any, All, FirstOrDefault, SingleOrDefault):用于检查集合中是否存在满足条件的元素。
var hasAny = collection.Any(item => condition); // 检查是否存在任何元素满足条件。
var allMatch = collection.All(item => condition); // 检查所有元素是否满足条件。
var firstItem = collection.FirstOrDefault(item => condition); // 获取第一个满足条件的元素,如果没有则返回默认值。
var singleItem = collection.SingleOrDefault(item => condition); // 获取唯一满足条件的元素,如果没有则返回默认值。
聚合函数(Sum, Average, Min, Max):对集合中的元素进行聚合计算。
var sum = collection.Sum(item => item.Value); // 计算集合中所有元素的和。
var average = collection.Average(item => item.Value); // 计算集合中所有元素的平均值。
var minValue = collection.Min(item => item.Value); // 找到集合中的最小值。
var maxValue = collection.Max(item => item.Value); // 找到集合中的最大值。
十五、多线程
15.1、基础概念
15.1.1 、线程和进程
进程:是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
线程:是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。
是不是有点抽象?没关系,我们拿王者荣耀来举个例子,比如我现在开了一把游戏,我这把游戏就可以看成是一个进程,而在这把游戏里的英雄,小兵,野怪等局内的元素就可以看作一个个线程。所以,多线程的概念也就呼之欲出了:一个进程里可以有多个线程就叫多线程。
需要注意的是,进程和线程还有下面这些特点:
1、线程在进程下进行
(单独的英雄角色、野怪、小兵肯定不能运行)
2、进程之间不会相互影响,主线程结束将会导致整个进程结束
(你和你朋友没人开一把游戏,你超神了对他那局游戏也起不到任何影响,水晶(主线程)推了游戏就结束了)
3、不同的进程数据很难共享
(两局游戏之间很难有联系,你无法在你的那局游戏里看到另外一局游戏的数据,除非王者出bug(串麦!!!))
4、同进程下的不同线程之间数据很容易共享
(同一局游戏里你可以看到队友和对手状态,血量,装备)
5、进程使用内存地址可以限定使用量
(三排、五排房间满了以后就没法再邀请玩家进如,只能有玩家退出房间后才能进入)
15.1.2 、线程的生命周期
线程的生命周期可以分为以下几个阶段:
创建(Created):线程对象被创建,但尚未开始执行。
就绪(Ready):线程已经准备好执行,但还没有被调度执行。
运行(Running):线程正在执行其任务。
阻塞(Blocked):线程被阻塞,暂时停止执行,等待某个条件满足或者等待某个资源可用。
终止(Terminated):线程执行完毕或者被强制终止,结束其生命周期。
在正常情况下,线程的生命周期是从创建开始,经过就绪、运行和阻塞等状态,最终结束于终止状态。然而,线程的生命周期也可能被一些特殊情况中断或改变,例如线程被中断、线程异常终止等
需要注意的是,线程的生命周期是由操作系统的调度器控制的,具体的状态转换和调度行为可能因操作系统的不同而有所差异。在C#中,可以使用Thread类来创建和管理线程,通过调用Thread类的方法和属性可以控制线程的生命周期。
15.1.3、 线程同步和互斥
线程同步:线程同步是指多个线程之间协调和同步执行的机制。由于多个线程同时访问共享资源可能会导致数据不一致或竞态条件等问题,因此需要使用线程同步来确保线程之间的顺序和正确性。
线程互斥:线程互斥是指多个线程对共享资源的访问进行协调和控制,以避免并发访问导致的数据不一致或竞态条件等问题。
*C#提供了多种机制来实现线程互斥,其中最常用的是互斥锁(Mutex)和监视器(Monitor)。
15.2、多线程的使用
15.2.1 、Thread类
15.2.1.1、常用属性
- Name:获取或设置线程的名称。可以用于标识和区分不同的线程。
- IsAlive:获取一个值,指示线程是否处于活动状态。
- IsBackground:获取或设置一个值,指示线程是否为后台线程。后台线程在主线程结束时会自动终止。
- Priority:获取或设置线程的优先级。可以设置为ThreadPriority枚举中的一个值,如ThreadPriority.Lowest(最低)、ThreadPriority.BelowNormal(低于正常)、ThreadPriority.Normal(正常)、ThreadPriority.AboveNormal(高于正常)、ThreadPriority.Highest(最高)。
- ThreadState:获取线程的当前状态,返回一个ThreadState枚举中的值,如ThreadState.Running、ThreadState.Stopped、ThreadState.WaitSleepJoin等。
- CurrentThread:获取当前正在执行的线程的Thread对象。
- ManagedThreadId:获取线程的唯一标识符。
- ApartmentState:获取或设置线程的单元状态。可以设置为
- ApartmentState枚举中的一个值,如ApartmentState.STA(单线程单元状态)或ApartmentState.MTA(多线程单元状态)。
15.2.1.2、常用方法
(1). start():启动线程,使其进入可运行状态。
(2). run():定义线程的执行逻辑,需要在子类中重写。
(3). sleep(long millis):使当前线程休眠指定的毫秒数。
(4). join():等待该线程执行完毕。
(5). interrupt():中断线程,给线程发送一个中断信号。
(6). isInterrupted():判断线程是否被中断。
(7). getName():获取线程的名称。
(8). setName(String name):设置线程的名称。
(9). isAlive():判断线程是否还活着。
(10). yield():暂停当前正在执行的线程,让其他线程有机会执行。
(11). setPriority(int priority):设置线程的优先级。
(12). getPriority():获取线程的优先级。
(13). currentThread():获取当前正在执行的线程对象。
(14). getState():获取线程的状态。
(15). setDaemon(boolean on):设置线程是否为守护线程。
*这些方法可以通过Thread类的实例对象调用,用于控制线程的执行和状态。
using System;
using System.Threading;
class Program
{
static void Main()
{
// 创建两个线程
Thread thread1 = new Thread(PrintThreadId);
Thread thread2 = new Thread(PrintThreadId);
// 启动线程
thread1.Start();
thread2.Start();
// 等待所有线程完成
thread1.Join();
thread2.Join();
Console.WriteLine("所有线程已完成。");
}
// 线程要执行的函数
static void PrintThreadId()
{
Console.WriteLine($"线程ID: {Thread.CurrentThread.ManagedThreadId}");
}
}
15.2.2 、ThreadPool
15.2.2.1、ThreadPool类的使用方法
QueueUserWorkItem:将工作项添加到线程池的队列中,以便由线程池中的线程执行。
ThreadPool.QueueUserWorkItem(DoWork, data);
GetMaxThreads:获取线程池允许的最大工作线程数和最大异步 I/O 线程数。
int maxWorkerThreads, maxIOThreads;
ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxIOThreads);
GetMinThreads:获取线程池的最小工作线程数和最小异步 I/O 线程数。
int minWorkerThreads, minIOThreads;
ThreadPool.GetMinThreads(out minWorkerThreads, out minIOThreads);
SetMaxThreads:设置线程池允许的最大工作线程数和最大异步 I/O 线程数。
ThreadPool.SetMaxThreads(maxWorkerThreads, maxIOThreads);
SetMinThreads:设置线程池的最小工作线程数和最小异步 I/O 线程数。
ThreadPool.SetMinThreads(minWorkerThreads, minIOThreads);
GetAvailableThreads:获取线程池中可用的工作线程数和可用的异步 I/O 线程数。
int availableWorkerThreads, availableIOThreads;
ThreadPool.GetAvailableThreads(out availableWorkerThreads, out availableIOThreads);
UnsafeQueueUserWorkItem:与 QueueUserWorkItem 类似,但不会捕获异常。
ThreadPool.UnsafeQueueUserWorkItem(DoWork, data);
15.2.2.2、如何使用线程池来管理和复用线程资源
使用线程池来管理和复用线程资源,以提高多线程程序的性能和效率。以下是使用线程池的步骤:
1.使用ThreadPool类的静态方法QueueUserWorkItem来将工作项添加到线程池中。例如:
ThreadPool.QueueUserWorkItem(DoWork, data);
*其中,DoWork是一个方法,用于执行具体的工作任务,data是传递给工作方法的参数。
2.定义工作方法,该方法会在线程池中的线程上执行。例如:
private static void DoWork(object state)
{
// 执行具体的工作任务
}
3.可以使用WaitHandle类的实现类(如ManualResetEvent、AutoResetEvent)来等待线程池中的工作项完成。例如:
ManualResetEvent waitHandle = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem(DoWork, waitHandle);
// 等待工作项完成
waitHandle.WaitOne();
4.可以使用ThreadPool.GetAvailableThreads方法获取线程池中可用的线程数量。例如:
int workerThreads;
int completionPortThreads;
ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
5.可以使用ThreadPool.SetMaxThreads方法设置线程池中的最大线程数量。例如:
int workerThreads = 100;
int completionPortThreads = 100;
ThreadPool.SetMaxThreads(workerThreads, completionPortThreads);
通过设置最大线程数量,可以控制线程池中的线程数量,避免线程过多导致系统资源耗尽。
使用线程池可以方便地管理和复用线程资源,避免频繁地创建和销毁线程,提高多线程程序的性能和效率。但需要注意,线程池中的线程是共享的,如果某个工作项执行时间过长,可能会影响其他工作项的执行。因此,在使用线程池时,需要合理安排工作项的执行顺序和时间,避免长时间占用线程池中的线程。
15.2.2.3、如何提交任务
使用 ThreadPool 类的 QueueUserWorkItem 方法来提交任务到线程池。以下是一个示例:
// 定义一个方法来执行任务
void DoWork(object state)
{
// 执行任务的代码
Console.WriteLine("Task executed: " + state.ToString());
}
// 提交任务到线程池
ThreadPool.QueueUserWorkItem(DoWork, "Task 1");
ThreadPool.QueueUserWorkItem(DoWork, "Task 2");
ThreadPool.QueueUserWorkItem(DoWork, "Task 3");
在上面的示例中,我们定义了一个名为 DoWork 的方法来执行任务。然后,我们使用 ThreadPool 的 QueueUserWorkItem 方法将任务提交到线程池。每个任务都会在线程池中的一个可用线程上执行。
在 QueueUserWorkItem 方法中,第一个参数是要执行的方法,第二个参数是传递给方法的参数。在上面的示例中,我们将字符串作为参数传递给 DoWork 方法。
当任务被提交到线程池时,线程池会自动选择一个可用的线程来执行任务。任务的执行顺序和线程的分配是由线程池来管理的。
15.2.2.4、如何设置最大线程数
使用 ThreadPool 类的 SetMaxThreads 方法来设置线程池的最大线程数。以下是一个示例:
// 设置线程池的最大线程数
int workerThreads;
int completionPortThreads;
ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads);
ThreadPool.SetMaxThreads(10, completionPortThreads);
在上面的示例中,我们首先使用 ThreadPool 的 GetMaxThreads 方法获取当前线程池的最大线程数。然后,我们使用 ThreadPool 的 SetMaxThreads 方法来设置新的最大线程数。在这里,我们将最大线程数设置为 10,而保持 completionPortThreads 不变。
需要注意的是,SetMaxThreads 方法的第一个参数是工作线程的最大数目,第二个参数是异步 I/O 线程的最大数目。在大多数情况下,我们只需要设置工作线程的最大数目,而将异步 I/O 线程的最大数目保持不变。
请注意,设置线程池的最大线程数是一个全局设置,会影响整个应用程序中使用线程池的所有任务。因此,需要谨慎设置最大线程数,以避免过多的线程导致性能问题。
15.2.3、 Task类
15.2.3.1、常用属性
- IsCompleted:一个只读属性,表示任务是否已完成。
- IsCanceled:一个只读属性,表示任务是否已取消。
- IsFaulted:一个只读属性,表示任务是否执行失败。
- Status:一个只读属性,表示任务的当前状态(如Running、WaitingForActivation、WaitingForChildrenToComplete、RanToCompletion或Canceled)。
- Exception:一个只读属性,表示任务执行期间发生的异常(如果有)。
- Result:一个只读属性,表示任务的结果(如果任务已完成)。
15.2.3.2、常用方法
- Start():启动任务,使其开始执行。
- Wait():等待任务完成。
- Wait(TimeSpan):等待任务完成或直到指定的时间间隔过去。
- WaitAll(Task[]):等待一组任务全部完成。
- WaitAny(Task[]):等待一组任务中任意一个完成。
- ContinueWith(Action<Task, Task>):在任务完成后,以指定的委托继续执行另一个任务。
- Delay(TimeSpan):使当前线程在指定的时间间隔后继续执行。
- WhenAll(Task[]):返回一个新的Task,该任务将在所有指定的任务完成后完成。
- WhenAny(Task[]):返回一个新的Task,该任务将在任意指定的任务完成后完成。
- ConfigureAwait(bool):配置当前任务的Await操作,指定是否捕捉原始Synchronization Context。
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// 创建两个任务
Task task1 = Task.Run(() => PrintTaskId("Task 1"));
Task task2 = Task.Run(() => PrintTaskId("Task 2"));
// 等待所有任务完成
task1.Wait();
task2.Wait();
Console.WriteLine("所有任务已完成。");
}
// 任务要执行的函数
static void PrintTaskId(string taskName)
{
Console.WriteLine($"任务ID: {Task.CurrentId} - 任务名称: {taskName}");
}
}
15.2.4 、异步方法(async/await)
async
和await
关键字被用来简化异步编程。使用async
关键字声明一个方法是异步的,而await
关键字用来等待一个异步操作完成。下面是一个使用async
和await
的简单示例:
假设我们有一个模拟的网络请求,使用HttpClient类:
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
// 模拟网络请求
HttpClient client = new HttpClient();
string url = "http://www.example.com";
string result = await client.GetStringAsync(url);
Console.WriteLine(result);
}
}
在这个例子中,我们创建了一个HttpClient实例并使用它来发送一个GET请求。GetStringAsync
方法是一个异步方法,它会立即返回,而实际的网络请求将在后台异步执行。然后我们使用await
关键字等待这个异步操作完成,并获取返回的结果。这个例子中的Main
方法也是异步的,因为它被声明为async
。
15.3、线程同步和互斥
线程同步是指多个线程之间协调和同步执行的机制。由于多个线程同时访问共享资源可能会导致数据不一致或竞态条件等问题,因此需要使用线程同步来确保线程之间的顺序和正确性。
线程互斥是指多个线程对共享资源的访问进行协调和控制,以避免并发访问导致的数据不一致或竞态条件等问题。
C#提供了多种机制来实现线程互斥,其中最常用的是互斥锁(Mutex)和监视器(Monitor)。
15.3.1 、使用锁(lock)机制实现线程同步
lock关键字用于在代码块中创建一个互斥锁,确保只有一个线程可以进入该代码块。当一个线程进入lock代码块时,其他线程将被阻塞,直到该线程退出lock代码块。
using System;
using System.Threading;
public class Counter
{
private int count = 0;
private readonly object lockObject = new object();
public void Increment()
{
lock (lockObject)
{
count++;
Console.WriteLine($"Count after increment: {count}");
}
}
}
public class Program
{
public static void Main()
{
Counter counter = new Counter();
Thread t1 = new Thread(new ThreadStart(delegate { counter.Increment(); }));
Thread t2 = new Thread(new ThreadStart(delegate { counter.Increment(); }));
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
}
15.3.2 、Monitor类
Monitor类提供了一些静态方法,用于实现线程同步和互斥。其中最常用的方法是Enter和Exit方法,用于在代码块中获取和释放锁。
using System;
using System.Threading;
public class Counter
{
private int count = 0;
private readonly object lockObject = new object();
public void Increment()
{
Monitor.Enter(lockObject);
try
{
count++;
Console.WriteLine($"Count after increment: {count}");
}
finally
{
Monitor.Exit(lockObject);
}
}
}
public class Program
{
public static void Main()
{
Counter counter = new Counter();
Thread t1 = new Thread(new ThreadStart(delegate { counter.Increment(); }));
Thread t2 = new Thread(new ThreadStart(delegate { counter.Increment(); }));
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
}
15.3.3、 信号量(Semaphore)和互斥体(Mutex)
信号量用于控制同时访问某个资源的线程数量。
using System;
using System.Threading;
public class SemaphoreExample
{
private static Semaphore semaphore = new Semaphore(initialCount: 1, maximumCount: 1);
public static void Main()
{
Thread t1 = new Thread(new ThreadStart(ThreadTask));
Thread t2 = new Thread(new ThreadStart(ThreadTask));
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
private static void ThreadTask()
{
Console.WriteLine($"Waiting for semaphore...");
semaphore.WaitOne(); // 等待获取信号量
Console.WriteLine($"Semaphore acquired by thread {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000); // 模拟耗时操作
Console.WriteLine($"Releasing semaphore...");
semaphore.Release(); // 释放信号量
}
}
Mutex类是一个系统级别的互斥锁,可以用于跨进程的线程同步。Mutex类提供了WaitOne和ReleaseMutex方法,用于获取和释放锁。
using System;
using System.Threading;
public class MutexExample
{
private static Mutex mutex = new Mutex();
public static void Main()
{
Thread t1 = new Thread(new ThreadStart(ThreadTask));
Thread t2 = new Thread(new ThreadStart(ThreadTask));
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
private static void ThreadTask()
{
Console.WriteLine($"Waiting for mutex...");
mutex.WaitOne(); // 等待获取互斥体
Console.WriteLine($"Mutex acquired by thread {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000); // 模拟耗时操作
Console.WriteLine($"Releasing mutex...");
mutex.ReleaseMutex(); // 释放互斥体
}
}
15.4、任务并行库(TPL)
任务并行库(Task Parallel Library,TPL)是一个强大的工具,用于简化多线程和并发编程。TPL 基于 .NET Framework 中的 System.Threading.Tasks 命名空间,它提供了一组类和接口,使开发人员能够更容易地创建和管理并行任务。
以下是使用 TPL 进行多线程编程的一些基本概念和示例:
1. 创建 Task
使用 Task
类来创建一个新任务。例如:
Task task = new Task(new Action(() =>
{
// 执行一些工作
Console.WriteLine("Task is running on a separate thread.");
}));
2. 异步方法(Async/Await)
使用 async
和 await
关键字来异步执行方法。这可以避免显式创建和管理任务。例如:
public async Task MyAsyncMethod()
{
// 使用 await 调用异步方法
await SomeAsyncOperation();
Console.WriteLine("This runs after SomeAsyncOperation()");
}
3. Task Parallelism
使用 Parallel
类来执行多个任务。例如,使用 Parallel.For
或 Parallel.ForEach
来并行处理集合中的元素:
int[] numbers = { 1, 2, 3, 4, 5 };
Parallel.For(0, numbers.Length, i =>
{
// 对 numbers[i] 进行操作
int square = numbers[i] * numbers[i];
Console.WriteLine($"Processing {square} on thread {Task.CurrentId}");
});
4. 数据并行(Parallel LINQ,PLINQ)
PLINQ 是 LINQ 的扩展,允许开发人员使用 LINQ 语法编写并行查询:
var query = numbers.AsParallel().Select(n => n * n); // 使用 AsParallel() 来启用并行执行。
foreach (var square in query)
{
Console.WriteLine(square); // 结果按任意顺序打印。
}
5. 任务调度器(TaskScheduler)和任务上下文(TaskContext)
TPL 支持多种任务调度器,包括默认的 TaskScheduler.Default
和 TaskScheduler.Current
。在某些情况下,可能需要自定义任务调度器或保留任务的上下文信息。例如:
TaskContext context = TaskContext.Capture(); // 捕获当前任务的上下文信息。
Task task = Task.Factory.StartNew(() => ProcessWithContext(context)); // 使用捕获的上下文信息创建新任务。
注意:过度使用并行和并发可能导致资源竞争、死锁和其他并发问题。在编写并行代码时,请务必小心并确保正确地同步访问共享资源。
15.5、异步编程
异步编程是一种处理长时间运行操作的方法,这些操作不需要用户交互,但可能阻止UI线程或阻止其他任务的执行。异步编程允许应用程序继续执行其他任务,而不需要等待当前操作完成。
异步编程通常使用async
和await
关键字来实现。async
关键字用于声明一个方法将异步执行,而await
关键字用于等待一个异步操作完成。
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("开始异步读取文件...");
string content = await ReadFileAsync("test.txt");
Console.WriteLine("文件内容: " + content);
Console.WriteLine("异步读取文件结束。");
}
static async Task<string> ReadFileAsync(string path)
{
await Task.Run(() =>
{
// 模拟耗时操作
System.Threading.Thread.Sleep(5000);
});
return File.ReadAllText(path);
}
}
在上面的例子中,Main
方法使用async
关键字声明为异步,并调用ReadFileAsync
方法来异步读取文件。ReadFileAsync
方法内部使用Task.Run
来在后台线程上执行耗时操作(模拟读取文件),并使用await
等待该任务完成。当文件读取完成后,方法返回文件内容。
使用异步编程可以显著提高应用程序的性能和响应能力,因为它允许应用程序在等待操作完成时继续执行其他任务。此外,使用异步编程还可以避免UI线程阻塞,提高用户体验。
15.6、取消任务和异常处理
取消任务
当使用Task
类或async/await
模式创建异步任务时,可以使用CancellationToken
来请求取消任务。CancellationToken
是一个包含取消令牌的对象,可以用来请求取消任务。
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
// 创建一个CancellationTokenSource,它将生成一个取消令牌。
using (var cts = new CancellationTokenSource())
{
// 创建一个CancellationToken,它将与取消令牌关联。
var token = cts.Token;
// 启动一个异步任务,并将取消令牌传递给它。
Task task = Task.Run(() => LongRunningOperation(token), token);
// 等待用户输入来决定是否取消任务。
Console.WriteLine("按Enter键来取消任务,或者等待任务完成...");
Console.ReadLine();
// 如果用户输入了Enter键,则请求取消任务。
if (token.CanBeCanceled)
{
Console.WriteLine("取消任务...");
token.ThrowIfCancellationRequested();
}
}
}
static void LongRunningOperation(CancellationToken token)
{
// 检查是否请求了取消,如果是,则抛出一个异常。
if (token.IsCancellationRequested)
{
throw new OperationCanceledException("任务被取消。");
}
// 执行长时间运行的操作...
}
}
在上面的例子中,LongRunningOperation
方法在执行长时间运行的操作之前检查是否请求了取消。如果请求了取消,该方法会抛出一个OperationCanceledException
异常。
异常处理
处理多线程中的异常与处理常规异常类似,但是需要确保正确地捕获和处理所有可能的异常。你可以使用try/catch
块来捕获异常,并采取适当的行动来处理它们。还可以使用Task
的ContinueWith
方法或async/await
结构中的catch
块来捕获异步任务的异常。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
try
{
await LongRunningOperationAsync();
}
catch (Exception ex)
{
Console.WriteLine("发生异常: " + ex.Message);
}
}
static async Task LongRunningOperationAsync()
{
// 模拟长时间运行的操作...
await Task.Run(() => SimulateLongRunningOperation());
}
static void SimulateLongRunningOperation()
{
// 执行长时间运行的操作...可能会抛出异常...
}
}
15.7、并行LINQ
使用LINQ(Language Integrated Query)可以方便地并行处理集合中的元素。通过使用Parallel
类和LINQ的AsParallel
扩展方法,可以很容易地实现并行版本的查询操作。
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
var numbers = new int[] { 1, 2, 3, 4, 5 };
// 使用LINQ的AsParallel方法并行处理集合中的元素
var parallelQuery = from number in numbers.AsParallel()
where IsPrime(number)
select number;
// 执行查询并处理结果
foreach (var prime in parallelQuery)
{
Console.WriteLine($"Prime number: {prime}");
}
}
static bool IsPrime(int number)
{
// 检查数字是否为素数的逻辑
return number > 1 && Enumerable.Range(2, (number - 1)).All(n => number % n != 0);
}
}
在上面的例子中,numbers.AsParallel()
将numbers
数组转换为并行可枚举的集合,然后可以使用标准的LINQ查询操作符(如where
和select
)对其进行操作。注意,这里的IsPrime
方法也应该是线程安全的,或者需要在每个线程上调用它时进行适当的同步。
使用并行LINQ (Parallel LINQ
) 可以提高对大型数据集的查询性能,但需要注意线程安全和资源竞争的问题。
15.8、线程安全
线程安全是指对共享数据的访问控制,以避免并发线程之间的冲突和不一致状态。线程安全问题通常发生在多个线程同时访问和修改同一资源时。
要实现线程安全,可以使用以下几种方法:
- 互斥锁(Mutex):通过
System.Threading.Mutex
类可以同步对共享资源的访问。这个类可以用来实现跨进程的同步,但它比Monitor
更难以使用和调试。 - 监视器(Monitor):使用
System.Threading.Monitor
类来锁定特定代码块,确保一次只有一个线程可以执行这些代码。 - 锁语句(lock):使用
lock
关键字来保护代码块,确保同一时间只有一个线程可以执行被保护的代码。 - 原子操作:某些操作在单个指令中完成,无法被中断或被其他线程干扰,因此是线程安全的。例如,
Interlocked
类提供了一些原子操作方法。 - 读写锁:
ReaderWriterLock
或ReaderWriterLockSlim
类用于同步对共享数据的读取和写入。它们允许同时有多个读取者,但只允许一个写入者。 - 避免共享状态:通过设计将数据局部化到单个线程,可以避免线程安全问题。例如,使用局部变量或使用线程局部存储(Thread Local Storage, TLS)。
- 避免死锁:通过设计算法和使用适当的同步原语,避免出现死锁的情况。死锁是指两个或多个线程永久地等待对方释放资源。
using System;
using System.Threading;
class Program
{
static void Main()
{
// 创建一个互斥锁实例
Mutex mutex = new Mutex();
// 启动两个线程,它们将访问共享资源并需要同步访问
Thread thread1 = new Thread(new ThreadStart(ThreadSafeMethod));
Thread thread2 = new Thread(new ThreadStart(ThreadSafeMethod));
thread1.Start(mutex); // 传递互斥锁对象给线程
thread2.Start(mutex); // 传递互斥锁对象给线程
thread1.Join();
thread2.Join();
}
static void ThreadSafeMethod(object mutexObj)
{
// 获取互斥锁对象
Mutex mutex = (Mutex)mutexObj;
try
{
// 请求互斥锁的拥有权,如果当前没有其他线程拥有该锁,则当前线程将获得它并继续执行被保护的代码块。
mutex.WaitOne(); // 阻塞当前线程直到获得互斥锁的拥有权。
// 在这里编写访问共享资源的代码...
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} inside the critical section.");
// 释放互斥锁的拥有权,允许其他线程获得它。
mutex.ReleaseMutex();
}
catch (AbandonedMutexException) { } // 互斥锁已被其他线程释放,这是一个正常情况。
}
}
在这个例子中,我们使用了Mutex
类来同步对共享资源的访问。每个线程在进入临界区之前必须获取互斥锁的拥有权,并在退出临界区时释放它。这样可以确保任何时候只有一个线程可以访问共享资源。
十六、反射
16.1、反射基础
反射(Reflection)是一种机制,它允许程序在运行时获取关于程序集、模块、类型以及成员的信息,并且可以动态地创建和调用类型。反射是.NET框架的一个重要组成部分,它使得程序能够以编程方式检查和操作代码的结构和行为。
以下是一些关于C#反射的基础知识:
-
获取Type对象:使用
System.Type
类的GetType()
或typeof()
方法可以获取一个类型的Type
对象。Type type = typeof(MyClass);
-
获取类型的名称:使用
Type.Name
属性可以获取类型的名称。string typeName = type.Name;
-
检查类型的方法:使用
Type.GetMethods()
方法可以获取类型的所有公共方法,并可以使用MethodInfo
类来访问方法的详细信息。MethodInfo[] methods = type.GetMethods(); foreach (MethodInfo method in methods) { Console.WriteLine(method.Name); }
-
创建类型的实例:使用
Activator.CreateInstance()
方法可以创建类型的实例。object instance = Activator.CreateInstance(type);
-
调用方法:使用
MethodInfo.Invoke()
方法可以调用一个实例方法。如果方法需要参数,可以将它们作为额外的参数传递给Invoke()
方法。object result = methodInfo.Invoke(instance, new object[] { /* method arguments */ });
-
访问字段和属性:使用
Type.GetFields()
或Type.GetProperties()
方法获取类型的所有公共字段或属性,然后可以使用相应的方法获取或设置它们的值。 -
访问构造函数:使用
Type.GetConstructors()
方法获取类型的所有公共构造函数,然后可以使用相应的方法来创建实例。 -
动态类型绑定:使用
dynamic
关键字可以在运行时解析类型信息,这样就可以像调用静态类型一样调用动态类型的方法和属性,而不需要在编译时知道确切的类型信息。dynamic myObject = Activator.CreateInstance(type); // 创建动态对象实例 myObject.SomeMethod(); // 调用方法,像静态类型一样操作动态对象(编译时不检查类型)
使用反射通常需要额外的计算资源和性能开销,因此在性能敏感的应用程序中应谨慎使用。反射通常在框架类库、插件系统、序列化/反序列化、动态代码生成等场景中使用较多。
16.2、运行时类型识别
在运行时,可以使用Type
类来表示和处理类型。例如,通过使用typeof()
函数或者直接获取实例类型的GetType()
方法,可以得到一个Type
对象,这个对象可以用来获取类型的各种信息。
// 获取类型
Type type = typeof(MyClass);
// 获取类型的名称
string typeName = type.Name;
// 检查类型的方法
MethodInfo[] methods = type.GetMethods();
foreach (MethodInfo method in methods)
{
Console.WriteLine(method.Name);
}
// 检查类型的属性
PropertyInfo[] properties = type.GetProperties();
foreach (PropertyInfo property in properties)
{
Console.WriteLine(property.Name);
}
除了获取类型信息,还可以使用反射来创建类型的实例、调用方法、访问字段和属性等。这些操作通常需要使用到Activator
类和MethodInfo
类等其他反射相关的类。
// 创建类型的实例
object instance = Activator.CreateInstance(type);
// 调用实例的方法(如果需要传递参数)
object result = type.InvokeMember(
"MyMethod",
BindingFlags.InvokeMethod,
null,
instance,
new object[] { /* method arguments */ });
16.3、反射高级用法
-
动态类型绑定:使用
dynamic
关键字可以在运行时解析类型信息,从而在编译时不检查类型。这允许像静态类型一样调用方法和属性。dynamic myObject = Activator.CreateInstance(type); // 创建动态对象实例 myObject.SomeMethod(); // 调用方法,像静态类型一样操作动态对象(编译时不检查类型)
-
创建泛型实例:反射也允许在运行时创建泛型类型的实例。
Type type = typeof(MyGenericClass<>); object instance = Activator.CreateInstance(type, new object[] { someArgument });
-
调用私有成员:通过使用
BindingFlags
枚举,可以指定在反射调用中包括或排除特定的成员(如私有成员)。object result = type.InvokeMember( "MyPrivateMethod", BindingFlags.InvokeMethod | BindingFlags.NonPublic, null, instance, new object[] { /* method arguments */ });
-
访问非公开字段和方法:除了公开的字段和方法,还可以使用反射来访问非公开的字段和方法。这需要设置
BindingFlags
来包括非公开的成员。object fieldValue = type.GetField("MyPrivateField", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(instance);
-
事件和委托处理:反射可以用来动态地附加或解除事件处理程序,以及创建委托实例。
EventInfo eventInfo = type.GetEvent("MyEvent"); eventInfo.AddEventHandler(instance, myEventHandler); // 附加事件处理程序 eventInfo.RemoveEventHandler(instance, myEventHandler); // 解除事件处理程序
-
访问非公共构造函数:如果需要使用非公共构造函数创建实例,可以使用
Type.GetConstructor()
方法来获取特定签名的构造函数。ConstructorInfo constructor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(string) }, null); object instance = constructor.Invoke(new object[] { "some argument" });
-
序列化和反序列化:反射常用于对象序列化和反序列化,例如使用
BinaryFormatter
或DataContractSerializer
类。这允许将对象状态转换为字节流或从字节流中恢复对象状态。
16.4、动态编程
-
动态类型检查:可以使用反射来检查对象的实际类型,并根据需要执行不同的操作。
object obj = GetSomeObject(); if (obj != null) { Type type = obj.GetType(); if (type == typeof(string)) { // 处理字符串 } else if (type == typeof(int)) { // 处理整数 } // 添加其他类型检查... }
-
动态创建对象:可以使用反射创建类型的实例,无需预先知道类型的名称或使用强类型引用。
Type type = typeof(MyClass); object instance = Activator.CreateInstance(type); // 创建实例
-
动态调用方法:可以使用反射动态地调用类型的方法,而无需在编译时知道方法的名称或签名。
MethodInfo methodInfo = type.GetMethod("MyMethod"); object result = methodInfo.Invoke(instance, new object[] { /* arguments */ });
-
动态属性访问:可以使用反射获取和设置类型的属性值,即使这些属性是私有的或受保护的。
PropertyInfo propertyInfo = type.GetProperty("MyProperty"); object value = propertyInfo.GetValue(instance); // 获取属性值 propertyInfo.SetValue(instance, newValue); // 设置属性值
-
动态事件订阅和取消订阅:可以使用反射来动态地附加或解除事件处理程序。
EventInfo eventInfo = type.GetEvent("MyEvent"); eventInfo.AddEventHandler(instance, myEventHandler); // 附加事件处理程序 eventInfo.RemoveEventHandler(instance, myEventHandler); // 解除事件处理程序
-
动态类型转换:可以使用反射来执行非标准的类型转换,比如将一个基类实例转换为派生类实例。
object obj = ...; // 获取对象实例 if (obj is SomeBaseType) // 检查对象是否为基类实例 { var derivedType = obj as SomeDerivedType; // 尝试转换为派生类类型(如果可能) if (derivedType != null) { // 使用派生类类型进行操作... } }
-
动态调用泛型方法:可以使用反射来调用泛型方法,并传递具体的类型参数。
MethodInfo methodInfo = type.GetMethod("MyGenericMethod"); // 获取泛型方法信息 MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(typeof(MyType)); // 创建泛型方法的实例(指定类型参数) object result = genericMethodInfo.Invoke(instance, new object[] { /* arguments */ }); // 调用泛型方法并执行操作...
16.5、安全性和性能考虑
安全性考虑:
- 代码注入攻击:如果允许用户输入类型名称,然后使用反射来实例化或调用方法,这可能导致潜在的安全风险。恶意用户可以输入恶意类型或方法,执行未经授权的操作。
- 性能影响:反射操作通常比直接调用方法要慢得多,因为反射涉及到运行时类型解析和动态绑定。这可能导致应用程序性能下降。
- 内存泄漏风险:反射可以用于访问私有字段和方法,这可能使内存泄漏更容易发生,因为可能违反了类型的安全封装边界。
- 权限和访问控制:某些情况下,反射可能会绕过正常的访问控制检查,导致对敏感资源的访问或修改。
性能考虑:
- 运行时开销:反射操作通常比直接调用方法要慢,因为反射涉及到运行时解析类型信息,而直接方法调用是基于编译时确定的。
- 增加内存足迹:频繁使用反射可能会增加应用程序的内存使用量,因为它涉及动态类型信息的加载和存储。
- 代码可维护性:过度依赖反射可能会降低代码的可读性和可维护性,因为代码中的逻辑与运行时行为紧密耦合。
为了提高安全性并优化性能,可以考虑以下措施:
最小化反射的使用:尽量减少反射的使用,只在必要时使用它。考虑使用其他技术来实现相同的功能,例如使用接口、继承或设计模式。
输入验证和过滤:在使用反射之前,确保对用户输入进行验证和过滤,以防止潜在的安全风险。
缓存反射信息:如果频繁地使用反射来查询类型信息,考虑将结果缓存起来,以减少重复的反射操作。
优化性能:对于性能关键的部分,尽量避免使用反射,或者寻找更高效的替代方案。
权限和访问控制:确保应用程序中的反射操作受到适当的权限和访问控制限制,以防止未经授权的访问或修改。
PS:因为内容太多可能显得有些杂乱,朋友们若是发现问题,欢迎指正