面向对象编程(OOP)是一种编程范式,它使用“对象”来设计软件和应用程序,当然,此“对象”非彼“对象”。大家都知道,编程的目的归根结底是为现实世界服务,而面向对象就是将现实世界中的事物抽象成一个个对象,通过对象模拟现实世界中事物的种种行为和客观规律来组成我们的软件。C#语言是一种面向对象的语言,C语言是一种面向过程的语言。除此之外,面向对象的语言还有C++、Java等。(*觉得理解不了的朋友可以去看看刘铁猛老师的视频,讲得很通俗易懂)
类是一个能够存储数据并执行代码的数据结构,例如,人类可以看成是一个类,人有身高体重名字等(存储代码),人能说话、写字、走路(执行代码)。而刘德华是人类中的一个人,则可以看作是类的一个实例。所以说,类的方方面面在现实生活中都是有迹可循的。
*一个运行中的C#程序可以看成是一组相互作用的类型对象,他们大部分都是类的实例
// 定义一个名为“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
类的成员包括数据成员和函数成员。数据成员可以是字段、常量或事件。函数成员包括方法、属性、构造函数、析构函数、运算符和索引器。
字段是类的数据成员,用于存储类实例的状态。根据作用域,字段可以分为两种类型:实例字段和静态字段。
实例字段:
每个类的对象实例都有其自己的实例字段副本。当我们创建类的实例时,这些实例字段会随着对象的创建而初始化。实例字段只能在其所属的类内部被访问,并且通过对象实例来访问。
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; } // 自动实现的属性
}
注意事项:
用于存储不会改变的值。常量是类的一种特殊字段,其值在编译时被设定并且不能修改。这意味着常量一旦在程序中被定义,它的值就永远保持不变。常量主要用于表示在程序的生命周期内不会更改的值,如数学常数π、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; // 使用π常量计算圆的周长
常量的特性:
使用场景:
注意事项:
属性是用于封装字段的一种机制,它提供了一种安全的方式来读取、写入或计算字段的值。通过属性,我们可以控制对字段的访问,确保数据完整性,并可以在访问字段时添加额外的逻辑。可以说,属性是对字段的扩展。
定义属性:
属性由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
访问器,则属性是只写的;如果两者都有,则属性是可读写的。属性的作用
对外:暴露数据,数据可以是存储在字段里的,也可以是动态计算出来的。
对外:保护字段不被非法值 污染。
定义类可以执行的操作。用于表示类的行为与功能。是类中用于执行特定操作或实现特定功能的成员函数。
**值传递:**默认情况下,参数是通过值传递的。这意味着当我们将一个值类型的变量作为参数传递给方法时,实际上是传递了该变量的一个副本,而不是原始变量本身。对副本的任何修改都不会影响原始变量。
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
{
// 方法体...
}
方法的返回值是方法执行后返回给调用者的数据。在C#中,方法的返回值可以是任何数据类型,包括基本数据类型、引用类型和自定义类型。方法的返回值通过return关键字进行返回。一个方法可以有多个返回语句,但只会执行其中的一个。
public int Add(int a, int b) // 返回整数类型的值
{
return a + b; // 返回两个整数的和作为返回值
}
构造函数是一种特殊的方法,用于在创建对象时初始化对象的内部状态。它与类同名,并且没有返回类型。当创建一个类的新实例时,构造函数会自动调用。
定义构造函数:
构造函数可以有不同的访问修饰符,包括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;
}
}
构造函数的特性:
析构函数(也称为终结器)是在.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;
}
}
}
析构函数的特性:
~
)来标记的。IDisposable
接口来提供Dispose
方法,以便显式地释放资源。这种做法更加灵活,因为它允许更细粒度的控制和资源的多次释放。在上面的示例中,我们显式地实现了Dispose
方法来释放非托管资源。Dispose
方法被调用时,会触发自动调用析构函数。Dispose
方法可以帮助尽早释放资源,而不是等待垃圾回收器自动释放它们。这有助于提高应用程序的性能和响应性。索引器是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]
的访问方式。使用:
myCollection
的MyCollection类实例,可以使用myCollection[0]
来访问第一个元素的值。myCollection[1] = newValue
将修改第二个元素的值。myCollection[0].AnotherProperty = newValue
。myCollection[index1, index2]
来访问特定位置的元素。运算符重载是一种强大的特性,它允许我们为自定义类型定义现有的运算符的行为。这意味着我们可以为自定义类型提供类似于内置类型的操作方式。通过运算符重载,我们可以使代码更加简洁、直观,并提高开发效率。
定义:
运算符重载是通过在类中定义特殊的方法来实现的。这些方法必须使用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
关键字来声明。适用场景:
属于类本身而不是类的实例的成员,可以直接通过类名访问。使用 static 修饰符可声明属于类型本身而不是属于特定对象的静态成员。
*static修饰符不能用于索引器或终结器
使用静态类的意义
(1)防止程序员写代码时实例化该静态类。
(2)防止在类的内部声明任何实例字段或方法。
使用静态类的要求
(1)静态类的成员都是静态的。
(2)静态类不能实例化
(3)C#编译器会自动把静态类标记为sealed。
若使用static关键字修饰类成员,则该成员为静态成员。(索引器和终结器不能被static修饰)
使用静态成员的要求
(1)无论对一个类创建多少个实例,它的静态成员都只有一个副本。
(2)静态方法只能被重载,而不能被重写,因为静态方法不属于类的实例成员。
(3)静态方法和属性不能访问其包含类型中的非静态字段和事件,并且不能访问任何对象的实例变量。
(4)在字段中,static不能与const同用,因为 const 字段的行为在本质上是静态的。这样的字段属于类。
用于初始化类中的静态成员
(1)静态类可以有静态构造函数,静态构造函数不可继承。
(2)静态构造函数可以用于静态类,也可用于非静态类。
(3)静态构造函数无访问修饰符、无参数,只有一个 static 标志。
(4)静态构造函数不可被直接调用,在创建第一个实例或引用任何静态成员之前,CLR(公共语言运行时)都将自动调用静态构造函数,静态构造函数将被自动执行,并且只执行一次。
允许类通知其他对象某些事情发生了。详见第十点。
这几种方法可以了解一下,本人之前面试有问过实例化类有几种方式。
new
关键字这是最直接的方法,通过new
关键字和类名来创建类的实例。
MyClass myObject = new MyClass();
反射允许在运行时动态地创建和操作类型,通过反射API可以实例化一个类。
Type type = typeof(MyClass);
object instance = Activator.CreateInstance(type);
在工厂模式中,通常会有一个工厂类来负责创建其他类的实例,这可以隐藏对象的创建逻辑,并使代码更加模块化。
object factory = new MyFactory();
MyClass myObject = factory.CreateMyClass();
在依赖注入中,对象的依赖关系通常由外部容器或框架来管理。对象的实例化不由直接调用new
来完成,而是由依赖注入框架来处理。
// 假设有一个IDependency interface 和它的实现类 MyDependency
IDependency myDependency = new MyDependency();
通过将对象序列化为字节流(例如JSON或XML)或将字节流反序列化为对象,可以在不同的应用程序或系统中共享对象状态。
// 序列化对象为JSON字符串
string json = JsonConvert.SerializeObject(myObject);
// 从JSON字符串反序列化为对象
MyClass myDeserializedObject = JsonConvert.DeserializeObject<MyClass>(json);
可以通过定义额外的构造函数或克隆方法来创建现有对象的一个副本。
public class MyClass
{
public MyClass Clone()
{
return new MyClass(this); // 假设有一个复制构造函数的实现
}
}
封装是指将数据(属性)与操作数据的函数(方法)捆绑到一个称为“对象”的单一实体中。通过封装,对象的内部状态变得不可见,只能通过定义好的公开方法来访问或修改其状态。
访问修饰符用于控制对类、属性、方法、事件等的可见性。通过访问修饰符,我们可以定义哪些代码可以访问某个成员,以及从哪个作用域可以访问它。
public
公共访问修饰符表示成员可以从任何其他类或方法中访问。这是最宽泛的访问级别。
public class MyClass
{
public int PublicProperty { get; set; }
public void PublicMethod() { }
}
private
私有访问修饰符表示成员只能在包含它的类内部访问。这是默认的访问级别,如果没有指定访问修饰符,则默认为私有访问。
class MyClass
{
private int PrivateProperty { get; set; }
private void PrivateMethod() { }
}
protected
保护访问修饰符表示成员可以在包含它的类以及派生自该类的任何子类中访问。
public class MyBaseClass
{
protected int ProtectedProperty { get; set; }
protected virtual void ProtectedMethod() { }
}
internal
内部访问修饰符表示成员只能在包含它的程序集内部访问。这通常用于那些不应该从其他程序集中公开的类或成员。
internal class InternalClass
{
internal int InternalProperty { get; set; }
internal void InternalMethod() { }
}
protected internal
保护内部访问修饰符表示成员可以在包含它的程序集或派生自该类的子类中访问。这相当于同时使用protected
和internal
修饰符。
public class MyBaseClass
{
protected internal int ProtectedInternalProperty { get; set; }
protected internal virtual void ProtectedInternalMethod() { }
}
private protected
私有保护访问修饰符表示成员可以在包含它的类以及任何从那个类派生的外部子类中访问。这相当于同时使用private
和protected
修饰符。
public class MyBaseClass
{
private protected int PrivateProtectedProperty { get; set; }
private protected virtual void PrivateProtectedMethod() { }
}
override
当一个成员在基类中定义为protected
或public
,并且被标记为virtual
或abstract
时,子类可以通过使用override
关键字来提供自己的实现。这是面向对象编程中的多态性的一种表现。
public class MyBaseClass
{
protected virtual void MyMethod() { }
}
public class MyDerivedClass : MyBaseClass
{
public override void MyMethod() { }
}
继承是面向对象编程中的一个核心概念,它允许一个类继承另一个类的属性和方法。继承在C#中为代码重用、多态和实现软件开发生命周期中的代码复用提供了强大的支持。通过合理地使用继承,可以提高代码的可维护性、可读性和可扩展性,有助于构建健壮、灵活和可重用的软件系统。
继承是一个类(派生类或子类)获取另一个类(基类或父类)的成员(属性、方法、事件等)的过程。通过继承,子类可以拥有父类的所有非私有成员,并且可以添加自己的新成员或覆盖父类的成员。
多态是面向对象编程的三大特性之一,它允许一个接口以多种形式出现。多态的存在增加了程序的灵活性和扩展性,是构建强大软件系统的关键。
多态是指一个接口在多种情况下有不同的表现形式。在C#中,多态允许子类对象以父类引用的形式存在,从而在运行时根据对象的实际类型来调用相应的方法。
当子类继承了父类的一个方法,并且想要提供一个新的实现时,可以使用方法重写。在子类中定义一个与父类方法签名完全相同的方法,并为其提供新的实现。这样,当通过父类引用调用该方法时,将执行子类中的实现。
class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("The animal makes a sound");
}
}
class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Meow");
}
}
方法重载是指在同一个类中定义多个同名方法,但它们的参数列表(参数类型、数量或顺序)不同。通过方法重载,可以实现同一个方法名在不同情况下执行不同的操作。
class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b)
{
return a + b;
}
}
抽象类是一种特殊的类,通常用于定义一个通用的、共享的基类,它可以包含抽象方法,具体方法,字段和属性等成员,而抽象方法的具体实现则由它的派生类来实现,使用Abstract修饰符修饰的类是抽象类,它不能被实例化。
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("我开着汽车!");
}
}
接口用于定义一组方法的契约,但不包含这些方法的实现。接口可以包含方法、属性、事件和索引器。其派生类必须提供接口中所有成员的具体实现。
public interface IMyInterface
{
string MyProperty { get; set; }
void MyMethod();
}
一个类只做好一件事情。它是面向对象编程中的一个重要原则,它要求一个类应该只有一个职责,或者说一个类只有一个改变的原因。这个原则的目的是提高代码的可维护性和可读性,降低类的复杂度,减少类之间的耦合度。
使用单一职责原则时,需要注意不要过度分解类。将一个大型类拆分成多个小型类并不一定就是好的设计,需要综合考虑类的职责、功能和耦合度等因素。同时,也需要考虑类的命名和注释,使得代码更加易于理解和维护。
对扩展开放,对修改封闭。它是面向对象编程中的一个重要原则,它要求软件实体(类、模块、函数等)对于扩展是开放的,但对于修改是封闭的。这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。
使用开放封闭原则时,可以通过抽象和继承来实现。通过定义抽象类或接口,可以定义一组通用的属性和方法,然后通过继承和实现来添加新的行为。这样可以使得代码更加模块化,提高了代码的可重用性和可维护性。
子类可以替换父类并出现在父类能够出现的任何地方。里氏替换原则是由美国计算机科学家Barbara Liskov提出的。这个原则是说,如果一个程序使用了一个基类的对象,那么使用该基类的任何子类的对象来替换它,程序的行为应该保持不变。
依赖于抽象,即高层模块不依赖于底层模块,二者都依赖于抽象。它要求高层模块不应该依赖于低层模块,而是应该依赖于抽象。换句话说,具体实现应该依赖于抽象,而不是抽象依赖于具体实现。这样可以降低模块间的耦合度,提高系统的可维护性和可扩展性。
使用多个小的专门接口,而不要使用一个大的总接口。它要求将大接口拆分成小接口,每个接口负责单一职责。这样可以降低类之间的耦合度,提高系统的可维护性和可扩展性。
是所有异常类的基类,它包含了异常的基本信息,如消息、堆栈跟踪等。它有两个主要的派生类:
它是系统定义的异常类的基类,通常由系统抛出。
它是用户定义的异常类的基类,通常由应用程序抛出。
当尝试访问空对象的成员时抛出的异常。
当尝试访问数组或集合中不存在的索引时抛出的异常。
当除数为零时抛出的异常。
算术运算异常的基类。
用户可以根据自己的需求,派生自System.ApplicationException类来定义自己的异常类型,以便更好地区分不同类型的异常情况。
try
块:包含可能引发异常的代码。
catch
块:用于捕获 try
块中抛出的异常,并执行相应的错误处理代码。
throw
语句用于引发(即重新抛出)异常。通常,当你在catch
块中处理一个异常时,你可能需要将该异常传递给更高级别的代码,以便进一步处理或记录。使用throw
语句可以重新引发该异常,使异常的处理流程得以继续。
try
{
// 一些可能抛出异常的代码
}
catch (Exception ex)
{
// 处理异常的代码
// ...
throw; // 重新引发异常
}
无论是否发生异常,都会执行此块中的代码。通常用于资源清理操作。
try
{
// 可能抛出异常的代码
int result = 10 / 0; // 除以零的异常
}
catch (DivideByZeroException ex)
{
// 处理除以零异常的代码
Console.WriteLine("发生除以零异常: " + ex.Message);
}
catch (Exception ex)
{
// 处理其他异常的代码
Console.WriteLine("发生其他异常: " + ex.Message);
}
finally
{
// 清理资源的代码,无论是否发生异常都会执行
Console.WriteLine("执行finally块中的代码。");
}
这是所有异常类型的基类。通常情况下,我们不会直接捕获该异常,而是使用它的子类来捕获特定类型的异常。
表示算术运算异常,例如除以零。
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块被执行。");
}
}
}
表示数组索引超出范围异常。
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块被执行。");
}
}
}
表示空引用异常,当尝试访问空引用对象的成员时抛出。
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块被执行。");
}
}
}
表示内存不足异常,当无法分配所需内存时抛出。
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块被执行。");
}
}
}
表示除以零异常,当除法或模运算的分母为零时抛出。
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块被执行。");
}
}
}
表示堆栈溢出异常,通常发生在递归调用过程中。
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(); // 无限递归调用
}
}
表示输入输出异常,用于处理文件和流的读写操作中的错误。
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块被执行。");
}
}
}
表示格式化异常,通常在字符串转换为其他类型时发生。
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块被执行。");
}
}
}
表示参数异常,通常在传递无效的参数值时抛出。
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块被执行。");
}
}
}
表示不支持的操作异常,当调用不支持的方法或功能时抛出。
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块被执行。");
}
}
}
委托是一种特殊的类型,它代表了一个具有特定参数列表和返回类型的可调用方法。委托可以被视为一个类型安全的函数指针,允许将方法作为参数传递给其他方法,或者赋值给变量。委托可以链接一系列方法调用,形成一个链式调用。委托的声明需要指定委托的返回类型和参数列表。委托的定义可以包含在类中,也可以作为独立类型定义。一旦定义了委托,就可以将任何具有匹配签名的方法赋值给该委托变量。简单来说,委托就是C#中的“函数指针”,它实现了对方法的间接调用。
委托的定义使用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);
}
}
需要注意的是,委托类型是引用类型,因此当委托作为参数传递或赋值时,实际上传递的是对方法的引用,而不是方法的副本。这使得委托在事件处理、回调函数和异步编程等方面非常有用。
-=
运算符,可以从委托链中移除方法,从而实现委托调用的取消。多播委托(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"
}
}
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
Action
委托用于封装没有返回值的方法。它也可以接受0到16个输入参数。
Action
Action<TArg1>
Action<TArg1, TArg2>
...
Action<TArg1, TArg2, ..., TArgN>
Action sayHello = () => Console.WriteLine("Hello!");
sayHello(); // 输出 "Hello!"
回调函数是指一个由方法签名的指针,它可以在运行时被调用。通过将一个方法作为参数传递给另一个方法,并在适当的时候调用该方法,可以实现回调。通过委托的机制,可以将一个函数作为参数传递给另一个函数,使得后者在适当的时机调用传入的函数。这种机制在需要异步操作、事件处理、用户交互等情况下非常有用。
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); // 调用委托链中的方法
}
}
}
事件处理是响应特定事件的过程,比如用户点击按钮、文本框输入等。通过使用委托,可以将多个方法与同一事件关联起来,以便在事件发生时调用这些方法。
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!"
}
}
通过使用委托,可以将多个方法与同一事件关联起来,并在事件发生时自动调用这些方法。这种机制使得事件处理更加灵活和可扩展。
委托在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
方法启动异步操作。该方法接受一个回调函数作为参数,该回调函数将在异步操作完成后被调用。在回调函数中,我们输出一条消息表示异步操作已完成。最后,我们继续执行主线程的其他任务。
通过使用委托和异步编程模式,可以方便地实现异步操作,并在操作完成后执行特定的回调函数。这使得程序更加高效和响应,并允许主线程继续执行其他任务。
事件是一种特殊的成员,用于在类或结构的实例上通知外部代码发生了某些情况。事件可以看作是一种观察者模式,其中类可以通知其他类或对象它所发生的更改或状态变化。
事件的概念基于委托,委托是一种类型安全的函数指针,可以指向具有相同签名的方法。事件就是一种特殊的委托,用于在特定情况下触发一系列方法。
事件通过使用 event
关键字进行定义,并且必须与一个委托类型匹配。事件可以有零个或多个订阅者,这些订阅者是在类外部定义的方法,它们与事件关联并被调用当事件被触发。
事件的使用允许类与类之间以松耦合的方式进行通信,即类之间不需要知道彼此的内部实现细节。当事件发生时,订阅了该事件的任何方法都会被自动调用,从而实现响应机制。
使用事件可以使代码更加灵活和可扩展,因为类可以添加或删除事件处理器而无需修改源代码。这有助于构建响应不同事件的自定义行为和插件系统。
事件是指能够使对象或类具备通知能力的成员。如把手机看成一个类,响铃使手机具备了通知的功能,则响铃就可以看成是事件。
事件主要用于实现对象之间的松耦合通信。当一个对象的状态发生变化或发生某些动作时,它可以触发一个事件,而其他对象可以订阅这个事件,以便在事件发生时得到通知并作出响应。
通过使用事件,可以实现以下目的:
简而言之,事件就是用来通知的,现实世界中的闹铃,车站广播等都可以抽象成事件。
事件拥有者(Event Source,对象):事件不会自己发生,它必须由一个对象触发。这个对象通常被称为事件源或事件拥有者。
事件(Event,成员):事件是一种特殊的成员,用于让类或对象具备通知能力。它是事件源的一部分,但不会主动发生,而是由事件源的某些内部逻辑触发。
事件响应者(Event Subscriber,对象):事件响应者是订阅了特定事件的类或对象。当事件被触发时,这些订阅者会收到通知并执行相应的操作。
事件处理器(Event Handler,方法成员或者委托):事件处理器本质上是一个回调方法,用于处理事件的逻辑。当事件被触发时,处理器方法会被调用。
事件订阅(Event Subscription):通过事件订阅,事件处理器与事件关联在一起。订阅过程本质上是将一个方法与特定事件关联起来,这样当事件发生时,该方法会被自动调用。
例如:闹钟响了我起床,他的五个部分分别为:闹钟-事件的拥有者,响了-事件,我-事件响应者,起床-事件处理器,我订的闹钟-事件的订阅。那么,别人的闹钟响了我为什么不起床呢?因为我没有订阅这个闹钟。
事件拥有者(Event Source,对象):事件不会自己发生,它必须由一个对象触发。这个对象通常被称为事件源或事件拥有者。
事件(Event,成员):事件是一种特殊的成员,用于让类或对象具备通知能力。它是事件源的一部分,但不会主动发生,而是由事件源的某些内部逻辑触发。
事件响应者(Event Subscriber,对象):事件响应者是订阅了特定事件的类或对象。当事件被触发时,这些订阅者会收到通知并执行相应的操作。
事件处理器(Event Handler,方法成员或者委托):事件处理器本质上是一个回调方法,用于处理事件的逻辑。当事件被触发时,处理器方法会被调用。
事件订阅(Event Subscription):通过事件订阅,事件处理器与事件关联在一起。订阅过程本质上是将一个方法与特定事件关联起来,这样当事件发生时,该方法会被自动调用。
例如:闹钟响了我起床,他的五个部分分别为:闹钟-事件的拥有者,响了-事件,我-事件响应者,起床-事件处理器,我订的闹钟-事件的订阅。那么,别人的闹钟响了我为什么不起床呢?因为我没有订阅这个闹钟。
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
方法。
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表达式,将事件参数打印到控制台。
Click
、Load
、MouseDown
等。Loading
、Loaded
等。DocumentOpen
、UserLoginFailed
等。+=
运算符来订阅事件,将事件处理器添加到事件的调用列表中。同样地,使用 -=
运算符来取消订阅事件,从事件的调用列表中移除事件处理器。泛型是C#中一个强大的特性,它允许开发者在类、结构、接口和方法中定义类型参数化,以便创建更加灵活和可重用的代码。泛型的主要目的是提高代码的重用性、类型安全性和性能。
泛型是一种编程技术,它允许在定义类、接口或方时使用类型参数。这些类型参数在实例化泛型类型时被具体的类型替换。通过这种方式,可以创建适用于任何数据类型的可重用代码。
泛型:泛型提供了编译时的类型安全。当你使用泛型类或方法时,你必须在编译时指定具体的类型参数。这允许编译器捕获可能的类型错误,并提供强类型检查。
Object 类型:Object 是所有类型的基类,它可以存储任何类型的值。但是,当你使用 Object 类型时,你会失去编译时的类型信息,这可能导致运行时的类型错误。此外,当你需要对 Object 类型的值执行操作时,通常需要进行显式的类型转换或使用反射,这可能会增加出错的可能性。
泛型:泛型可以提供更好的性能,因为它允许编译器生成针对特定类型的优化代码。这避免了装箱和拆箱操作,减少了类型转换的开销,并允许值类型保持其值语义。
Object 类型:使用 Object 类型可能会导致性能下降,特别是当涉及值类型时。因为值类型在存储为 Object 时需要进行装箱操作,从 Object 提取值类型时需要进行拆箱操作。这些操作涉及额外的内存分配和类型转换开销。
泛型:泛型允许你编写可重用的代码逻辑,该逻辑可以适用于多种类型,而无需为每种类型编写特定的代码。这提高了代码的重用性,并减少了维护工作量。此外,泛型代码更具可读性,因为类型参数化使得代码的意图更加明确。
Object 类型:虽然使用 Object 类型可以实现一定程度的代码重用,但它通常会导致更多的运行时类型检查和类型转换代码。这可能会降低代码的可读性和可维护性。
类型安全:泛型的主要优势之一是提供类型安全。在非泛型编程中,许多操作都需要进行类型转换,这增加了出错的可能性。使用泛型,你可以明确指定类型参数,编译器可以在编译时检查类型错误,从而提高安全性。
代码重用:泛型通过创建独立于特定类型的类和方法,显著提高了代码的重用性。这意味着你可以编写一次代码,并使用不同的类型参数多次调用该代码。这减少了重复的代码,并提高了开发效率。
性能优化:泛型通过减少类型转换和避免装箱和拆箱操作,可以提高性能。在非泛型编程中,将简单类型作为 Object 传递会引起装箱和拆箱操作,这些操作具有较大的开销。使用泛型可以避免这些开销,从而提高性能。
灵活性:泛型提供了很大的灵活性,允许你编写适用于多种数据类型的代码。这使得你可以在不牺牲类型安全性的情况下,更加灵活地处理数据。
可扩展性:泛型使得代码更容易扩展。你可以定义泛型接口、泛型类、泛型方法等,并在需要时添加新的实现。这使得代码更容易适应未来的需求变化。
二进制代码重用:由于泛型在编译时实例化,生成的二进制代码可以重用。这减少了程序的大小,并提高了加载性能。
声明一个泛型类使用
语法,其中T
是一个类型参数。类型参数用于指定泛型类中使用的数据类型。
public class GenericClass<T>
{
// 泛型类的成员
}
要创建泛型类的实例,需要指定类型参数的值。
var instance = new GenericClass<int>(); // 使用 int 作为类型参数
var otherInstance = new GenericClass<string>(); // 使用 string 作为类型参数
在某些情况下,可能希望对泛型类使用的类型参数施加一些限制。这可以通过使用约束来实现。约束告诉编译器类型参数必须满足某些条件。
引用类型约束 (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必须有一个无参构造函数
{
// 泛型类的成员和构造函数调用逻辑等。
}
声明一个泛型接口使用相同的
语法,其中T
是一个类型参数。类型参数用于指定泛型接口中使用的数据类型。
public interface GenericInterface<T>
{
// 泛型接口的成员
}
要实现一个泛型接口,需要创建一个类或结构体,并实现接口中定义的所有成员。在实现泛型接口时,需要指定与接口相同的类型参数。
public class MyClass : GenericInterface<int> // 实现具有 int 类型参数的泛型接口
{
public int GetValue() // 实现接口中的方法
{
return 42; // 返回一个整数值
}
}
需要注意的是,也可以通过定义泛型方法来间接实现泛型接口。泛型方法是类的成员,它返回一个值或者没有返回值,并可以接受类型参数。通过定义泛型方法,可以编写适用于多种数据类型的通用逻辑,并在需要时将其应用于不同的类型。
声明一个泛型方法使用相同的
语法,其中T
是一个类型参数。类型参数用于指定泛型方法中使用的数据类型。
public static int GenericMethod<T>(T arg)
{
// 泛型方法的实现
return (int)arg; // 假设T是可转换为int的类型
}
要调用一个泛型方法,需要指定类型参数的值。类型参数的值可以是任何有效的C#类型。
int result = GenericMethod<int>(42); // 调用具有 int 类型参数的泛型方法,并传递一个 int 类型的参数
Console.WriteLine(result); // 输出 42
编译器可以自动推断泛型方法的类型参数值。当在调用泛型方法时,如果没有显式指定类型参数的值,编译器将根据传递给方法的实际参数类型来推断类型参数的值。
string str = "Hello";
int result = GenericMethod(str); // 调用泛型方法时没有指定类型参数的值,编译器将自动推断为 string 类型
Console.WriteLine(result); // 输出 0,因为 string 不能直接转换为 int(这里仅为示例)
泛型委托允许定义一个委托,该委托可以引用任何返回类型和任何参数类型的任何方法。声明泛型委托时,使用delegate
关键字后跟泛型标记
。
public delegate T GenericDelegate<T>(T arg); // 泛型委托,T 是返回类型,T 是参数类型
要使用泛型委托,需要创建一个委托实例,并将其指向一个具有匹配签名的方法。然后可以通过该委托调用该方法。
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
实例,并将其指向名为PrintString
的方法。然后我们通过委托del
调用PrintString
方法,并传递字符串参数"Hello, World!"
。由于PrintString
方法的签名与委托签名匹配(返回类型为string
,参数类型为string
),因此该方法可以通过委托进行调用。最后,我们打印出传递给方法的字符串参数,并返回它作为委托的返回值。
where子句的使用:可以使用where
子句来指定泛型参数的类型约束。
public class Box<T> where T : IEquatable<T>
{
// 类的实现
}
在上面的例子中,T
必须实现IEquatable
接口。这样的约束确保了可以安全地对泛型类型进行比较操作。
主约束和次要约束:可以使用多种约束来限制泛型参数的类型。主要的约束包括:
T
必须是引用类型。T
必须是值类型。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 接口
{
// 类的实现
}
协变允许将派生类的实例赋值给基类的引用。在泛型接口或委托中,可以使用out
关键字来声明协变参数。这意味着,如果一个泛型接口或委托有一个协变参数,那么可以传递一个更具体的类型(派生自原始类型的类型)给这个参数。
interface ICovariant<out T>
{
T Get();
}
在上面的例子中,T
被声明为协变参数,这意味着你可以有一个ICovariant
的实例,或者有一个更具体的派生自string
的类型。
逆变允许将基类的实例赋值给派生类的引用。在泛型接口或委托中,可以使用in
关键字来声明逆变参数。这意味着,如果一个泛型接口或委托有一个逆变参数,那么可以传递一个更具体的类型(派生自原始类型的类型)给这个参数。
interface IContravariant<in T>
{
void Set(T value);
}
在上面的例子中,T
被声明为逆变参数,这意味着你可以有一个IContravariant
的实例,或者有一个更具体的派生自object
的类型。
struct
约束一起使用:不能在同时声明struct
约束和协变或逆变的类型参数上使用struct
约束。out
和in
同时使用:在同一个泛型接口或委托中,一个类型参数不能同时声明为协变和逆变。泛型缓存通常是一个技术概念,指的是为了提升性能和避免不必要的重复工作,将已经创建过的泛型类型实例存储起来以便重用。由于.NET中的泛型类型在每次使用不同的类型参数时都会生成新的类型,这可能导致大量的重复工作,尤其是在涉及到反射和动态类型生成时。
在.NET中,每个唯一的泛型类型参数组合都会产生一个新的、唯一的运行时类型。例如,List
和List
是两个完全不同的类型,尽管它们都来自于List
的泛型定义。这种唯一性确保了类型安全,但也可能导致性能开销,因为每个泛型实例化都需要单独的JIT编译和类型元数据。
由于每次使用不同的类型参数都会创建新的泛型类型实例,这可能会导致性能问题,尤其是在需要频繁创建和销毁这些实例的情况下。泛型缓存是一种技术,它存储了已经创建过的泛型实例,以便在后续需要相同类型的实例时可以重用它们,而不是重新创建。这可以显著减少内存分配和垃圾回收的开销,以及减少JIT编译器的负担。
要使用泛型缓存提高性能,需要设计一个能够存储和检索泛型实例的机制。这通常涉及到使用字典或其他键值存储结构来保存泛型实例,其中键是类型参数的组合,值是对应的泛型实例。以下是一个简单的示例来说明这个概念:
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。这些机制可以透明地处理许多与缓存相关的复杂问题,如过期策略、并发控制和内存管理。
如果你确实需要实现一个针对泛型的缓存机制,你可能需要设计一个更为复杂的系统,它能够根据传入的类型参数动态地生成键,并在内部维护这些键与对应泛型实例之间的映射关系。这样的系统可能还需要处理如何安全地在多线程环境中访问和更新缓存的问题。
List
是 .NET Framework 中最常用的泛型集合之一,它表示一个动态数组,可以存储任何类型的元素。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 创建一个存储整数的 List
List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
// 遍历 List 中的元素
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}
}
Dictionary
是一个关联数组,它存储键值对集合,其中每个键都是唯一的。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 创建一个存储字符串到整数的 Dictionary
Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Alice"] = 25;
ages["Bob"] = 30;
ages["Charlie"] = 35;
// 遍历 Dictionary 中的键值对
foreach (KeyValuePair<string, int> entry in ages)
{
Console.WriteLine($"{entry.Key}: {entry.Value}");
}
}
}
这是一个先进先出(FIFO)的队列,可以通过指定T来存储任何类型的元素。
Queue<int> numbers = new Queue<int>(); // 存储整数的队列
numbers.Enqueue(1);
numbers.Enqueue(2);
这是一个后进先出(LIFO)的栈,可以通过指定T来存储任何类型的元素。
Stack<int> stack = new Stack<int>(); // 存储整数的栈
stack.Push(1);
stack.Push(2);
这是一个不包含重复元素的集合。可以通过指定T来存储任何类型的元素。
HashSet<string> uniqueNames = new HashSet<string>(); // 存储唯一字符串的集合
uniqueNames.Add("Alice");
uniqueNames.Add("Bob");
这是一个双向链表,可以通过指定T来存储任何类型的元素。
LinkedList<string> linkedList = new LinkedList<string>(); // 存储字符串的链表
linkedList.AddLast("Alice");
linkedList.AddLast("Bob");
下面是一个简单的泛型类示例,它使用泛型来存储并操作任何类型的列表
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"
下面是一个简单的泛型方法示例,该方法接受一个泛型参数并返回其类型:
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 的默认构造函数会返回空字符串)
假设有两个变量,我们想要交换它们的值,可以使用泛型方法来做到这一点,而不必担心具体的类型。
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
使用泛型,可以编写一个查找数组中最大值的通用方法,无论数组包含何种类型的数据。
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",取决于字母顺序)
我们想要一个泛型方法,它可以接受任何类型的集合,并将元素插入到该集合中。我们可以利用IEnumerable
和IAddable
接口来实现这一点。
首先,为泛型添加一个约束,确保它实现了IAddable
接口:
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接口假设有一个Add方法。
}
}
注意:这个例子中的IAddable
接口和AddToCollection
方法都是假设的,因为C#标准库中并没有这样的接口和实现。这只是一个理论上的示例来说明如何使用泛型约束来操作集合。在实际应用中,可能需要根据具体的集合类型来实现这些操作。
使用LINQ查询内存中的集合时,例如List
,可以使用泛型来定义查询。
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
}
在处理XML文档时,可以使用泛型来定义查询。
XDocument doc = XDocument.Parse("- 1
- 2
- 3
");
var query = from el in doc.Descendants("item") where (int)el > 2 select el;
foreach (var element in query)
{
Console.WriteLine(element); // 输出: - 3
}
在处理数据库时,可以使用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的客户信息。
}
}
使用泛型扩展方法来扩展LINQ查询。例如,可以编写一个泛型的方法来获取集合中的第一个元素。
public static T First<T>(this IEnumerable<T> source) => source.FirstOrDefault<T>(); // 使用默认值,如果集合为空则返回默认值。
然后可以这样使用它:
var firstNumber = numbers.First(); // 返回1,因为它是第一个元素。
在工厂模式中,泛型可以用来创建泛型工厂,以生成指定类型的对象。
public interface IProduct<T> { }
public class ConcreteProductA : IProduct<string> { }
public class ConcreteProductB : IProduct<int> { }
public class GenericFactory<T>
{
public IProduct<T> CreateProduct()
{
// 这里可以根据某种逻辑来创建IProduct的实例。
// 例如,根据配置文件、数据库或其他方式来动态决定创建哪个具体产品。
return new ConcreteProductA(); // 示例: 返回一个字符串类型的具体产品。
}
}
使用泛型可以在单例模式中创建一个泛型单例类,该类可以处理任何类型的单例。
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; } }
}
在策略模式中,可以使用泛型来定义一个策略接口,然后创建实现该接口的策略类。
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) { /*...*/ } }
在装饰器模式中,可以使用泛型来定义一个通用的装饰器类。
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();
}
}
泛型在运行时可能会涉及装箱和拆箱操作,这可能会影响性能。例如,当值类型用作泛型参数时,可能会发生装箱(将值类型转换为对象引用)和拆箱(将对象引用转换回值类型)操作。
避免方法:
泛型通过类型参数化增加了类型安全性,这有助于减少运行时错误和潜在的性能损失。
优化建议:
对于频繁使用的泛型实例,可以考虑使用缓存来避免重复创建相同的实例。
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;
}
使用Object
类型会丧失类型安全性,可能导致运行时异常和性能下降。相比之下,泛型提供了更好的类型控制和编译器级别的检查。
差异分析:
Object
类型可能会导致运行时类型转换、装箱和拆箱操作,从而影响性能。选择合适的类型参数可以平衡代码的可读性与性能。例如,选择常用的值类型作为泛型参数可以减少装箱和拆箱操作。同时,根据实际需求选择合适的约束可以进一步优化性能。
示例:
where T : struct
约束来限制泛型参数为值类型,以减少装箱操作。where T : new()
约束来确保泛型实例化时可以调用默认构造函数。where T : IEquatable
约束来提供自定义的相等性比较逻辑。集合是一种数据结构,它可以包含多个元素,并且这些元素可以是相同类型或不同类型。在C#中,集合通常用于存储具有共同特征的对象集合。
添加元素:向集合中添加一个或多个元素。
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}");
}
数据存储:集合可以用来存储和管理大量数据,提供了灵活的数据结构来组织数据。
数据操作:通过集合提供的操作,可以对数据进行添加、删除、查找、排序等操作,方便对数据的处理和操作。
数据访问:通过集合,可以方便地访问和遍历其中的元素,支持各种数据检索和访问需求。
提高性能:使用集合可以有效地管理内存和避免重复创建相同对象,从而提高性能和效率。
代码复用:通过泛型集合,可以实现代码的复用,减少重复的代码编写工作量。
数组是一个有序的数据集合,它包含固定数量的元素,每个元素都分配一个唯一的索引。数组中的每个元素都具有相同的类型,因此数组是一种类型安全的数据结构。
// 声明并初始化一个整型数组
int[] intArray = new int[] { 1, 2, 3, 4, 5 };
// 声明并初始化一个二维整型数组
int[,] int2DArray = new int[3, 3] { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };
访问元素:通过索引访问数组中的元素。
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
泛型集合类可以创建动态增长的数组。
类型安全:与集合一样,数组是类型安全的,因为它们只能存储特定类型的元素。
有序性:与集合不同,数组中的元素具有有序性,可以通过索引访问和修改元素。
固定大小:与集合相比,数组的大小是固定的,一旦创建就不能改变大小。不过,可以使用List
等集合类来模拟动态增长的行为。
泛型集合是通过在集合类定义中包含类型参数来创建的。这个类型参数在实例化集合时被具体的类型替换。通过使用泛型集合,可以编写适用于任何数据类型的代码,而不是为每种数据类型编写特定代码。
GetEnumerator()
或foreach
循环。IEnumerable
,并添加了一些用于操作集合中元素的方法,比如Add()
, Remove()
, Count
等。ICollection
的扩展,它提供了一系列用于访问和修改列表中特定位置元素的方法。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); // 输出每个元素的值
}
类型安全:使用泛型集合时,类型在编译时确定,减少了运行时的类型转换错误的可能性。
代码重用:通过编写泛型代码,你可以一次编写并在多种数据类型上重用代码,提高了代码的复用性。
性能优化:由于泛型集合在编译时知道具体的类型,因此它们可以生成特定于该类型的优化的代码。
更好的设计:使用泛型可以减少类、接口和方法之间的耦合度,使代码更加模块化和可维护。
可读性和可维护性:通过使用泛型,可以避免编写冗余的代码来处理不同数据类型的集合,从而提高代码的可读性和可维护性。
ArrayList
类提供了一个动态数组,该数组在运行时可以重新分配大小。
ArrayList<T> list = new ArrayList<T>();
list.Add(item); // 添加元素
T item = list[index]; // 通过索引访问元素
ArrayList
提供了类似数组的功能,但动态地调整大小。适用于不经常修改大小且需要动态增长的集合。
LinkedList
类实现了双向链表数据结构。每个节点包含一个指向下一个节点的引用和一个指向前一个节点的引用。
LinkedList<T> list = new LinkedList<T>();
list.AddLast(item); // 在链表末尾添加元素
T item = list.First.Value; // 获取链表中的第一个元素
适用于需要频繁地在集合的开头和结尾进行插入和删除操作的场景。
HashSet
类实现了一个不包含重复元素的集合。它通过散列函数来存储元素,使得添加、删除和查找操作具有高效性能。
HashSet<T> set = new HashSet<T>();
set.Add(item); // 添加元素
bool contains = set.Contains(item); // 检查集合是否包含特定元素
适用于需要快速检查元素是否存在、且不关心元素顺序的场景。
Queue
类实现了一个先进先出 (FIFO) 的集合。元素被添加到队列的末尾,并从队列的开头移除。
Queue<T> queue = new Queue<T>();
queue.Enqueue(item); // 将元素添加到队列末尾
T item = queue.Dequeue(); // 从队列头部移除并返回元素
适用于需要按顺序处理元素的场景,比如任务调度或缓冲数据流。
Stack
类实现了一个后进先出 (LIFO) 的集合。最后一个添加到堆栈的元素总是第一个被移除的。
Stack<T> stack = new Stack<T>();
stack.Push(item); // 将元素添加到堆栈顶部
T item = stack.Pop(); // 从堆栈顶部移除并返回元素
适用于需要按照相反顺序处理元素的场景,比如括号匹配或表达式求值。
可以通过创建一个泛型类来实现自定义的泛型集合。泛型集合类可以继承自IEnumerable
接口或实现ICollection
接口,或者可以自定义一个泛型接口,并让集合类实现这个接口。
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
类继承自IEnumerable
接口,这意味着它自动实现了IEnumerable
接口中的所有方法,包括GetEnumerator()
方法。这个类还实现了ICollection
接口的Count
属性,并添加了自己的Add
和Remove
方法以及索引器。最后,我们覆盖了两个GetEnumerator
方法,一个是IEnumerable
的版本,另一个是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没有提供直接移除元素的方法,所以这里可能需要使用其他方式来实现。例如使用RemoveAt(int index)或者遍历列表来移除元素。
public T this[int index] { get { return items[index]; } set { items[index] = value; } } // 索引器实现。支持通过索引访问元素。注意:这里的索引是从0开始的整数,而且对于List来说,索引越界会导致异常。你可能需要添加一些错误检查机制来处理越界问题。
public int Count { get { return items.Count; } } // 返回元素数量。注意:这里的Count属性仅仅返回存储在List中的元素数量,如果你的集合还包含其他来源的元素或者有特殊计数逻辑,你可能需要修改这个属性的实现。
}
减少内存分配:
使用List
的Capacity
属性预先分配足够的空间,以减少元素添加时频繁的内存重新分配。
考虑使用Array
或Span
来避免在处理大量数据时频繁的内存分配。
避免装箱和拆箱:
装箱是将值类型转换为引用类型的过程,拆箱则是相反的过程。尽量避免装箱和拆箱,尤其是在循环中。
使用值类型(如int
, float
, struct
等)而不是引用类型(如class
)。
使用缓存:
如果集合中的对象是可重用的,使用缓存来存储它们,避免重复创建相同的实例。
MemoryCache
类可用于创建内存中的缓存。
避免频繁的集合创建:
避免在循环中频繁创建新的集合实例。这可以通过重用现有集合或使用可变集合(如List
)来完成。
使用适当的集合类型:
根据需求选择最合适的集合类型。例如,HashSet
对于检查元素是否存在非常高效,而List
则适用于需要顺序访问元素的场景。
使用并行处理:
对于可以并行处理的数据,使用Parallel
类来提高性能。例如,使用Parallel.ForEach
或Parallel.Invoke
。
减少锁的使用:
如果集合需要在多线程环境中使用,确保正确地管理锁以避免死锁和性能下降。考虑使用ConcurrentBag
或ConcurrentQueue
等线程安全的集合。
避免异常处理:
在循环或其他性能敏感代码中避免使用异常处理,因为异常可能会导致性能下降。如果可能,使用错误代码或其他机制来处理错误情况。
优化查询:
对于大型集合,优化查询操作可以提高性能。例如,对于需要频繁查找的场景,可以考虑使用字典结构(如Dictionary
)来存储数据。
使用LINQ:
LINQ表达式通常会编译成高效的代码,但要确保你的LINQ查询不会产生大量的中间对象或导致其他性能问题。如果可能,尽量减少LINQ查询的复杂性或将其与循环结合使用。
利用值类型和引用类型的差异:
根据需要选择正确的数据类型。值类型通常更快,因为它们存储在栈上而不是堆上,但它们的大小可能更大。另一方面,引用类型通常占用更少的空间,但它们需要额外的内存管理开销。
优化字符串操作:
如果你在集合中处理大量字符串,考虑使用字符串池(String Interning)来重用字符串实例,以减少内存分配和垃圾收集的开销。
减少不必要的装箱和拆箱操作:
如果你的集合包含值类型,但你经常将它们转换为引用类型(例如传递给需要引用类型的API),这可能会导致装箱和拆箱操作。考虑重新设计API或数据结构以避免这些操作。
避免不必要的复制操作:
在某些情况下,集合的复制操作可能是昂贵的。如果可能,尽量在原始集合上进行操作而不是复制它。例如,如果你需要对集合进行排序或筛选操作,而不需要修改原始集合,你可以使用LINQ的OrderBy
或Where
方法而不是复制整个集合。
合理使用异步编程模型:
对于I/O密集型操作(如文件读写、数据库查询等),异步编程可以提高性能并减少线程阻塞。使用C#的异步方法(如async
和await
关键字)来编写异步代码,并确保正确地管理异步操作的取消、异常处理和资源释放。
当集合的参数为null时,可能会抛出此异常。例如,调用一个需要非null参数的集合方法时传入null。
处理方法:在调用集合方法之前,检查参数是否为null,如果是,则抛出异常或采取其他适当的措施。
当传递给集合方法的参数超出有效范围时,可能会抛出此异常。例如,尝试访问数组中不存在的索引。
处理方法:验证参数的有效性,确保它们在预期的范围内。
当集合处于无效状态,无法执行某个操作时,可能会抛出此异常。例如,尝试对只读集合进行修改操作。
处理方法:检查集合的状态,确保它处于可以执行所需操作的状态。
当尝试访问字典或其他键值对集合中不存在的键时,可能会抛出此异常。
处理方法:在访问键之前检查键是否存在于集合中,或者使用TryGetValue等不会抛出异常的方法。
当多个线程同时修改集合时,可能会抛出此异常。
处理方法:使用线程安全的集合类型(如ConcurrentBag
),或在使用集合时使用锁来同步对集合的访问。
当集合不支持某个操作时,可能会抛出此异常。例如,尝试对只读集合进行修改操作。
处理方法:检查集合是否支持所需的操作,如果不支持,则抛出异常或采取其他适当的措施。
当引用类型为null时,访问其成员或调用其方法时可能会抛出此异常。
处理方法:在访问引用类型的成员或方法之前,检查引用是否为null。
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);
}
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);
}
延迟执行和立即执行是 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表达式是一种简洁的匿名函数表示方式,可以用于编写简洁、可读性强的代码。它常用于LINQ查询操作和委托回调。
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
Lambda表达式可以隐式转换为委托类型,如果Lambda表达式没有返回值,则转换为Action委托类型;如果有返回值,则转换为Func委托类型。
Lambda表达式实际上是一种匿名函数,可以用于代替传统的显式声明函数的方式,使代码更加简洁。
Lambda表达式可以使代码更加简洁、可读性更强,但也可能会使代码难以理解,特别是对于初学者。因此,在使用Lambda表达式时需要注意适度。
除了基本语法外,C# 还提供了多种扩展方法来进一步简化 Lambda 表达式的编写,如使用 Lamda 捕捉变量、使用 out/ref 参数、使用默认参数等。
以下所有例子均使用SQL server,因为内容比较多,这里只简单介绍,后面新开一篇详细介绍C#和数据库的交互
数据提供程序是一组类库,用于连接到数据库、执行命令和检索数据。ADO.NET提供了多种数据提供程序,例如SqlClient用于连接和操作SQL Server数据库,OleDb用于连接和操作其他数据库,如Access、Oracle等。
ADO.NET的对象模型包括几个核心的类,这些类提供用于连接到数据库、执行SQL命令以及读取结果的方法和属性。主要包括以下部分:
Connection
类:负责建立和管理数据库连接。Command
类:用于执行SQL命令或存储过程。DataReader
类:用于读取从数据库返回的结果集。DataAdapter
类:用于在数据源和DataSet之间进行桥接。DataSet是一个内存中的数据表示,它可以包含多个DataTable对象,这些对象表示从数据库检索出来的表。DataSet还可以包含数据列、数据行、主键、外键等关系信息。DataSet是独立于任何特定数据源的,可以与多种数据源进行交互。
ADO.NET与XML紧密集成,可以通过XML格式在服务器和客户端之间传输数据,也可以将数据转换为XML格式进行存储或传输。
使用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)
{
// 处理异常...
}
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类型)
}
}
}
}
ORM(Object-Relational Mapping): 对象关系映射,是一种将关系型数据库的数据映射成对象的方法。通俗地说,就是用操作对象的方式来操作数据库。
Entity Framework: 微软官方的ORM框架,基于ADO.NET,支持多种数据库。
数据库优先(Database First): 通过设计数据库,然后生成实体类和数据上下文。
模型优先(Model First): 在Visual Studio中通过设计界面来创建实体类和数据库关系。
代码优先(Code First): 通过编写C#代码来定义实体类和数据上下文,然后根据这些代码来创建数据库。
EDM(Entity Data Model): 包括概念模型、映射和存储模型。概念模型定义了实体类及其之间的关系。
Entity Client Data Provider: 负责将L2E或Entity SQL转换为数据库可以识别的SQL查询语句。
ADO.NET Data Provider: 使用标准的ADO.NET与数据库进行通信。
基础查询:
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); // 找到集合中的最大值。
设置数据库上下文和实体类
假设我们有一个简单的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 = "[email protected]" };
// 添加到数据库
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 = "[email protected]"; // 修改用户数据
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();
}
LINQ(Language Integrated Query)是一种强大的查询语言,它允许开发人员在C#代码中使用类似于SQL的语法来查询各种数据源,包括内存中的集合、数据库、XML文档等。
LINQ提供了一种统一的方法来查询和操作数据,无论数据存储在何处。它基于表达式编程,允许使用声明性语法来表示查询,从而使代码更易于阅读和理解。
基础查询(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); // 找到集合中的最大值。
进程:是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
线程:是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。
是不是有点抽象?没关系,我们拿王者荣耀来举个例子,比如我现在开了一把游戏,我这把游戏就可以看成是一个进程,而在这把游戏里的英雄,小兵,野怪等局内的元素就可以看作一个个线程。所以,多线程的概念也就呼之欲出了:一个进程里可以有多个线程就叫多线程。
需要注意的是,进程和线程还有下面这些特点:
1、线程在进程下进行
(单独的英雄角色、野怪、小兵肯定不能运行)
2、进程之间不会相互影响,主线程结束将会导致整个进程结束
(你和你朋友没人开一把游戏,你超神了对他那局游戏也起不到任何影响,水晶(主线程)推了游戏就结束了)
3、不同的进程数据很难共享
(两局游戏之间很难有联系,你无法在你的那局游戏里看到另外一局游戏的数据,除非王者出bug(串麦!!!))
4、同进程下的不同线程之间数据很容易共享
(同一局游戏里你可以看到队友和对手状态,血量,装备)
5、进程使用内存地址可以限定使用量
(三排、五排房间满了以后就没法再邀请玩家进如,只能有玩家退出房间后才能进入)
线程的生命周期可以分为以下几个阶段:
创建(Created):线程对象被创建,但尚未开始执行。
就绪(Ready):线程已经准备好执行,但还没有被调度执行。
运行(Running):线程正在执行其任务。
阻塞(Blocked):线程被阻塞,暂时停止执行,等待某个条件满足或者等待某个资源可用。
终止(Terminated):线程执行完毕或者被强制终止,结束其生命周期。
在正常情况下,线程的生命周期是从创建开始,经过就绪、运行和阻塞等状态,最终结束于终止状态。然而,线程的生命周期也可能被一些特殊情况中断或改变,例如线程被中断、线程异常终止等
需要注意的是,线程的生命周期是由操作系统的调度器控制的,具体的状态转换和调度行为可能因操作系统的不同而有所差异。在C#中,可以使用Thread类来创建和管理线程,通过调用Thread类的方法和属性可以控制线程的生命周期。
线程同步:线程同步是指多个线程之间协调和同步执行的机制。由于多个线程同时访问共享资源可能会导致数据不一致或竞态条件等问题,因此需要使用线程同步来确保线程之间的顺序和正确性。
线程互斥:线程互斥是指多个线程对共享资源的访问进行协调和控制,以避免并发访问导致的数据不一致或竞态条件等问题。
*C#提供了多种机制来实现线程互斥,其中最常用的是互斥锁(Mutex)和监视器(Monitor)。
(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}");
}
}
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);
使用线程池来管理和复用线程资源,以提高多线程程序的性能和效率。以下是使用线程池的步骤:
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);
通过设置最大线程数量,可以控制线程池中的线程数量,避免线程过多导致系统资源耗尽。
使用线程池可以方便地管理和复用线程资源,避免频繁地创建和销毁线程,提高多线程程序的性能和效率。但需要注意,线程池中的线程是共享的,如果某个工作项执行时间过长,可能会影响其他工作项的执行。因此,在使用线程池时,需要合理安排工作项的执行顺序和时间,避免长时间占用线程池中的线程。
使用 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 方法。
当任务被提交到线程池时,线程池会自动选择一个可用的线程来执行任务。任务的执行顺序和线程的分配是由线程池来管理的。
使用 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 线程的最大数目保持不变。
请注意,设置线程池的最大线程数是一个全局设置,会影响整个应用程序中使用线程池的所有任务。因此,需要谨慎设置最大线程数,以避免过多的线程导致性能问题。
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}");
}
}
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
。
线程同步是指多个线程之间协调和同步执行的机制。由于多个线程同时访问共享资源可能会导致数据不一致或竞态条件等问题,因此需要使用线程同步来确保线程之间的顺序和正确性。
线程互斥是指多个线程对共享资源的访问进行协调和控制,以避免并发访问导致的数据不一致或竞态条件等问题。
C#提供了多种机制来实现线程互斥,其中最常用的是互斥锁(Mutex)和监视器(Monitor)。
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();
}
}
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();
}
}
信号量用于控制同时访问某个资源的线程数量。
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(); // 释放互斥体
}
}
任务并行库(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)); // 使用捕获的上下文信息创建新任务。
注意:过度使用并行和并发可能导致资源竞争、死锁和其他并发问题。在编写并行代码时,请务必小心并确保正确地同步访问共享资源。
异步编程是一种处理长时间运行操作的方法,这些操作不需要用户交互,但可能阻止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线程阻塞,提高用户体验。
取消任务
当使用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()
{
// 执行长时间运行的操作...可能会抛出异常...
}
}
使用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
) 可以提高对大型数据集的查询性能,但需要注意线程安全和资源竞争的问题。
线程安全是指对共享数据的访问控制,以避免并发线程之间的冲突和不一致状态。线程安全问题通常发生在多个线程同时访问和修改同一资源时。
要实现线程安全,可以使用以下几种方法:
System.Threading.Mutex
类可以同步对共享资源的访问。这个类可以用来实现跨进程的同步,但它比Monitor
更难以使用和调试。System.Threading.Monitor
类来锁定特定代码块,确保一次只有一个线程可以执行这些代码。lock
关键字来保护代码块,确保同一时间只有一个线程可以执行被保护的代码。Interlocked
类提供了一些原子操作方法。ReaderWriterLock
或ReaderWriterLockSlim
类用于同步对共享数据的读取和写入。它们允许同时有多个读取者,但只允许一个写入者。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
类来同步对共享资源的访问。每个线程在进入临界区之前必须获取互斥锁的拥有权,并在退出临界区时释放它。这样可以确保任何时候只有一个线程可以访问共享资源。
反射(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(); // 调用方法,像静态类型一样操作动态对象(编译时不检查类型)
使用反射通常需要额外的计算资源和性能开销,因此在性能敏感的应用程序中应谨慎使用。反射通常在框架类库、插件系统、序列化/反序列化、动态代码生成等场景中使用较多。
在运行时,可以使用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 */ });
动态类型绑定:使用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
类。这允许将对象状态转换为字节流或从字节流中恢复对象状态。
动态类型检查:可以使用反射来检查对象的实际类型,并根据需要执行不同的操作。
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 */ }); // 调用泛型方法并执行操作...
安全性考虑:
性能考虑:
为了提高安全性并优化性能,可以考虑以下措施:
最小化反射的使用:尽量减少反射的使用,只在必要时使用它。考虑使用其他技术来实现相同的功能,例如使用接口、继承或设计模式。
输入验证和过滤:在使用反射之前,确保对用户输入进行验证和过滤,以防止潜在的安全风险。
缓存反射信息:如果频繁地使用反射来查询类型信息,考虑将结果缓存起来,以减少重复的反射操作。
优化性能:对于性能关键的部分,尽量避免使用反射,或者寻找更高效的替代方案。
权限和访问控制:确保应用程序中的反射操作受到适当的权限和访问控制限制,以防止未经授权的访问或修改。
PS:因为内容太多可能显得有些杂乱,朋友们若是发现问题,欢迎指正