pengjingya

基础进阶


文件操作

文件操作类在 System.IO 命名空间中,包括 Driveinfo 类、Directory 类、Directoryinfo 类、File 类、Filelnfo 类、Path 类等。

DriveInfo

查看计算机驱动器信息主要包括查看磁盘的空间、磁盘的文件格式、磁盘的卷标等,在 C# 语言中这些操作可以通过 Driveinfo 类来实现。

Driveinfo 类是一个密封类,即不能被继承,其仅提供了一个构造方法,语法形式如下。

Driveinfo(string driveName)

其中,dirveName 参数是指有效驱动器路径或驱动器号,Null 值是无效的。

Driveinfo 类中的常用属和方法如下表所示。

属性或方法 作用
AvailableFreeSpace 只读属性,获取驱动器上的可用空闲空间量 (以字节为单位)
DriveFormat 只读属性,获取文件系统格式的名称,例如 NTFS 或 FAT32
DriveType 只读属性,获取驱动器的类型,例如 CD-ROM、可移动驱动器、网络驱动器或固定驱动器
IsReady 只读属性,获取一个指示驱动器是否已准备好的值,True 为准备好了, False 为未准备好
Name 只读属性,获取驱动器的名称,例如 C:\
RootDirectory 只读属性,获取驱动器的根目录
TotalFreeSpace 只读属性,获取驱动器上的可用空闲空间总量 (以字节为单位)
TotalSize 只读属性,获取驱动器上存储空间的总大小 (以字节为单位)
VolumeLabel 属性, 获取或设置驱动器的卷标
Driveinfo[] GetDrives() 静态方法,检索计算机上所有逻辑驱动器的驱动器名称

下面通过实例来演示 Driveinfo 类的使用。

获取 D 盘中的驱动器类型、名称、文件系统名称、可用空间以及总空间大小

class Program {
    static void Main(string[] args) {
        DriveInfo driveInfo = new DriveInfo("C");
        Console.WriteLine("驱动器的名称:" + driveInfo.Name);
        Console.WriteLine("驱动器类型:" + driveInfo.DriveType);
        Console.WriteLine("驱动器的文件格式:" + driveInfo.DriveFormat);
        Console.WriteLine("驱动器中可用空间大小:" + driveInfo.TotalFreeSpace);
        Console.WriteLine("驱动器总大小:" + driveInfo.TotalSize);
    }
}

执行上面的代码,效果如下:

驱动器的名称:C:
驱动器类型:Fixed
驱动器的文件格式:NTFS
驱动器中可用空间大小:71449812992
驱动器总大小:127821410304

Directoryinfo

在 C# 语言中 Directory 类和 Directoryinfo 类都是对文件夹进行操作的。

DirectoryInfo 类提供了一个构造方法,语法形式如下:

DirectoryInfo(string path)

需要注意的是路径中如果使用 \,要使用转义字符来表示,即 \\;或者在路径中将 \ 字符换成 /

DirectoryInfo 类中常用的属性和方法如下表所示。

属性或方法 作用
Exists 只读属性,获取指示目录是否存在的值
Name 只读属性,获取 Directorylnfo 实例的目录名称
Parent 只读属性,获取指定的子目录的父目录
Root 只读属性,获取目录的根部分
void Create() 创建目录
DirectoryInfo CreateSubdirectory(string path) 在指定路径上创建一个或多个子目录
void Delete() 如果目录中为空,则将目录删除
void Delete(bool recursive) 指定是否删除子目录和文件,如果 recursive 参数的值为 True,则删除,否则不删除
IEnumerable EnumerateDirectories() 返回当前目录中目录信息的可枚举集合
IEnumerable EnumerateDirectories(string searchPattern) 返回与指定的搜索模式匹配的目录信息的可枚举集合
IEnumerable EnumerateFiles() 返回当前目录中的文件信息的可枚举集合
IEnumerable EnumerateFiles(string searchPattern) 返回与搜索模式匹配的文件信息的可枚举集合
IEnumerable EnumerateFileSystemInfos() 返回当前目录中的文件系统信息的可枚举集合
IEnumerable EnumerateFileSystemInfos(string searchPattern) 返回与指定的搜索模式匹配的文件系统信息的可枚举集合
DirectoryInfo[] GetDirectories() 返回当前目录的子目录
DirectoryInfo[] GetDirectories(string searchPattern) 返回匹配给定的搜索条件的当前目录
FileInfo[] GetFiles() 返回当前目录的文件列表
FileInfo[] GetFiles(string searchPattern) 返回当前目录中与给定的搜索模式匹配的文件列表
FileSystemInfo[] GetFileSystemInfos() 返回所有文件和目录的子目录中的项
FileSystemInfo[] GetFileSystemInfos(string searchPattern) 返回与指定的搜索条件匹配的文件和目录的子目录中的项
void MoveTo(string destDirName) 移动 DirectoryInfo 实例中的目录到新的路径

1)在 D 盘下创建文件夹 code,并在该文件夹中创建 code-1和 code-2 两个子文件夹。

class Program {
    static void Main(string[] args) {
        DirectoryInfo directoryInfo = new DirectoryInfo("D:\\code");
        directoryInfo.Create();
        directoryInfo.CreateSubdirectory("code-1");
        directoryInfo.CreateSubdirectory("code-2");
    }
}

需要注意的是,在创建文件夹时即使磁盘上存在同名文件夹也可以直接创建,不会出现异常。

2)查看 D 盘下 code 文件夹中的文件夹

class Program {
    static void Main(string[] args) {
        DirectoryInfo directoryInfo = new DirectoryInfo("D:\\code");
        IEnumerable<DirectoryInfo> directoryInfos = directoryInfo.EnumerateDirectories();
        foreach (DirectoryInfo dir in directoryInfos) {
            Console.WriteLine(dir.Name);
        }
    }
}

Directory

Directory 类是一个静态类, 不能创建该类的实例,直接通过“类名 . 类成员”的形式调用其属性和方法。

Directory 类省去了创建类实例的步骤,其他操作也与 Directoryinfo 类似。

1)使用 Directory 类在 D 盘上操作 code 文件夹,要求先判断是否存在该文件夹,如果存在则删除,否则创建该文件夹。

class Program {
    static void Main(string[] args) {
        String path = "D:\\code";
        if (Directory.Exists(path)) {
            Directory.Delete(path, true);
        } else {
            Directory.CreateDirectory(path);
        }
    }
}

执行上面的代码,即可完成文件夹 code 的删除或创建操作。

FileInfo

C# 语言中 File 类和 FileInfo 类都是用来操作文件的,并且作用相似,它们都能完成对文件的创建、更改文件的名称、删除文件、移动文件等操作。

File 类是静态类,其成员也是静态的,通过类名即可访问类的成员;FileInfo 类不是静态成员,其类的成员需要类的实例来访问。

在 FileInfo 类中提供了一个构造方法,语法形式如下:

FileInfo(string fileName)

在这里 fileName 参数用于指定新文件的完全限定名或相对文件名。

FileInfo 类中常用的属性和方法如下表所示。

属性或方法 作用
Directory 只读属性,获取父目录的实例
DirectoryName 只读属性,获取表示目录的完整路径的字符串
Exists 只读属性,获取指定的文件是否存在,若存在返回 True,否则返回 False
IsReadOnly 属性,获取或设置指定的文件是否为只读的
Length 只读属性,获取文件的大小
Name 只读属性,获取文件的名称
Filelnfo CopyTo(string destFileName) 将现有文件复制到新文件,不允许覆盖现有文件
Filelnfo CopyTo(string destFileName, bool overwrite) 将现有文件复制到新文件,允许覆盖现有文件
FileStream Create() 创建文件
void Delete() 删除文件
void MoveTo(string destFileName) 将指定文件移到新位置,提供要指定新文件名的选项
Filelnfo Replace(string destinationFileName, string destinationBackupFileName) 使用当前文件对象替换指定文件的内容,先删除原始文件, 再创建被替换文件的备份

1)在 D 盘的 code 文件夹下创建名为 test.txt 的文件,并获取该文件的相关属性,然后将其移动到D盘下的 code-1 文件夹中。

class Program {
    static void Main(string[] args) {
        Directory.CreateDirectory("D:\\code");
        FileInfo fileInfo = new FileInfo("D:\\code\\test.txt");
        if (!fileInfo.Exists) {
            fileInfo.Create().Close();
        }
        fileInfo.Attributes = FileAttributes.Normal;    // 设置文件属性
        Console.WriteLine("文件路径:" + fileInfo.Directory);
        Console.WriteLine("文件名称:" + fileInfo.Name);
        Console.WriteLine("文件是否只读:" + fileInfo.IsReadOnly);
        Console.WriteLine("文件大小:" + fileInfo.Length);
        Directory.CreateDirectory("D:\\code-1");
        FileInfo newFileInfo = new FileInfo("D:\\code-1\\test.txt");
        if (!newFileInfo.Exists) {
            fileInfo.MoveTo("D:\\code-1\\test.txt");
        }
    }
}

执行上面的代码,效果如下:

文件路径:D:\code
文件名称:test.txt
文件是否只读:False
文件大小:0

File

C# 语言中 File 类同样可以完成与 FileInfo 类相似的功能,但 File 类中也提供了一些不同的方法。

File 类中获取或设置文件信息的常用方法如下表所示。

属性或方法 作用
DateTime GetCreationTime(string path) 返回指定文件或目录的创建日期和时间
DateTime GetLastAccessTime(string path) 返回上次访问指定文件或目录的日期和时间
DateTime GetLastWriteTime(string path) 返回上次写入指定文件或目录的日期和时间
void SetCreationTime(string path, DateTime creationTime) 设置创建该文件的日期和时间
void SetLastAccessTime(string path, DateTime lastAccessTime) 设置上次访问指定文件的日期和时间
void SetLastWriteTime(string path, DateTime lastWriteTime) 设置上次写入指定文件的日期和时间

File 类是静态类,所提供的类成员也是静态的,调用其类成员直接使用 File 类的名称调用即可。

1)在 D 盘的 code 文件夹下创建名为 test.txt 的文件,并获取该文件的相关属性,然后将其移动到D盘下的 code-1 文件夹中。

class Program {
    static void Main(string[] args) {
        Directory.CreateDirectory("D:\\code");
        Directory.CreateDirectory("D:\\code-1");
        string path = "D:\\code\\text.txt";
        FileStream fileStream = File.Create(path);
        Console.WriteLine("文件创建时间:" + File.GetCreationTime(path));
        Console.WriteLine("文件最后被写入时间:" + File.GetLastWriteTime(path));
        fileStream.Close();
        string newPath = "D:\\code-1\\test.txt";
        if (File.Exists(newPath)) {
            File.Delete(newPath);
        }
        File.Move(path, newPath);
    }
}

执行上面的代码,效果如下:

文件创建时间:2021/2/14 10:05:23
文件最后被写入时间:2021/2/14 10:05:23

Path

在 C# 语言中 Path 类主要用于文件路径的一些操作,它也是一个静态类。

Path 类中常用的属性和方法如下表所示。

属性或方法 作用
string ChangeExtension(string path, string extension) 更改路径字符串的扩展名
string Combine(params string[] paths) 将字符串数组组合成一个路径
string Combine(string path1, string path2) 将两个字符串组合成一个路径
string GetDirectoryName(string path) 返回指定路径字符串的目录信息
string GetExtension(string path) 返回指定路径字符串的扩展名
string GetFileName(string path) 返回指定路径字符串的文件名和扩展名
string GetFileNameWithoutExtension(string path) 返回不具有扩展名的指定路径字符串的文件名
string GetFullPath(string path) 返回指定路径字符串的绝对路径
char[] GetInvalidFileNameChars() 获取包含不允许在文件名中使用的字符的数组
char[] GetInvalidPathChars() 获取包含不允许在路径名中使用的字符的数组
string GetPathRoot(string path) 获取指定路径的根目录信息
string GetRandomFileName() 返回随机文件夹名或文件名
string GetTempPath() 返回当前用户的临时文件夹的路径
bool HasExtension(string path) 返回路径是否包含文件的扩展名
bool IsPathRooted(string path) 返回路径字符串是否包含根

下面通过实例来演示 Path 类的应用。

1)从控制台输入一个路径,输出该路径的不含扩展名的路径、扩展名、文件全 名、文件路径、更改文件扩展名。

static void Main(string[] args) {
    Console.WriteLine("请输入一个文件路径:");
    string path = Console.ReadLine();
    Console.WriteLine("不包含拓展名的文件名:" + Path.GetFileNameWithoutExtension(path));
    Console.WriteLine("文件拓展名:" + Path.GetExtension(path));
    Console.WriteLine("文件全名:" + Path.GetFileName(path));
    Console.WriteLine("文件路径:" + Path.GetDirectoryName(path));
    // 更改文件拓展名
    string newPath = Path.ChangeExtension(path, "doc");
    Console.WriteLine("更改后的文件全名:" + Path.GetFileName(newPath));
}

流操作

在计算机编程中,流就是一个类的对象,很多文件的输入输出操作都以类的成员函数的方式来提供。

流所在的命名空间也是System.IO,主要包括文本文件的读写、图像和声音文件的读写、二进制文件的读写等。

StreamReader

在 C# 语言中 StreamReader 类用于从流中读取字符串。它继承自 TextReader 类。

StreamReader 类的构造方法有很多,这里介绍一些常用的构造方法,如下表所示。

构造方法 说明
StreamReader(Stream stream) 为指定的流创建 StreamReader 类的实例
StreamReader(string path) 为指定路径的文件创建 StreamReader 类的实例
StreamReader(Stream stream, Encoding encoding) 用指定的字符编码为指定的流初始化 StreamReader 类的一个新实例
StreamReader(string path, Encoding encoding) 用指定的字符编码为指定的文件名初始化 StreamReader 类的一个新实例

StreamReader 类中的常用属性和方法如下表所示。

属性或方法 作用
Encoding CurrentEncoding 只读属性,获取当前流中使用的编码方式
bool EndOfStream 只读属性,获取当前的流位置是否在流结尾
void Close() 关闭流
int Peek() 获取流中的下一个字符的整数,如果没有获取到字符, 则返回 -1
int Read() 获取流中的下一个字符的整数
int Read(char[] buffer, int index, int count) 从指定的索引位置开始将来自当前流的指定的最多字符读到缓冲区
string ReadLine() 从当前流中读取一行字符并将数据作为字符串返回
string ReadToEnd() 读取来自流的当前位置到结尾的所有字符

1)读取 D 盘 code 文件夹下 test.txt 文件中的信息。

class Program {
    static void Main(string[] args) {
        string path = @"D:\\code\\test.txt";
        StreamReader streamReader = new StreamReader(path);
        while (streamReader.Peek() != -1) {
            string str = streamReader.ReadLine();
            Console.WriteLine(str);
        }
        streamReader.Close();
    }
}

执行上面的代码,效果如下:

Hello
World

StreamWriter

StreamWriter 类主要用于向流中写入数据。

StreamWriter 类的构造方法也有很多,这里只列出一些常用的构造方法,如下表所示。

构造方法 说明
StreamWriter(Stream stream) 为指定的流创建 StreamWriter 类的实例
StreamWriter(string path) 为指定路径的文件创建 StreamWriter 类的实例
StreamWriter(Stream stream, Encoding encoding) 用指定的字符编码为指定的流初始化 StreamWriter 类的一个新实例
StreamWriter(string path, Encoding encoding) 用指定的字符编码为指定的文件名初始化 StreamWriter 类的一个新实例

在创建了 StreamWriter 类的实例后即可调用其类成员,完成向文件中写入信息的操作。

StreamWriter 类中常用的属性和方法如下表所示。

属性或方法 作用
bool AutoFlush 属性,获取或设置是否自动刷新缓冲区
Encoding Encoding 只读属性,获取当前流中的编码方式
void Close() 关闭流
void Flush() 刷新缓冲区
void Write(char value) 将字符写入流中
void WriteLine(char value) 将字符换行写入流中
Task WriteAsync(char value) 将字符异步写入流中
Task WriteLineAsync(char value) 将字符异步换行写入流中

在上表中给出的方法中,Write、WriteAsync、WriteLineAsync 方法还有很多不同类型写入的重载方法,这里没有一一列出。

1)向 D 盘 code 文件夹的 test.txt 文件中写入姓名和手机号码。

class Program {
    static void Main(string[] args) {
        string path = @"D:\\code\\test.txt";
        StreamWriter streamWriter = new StreamWriter(path);
        streamWriter.WriteLine("Legend");
        streamWriter.WriteLine("13888888888");
        streamWriter.Flush();
        streamWriter.Close();
    }
}

FileStream

文件读写流使用 FileStream 类来表示,FileStream 类主要用于文件的读写,不仅能读写普通的文本文件,还可以读取图像文件、声音文件等不同格式的文件。

FileAccess 枚举类型主要用于设置文件的访问方式,具体的枚举值如下:

  • Read:以只读方式打开文件。
  • Write:以写方式打开文件。
  • ReadWrite:以读写方式打开文件

FileMode 枚举类型主要用于设置文件打开或创建的方式,具体的枚举值如下:

  • CreateNew:创建新文件,如果文件已经存在,则会抛出异常。
  • Create:创建文件,如果文件不存在,则删除原来的文件,重新创建文件。
  • Open:打开已经存在的文件,如果文件不存在,则会抛出异常。
  • OpenOrCreate:打开已经存在的文件,如果文件不存在,则创建文件。
  • Truncate:打开已经存在的文件,并清除文件中的内容,保留文件的创建日期。如果文件不存在,则会抛出异常。
  • Append:打开文件,用于向文件中追加内容,如果文件不存在,则创建一个新文件。

FileShare 枚举类型主要用于设置多个对象同时访问同一个文件时的访问控制,具体的枚举值如下:

  • None:谢绝共享当前的文件。
  • Read:允许随后打开文件读取信息。
  • ReadWrite:允许随后打开文件读写信息。
  • Write:允许随后打开文件写入信息。
  • Delete:允许随后删除文件。
  • Inheritable:使文件句柄可由子进程继承。

FileOptions 枚举类型用于设置文件的高级选项,包括文件是否加密、访问后是否删除等,具体的枚举值如下

  • WriteThrough:指示系统应通过任何中间缓存、直接写入磁盘。
  • None:指示在生成 System.IO.FileStream 对象时不应使用其他选项。
  • Encrypted:指示文件是加密的,只能通过用于加密的同一用户账户来解密。
  • DeleteOnClose:指示当不再使用某个文件时自动删除该文件。
  • SequentialScan:指示按从头到尾的顺序访问文件。
  • RandomAccess:指示随机访问文件。
  • Asynchronous:指示文件可用于异步读取和写入。

FileStream 类的构造方法有很多,这里介绍一些常用的构造方法,如下表所示:

构造方法 说明
FileStream(string path, FileMode mode) 使用指定路径的文件、文件模式创建 FileStream 类的实例
FileStream(string path, FileMode mode, FileAccess access) 使用指定路径的文件、文件打开模式、文件访问模式创建 FileStream 类的实例
FileStream(string path, FileMode mode, FileAccess access, FileShare share) 使用指定的路径、创建模式、读写权限和共享权限创建 FileStream 类的一个新实例
FileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) 使用指定的路径、创建模式、读写权限和共享权限、其他 文件选项创建 FileStream 类的实例

下面使用 FileStream 类的构造方法创建 FileStream 类的实例,语法形式如下:

string path = "D:\\test.txt";
FileStream fileStream1 = new FileStream(path, FileMode.Open);
FileStream fileStream2 = new FileStream(path, FileMode.Open, FileAccess.Read);
FileStream fileStream3 = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
FileStream fileStream4 = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 10, FileOptions.None);

在创建好 FileStream 类的实例后,即可调用该类中的成员完成读写数据的操作。

FileStream 类中常用的属性和方法如下图所示:

属性或方法 作用
bool CanRead 只读属性,获取一个值,该值指示当前流是否支持读取
bool CanSeek 只读属性,获取一个值,该值指示当前流是否支持查找
bool CanWrite 只读属性,获取一个值,该值指示当前流是否支持写入
bool IsAsync 只读属性,获取一个值,该值指示 FileStream 是异步还 是同步打开的
long Length 只读属性,获取用字节表示的流长度
string Name 只读属性,获取传递给构造方法的 FileStream 的名称
long Position 属性,获取或设置此流的当前位置
int Read(byte[] array, int offset, int count) 从流中读取字节块并将该数据写入给定缓冲区中
int ReadByte() 从文件中读取一个字节,并将读取位置提升一个字节
long Seek(lorig offset, SeekOrigin origin) 将该流的当前位置设置为给定值
void Lock(long position, long length) 防止其他进程读取或写入 System.IO.FileStream
void Unlock(long position, long length) 允许其他进程访问以前锁定的某个文件的全部或部分
void Write(byte[] array, int offset, int count) 将字节块写入文件流
void WriteByte(byte value) 将一个字节写入文件流中的当前位置

1)在 D 盘 code 文件夹的 student.txt 文件中写入学生的学号信息。

class Program {
    static void Main(string[] args) {
        string path = @"D:\\code\\student.txt";
        FileStream fileStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
        string msg = "1710026";
        byte[] bytes = Encoding.UTF8.GetBytes(msg);
        fileStream.Write(bytes, 0, bytes.Length);
        fileStream.Flush();
        fileStream.Close();
    }
}

2)从 D 盘的 code 文件夹中将 student.txt 文件中的学号读取出来,并显示到控制台上。

class Program {
    static void Main(string[] args) {
        string path = @"D:\\code\\student.txt";
        if (File.Exists(path)) {
            FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
            byte[] bytes = new byte[fileStream.Length];
            fileStream.Read(bytes, 0, bytes.Length);
            char[] c = Encoding.UTF8.GetChars(bytes);
            Console.WriteLine("学生的学号为:");
            Console.WriteLine(c);
            fileStream.Close();
        } else {
            Console.WriteLine("查看的文件不存在!");
        }
    }
}

BinaryReader

在 C# 以二进制形式读取数据时使用的是 BinaryReader 类。

BinaryReader 类中提供的构造方法有 3 种,具体的语法形式如下:

BinaryReader(Stream input) //其中,input 参数是输入流。
BinaryReader(Stream input, Encoding encoding) //其中,input 是指输入流,encoding 是指编码方式。
BinaryReader(Stream input, Encoding encoding, bool leaveOpen) //其中,input 是指输入流,encoding 是指编码方式,leaveOpen 是指在流读取后是否包括流的打开状态。

下面分别使用不同的构造方法创建 BinaryReader 类的实例,代码如下:

//创建文件流的实例
FileStream fileStream = new FileStream("D:\\code\\test.txt", FileMode.Open);
BinaryReader binaryReader1 = new BinaryReader(fileStream);
BinaryReader binaryReader2 = new BinaryReader(fileStream, Encoding.UTF8);
BinaryReader binaryReader3 = new BinaryReader(fileStream, Encoding.UTF8, true);

在完成 BinaryReader 类的实例的创建后,即可完成对文件以二进制形式的读取。

BinaryReader 类中的常用属性和方法如下表所示。

属性或方法 作用
int Read() 从指定的流中读取字符
int Read(byte[] buffer, int index, int count) 以 index 为字节数组中的起始点,从流中读取 count 个字节
int Read(char[] buffer, int index, int count) 以 index 为字符数组的起始点,从流中读取 count 个字符
bool ReadBoolean() 从当前流中读取 Boolean 值,并使该流的当前位置提升 1 个字节
byte ReadByte() 从当前流中读取下一个字节,并使流的当前位置提升 1 个字节
byte[] ReadBytes(int count) 从当前流中读取指定的字节数写入字节数组中,并将当前 位置前移相应的字节数
char ReadChar() 从当前流中读取下一个字符,并根据所使用的 Encoding 和从流中读取的特定字符提升流的当前位置
char[] ReadChars(int count) 从当前流中读取指定的字符数,并以字符数组的形式返回 数据,然后根据所使用的 Encoding 和从流中读取的特定字符将当前位置前移
decimal ReadDecimal() 从当前流中读取十进制数值,并将该流的当前位置提升 16 个字节
double ReadDouble() 从当前流中读取 8 字节浮点值,并使流的当前位置提升 8 个字节
short ReadInt16() 从当前流中读取 2 字节有符号整数,并使流的当前位置提升 2 个字节
int ReadInt32() 从当前流中读取 4 字节有符号整数,并使流的当前位置提升 4 个字节
long ReadInt64() 从当前流中读取 8 字节有符号整数,并使流的当前位置提升 8 个字节
sbyte ReadSByte() 从该流中读取 1 个有符号字节,并使流的当前位置提升 1 个字节
float ReadSingle() 从当前流中读取 4 字节浮点值,并使流的当前位置提升 4 个字节
string ReadString() 从当前流中读取一个字符串。字符串有长度前缀,一次 7 位地被编码为整数
ushort ReadUInt16() 从该流中读取的 2 字节无符号整数
uint ReadUInt32() 从该流中读取的 4 字节无符号整数
ulong ReadUInt64() 从该流中读取的 8 字节无符号整数
void FillBuffer(int numBytes) 用从流中读取的指定字节数填充内部缓冲区

在 BinaryReader 类中提供的方法并不是直接读取文件中指定数据类型的值,而是读取由 BinaryWriter 类写入到文件中的。

在上述方法中只有 Read 方法不要求读取的值必须由 BinaryWriter 类写入到文件中。

1)使用 BinaryReader 类读取记事本文件中的信息。

class Program {
    static void Main(string[] args) {
        string path = @"D:\\code\\test.txt";
        FileStream fileStream = new FileStream(path, FileMode.Open);
        BinaryReader binaryReader = new BinaryReader(fileStream);
        int a = binaryReader.Read();
        //判断文件中是否含有字符,若不含字符,a 的值为 -1
        while (a != -1) {
            Console.Write((char)a);
            a = binaryReader.Read();
        }
    }
}

其中Read还有其他的重载方法读取文件中的内容:

class Program {
    static void Main(string[] args) {
        string path = @"D:\\code\\test.txt";
        FileStream fileStream = new FileStream(path, FileMode.Open);
        BinaryReader binaryReader = new BinaryReader(fileStream);
        long length = fileStream.Length;
        byte[] bytes = new byte[length];
        binaryReader.Read(bytes, 0, bytes.Length);
        string str = Encoding.Default.GetString(bytes);
        Console.WriteLine(str);
    }
}

BinaryWriter

BinaryWriter 类用于向流中写入内容。

BinaryWriter(Stream output)
BinaryWriter(Stream output, Encoding encoding)
BinaryWriter(Stream output, Encoding encoding, bool leaveOpen)

BinaryWriter 类中常用的属性和方法如下表所示。

属性或方法 作用
void Close() 关闭流
void Flush() 清理当前编写器的所有缓冲区,使所有缓冲数据写入基础设备
long Seek(int offset, SeekOrigin origin) 返回查找的当前流的位置
void Write(char[] chars) 将字符数组写入当前流
Write7BitEncodedInt(int value) 以压缩格式写出 32 位整数

除了上面的方法以外,Write 方法还提供了多种类型的重载方法。

1)在 D 盘 code 文件夹的 test.txt 文件中写入图书的名称和价格,使用 BinaryReader 类读取写入的内容。

class Program {
    static void Main(string[] args) {
        string path = @"D:\\code\\test.txt";
        FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Write);
        BinaryWriter binaryWriter = new BinaryWriter(fileStream);
        binaryWriter.Write("C#基础教程");
        binaryWriter.Write(49.5);
        binaryWriter.Flush();
        binaryWriter.Close();
        fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
        BinaryReader binaryReader = new BinaryReader(fileStream);
        Console.WriteLine(binaryReader.ReadString());
        Console.WriteLine(binaryReader.ReadDouble());
        binaryReader.Close();
        fileStream.Close();
    }
}

委托和事件

C# 中的委托类似于 C 或 C++ 中函数的指针。委托是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。

委托在使用时遵循三步走的原则,即定义声明委托、实例化委托以及调用委托。

委托是 C# 语言中的一个特色,通常将委托分为命名方法委托、多播委托、匿名委托,其中命名方法委托是使用最多的一种委托。

定义委托

1)在 C# 语言中命名方法委托是最常用的一种委托,其定义的语法形式如下。

修饰符 delegate 返回值类型 委托名 ( 参数列表 );

示例代码如下所示:

public delegate void MyDelegate();

2)实例化委托的语法形式如下:

委托名 委托对象名 = new 委托名 ( 方法名 );

委托中传递的方法名既可以是静态方法的名称,也可以是实例方法的名称。

3)在实例化委托后即可调用委托,语法形式如下

委托对象名 ( 参数列表 );

下面分别通过实例来演示在委托中应用静态方法和实例方法的形式。

class Program {
    public delegate void MyDelegate();
    static void Main(string[] args) {
        MyDelegate myDelegate = new MyDelegate(Test.sayHello);
        myDelegate();
    }
}

class Test {
    public static void sayHello() {
        Console.WriteLine("Hello World");
    }
}

注意:若使用静态方法,在向委托中传递方法名时只需要用类名.方法名的形式。

将实例 1 中的静态方法改成实例方法。

class Program {
    public delegate void MyDelegate();
    static void Main(string[] args) {
        MyDelegate myDelegate = new MyDelegate(new Test().sayHello);
        myDelegate();
    }
}

class Test {
    public void sayHello() {
        Console.WriteLine("Hello World");
    }
}

由于在委托中使用的是实例方法,则需要通过类的实例来调用方法,即使用new 类名().方法名的形式。

使用委托完成将图书信息按照价格升序排序的操作

class Book : IComparable<Book> {
    private string name;
    private double price;

    public Book(string name, double price) {
        this.name = name;
        this.price = price;
    }

    public string Name {
        get => name;
        set => name = value;
    }

    public double Price {
        get => price;
        set => price = value;
    }

    public int CompareTo(Book? other) {
        return (int) (this.price - other.price);
    }

    public override string ToString() {
        return name + ":" + price;
    }

    public static void bookSort(Book[] books) {
        Array.Sort(books);
    }
}

在 Main 方法中定义委托调用图书排序的方法,代码如下:

class Program {
    public delegate void BookDelegate(Book[] books);
    static void Main(string[] args) {
        BookDelegate bookDelegate = new BookDelegate(Book.bookSort);
        Book[] books = new Book[3];
        books[0] = new Book("计算机应用", 50);
        books[1] = new Book("C#教程", 59);
        books[2] = new Book("数据库实践", 49);
        bookDelegate(books);
        foreach (Book book in books) {
            Console.WriteLine(book);
        }
    }
}

通过委托调用的图书排序方法 (BookSort) 按照图书价格升序排列了图书信息。

多播委托

在 C# 语言中多播委托是指在一个委托中注册多个方法,在注册方法时可以在委托中使用加号运算符或者减号运算符来实现添加或撤销方法。

在现实生活中,多播委托的实例是随处可见的,例如某点餐的应用程序,既可以预定普通的餐饮也可以预定蛋糕、鲜花、水果等商品。

在这里委托相当于点餐平台,每一个类型的商品可以理解为在委托上注册的一个方法。

1)模拟点餐平台预定不同类型的商品。

class Program {
    public delegate void OrderDelegate();
    static void Main(string[] args) {
        OrderDelegate orderDelegate = new OrderDelegate(Order.buyFood);
        // 向委托中注册方法
        orderDelegate += Order.buyCake;
        orderDelegate += Order.buyFlower;
        orderDelegate();
    }
}

class Order {
    public static void buyFood() {
        Console.WriteLine("购买快餐!");
    }
    public static void buyCake() {
        Console.WriteLine("购买蛋糕!");
    }

    public static void buyFlower() {
        Console.WriteLine("购买鲜花!");
    }
}

如果已经购买了鲜花,在未调用委托时也可以撤销,在委托注册方法时使用 -= 操作符即可。

匿名委托

在 C# 语言中匿名委托是指使用匿名方法注册在委托上,实际上是在委托中通过定义代码块来实现委托的作用,具体的语法形式如下。

//1. 定义委托
修饰符 delegate 返回值类型 委托名 ( 参数列表 );

//2. 定义匿名委托
委托名 委托对象 = delegate {
//代码块
};

//3. 调用匿名委托
委托对象名 ( 参数列表 );

通过上面 3 个步骤即可完成匿名委托的定义和调用,需要注意的是,在定义匿名委托时代码块结束后要在 {} 后加上分号。

使用匿名委托计算长方形的面积。

class Program {
    public delegate void AreaDelegate(double length, double width);
    static void Main(string[] args) {
        Console.WriteLine("请输入长方形的长:");
        double length = double.Parse(Console.ReadLine());
        Console.WriteLine("请输入长方形的宽:");
        double width = double.Parse(Console.ReadLine());
        AreaDelegate areaDelegate = delegate(double d, double width1) {
            Console.WriteLine("长方形的面积为:" + length * width);
        };
        areaDelegate(length, width);
    }
}

执行上面的代码,效果如下:

ll
请输入长方形的长:
15
请输入长方形的宽:
10
长方形的面积为:150

Event事件

事件是一种引用类型,实际上也是一种特殊的委托。

事件定义的语法形式如下。

访问修饰符 event 委托名 事件名 ;

在这里,由于在事件中使用了委托,因此需要在定义事件前先定义委托。

通过事件完成在控制台上输岀“Hello Event!”的操作。

class Program {
    public delegate void sayDelegate();
    public event sayDelegate sayEvent;

    public void sayHello() {
        Console.WriteLine("Hello Event!");
    }

    public void sayEventTrigger() {
        sayEvent();
    }
    static void Main(string[] args) {
        Program program = new Program();
        program.sayEvent = new sayDelegate(program.sayHello);
        program.sayEventTrigger();
    }
}

在事件中使用多播委托完成预定不同商品的操作。

class Program {
    static void Main(string[] args) {
        MyEvent myEvent = new MyEvent();
        myEvent.buyEvent += new MyEvent.BuyDelegate(MyEvent.buyFood);
        myEvent.buyEvent += new MyEvent.BuyDelegate(MyEvent.buyCake);
        myEvent.buyEvent += new MyEvent.BuyDelegate(MyEvent.buyFlower);
        myEvent.invokeEvent();
    }
}

class MyEvent {
    public delegate void BuyDelegate();
    public event BuyDelegate buyEvent;
    public static void buyFood() {
        Console.WriteLine("购买快餐!");
    }

    public static void buyCake() {
        Console.WriteLine("购买蛋糕!");
    }

    public static void buyFlower() {
        Console.WriteLine("购买鲜花!");
    }

    public void invokeEvent() {
        buyEvent();
    }
}

事件是每一个 Windows 应用程序中必备的,很多事件的操作都是自动生成的。

异常与调试

.NET Framework 类库中的所有异常都派生于 Exception 类,异常包括系统异常和应用异常。

img

常用的系统异常类如下表所示。

异常类 说明
System.OutOfMemoryException 用 new 分配内存失败
System.StackOverflowException 递归过多、过深
System.NullReferenceException 对象为空
Syetem.IndexOutOfRangeException 数组越界
System.ArithmaticException 算术操作异常的基类
System.DivideByZeroException 除零错误

异常的语句使用try{...}catch(Exception e){...}包裹,其实finally{}语句是必定会执行的。

class Program {
    static void Main(string[] args) {
        int i = 0;
        try {
            int result = 10 / i;
        } catch (Exception e) {
            Console.WriteLine(e);
        } finally {
            Console.WriteLine("finally");
        }
    }
}

运行上面的程序,输出结果如下:

at Demo01.Program.Main(String[] args) in D:\CodeSpace\C#Space\Demo01\Demo01\Program.cs:line 13
finally

注意:自定义异常只需要自定义的类继承自Exception即可。

进程和线程

Process(进程)

Process 类主要提供对本地和远程进程的访问,并提供对本地进程的启动、停止等操作。

Process 类的常用属性和方法如下表所示。

属性或方法 说明
MachineName 属性,获取关联进程正在其上运行的计算机的名称
Id 属性,获取关联进程的唯一标识符
ExitTime 属性,获取关联进程退出的时间
ProcessName 属性,获取该进程的名称
StartTime 属性,获取关联进程启动的时间
Threads 属性,获取在关联进程中运行的一组线程
TotalProcessorTime 属性,获取此进程的总的处理器时间
UserProcessorTime 属性,获取此进程的用户处理器时间
Close() 方法,释放与此组件关联的所有资源
CloseMainWindow() 方法,通过向进程的主窗口发送关闭消息来关闭拥有用户界面的进程
Dispose() 方法,释放由 Component 使用的所有资源
GetCurrentProcess() 方法,获取新的 Process 组件,并将其与当前活动的进程关联
GetProcesses() 方法,为本地计算机上的每个进程资源创建一个新的 Process 组件
GetProcesses(String) 方法,为指定计算机上的每个进程资源创建一个新的 Process 组件
GetProcessesByName(String) 方法,创建新的 Process 组件的数组,并将它们与本地计算机上共享指定的进程名称的所有进程资源关联
Kill() 方法,立即停止关联的进程
Start() 方法,启动(或重用)此 Process 组件的 Startinfo 属性指定的进程资源, 并将其与该组件关联
Start(String) 方法,通过指定文档或应用程序文件的名称来启动进程资源,并将资源与新的 Process 组件关联

在控制台输出所有的进程信息:

class Program {
    static void Main(string[] args) {
        Process[] processes = Process.GetProcesses();
        foreach (Process process in processes) {
            Console.WriteLine(process.ProcessName);
        }
    }
}

通过进程名称来启动某个进程:

class Program {
    static void Main(string[] args) {
        string processName = "mspaint";
        Process process = new Process();
        process.StartInfo.FileName = processName;
        process.Start();
    }
}

当然,这种操作只能启动系统的应用。

Thread(线程)

在 C# 语言中线程(Thread)是包含在进程中的,它位于 System.Threading 命名空间中。

与线程有关的类同样也都在 System.Threading 命名空间中,主要的类如下表所示。

类名 说明
Thread 在初始的应用程序中创建其他的线程
ThreadState 指定 Thread 的执行状态,包括开始、运行、挂起等
ThreadPriority 线程在调度时的优先级枚举值,包括 Highest、AboveNormal、Normal、BelowNormal、Lowest
ThreadPool 提供一个线程池,用于执行任务、发送工作项、处理异步I/O等操作
Monitor 提供同步访问对象的机制
Mutex 用于线程间同步的操作
ThreadAbortException 调用Thread类中的Abort方法时出现的异常
ThreadStateException Thead处于对方法调用无效的ThreadState时出现的异常

Thread 类主要用于实现线程的创建以及执行,其常用的属性和方法如下表所示。

属性或方法 说明
Name 属性,获取或设置线程的名称
Priority 属性,获取或设置线程的优先级
ThreadState 属性,获取线程当前的状态
IsAlive 属性,获取当前线程是否处于启动状态
IsBackground 属性,获取或设置值,表示该线程是否为后台线程
CurrentThread 属性,获取当前正在运行的线程
Start() 方法,启动线程
Sleep(int millisecondsTimout) 方法,将当前线程暂停指定的毫秒数
Suspend() 方法,挂起当前线程(已经被弃用)
Join() 方法,阻塞调用线程,直到某个线程终止为止
Interrupt() 方法,中断当前线程
Resume() 方法,继续已经挂起的线程(已经被弃用)
Abort() 方法,终止线程

ThreadStart

ThreadStart 委托只能用于无返回值、无参数的方法,ParameterizedThreadStart 委托则可以用于带参数的方法。

使用 ThreadStart 创建线程首先需要创建 ThreadStart 委托的实例,然后再创建 Thread 类的实例。

ThreadStart ts = new ThreadStart( 方法名 );
Thread t = new Thread(ts);

使用 ThreadStart 委托创建线程,并定义一个方法输出 0〜10 中所有的偶数。

class Program {
    static void Main(string[] args) {
        ThreadStart  threadStart = new ThreadStart(printEven);
        Thread thread = new Thread(threadStart);
        thread.Start();
    }

    private static void printEven() {
        for (int i = 0; i <= 10; i = i + 2) {
            Console.WriteLine(i);
        }
    }
}

在上面的实例中启动两个线程分别打印奇数和偶数的方法:

class Program {
    static void Main(string[] args) {
        ThreadStart  threadStart1 = new ThreadStart(printEven);
        ThreadStart  threadStart2 = new ThreadStart(printOdd);
        Thread thread1 = new Thread(threadStart1);
        Thread thread2 = new Thread(threadStart2);
        thread1.Start();
        thread2.Start();
    }

    private static void printEven() {
        for (int i = 0; i <= 100; i = i + 2) {
            Console.WriteLine(i);
        }
    }

    private static void printOdd() {
        for (int i = 1; i <= 100; i = i + 2) {
            Console.WriteLine(i);
        }
    }
}

两个线程分别打印了 1〜100 中的奇数和 0〜100 中的偶数,但并不是按照线程的调用顺序先打印出所有的偶数再打印奇数。

由于没有对线程的执行顺序和操作做控制,所以运行该程序每次打印的值的顺序是不一样的。

ParameterizedThreadStart

使用 ParameterizedThreadStart 创建进程,首先需要创建 ParameterizedThreadStart 委托的实例,然后再创建 Thread 类的实例。

ParameterizedThreadStart pts=new ParameterizedThreadStart( 方法名 );
Thread t=new Thread(pts);

创建方法输出0〜n的所有偶数,使用 ParameterizedThreadStart 委托调用该方法,并启动打印偶数的线程。

class Program {
    static void Main(string[] args) {
        ParameterizedThreadStart parameterizedThreadStart = new ParameterizedThreadStart(PrintEven);
        Thread thread = new Thread(parameterizedThreadStart);
        thread.Start(10);
    }

    private static void PrintEven(Object n) {
        for (int i = 0; i <= (int) n; i = i + 2) {
            Console.WriteLine(i);
        }
    }
}

需要注意的是,在使用 ParameterizedThreadStart 委托调用带参数的方法时,方法中的参数只能是 object 类型并且只能含有一个参数。

在启动线程时要在线程的 Start() 方法中为委托的方法传递参数。

如果需要通过 ParameterizedThreadStart 委托引用多个参数的方法,由于委托方法中的参数是 object 类型的,传递多个参数可以通过类的实例来实现。

创建一个方法输出指定范围内数值的偶数,并创建线程调用该方法。

class Program {
    static void Main(string[] args) {
        ParameterizedThreadStart parameterizedThreadStart = new ParameterizedThreadStart(PrintEven);
        Thread thread = new Thread(parameterizedThreadStart);
        Parameter parameter = new Parameter(1, 10);
        thread.Start(parameter);
    }

    private static void PrintEven(Object n) {
        if (n is Parameter == false) return;
        int beginNum = ((Parameter)n).beginNum;
        int endNum = ((Parameter)n).endNum;
        for (int i = beginNum; i <= endNum; i++) {
            if (i % 2 == 0) {
                Console.WriteLine(i);
            }
        }
    }

    public class Parameter {
        public int beginNum;
        public int endNum;

        public Parameter(int beginNum, int endNum) {
            this.beginNum = beginNum;
            this.endNum = endNum;
        }
    }
}

Priority(优先级)

如果需要控制输出值的顺序,可以通过对线程优先级的设置以及线程调度来实现。

在 C# 中线程的优先级使用线程的 Priority 属性设置即可,默认的优先级是 Normal。

优先级的值通过 ThreadPriority 枚举类型来设置,从低到高分别为Lowest、BelowNormal、Normal、AboveNormal、Highest。

通过设置线程的优先级来控制输出奇数和偶数的线程:

class Program {
    static void Main(string[] args) {
        ThreadStart threadStart1 = new ThreadStart(PrintEven);
        Thread thread1 = new Thread(threadStart1);
        //设置打印偶数线程的优先级
        thread1.Priority = ThreadPriority.Lowest;
        ThreadStart threadStart2 = new ThreadStart(PrintOdd);
        Thread thread2 = new Thread(threadStart2);
        //设置打印奇数线程的优先级
        thread2.Priority = ThreadPriority.Highest;
        thread1.Start();
        thread2.Start();
    }
    //打印1~100中的奇数
    public static void PrintOdd() {
        for(int i = 1; i <= 100; i = i + 2) {
            Console.WriteLine(i);
        }
    }
    //打印0~100中的偶数
    public static void PrintEven() {
        for(int i = 0; i <= 100; i = i + 2) {
            Console.WriteLine(i);
        }
    }
}

由于输岀奇数的线程的优先级高于输出偶数的线程,所以在输出结果中优先输出奇数的次数会更多。

使用暂停线程 (Sleep) 的方法让打印奇数和打印偶数的线程交替执行,即打印 0〜10 的数。

class Program {
    static void Main(string[] args) {
        ThreadStart threadStart1 = new ThreadStart(PrintEven);
        Thread thread1 = new Thread(threadStart1);
        ThreadStart threadStart2 = new ThreadStart(PrintOdd);
        Thread thread2 = new Thread(threadStart2);
        thread1.Start();
        thread2.Start();
    }
    //打印1~100中的奇数
    public static void PrintOdd() {
        for(int i = 1; i <= 10; i = i + 2) {
            Thread.Sleep(1000);
            Console.WriteLine(i);
        }
    }
    //打印0~100中的偶数
    public static void PrintEven() {
        for(int i = 0; i <= 10; i = i + 2) {
            Thread.Sleep(1000);
            Console.WriteLine(i);
        }
    }
}

需要注意的是,两个线程虽然交替执行,但每次运行该程序的效果依然是不同的。

如果需要终止线程只需要调用如下方法即可:

Thread.CurrentThread.Abort();

由于挂起线程 (Suspend) 和唤醒线程 (Resume) 的操作很容易造成线程的死锁状态,已经被弃用了,而是使用标识字段来设置线程挂起和唤醒的状态。

所谓线程死锁就是多个线程之间处于相互等待的状态。

将该线程设置为后台线程,可以通过如下字段:

IsBackground = true;

lock(锁)

虽然 Sleep 方法能控制线程的暂停时间,从而改变多个线程之间的先后顺序,但每次调用线程的结果是随机的。

线程同步的方法是将线程资源共享,允许控制每次执行一个线程,并交替执行每个线程。

lock 的语法形式如下:

lock(object) {
//临界区代码
}

这里 lock 后面通常是一个 Object 类型的值,也可以使用 this 关键字来表示。

最好是在 lock 中使用私有的非静态或负变量或私有的静态成员变量,即使用 Private 或 Private static 修饰的成员。

创建控制台应用程序,使用 lock 关键字控制打印奇数和偶数的线程,要求先执行奇数线程,再执行偶数线程。

class Program {
    private static string LOCKSTR = "Lock";
    static void Main(string[] args) {
        ThreadStart threadStart1 = new ThreadStart(PrintEven);
        Thread thread1 = new Thread(threadStart1);
        thread1.Name = "偶数的线程";
        thread1.Start();
        
        ThreadStart threadStart2 = new ThreadStart(PrintOdd);
        Thread thread2 = new Thread(threadStart2);
        thread2.Name = "奇数的线程";
        thread2.Start();
    }
    public static void PrintOdd() {
        lock (LOCKSTR) {
            for(int i = 1; i <= 10; i = i + 2) {
                Console.WriteLine(Thread.CurrentThread.Name + "---" + i);
            }
        }
    }
    
    public static void PrintEven() {
        lock (LOCKSTR) {
            for(int i = 0; i <= 10; i = i + 2) {
                Console.WriteLine(Thread.CurrentThread.Name + "---" + i);
            }
        }
    }
}

当打印奇数的线程结束后才执行打印偶数的线程或者偶数线程执行结束后才执行奇数线程。

Monitor(锁)

Monitor 类的命名空间是 System.Threading,它的用法要比 lock 的用法复杂一些,但本质是一样的。

使用 Monitor 类锁定资源的代码如下:

Monitor.Enter(object);
try {
    //临界区代码
} finally {
    Monitor.Exit(object);
}

在这里,object 值与 lock 中的 object 值是一样的。

将lock中的实例修改为使用monitor操作:

class Program {
    private static string LOCKSTR = "Locke";
    static void Main(string[] args) {
        ThreadStart threadStart1 = new ThreadStart(PrintEven);
        Thread thread1 = new Thread(threadStart1);
        thread1.Name = "偶数的线程";
        thread1.Start();
        
        ThreadStart threadStart2 = new ThreadStart(PrintOdd);
        Thread thread2 = new Thread(threadStart2);
        thread2.Name = "奇数的线程";
        thread2.Start();
    }
    public static void PrintOdd() {
        Monitor.Enter(LOCKSTR);
        try {
            for (int i = 1; i <= 10; i = i + 2) {
                Console.WriteLine(Thread.CurrentThread.Name + "---" + i);
            }
        } finally {
            Monitor.Exit(LOCKSTR);
        }
        
    }
    
    public static void PrintEven() {
        Monitor.Enter(LOCKSTR);
        try {
            for(int i = 0; i <= 10; i = i + 2) {
                Console.WriteLine(Thread.CurrentThread.Name + "---" + i);
            }
        } finally {
            Monitor.Exit(LOCKSTR);
        }
    }
}

Monitor 类的用法虽然比 lock 关键字复杂,但其能添加等待获得锁定的超时值,这样就不会无限期等待获得对象锁。

使用 TryEnter() 方法可以给它传送一个超时值,决定等待获得对象锁的最长时间。

Monitor.TryEnter(object, 毫秒数 );

该方法能在指定的毫秒数内结束线程,这样能避免线程之间的死锁现象。

还能使用 Monitor 类中的 Wait() 方法让线程等待一定的时间,使用 Pulse() 方法通知处于等待状态的线程。

Mutex(互斥锁)

Mutex 类也是用于线程同步操作的类,例如,当多个线程同时访问一个资源时保证一次只能有一个线程访问资源。

在 Mutex 类中,WaitOne() 方法用于等待资源被释放, ReleaseMutex() 方法用于释放资源。

WaitOne() 方法在等待 ReleaseMutex() 方法执行后才会结束。

使用线程互斥实现每个车位每次只能停一辆车的功能。

class Program {
    private static Mutex mutex = new Mutex();
    public static void PakingSpace(object num) {
        if (mutex.WaitOne()) {
            try {
                Console.WriteLine("车牌号{0}的车驶入!", num);
                Thread.Sleep(1000);
            } finally {
                Console.WriteLine("车牌号{0}的车离开!", num);
                mutex.ReleaseMutex();
            }
        }
    }
    static void Main(string[] args) {
        ParameterizedThreadStart ts = new ParameterizedThreadStart(PakingSpace);
        Thread t1 = new Thread(ts);
        t1.Start("冀A12345");
        Thread t2 = new Thread(ts);
        t2.Start("京A00000");
    }
}

当一个线程占用资源时,其他线程是不使用该资源的。

C#线程通信

传递参数通信

在C#中创建线程Thread时,可以有多种方法,而主线程和子线程之间又如何实现互相传递数据,每种创建方法传递参数的效果是不同的

不带参数的Thread

下面创建一个不带参数的Thread:

class Program {
    static void Main(string[] args) {
        Thread thread = new Thread(new ThreadStart(onWork));
        thread.Start();
    }

    private static void onWork() {
        Console.WriteLine("不带参数的方式");
    }
}

带一个参数的Thread

由于ParameterizedThreadStart要求参数类型必须为object,所以定义的方法B形参类型必须为object。

class Program {
    static void Main(string[] args) {
        Thread thread = new Thread(new ParameterizedThreadStart(onWork));
        thread.Start("datas");
    }

    private static void onWork(object obj) {
        Console.WriteLine("带一个参数:" + obj);
    }
}

注意:由于Thread默认只提供了这两种构造函数,如果需要传递多个参数,可以基于第二种方法,将参数作为类的属性传给线程。

回调函数给主线程传递参数

我们可以基于带参数的Thread,将回调函数作为类的一个方法传进线程,方便线程回调使用。

class Program {
    private delegate void ThreadCallBackDelegate(string msg);
    static void Main(string[] args) {
        CustomParam customParam = new CustomParam();
        customParam.CallBack = ThreadCallBack;

        Thread thread = new Thread(new ThreadStart(customParam.send));
        thread.Start();
    }

    private static void ThreadCallBack(string msg) {
        Console.WriteLine("CallBack:" + msg);
    }

    class CustomParam {
        public ThreadCallBackDelegate CallBack;
        public void send() {
            CallBack("datas");
        }
    }
}

SynchronizationContext

线程间的通信除了使用委托的方式外,还有一种简单方便的方式SynchronizationContext`。

示例代码如下所示:

public partial class Form1 : Form {
    private Thread _workThread;
    private SynchronizationContext _mainSynchronizationContext;
    public Form1() {
        InitializeComponent();
        _mainSynchronizationContext = SynchronizationContext.Current;
        _workThread = new Thread(new ThreadStart(DoInBackground));
        _workThread.Start();
    }
    
    // 主线程调用
    private void DoOnMainThread(object state) {
        Console.WriteLine(state);
    }

    // 子线程调用
    private void DoInBackground() {
        // ......
        // 使用委托的方式通知主线程,其中Send是同步的,而Post是异步的
        _mainSynchronizationContext.Post(new SendOrPostCallback(DoOnMainThread), null);
    }
}

注意:SynchronizationContext的对象不是所有线程都被附加的,只有UI主线程会被附加。

SynchronizationContext的Sendt()和Post()二个方法的区别:

Send() 是简单的在当前线程上去调用委托来实现(同步调用)。
Post() 是在线程池上去调用委托来实现(异步调用)

主线程跟子线程通信

主线程如果想传递参数给子线程需要使用ParameterizedThreadStart类即可,示例代码如下:

public partial class Form1 : Form {
    private Thread _workThread;
    private SynchronizationContext _mainSynchronizationContext;
    public Form1() {
        InitializeComponent();
        _mainSynchronizationContext = SynchronizationContext.Current;
        _workThread = new Thread(new ParameterizedThreadStart(DoInBackground));
        _workThread.Start("Send Message to Thread");
    }
    
    // 主线程调用
    private void DoOnMainThread(object state) {
        Console.WriteLine(state);
    }

    // 子线程调用
    private void DoInBackground(object obj) {
        Console.WriteLine(obj);
        // 使用委托的方式通知主线程,其中Send是同步的,而Post是异步的
        _mainSynchronizationContext.Post(new SendOrPostCallback(DoOnMainThread), null);
    }
}

如果想传递多个参数的情况,只需要使用包装类来包装不同的参数传递即可。

子线程跟主线程通信

子线程和主线程的通信则更加简单,只需要在SynchronizationContext的Send和Post方法中传递即可:

_mainSynchronizationContext.Post(new SendOrPostCallback(DoOnMainThread), "Send The Message to Main");

分类:

技术点:

相关文章:

  • 2022-12-23
  • 2022-12-23
  • 2021-10-15
  • 2021-12-07
  • 2022-02-22
  • 2021-12-09
  • 2021-06-21
猜你喜欢
  • 2021-09-15
  • 2022-01-15
  • 2021-05-25
  • 2021-10-11
  • 2022-12-23
  • 2021-10-12
  • 2022-12-23
相关资源
相似解决方案