elric
21 次阅读
约 28 分钟 · 8828 字
C# 学习笔记

C#学习笔记

2024-7-8-14:00~ 最后更新时间 2025-2-20

读音:C+Sharp [ʃɑːp](夏普)

##简介:
编程的学习路径:

  • 纵向:语言->类库->框架->架构
  • 横向:命令行、图形界面、Web、移动端、游戏、物联网、大数据、人工智能、区块链

编写程序的步骤:

  • 编辑->编译->运行->调试->发布

有用的资源:

C# 与 .NET 的关系:

  • C#是一种编程语言
  • .NET 是一个平台,包括了运行时环境、类库、工具等
  • C# ∈ .NET

C# 程序结构

主要包含:

  • 命名空间声明
  • 一个 Class 类
  • 一个 Main 方法

后缀为.cs

Hello World

C#
using System; namespace HelloWorld {    class Hello {        static void Main() {            Console.WriteLine("Hello,World");            Console.ReadKey(); // 防止命令行窗口一闪而过        }    }}

using -> 引用命名空间

如上,using System;允许直接使用System命名空间下的Console类,而不需要完整地写出System.Console.WriteLine("Hello, World");。

和其他语法的关系

更像Java

基本语法

和Java差不多

所有关键字(表格):

保留关键字
abstractasbaseboolbreakbytecase
catchcharcheckedclassconstcontinuedecimal
defaultdelegatedodoubleelseenumevent
explicitexternfalsefinallyfixedfloatfor
foreachgotoifimplicitinin (generic
modifier)
int
interfaceinternalislocklongnamespacenew
nullobjectoperatoroutout
(generic
modifier)
overrideparams
privateprotectedpublicreadonlyrefreturnsbyte
sealedshortsizeofstackallocstaticstringstruct
switchthisthrowtruetrytypeofuint
ulonguncheckedunsafeushortusingvirtualvoid
volatilewhile
上下文关键字
addaliasascendingdescendingdynamicfromget
globalgroupintojoinletorderbypartial
(type)
partial
(method)
removeselectset

数据类型

分为值类型(Value Type)、引用类型(Reference Type)和指针类型(Pointer Type)。

值类型

  • 整数类型:byte、short、int、long、sbyte、ushort、uint、ulong
  • 浮点类型:float、double、decimal
  • 字符类型:char
  • 布尔类型:bool
类型描述范围默认值
bool布尔值True 或 FalseFalse
byte8 位无符号整数0 到 2550
char16 位 Unicode 字符U +0000 到 U +ffff'\0'
decimal128 位精确的十进制值,28-29 有效位数(-7.9 x 1028 到 7.9 x 1028) / 100 到 28 0.0M
double64 位双精度浮点型(+/-)5.0 x 10-324 到 (+/-)1.7 x 103080.0D
float32 位单精度浮点型-3.4 x 1038 到 + 3.4 x 10380.0F
int32 位有符号整数类型-2,147,483,648 到 2,147,483,6470
long64 位有符号整数类型-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 0L
sbyte8 位有符号整数类型-128 到 1270
short16 位有符号整数类型-32,768 到 32,7670
uint32 位无符号整数类型0 到 4,294,967,2950
ulong64 位无符号整数类型0 到 18,446,744,073,709,551,6150
ushort16 位无符号整数类型0 到 65,5350

可空类型

  • 可空类型:Nullable<T>或T?

    可空类型是一种特殊的值类型,可以存储 null 值

  • 单问号?:Nullable<T>的简写

    Nullable<T> i = 3; <==> int? i = 3;

  • 双问号??:空合并运算符

    int? i = null; int j = i ?? 3; // 如果i为空,则j为 3

C#
int? i = null;int ii; // 默认值为0if (i.HasValue) { //`HasValue`属性:判断是否有值    Console.WriteLine(i.Value);}else {    Console.WriteLine("i is null");}int j = i ?? 3;Console.WriteLine(j);

引用类型

  • 内置引用类型:object、string、dynamic

对象类型:class、interface、delegate、array

当一个值类型转换为对象类型时,则被称为 装箱;另一方面,当一个对象类型转换为值类型时,则被称为 拆箱

C#
int i = 123;object o = i; // 装箱int j = (int)o; // 拆箱

动态类型:dynamic

dynamic类型是在编译时不知道其类型,而是在运行时确定的类型。(python 中只有动态类型,而 C#中有静态类型和动态类型)

C#
dynamic d = 20;d = "Hello, World";

字符串类型:string

字符串是引用类型,但是使用起来像值类型

C#
string str = "Hello, World";

@符号:

  • @符号用于定义字符串字面量,可以包含多行,转义字符会被忽略
C#
string str = @"C:/Windows"; // == string str =  "C://Windows"
  • @字符串中可以任意换行,但是不能有连续的两个@
C#
string str = @"C:/WindowsC:/Program Files"; // 换行符及缩进空格都计算在字符串长度之内

指针类型

指针类型和 C/C++中的指针类似,但是 C#中的指针类型只能在不安全代码块(unsafe)中使用。

C#
unsafe{    int var = 20;    int* p = &var;    Console.WriteLine((int)p); // 输出地址    Console.WriteLine(*p); // 输出值}

数组、字符串、枚举、结构体类型

数组与Java类似,
其他的与C++类似

C#
### 类型转换- 隐式转换:从小范围类型到大范围类型(自动转换)- 显式转换:从大范围类型到小范围类型(强制转换) ```csharpint i = 10;double d = i; // 隐式转换int j = (int)d; // 显式转换
  • Convert 类:Convert.To大写数据类型(变量)
C#
int i = 10;string str = Convert.ToString(i);
  • Parse 方法:大写数据类型.Parse(字符串)
C#
string str = "10";int i = int.Parse(str);
  • TryParse 方法:大写数据类型.TryParse(字符串, out 变量)
C#
string str = "10";int i;if (int.TryParse(str, out i)) {    Console.WriteLine(i);}else {    Console.WriteLine("转换失败");}

封装

访问修饰符:

  • public:公有的,所有类都可以访问
  • private:私有的,只有本类可以访问 (默认)
  • protected:受保护的,只有本类和子类可以访问
  • internal:内部的,只有本程序集可以访问
  • protected internal:受保护的内部的,本类、子类和本程序集可以访问

程序集:编译后的文件,包括了所有的类、接口、枚举等

继承

  • class类可以继承另一个class类 (父类又叫基类,子类又叫派生类)

  • interface接口可以继承另一个interface接口

    C#
    class A {    public void Print() {        Console.WriteLine("A");    }} class B : A { // 继承的语法    public void Print() {        Console.WriteLine("B");    }} B b = new B();b.Print(); // 输出B
  • class类不支持多继承,但是interface接口支持多继承

    C#
    interface A {    void Print();} interface B {    void Print();} class C : A, B {    public void Print() {        Console.WriteLine("C");    }}

多态

  • 多态是指一个类实例的相同方法在不同情况下有不同的表现形式
  • c#通过虚方法和抽象类实现动态多态性
C#
using System;using System.Collections.Generic; public class Shape {    public int X { get; private set; }    public int Y { get; private set; }    public int Height { get; set; }    public int Width { get; set; }     // 虚方法    public virtual void Draw() {        Console.WriteLine("执行基类的画图任务");    }} class Circle : Shape{    public override void Draw() {        Console.WriteLine("画一个圆形");        base.Draw();    }}class Rectangle : Shape {    public override void Draw() {        Console.WriteLine("画一个长方形");        base.Draw();    }}class Triangle : Shape {    public override void Draw()n{        Console.WriteLine("画一个三角形");        base.Draw();    }} class Program {    static void Main(string[] args) {        // 创建一个 List<Shape> 对象,并向该对象添加 Circle、Triangle 和 Rectangle        var shapes = new List<Shape> {            new Rectangle(),            new Triangle(),            new Circle()        };         // 使用 foreach 循环对该列表的派生类进行循环访问,并对其中的每个 Shape 对象调用 Draw 方法        foreach (var shape in shapes) {            shape.Draw(); // 调用子类的Draw方法        }         Console.WriteLine("按下任意键退出。");        Console.ReadKey();    } }/* 运行结果:画一个长方形执行基类的画图任务画一个三角形执行基类的画图任务画一个圆形执行基类的画图任务按下任意键退出。*/
  • 静态多态性:方法重载与运算符重载

    • 方法重载:方法名相同,参数列表不同

    • 不可重载的运算符:=, ?:, ::, ., is, new 等等。(但像是 C++就支持重载=运算符)

      这些运算符不可重载主要是因为它们要么涉及到基础的内存操作和类型检查,要么是语言的核心特性,允许重载这些运算符可能会导致代码难以理解和维护

接口

  • 接口相当于语法合同(契约),定义了“是什么”部分,让子类定义了“怎么做”部分。
  • 接口只包含了成员的声明

接口的定义与继承

C#
using System; interface IParentInterface{    void ParentInterfaceMethod();} interface IMyInterface : IParentInterface{    void MethodToImplement();} class InterfaceImplementer : IMyInterface{    static void Main()    {        InterfaceImplementer iImp = new InterfaceImplementer();        iImp.MethodToImplement();        iImp.ParentInterfaceMethod();    }     public void MethodToImplement()    {        Console.WriteLine("MethodToImplement() called.");    }     public void ParentInterfaceMethod()    {        Console.WriteLine("ParentInterfaceMethod() called.");    }}/** 结果输出:MethodToImplement() called.ParentInterfaceMethod() called.*/

命名空间

命名空间的设计目的是提供一种让一组名称与其他名称分隔开的方式。在一个命名空间中声明的类的名称与另一个命名空间中声明的相同的类的名称不冲突。

举一个计算机系统中的例子,一个文件夹(目录)中可以包含多个文件夹,每个文件夹中不能有相同的文件名,但不同文件夹中的文件可以重名。

定义命名空间

C#
namespace namespace_name{   // 代码声明   // 命名空间可以嵌套}
  • C#中通过.来访问嵌套命名空间中的成员

using 的用法

  1. using 指令:引入命名空间

这是最常见的用法,例如:

using System;
using Namespace1.SubNameSpace; 2. using static 指令:指定无需指定类型名称即可访问其静态成员的类型

using static System.Math;var = PI; // 直接使用 System.Math.PI 3. 起别名

using Project = PC.MyCompany.Project; 4. using 语句:将实例与代码绑定

using (Font font3 = new Font("Arial", 10.0f),
font4 = new Font("Arial", 10.0f))
{
// Use font3 and font4.
}
代码段结束时,自动调用 font3 和 font4 的 Dispose 方法,释放实例。

预处理器指令

  • 用于指导编译器在实际编译开始之前对信息进行预处理。
  • 预处理器指令不是语句,所以它们不以分号 ; 结束。
  • 与 C 和 C++ 不同的是,它们不是用来创建宏(宏定义)。一个预处理器指令必须是该行上的唯一指令。
    • 如:#define PI = 3.14 ×
指令描述
#define定义一个符号,可以用于条件编译。
#undef取消定义一个符号。
#if开始一个条件编译块,如果符号被定义则包含代码块。
#elif如果前面的 #if 或 #elif 条件不满足,且当前条件满足,则包含代码块。
#else如果前面的 #if 或 #elif 条件不满足,则包含代码块。
#endif结束一个条件编译块。
#warning生成编译器警告信息。
#error生成编译器错误信息。
#region标记一段代码区域,可以在IDE中折叠和展开这段代码,便于代码的组织和阅读。
#endregion结束一个代码区域。
#line更改编译器输出中的行号和文件名,可以用于调试或生成工具的代码。
#pragma用于给编译器发送特殊指令,例如禁用或恢复特定的警告。
#nullable控制可空性上下文和注释,允许启用或禁用对可空引用类型的编译器检查。

正则表达式

  • C#中可以在字符串" "中使用正则表达式
  • 一般在使用是,会在字符串前加@,以便让""中变成字面量,方便符号\的书写
  • 若要在字符串中插入值,需要在前面加上$
C#
int num = 123;string pattern = @$"\d+{num}";或者int num = 123;string pattern = $@"\d+{num}";

验证邮箱地址:

C#
using System.Text.RegularExpressions; string email = "example@example.com";Regex regex = new Regex(@"^\S+@\S+\.\S+$");bool isValid = regex.IsMatch(email);

异常处理

预定义的异常类:

异常类描述
System.IO.IOException处理 I/O 错误。
System.IndexOutOfRangeException处理当方法指向超出范围的数组索引时生成的错误。
System.ArrayTypeMismatchException处理当数组类型不匹配时生成的错误。
System.NullReferenceException处理当依从一个空对象时生成的错误。
System.DivideByZeroException处理当除以零时生成的错误。
System.InvalidCastException处理在类型转换期间生成的错误。
System.OutOfMemoryException处理空闲内存不足生成的错误。
System.StackOverflowException处理栈溢出生成的错误。

异常处理

通过try与chach块,来将核心程序语句与错误处理语句分离开

C#
using System;namespace ErrorHandlingApplication{    class DivNumbers    {        int result;        DivNumbers()        {            result = 0;        }        public void division(int num1, int num2)        {            try            {                result = num1 / num2;            }            catch (DivideByZeroException e)            {                Console.WriteLine("Exception caught: {0}", e);            }            finally            {                Console.WriteLine("Result: {0}", result);            }         }        static void Main(string[] args)        {            DivNumbers d = new DivNumbers();            d.division(25, 0);            Console.ReadKey();        }    }}

创建自己自定义的异常

您也可以定义自己的异常。用户自定义的异常类是派生自 ApplicationException 类。下面的实例演示了这点:

C#
using System;namespace UserDefinedException{   class TestTemperature   {      static void Main(string[] args)      {         Temperature temp = new Temperature();         try         {            temp.showTemp();         }         catch(TempIsZeroException e)         {            Console.WriteLine("TempIsZeroException: {0}", e.Message);         }         Console.ReadKey();      }   }}public class TempIsZeroException: ApplicationException{   public TempIsZeroException(string message): base(message)   {   }}public class Temperature{   int temperature = 0;   public void showTemp()   {      if(temperature == 0)      {         throw (new TempIsZeroException("Zero Temperature found"));      }      else      {         Console.WriteLine("Temperature: {0}", temperature);      }   }}/* 执行结果:TempIsZeroException: Zero Temperature found*/

抛出异常对象:

如果异常是直接或间接派生自 System.Exception 类,您可以抛出一个对象。您可以在 catch 块中使用 throw 语句来抛出当前的对象,如下所示:

C#
catch(Exception e){   ...   Throw e}

作用:错误通知、提供错误细节、让上层调用者处理,让自定义异常类来处理

文件的输入与输出

当打开文件进行读写时,它变成一个流。

从根本上说,流是通过通信路径传递的字节序列。有两个主要的流:输入流 和 输出流*。**输入流用于从文件读取数据(读操作),输出流**用于向文件写入数据(写操作)*

I/O 类

放在System.IO中。
下表是一些 System.IO 命名空间中常用的非抽象类:

I/O 类描述
BinaryReader从二进制流读取原始数据。
BinaryWriter以二进制格式写入原始数据。
BufferedStream字节流的临时存储。
Directory有助于操作目录结构。
DirectoryInfo用于对目录执行操作。
DriveInfo提供驱动器的信息。
File有助于处理文件。
FileInfo用于对文件执行操作。
FileStream用于文件中任何位置的读写。
MemoryStream用于随机访问存储在内存中的数据流。
Path对路径信息执行操作。
StreamReader用于从字节流中读取字符。
StreamWriter用于向一个流中写入字符。
StringReader用于读取字符串缓冲区。
StringWriter用于写入字符串缓冲区。

FileStream 类

System.IO 命名空间中的 FileStream 类有助于文件的读写与关闭。该类派生自抽象类 Stream。

创建一个 FileStream 对象:

C#
FileStream 对象名 = new FileStream("文件路径", 文件打开时的操作, 文件访问方法, 文档关闭时的操作);
C#
FileStream F = new FileStream("sample.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
参数描述
FileMode

FileMode 枚举定义了各种打开文件的方法。FileMode 枚举的成员有:

  • Append:打开一个已有的文件,并将光标放置在文件的末尾。如果文件不存在,则创建文件。
  • Create:创建一个新的文件。如果文件已存在,则删除旧文件,然后创建新文件。
  • CreateNew:指定操作系统应创建一个新的文件。如果文件已存在,则抛出异常。
  • Open:打开一个已有的文件。如果文件不存在,则抛出异常。
  • OpenOrCreate:指定操作系统应打开一个已有的文件。如果文件不存在,则用指定的名称创建一个新的文件打开。
  • Truncate:打开一个已有的文件,文件一旦打开,就将被截断为零字节大小。然后我们可以向文件写入全新的数据,但是保留文件的初始创建日期。如果文件不存在,则抛出异常。
FileAccess

FileAccess 枚举的成员有:Read、ReadWrite 和 Write。

FileShare

FileShare 枚举的成员有:

  • Inheritable:允许文件句柄可由子进程继承。Win32 不直接支持此功能。
  • None:谢绝共享当前文件。文件关闭前,打开该文件的任何请求(由此进程或另一进程发出的请求)都将失败。
  • Read:允许随后打开文件读取。如果未指定此标志,则文件关闭前,任何打开该文件以进行读取的请求(由此进程或另一进程发出的请求)都将失败。但是,即使指定了此标志,仍可能需要附加权限才能够访问该文件。
  • ReadWrite:允许随后打开文件读取或写入。如果未指定此标志,则文件关闭前,任何打开该文件以进行读取或写入的请求(由此进程或另一进程发出)都将失败。但是,即使指定了此标志,仍可能需要附加权限才能够访问该文件。
  • Write:允许随后打开文件写入。如果未指定此标志,则文件关闭前,任何打开该文件以进行写入的请求(由此进程或另一进过程发出的请求)都将失败。但是,即使指定了此标志,仍可能需要附加权限才能够访问该文件。
  • Delete:允许随后删除文件。
C#
using System;using System.IO; namespace FileIOApplication{    class Program    {        static void Main(string[] args)        {            FileStream F = new FileStream("test.dat",            FileMode.OpenOrCreate, FileAccess.ReadWrite);             for (int i = 1; i <= 20; i++)            {                F.WriteByte((byte)i);            }             F.Position = 0;             for (int i = 0; i <= 20; i++)            {                Console.Write(F.ReadByte() + " ");            }            F.Close();            Console.ReadKey();        }    }}/* 输出:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 -1*/

当然还有其他高级方法用于操作文档,但这里不在赘述

高级语法

特性

  • 特性是一种声明代码的方式,它可以添加元数据、提供编译时和运行时的信息。
  • 特性是放在方括号[]中的,可以放在类、方法、属性、参数等上面。

规定特性

语法:

C#
[特性名称(必要参数,可选参数=值,...)]

内置特性

三种预定义特性:

  • AttributeUsage - 用于定义自定义特性的使用范围
C#
[AttributeUsage(AttributeTargets.All)]
  • Conditional - 指示编译器在指定的条件下是否编译方法调用
C#
[Conditional("DEBUG")]
  • Obsolete - 标记程序中已过时的元素
C#
[Obsolete("该方法已过时", true)]

自定义特性

步骤:

  1. 创建一个类并继承自System.Attribute
  2. 在类中定义构造函数和字段
  3. 在目标程序元素上应用特性
  4. 使用反射获取特性信息
C#
using System; [AttributeUsage(AttributeTargets.All)]public class HelpAttribute : Attribute // 继承自Attribute{    public readonly string Url; // 只读的,只能在构造函数中修改    private string topic;     public string Topic    {        get        {            return topic;        }        set        {            topic = value;        }    }     public HelpAttribute(string url)    {        this.Url = url;    }} [HelpAttribute("Information on the class MyClass")] // 通过将特性放在类的前面,将特性应用于类class MyClass{} class Program{    static void Main()    {        System.Reflection.MemberInfo info = typeof(MyClass); // 获取类的信息        object[] attributes = info.GetCustomAttributes(true); // 获取类的特性        for (int i = 0; i < attributes.Length; i++)        {            System.Console.WriteLine(attributes[i]); // 输出特性        }    }}

反射

  • 反射是指程序可以访问、检测和修改它本身状态或行为的一种能力。
  • 反射是通过元数据来实现的,元数据是描述程序的数据。
  • 反射是.NET Framework 的一部分,它提供了一组类,用于检查程序集、模块、类型和成员。

反射的优点

  • 提高了程序的灵活性与扩展性,降低了程序的耦合度
  • 可以在运行时获取类型信息,动态创建类型实例,调用方法,获取或设置属性值

反射的缺点

  • 反射的性能较低,因为它是在运行时进行的
  • 使用反射会模糊程序内部逻辑;程序员希望在源代码中看到程序的逻辑,反射却绕过了源代码的技术,因此会使程序难以理解和维护

属性

  • 属性是一种特殊的类成员,它们允许你通过定义访问器方法(getter 和 setter)来控制对私有字段(private fields)的访问。
  • 就是将 getter 与 setter 函数头封装成了两个关键字get与set,它两又称访问器
  • 叫做属性是因为只有get就是只读属性等等

定义

C#
public class Person{    private string name;     public string Name    {        get { return name; }        set { name = value; }    }}

自动创建属性

C#
public class Person{    public string Name { get; set; }}

通过这串代码,可以让编译器自动创建默认的这两个属性

属性的访问性

也可以通过 public、private、protected 等访问修饰符来控制属性的访问性,决定谁可以访问和修改属性的值。

索引器

  • 索引器是一种特殊的属性,允许对象的实例像访问数组元素一样访问对象的实例成员。
  • 索引器允许你通过类似数组下标的方式来访问对象中的元素。

定义

  • 索引器的语法与属性类似,但是使用 this 关键字定义。可以有一个或多个形参,

    C#
    public class MyClass{    private string[] data = new string[10];     // 索引器的定义    public string this[int index]    {        get { return data[index]; }        set { data[index] = value; }    }}

    MyClass 类定义了一个索引器,可以通过整数索引来访问 data 数组中的元素。

访问

  • 类似于数组的访问:
C#
MyClass obj = new MyClass();obj[0] = "First element";string element = obj[0];```通过 obj[0] 访问了 MyClass 对象中的索引器,但实际上是调用了定义在类中的 get 和 set 方法

多维索引器

  • 通过多个参数来支持多维访问

    C#
    public class Matrix{    private int[,] data = new int[3, 3];     public int this[int row, int column]    {        get { return data[row, column]; }        set { data[row, column] = value; }    }} Matrix matrix = new Matrix();matrix[0, 0] = 1;int value = matrix[0, 0];

特性

  • 索引器可以是 public、private、protected 等访问修饰符。
  • 索引器可以是只读的(只有 get 方法),也可以是可读写的(包含 get 和 set 方法)。
  • 索引器可以与其他属性一样使用 readonly 修饰符,以提供只读的索引器。

委托

  • 委托可以看做是函数指针的安全版
  • 可以用于将方法作为参数传递给其他方法,或者作为数据结构中的一个元素
  • 委托(Delegate)是一种类型,它允许你引用一个或多个方法,并且可以将这些方法视为单个实体

定义与使用

  • 定义包含两个部分:声明与实例化

    C#
    // 委托声明public delegate void MyDelegate(string message); // 方法符合委托的签名public static void Method1(string message){    Console.WriteLine(message);} public static void Method2(string message){    Console.WriteLine(message.ToUpper());} // 创建委托实例MyDelegate delegate1 = Method1;MyDelegate delegate2 = Method2; // 调用委托delegate1("Hello, world!");delegate2("Goodbye, world!");

    MyDelegate 是一个委托类型,它可以引用任何接受一个 string 参数且没有返回值的方法。delegate1 和 delegate2 是 MyDelegate 类型的委托实例,它们分别引用了 Method1 和 Method2 方法。当调用 delegate1("Hello, world!") 时,实际上会调用 Method1("Hello, world!")

特点与用途:

  1. 方法引用: 委托可以用来引用一个或多个具有相同签名(方法签名包括返回类型和参数列表)的方法。这些方法可以是静态方法、实例方法或者匿名方法(Lambda 表达式)。
  2. 事件处理: 委托在事件驱动编程中尤为重要。事件处理器就是委托的实例,它们可以订阅事件并在事件发生时执行特定的操作。
  3. 多播委托: 委托可以组合多个方法。通过使用 + 和 - 运算符,可以将多个委托实例合并成一个多播委托。多播委托按顺序调用其包含的方法。
  4. 异步编程: 委托在异步编程中有着重要的作用,特别是在使用 BeginInvoke 和 EndInvoke 方法执行异步操作时。

事件

  • 事件是一种特殊的委托类型,它用于实现发布者-订阅者模式,也称为观察者模式。
    • 事件允许一个对象(通常是发布者或源对象)通知其他对象(订阅者或监听器对象)发生了特定的动作或状态改变。

主要特点和用途

  1. 发布者和订阅者模式: 事件允许对象在某个特定的时刻通知其他对象,而无需知道这些对象的具体实现。这种解耦合的机制非常适合用于处理用户界面元素、异步任务完成、消息传递等场景。
  2. 声明和使用:
    • 事件声明:事件的声明与委托类似,但使用 event 关键字修饰。
    • 事件订阅和取消订阅:订阅者可以通过 += 运算符订阅事件,而使用 -= 运算符取消订阅。
  3. 安全性和可靠性:
    • 事件的订阅和通知是线程安全的,因为事件系统会自动处理多线程访问。
    • 订阅者不会直接调用事件的发布者,而是通过事件来间接通信,这样有助于确保程序的健壮性和可维护性。

定义

C#
using System; public class Publisher  // 发布者{    // 声明一个事件,使用 EventHandler 委托    public event EventHandler<MyEventArgs> MyEvent;     // 通过调用 OnMyEvent 来触发事件,并将事件数据 MyEventArgs 传递给订阅者    public void RaiseEvent()    {        Console.WriteLine("触发事件了!");        OnMyEvent(new MyEventArgs("事件数据"));    }     protected virtual void OnMyEvent(MyEventArgs e)    {        MyEvent?.Invoke(this, e);    }} public class Subscriber // 订阅者{    // 通过 Subscribe 方法订阅了 Publisher 的事件    public void Subscribe(Publisher pub)    {        // 订阅事件        pub.MyEvent += HandleEvent;    }     public void Unsubscribe(Publisher pub)    {        // 取消订阅事件        pub.MyEvent -= HandleEvent;    }     // 封装事件的相关数据    // 通过 HandleEvent 方法来处理事件    private void HandleEvent(object sender, MyEventArgs e)    {        Console.WriteLine($"事件处理结果: {e.Message}");    }} public class MyEventArgs : EventArgs{    public string Message { get; }     public MyEventArgs(string message)    {        Message = message;    }} 

使用

C#
public class Program{    public static void Main()    {        // 创建了一个发布者与两个订阅者        Publisher publisher = new Publisher();        Subscriber subscriber1 = new Subscriber();        Subscriber subscriber2 = new Subscriber();         // 订阅事件        subscriber1.Subscribe(publisher);        subscriber2.Subscribe(publisher);         // 触发事件            // 此时,两个订阅者都知道了改事件        publisher.RaiseEvent();         // 取消订阅事件(可选)        subscriber1.Unsubscribe(publisher);        subscriber2.Unsubscribe(publisher);         // 再次触发事件        publisher.RaiseEvent();    }}/* 输出:触发事件了!事件处理结果:事件数据事件处理结果:事件数据触发事件了!*/

集合

  • 集合类是专门用于数据存储和检索的类
  • 其中包括了动态数组 ArrayList、列表 List、栈 Stack、队列 Queue、排序列表 SortedList、哈希表 Hashtable、点阵列 BitArray 的支持

泛型<T>

作用是允许您编写可以在多种数据类型上重用的代码,而不需要为每种数据类型编写重复的代码。这种重用性提高了代码的灵活性和可维护性

C#
public class GenericList<T> // 创建了一个泛型类{    private T[] data;    private int index;     public GenericList(int capacity)    {        data = new T[capacity];        index = 0;    }     public void Add(T item)    {        if (index < data.Length)        {            data[index] = item;            index++;        }        else        {            throw new IndexOutOfRangeException("超出列表容量");        }    }     public T Get(int index)    {        if (index >= 0 && index < this.index)        {            return data[index];        }        else        {            throw new IndexOutOfRangeException("索引超出范围");        }    }} public class Program{    public static void Main()    {        // 创建一个存储整数的泛型列表        GenericList<int> intList = new GenericList<int>(5);        intList.Add(10);        intList.Add(20);         Console.WriteLine("列表intList中的元素");        Console.WriteLine(intList.Get(0));        Console.WriteLine(intList.Get(1));         // 创建一个存储字符串的泛型列表        GenericList<string> stringList = new GenericList<string>(3);        stringList.Add("Hello");        stringList.Add("World");         Console.WriteLine("\n列表stringList中的元素");        Console.WriteLine(stringList.Get(0));        Console.WriteLine(stringList.Get(1));    }}

匿名方法与 Lambda 表达式

匿名方法

特点:

  1. 无需命名:
    • 匿名方法不需要显式的方法名称,它们通常通过关键字 delegate 来定义。
  2. 定义格式:
    • 使用 delegate 关键字定义一个匿名方法,例如: delegate (参数们) { /* 方法体 */ };
    • 这种语法允许您定义一个方法体,可以包含参数列表和方法体代码,但没有方法名。
  3. 适用场景:
    • 常用于委托的直接赋值,例如事件处理程序的订阅。
    • 用于传递给需要委托作为参数的方法或 API 中。
C#
using System; public class Program{    public static void Main()    {        // 使用匿名方法创建委托        Action<string> printMessage = delegate(string message)        {            Console.WriteLine("Message: " + message);        };         // 调用委托        printMessage("Hello, world!");    }}

Lambda 表达式

特点:

    • Lambda 表达式以 => 符号分隔两部分:左边是参数列表,右边是方法体。
    • 格式为:(parameters) => expression 或 (parameters) => { statements }。
  1. 参数列表:
    • 可以是零个或多个参数,参数可以有类型推断,也可以显式声明类型。
  2. 方法体:
    • 可以是单个表达式或一系列语句块。
    • 如果是单个表达式,返回值类型会根据表达式自动推断。
    • 如果是语句块,则需要显式指定返回值。
  3. 简洁性:
    • Lambda 表达式通常比等效的委托或匿名方法更简洁,因为它们允许您在需要时直接内联定义函数。
C#
using System; public class Program{    public static void Main()    {        // Lambda 表达式创建委托        Action<string> printMessage = message => Console.WriteLine("Message: " + message);         // 调用委托        printMessage("Hello, world!");    }}

总之:Lambda 表达式在作为匿名函数方面,比原匿名函数更加简介与直观

不安全代码

在计算机编程中,"不安全代码"(Unsafe Code)通常指的是在编程语言中允许直接访问计算机内存的特性或语法。这种代码允许开发人员绕过编译器的某些安全检查,以获得更高的灵活性和性能,但也带来了潜在的安全风险和错误处理挑战。

  • 当一个代码块使用 unsafe 修饰符标记时,C# 允许在函数中使用指针变量。

指针变量

指针是值为另一个变量的地址的变量,即,内存位置的直接地址。就像其他变量或常量,您必须在使用指针存储其他变量地址之前声明指针。

实例 描述
int* p p 是指向整数的指针。
double* p p 是指向双精度数的指针。
float* p p 是指向浮点数的指针。
int** p p 是指向整数的指针的指针。
int*[] p p 是指向整数的指针的一维数组。
char* p p 是指向字符的指针。
void* p p 是指向未知类型的指针。
C#
int* p1, p2, p3;     // 正确int *p1, *p2, *p3;   // 错误
C#
using System;namespace UnsafeCodeApplication{    class Program    {        static unsafe void Main(string[] args)        {            int var = 20;            int* p = &var;            Console.WriteLine("Data is: {0} ",  var);            Console.WriteLine("Address is: {0}",  (int)p);            Console.ReadKey();        }    }}

编译不安全代码

如果您使用的是 Visual Studio IDE,那么您需要在项目属性中启用不安全代码。

步骤如下:

  • 通过双击资源管理器(Solution Explorer)中的属性(properties)节点,打开项目属性(project properties)。
  • 点击 Build 标签页。
  • 选择选项"Allow unsafe code"。

使用指针

使用指针检索数据

C#
using System;namespace UnsafeCodeApplication{   class Program   {      public static void Main()      {         unsafe         {            int var = 20;            int* p = &var;            Console.WriteLine("Data is: {0} " , var);            Console.WriteLine("Data is: {0} " , p->ToString());            Console.WriteLine("Address is: {0} " , (int)p);         }         Console.ReadKey();      }   }}

传递指针作为方法的参数

C#
using System;namespace UnsafeCodeApplication{   class TestPointer   {      public unsafe void swap(int* p, int *q)      {         int temp = *p;         *p = *q;         *q = temp;      }       public unsafe static void Main()      {         TestPointer p = new TestPointer();         int var1 = 10;         int var2 = 20;         int* x = &var1;         int* y = &var2;          Console.WriteLine("Before Swap: var1:{0}, var2: {1}", var1, var2);         p.swap(x, y);          Console.WriteLine("After Swap: var1:{0}, var2: {1}", var1, var2);         Console.ReadKey();      }   }}

使用指针访问数组元素

C#
using System;namespace UnsafeCodeApplication{   class TestPointer   {      public unsafe static void Main()      {         int[]  list = {10, 100, 200};         fixed(int *ptr = list)          /* 显示指针中数组地址 */         for ( int i = 0; i < 3; i++)         {            Console.WriteLine("Address of list[{0}]={1}",i,(int)(ptr + i));            Console.WriteLine("Value of list[{0}]={1}", i, *(ptr + i));         }         Console.ReadKey();      }   }}

多线程

线程 被定义为程序的执行路径。每个线程都定义了一个独特的控制流。如果您的应用程序涉及到复杂的和耗时的操作,那么设置不同的线程执行路径往往是有益的,每个线程执行特定的工作。
线程是轻量级进程。一个使用线程的常见实例是现代操作系统中并行编程的实现。使用线程节省了 CPU 周期的浪费,同时提高了应用程序的效率。
到目前为止我们编写的程序是一个单线程作为应用程序的运行实例的单一的过程运行的。但是,这样子应用程序同时只能执行一个任务。为了同时执行多个任务,它可以被划分为更小的线程。

  • 多线程(Multithreading)指的是同时执行多个线程以实现并发处理的编程技术
  • 多线程允许程序在同一时间内执行多个独立的任务或操作,从而提高程序的效率和响应性

线程生命周期

  • 线程生命周期开始于 System.Threading.Thread 类的对象被创建时,结束于线程被终止或完成执行时。
  • 线程的生命周期包括以下状态:
    • 未启动状态:当线程实例被创建但 Start 方法未被调用时的状况。
    • 就绪状态:当线程准备好运行并等待 CPU 周期时的状况。
    • 不可运行状态:下面的几种情况下线程是不可运行的:
      • 已经调用 Sleep 方法
      • 已经调用 Wait 方法
      • 通过 I/O 操作阻塞
    • 死亡状态:当线程已完成执行或已中止时的状况。

主线程

  • 在 C# 中,System.Threading.Thread 类用于线程的工作。它允许创建并访问多线程应用程序中的单个线程。进程中第一个被执行的线程称为主线程。
  • 当 C# 程序开始执行时,主线程自动创建。使用 Thread 类创建的线程被主线程的子线程调用。您可以使用 Thread 类的 CurrentThread 属性访问线程。
C#
using System;using System.Threading; namespace MultithreadingApplication{    class MainThreadProgram    {        static void Main(string[] args)        {            Thread th = Thread.CurrentThread;            th.Name = "MainThread";            Console.WriteLine("This is {0}", th.Name);            Console.ReadKey();        }    }}/* 输出:This is MainThread*/

创建线程

  • 线程是通过扩展 Thread 类创建的。扩展的 Thread 类调用 Start() 方法来开始子线程的执行
C#
using System;using System.Threading; namespace MultithreadingApplication{    class ThreadCreationProgram    {        public static void CallToChildThread()        {            Console.WriteLine("子线程启动");        }         static void Main(string[] args)        {            ThreadStart childref = new ThreadStart(CallToChildThread);            Console.WriteLine("在 Main: 创建子线程");            Thread childThread = new Thread(childref);            childThread.Start();            Console.ReadKey();        }    }}/* 输出:在 Main: 创建子线程子线程启动*/

管理线程

  • 线程的管理是通过 System.Threading 命名空间中的 Thread 类完成的。Thread 类的方法和属性可用于管理线程的执行和状态。

下面的实例演示了 sleep() 方法的使用,用于在一个特定的时间暂停线程

C#
using System;using System.Threading; namespace MultithreadingApplication{    class ThreadCreationProgram    {        public static void CallToChildThread()        {            Console.WriteLine("子线程启动");            // 线程暂停 5000 毫秒            int sleepfor = 5000;            Console.WriteLine("子线程暂停 {0} 秒", sleepfor / 1000);            Thread.Sleep(sleepfor);            Console.WriteLine("子线程终止");        }         static void Main(string[] args)        {            ThreadStart childref = new ThreadStart(CallToChildThread);            Console.WriteLine("在 Main: 创建子线程");            Thread childThread = new Thread(childref);            childThread.Start();            Console.ReadKey();        }    }}/* 输出:在 Main: 创建子线程子线程启动子线程暂停 5 秒子线程终止*/

加入线程

  • Join() 方法允许一个线程等待另一个线程完成。
  • 如果一个线程调用其他线程的 Join() 方法,调用线程将被阻塞,直到被调用线程完成。
C#
using System;using System.Threading; namespace MultithreadingApplication{    class ThreadCreationProgram    {        public static void CallToChildThread()        {            Console.WriteLine("子线程启动");            for (int i = 0; i <= 5; i++)            {                Console.WriteLine(i);            }            Console.WriteLine("子线程终止");        }         static void Main(string[] args)        {            ThreadStart childref = new ThreadStart(CallToChildThread);            Console.WriteLine("在 Main: 创建子线程");            Thread childThread = new Thread(childref);            childThread.Start();            childThread.Join();            Console.WriteLine("子线程终止");            Console.ReadKey();        }    }}/* 输出:在 Main: 创建子线程子线程启动012345子线程终止子线程终止*/

线程优先级

  • 线程优先级是操作系统用于决定线程在 CPU 中执行的顺序。每个线程都有一个优先级。优先级较高的线程在 CPU 中执行的时间较长。
  • 线程的优先级是通过 Priority 属性设置的。线程的优先级可以是以下值之一:
    • ThreadPriority.Highest
    • ThreadPriority.AboveNormal
    • ThreadPriority.Normal
    • ThreadPriority.BelowNormal
    • ThreadPriority.Lowest
C#
using System;using System.Threading; namespace MultithreadingApplication{    class ThreadCreationProgram    {        public static void CallToChildThread()        {            Console.WriteLine("子线程启动");        }         static void Main(string[] args)        {            ThreadStart childref = new ThreadStart(CallToChildThread);            Console.WriteLine("在 Main: 创建子线程");            Thread childThread = new Thread(childref);            childThread.Start();            childThread.Priority = ThreadPriority.Highest;            Console.ReadKey();        }    }}/* 输出:在 Main: 创建子线程子线程启动*/

后台线程

  • 后台线程是在应用程序的主线程(或前台线程)运行时运行的线程。如果主线程终止,则后台线程也会终止。
  • 后台线程的 IsBackground 属性设置为 true,以将其标记为后台线程。
C#
using System;using System.Threading; namespace MultithreadingApplication{    class ThreadCreationProgram    {        public static void CallToChildThread()        {            int sleepfor = 5000;            Console.WriteLine("子线程启动");            Console.WriteLine("子线程暂停 {0} 秒", sleepfor / 1000);            Thread.Sleep(sleepfor);            Console.WriteLine("子线程终止");        }         static void Main(string[] args)        {            ThreadStart childref = new ThreadStart(CallToChildThread);            Console.WriteLine("在 Main: 创建子线程");            Thread childThread = new Thread(childref);            childThread.IsBackground = true;            childThread.Start();            Console.WriteLine("主线程终止");        }    }}/* 输出:在 Main: 创建子线程主线程终止子线程启动子线程暂停 5 秒*/

线程池

  • 线程池是一组线程,它们在后台运行并且可用于执行异步操作。线程池中的线程是可重用的,这样可以减少线程的创建和销毁次数,从而提高应用程序的性能。
  • 线程池中的线程是可重用的,这样可以减少线程的创建和销毁次数,从而提高应用程序的性能。
  • 线程池中的线程是可重用的,这样可以减少线程的创建和销毁次数,从而提高应用程序的性能。
C#
using System;using System.Threading; namespace MultithreadingApplication{    class ThreadPoolExample    {        public static void CallToChildThread(object state)        {            Console.WriteLine("线程池线程开始: {0}", state);        }         static void Main(string[] args)        {            for (int i = 0; i <= 5; i++)            {                ThreadPool.QueueUserWorkItem(new WaitCallback(CallToChildThread), i);            }            Console.ReadKey();        }    }}/* 输出:线程池线程开始: 0线程池线程开始: 1线程池线程开始: 2线程池线程开始: 3线程池线程开始: 4线程池线程开始: 5*/

线程同步

  • 线程同步是一种机制,用于防止多个线程同时访问共享资源。线程同步可通过以下方式实现:
    • 互斥锁:一次只允许一个线程访问共享资源。
    • 信号量:允许多个线程同时访问共享资源。
    • 事件:允许一个线程通知其他线程发生了某个事件。
    • 互斥锁:一次只允许一个线程访问共享资源。
    • 信号量:允许多个线程同时访问共享资源。
    • 事件:允许一个线程通知其他线程发生了某个事件。
C#
using System;using System.Threading; namespace MultithreadingApplication{    class ThreadSynchronization    {        public static int flag = 1;        public static int flag2 = 1;        public static object lock1 = new object();        public static object lock2 = new object();         public static void function1()        {            lock (lock1)            {                Console.WriteLine("function1: 锁住 lock1");                Thread.Sleep(3000);                lock (lock2)                {                    Console.WriteLine("function1: 锁住 lock2");                }            }        }         public static void function2()        {            lock (lock2)            {                Console.WriteLine("function2: 锁住 lock2");                Thread.Sleep(3000);                lock (lock1)                {                    Console.WriteLine("function2: 锁住 lock1");                }            }        }         static void Main(string[] args)        {            Thread t1 = new Thread(function1);            Thread t2 = new Thread(function2);            t1.Start();            t2.Start();        }    }}/* 输出:function1: 锁住 lock1function2: 锁住 lock2*/

线程死锁

  • 线程死锁是指两个或多个线程互相等待对方释放资源,从而导致程序无法继续执行的情况。
  • 线程死锁通常发生在多个线程同时访问共享资源时,如果线程无法获取所需的资源,就会发生死锁。
  • 线程死锁的解决方案是避免使用多个锁,或者确保线程在获取锁时按照相同的顺序获取。
C#
using System;using System.Threading; namespace MultithreadingApplication{    class ThreadSynchronization    {        public static int flag = 1;        public static int flag2 = 1;        public static object lock1 = new object();        public static object lock2 = new object();         public static void function1()        {            lock (lock1)            {                Console.WriteLine("function1: 锁住 lock1");                Thread.Sleep(3000);                lock (lock2)                {                    Console.WriteLine("function1: 锁住 lock2");                }            }        }         public static void function2()        {            lock (lock2)            {                Console.WriteLine("function2: 锁住 lock2");                Thread.Sleep(3000);                lock (lock1)                {                    Console.WriteLine("function2: 锁住 lock1");                }            }        }         static void Main(string[] args)        {            Thread t1 = new Thread(function1);            Thread t2 = new Thread(function2);            t1.Start();            t2.Start();        }    }}/* 输出:function1: 锁住 lock1function2: 锁住 lock2*/

线程安全集合

  • 线程安全集合是一种特殊的集合,它允许多个线程同时访问集合中的元素,而不会导致数据损坏或不一致。
  • 线程安全集合通过 System.Collections.Concurrent 命名空间中的类实现。这些类提供了线程安全的集合类型,如 ConcurrentDictionary、ConcurrentQueue 和 ConcurrentStack。
C#
using System;using System.Collections.Concurrent;using System.Threading; namespace MultithreadingApplication{    class ThreadSafeCollection    {        public static ConcurrentQueue<string> queue = new ConcurrentQueue<string>();         public static void Producer()        {            for (int i = 0; i < 5; i++)            {                queue.Enqueue("产品 " + i);                Console.WriteLine("生产者: 产品 " + i);                Thread.Sleep(1000);            }        }         public static void Consumer()        {            string result;            for (int i = 0; i < 5; i++)            {                queue.TryDequeue(out result);                Console.WriteLine("消费者: " + result);                Thread.Sleep(1000);            }        }         static void Main(string[] args)        {            Thread producer = new Thread(Producer);            Thread consumer = new Thread(Consumer);            producer.Start();            consumer.Start();        }    }}/* 输出:生产者: 产品 0消费者: 产品 0生产者: 产品 1消费者: 产品 1生产者: 产品 2消费者: 产品 2生产者: 产品 3消费者: 产品 3生产者: 产品 4消费者: 产品 4*/

销毁线程

  • 线程在执行完任务后会自动销毁。如果您想要手动销毁线程,可以使用 Abort() 方法。
  • Abort() 方法会引发 ThreadAbortException 异常,该异常会终止线程的执行。
C#
using System;using System.Threading; namespace MultithreadingApplication{    class ThreadCreationProgram    {        public static void CallToChildThread()        {            try            {                Console.WriteLine("子线程开始");                for (int counter = 0; counter <= 10; counter++)                {                    Thread.Sleep(500);                    Console.WriteLine(counter);                }                Console.WriteLine("子线程结束");            }            catch (ThreadAbortException e)            {                Console.WriteLine("线程终止异常");            }            finally            {                Console.WriteLine("无法捕获线程异常");            }        }         static void Main(string[] args)        {            ThreadStart childref = new ThreadStart(CallToChildThread);            Console.WriteLine("在 Main: 创建子线程");            Thread childThread = new Thread(childref);            childThread.Start();            // 停止主线程一段时间            Thread.Sleep(2000);            // 现在中止子线程            Console.WriteLine("在 Main: 终止子线程");            childThread.Abort();            Console.ReadKey();        }    }}/* 输出:在 Main: 创建子线程子线程开始012345678910子线程结束在 Main: 终止子线程线程终止异常无法捕获线程异常*/

进阶教程

评论区

分享你的想法,与大家交流讨论

Twikoo 加载失败

Twikoo 脚本加载失败