C#基礎提升系列——C#文件和流


C#文件和流

本文主要是對C#中的流進行詳細講解,關於C#中的文件操作,考慮到后期.net core跨平台,相關操作可能會發生很大變化,所以此處不對文件系統(包括目錄、文件)過多的講解,只會描述出在.net framework下常用的類,具體用法請參見官方API文檔。

管理文件系統

在Windows上,用於瀏覽文件系統和執行操作的相關類有:

  • FileSystemInfo:這是表示任何文件系統對象的基類。
  • FileInfoFile:這些類表示文件系統上的文件。
  • DirectoryInfoDirectory:這些類表示文件系統上的目錄。
  • Path:這個類包含的靜態成員可以用於處理路徑名。
  • DriveInfo:這個類的屬性和方法提供了指定驅動器的信息。

Directory類和File類只包含靜態方法,不能被實例化。如果只對文件夾或文件執行一個操作,使用這個類就很有效,因為這樣可以省去創建.NET對象的系統開銷。

DirectoryInfo類和FileInfo類的成員都不是靜態的,使用時需要被實例化。如果使用同一個對象執行多個操作,使用這些類就比較有效。【這是因為在構造它們將讀取合適文件系統對象的身份驗證和其他信息,無論對每個對象(類實例)調用多少方法,都不需要再次讀取這些信息】

檢查驅動器信息

有時在處理文件和目錄之前,需要檢查驅動器信息,可以使用DriveInfo類實現。

DriveInfo類可以掃描系統,提供可用驅動器的列表,還可以進一步提供任何驅動器的大量細節信息。

關於DriveInfo的用法,請參考官方API說明:https://docs.microsoft.com/zh-cn/dotnet/api/system.io.driveinfo

使用Path類處理文件和目錄的路徑

如果只是單純的使用字符串連接操作符合並多個文件夾和文件時,很容易遺漏單個分隔符或使用太多的字符。可以使用Path類代替字符串拼接路徑,Path類會添加缺少的分隔符,而且可以基於Windows和Unix系統,處理不同的平台需求。

Path類可以使用以下幾個屬性處理Windows和Unix平台的路徑特殊符號:

  • Path.VolumeSeparatorChar:提供特定於平台的卷分隔符。 此字段的值在Windows和Macintosh上為冒號(:),在UNIX操作系統上為斜杠(/)。這對於解析諸如“c:\ windows”或“MacVolume:System Folder”之類的路徑非常有用。
  • Path.DirectorySeparatorChar:提供特定於平台的字符,用於分隔反映分層文件系統組織的路徑字符串中的目錄級別 ,該屬性的值為左斜杠(\)。
  • Path.AltDirectorySeparatorChar:提供特定於平台的備用字符,用於分隔反映分層文件系統組織的路徑字符串中的目錄級別 。此字段可以設置為與DirectorySeparatorChar相同的值。AltDirectorySeparatorCharDirectorySeparatorChar都可用於分隔路徑字符串中的目錄級別。 Windows、UNIX和Macintosh上該字段的值是斜杠(/)。
  • Path.PathSeparator:特定於平台的分隔符,用於分隔環境變量中的路徑字符串。 在基於Windows的桌面平台上,默認情況下,此字段的值為分號(;),但在其他平台上可能會有所不同。

除此之外,Path類還有如下幾個非常適用的方法:

  • Path.GetInvalidPathChars():獲取包含不允許在路徑名中使用的字符的數組。
  • Path.GetInvalidFileNameChars():獲取包含不允許在文件名中使用的字符的數組。
  • Path.GetTempPath():返回當前用戶的臨時文件夾的路徑。
  • Path.GetTempFileName():在磁盤上創建磁唯一命名的零字節的臨時文件並返回該文件的完整路徑。
  • Path.GetRandomFileName():返回隨機文件夾名或文件名。
  • Path.Combine():合並路徑。
  • Path.ChangeExtension():更改路徑字符串的擴展名。

使用Environment類處理特殊文件夾

Environment類定義了一組特殊的文件夾。注意:該類不能用於.net core中。

EnvironmentSpecialFolder屬性是一個巨大的枚舉,通過該屬性,可以得到window系統上的音樂、圖片、程序文件、應用程序數據、以及許多其他文件夾的路徑值。

除此之外,還可以調用Environment.GetEnvironmentVariable()方法,根據指定的環境變量名得到該變量的具體值。

關於Environment的具體使用,請參見官方API說明:https://docs.microsoft.com/zh-cn/dotnet/api/system.environment

使用File、FileInfo和Directory、DirectoryInfo類處理文件和文件夾

FileFileInfoDirectoryDirectoryInfo的詳細操作,可直接參見官方API文檔。這里只對其中需要注意的地方進行敘述。

File類提供了簡單創建文件並寫入和讀取文本的操作方法,如File.WriteAllText()File.ReadAllText()方法,但是需要注意的是,使用File在字符串中讀寫文件只適用於小型文本文件。並且,以這種方式讀取、保存完整的文件是有限制的。.NET字符串的限制是2GB,雖然對於許多文本文件而言,這已經足夠了,但是最好不要讓用戶等待將1GB的文件加載到字符串中,這將非常耗時和耗資源,而應該使用流進行讀寫操作。

使用流處理文件

流是一個用於傳輸數據的對象,分為讀取流和寫入流。傳輸數據可以基於文件、內存、網絡或其他任意數據源。.NET中提供了一下幾種用來操作的流:

  • MemoryStream:創建其后備存儲為內存的流。 用來讀寫內存。
  • System.Net.Sockets.NetworkStream:為網絡訪問提供基礎數據流。 用來處理網絡數據。
  • FileStream:創建用來處理文件的流,支持同步和異步。這個類主要用於在二進制文件中讀寫二進制數據。
  • System.IO.Compression.DeflateStream:提供使用Deflate算法壓縮和解壓縮流的方法和屬性。
  • System.Security.Cryptography.CryptoStream: 定義將數據流鏈接到加密轉換的流。

上述的這些類都派生自基類Stream,多個流之間可以相互鏈接(轉換),相互寫入。

對於文件的讀寫,最常用的類如下:

  • FileStream:文件流,這個類主要用於在二進制文件中讀寫二進制數據。
  • StreamReaderStreamWriter:這兩個類專門用於讀寫文本格式的流產品API。可以通過它們的基類看出主要針對的是文本格式的文件。
  • BinaryReaderBinaryWriter:這兩個類專門用於讀寫二進制格式的流產品API。

各種流如何選擇

在不使用其他流的情況下,單純的使用FileStream既可以處理文本格式的文件也可以處理二進制數據文件,它是基於字節來讀取或寫入數據的。對於文本文件,可能需要先分析文本文件的編碼,以便能夠正確讀取和寫入不同編碼格式的文本。

如果內容是文本格式的文件,推薦使用StreamReaderSreamWriter進行讀寫操作,不需要考慮編碼格式問題,默認采用UTF-8編碼(可在構造函數中自定義編碼格式)。

如果內容是二進制格式的文件,推薦使用BinaryReader和BinaryWriter進行讀寫操作,將以二進制格式而不是文本格式寫入文件,並且不需要使用任何編碼。

使用文件流FileStream

可以使用FileStream的構造函數來創建FileStream對象,除了這種方式外,還可以直接使用File類的OpenRead()方法創建FileStreamOpenRead()方法打開一個文件(類似於FileMode.Open),返回一個可以讀取的流(FileAccess.Read),也允許其他進程執行讀取訪問(FileShare.Read)。對應的寫入流可以使用File.OpenWrite()方法得到。

無論是讀取流還是寫入流,它們都是FileStream對象,只是對應的FileAccess代表的操作不同,建議在給流對象命名時,最好有是讀取還是寫入的標識名。

獲取流相關信息

在下面的示例中,分別獲取Stream類的成員屬性,得到流處理相關信息。

private void ShowStreamInfomation(FileStream stream) { Console.WriteLine("當前流是否可讀取:" + stream.CanRead); Console.WriteLine("當前流是否可寫入:" + stream.CanWrite); Console.WriteLine("當前流是否支持搜索:" + stream.CanSeek); Console.WriteLine("當前流是否可以超時:" + stream.CanTimeout); Console.WriteLine("當前流長度:" + stream.Length); Console.WriteLine("當前流的位置:" + stream.Position); //如果可以超時 if (stream.CanTimeout) { Console.WriteLine("流在嘗試讀取多少毫秒后超時:" + stream.ReadTimeout); Console.WriteLine("流在嘗試寫入多少毫秒后超時:" + stream.WriteTimeout); } }

分析文本文件的編碼

對於文本文件,首先是讀取流中的第一個字節——序言。序言提供了文件如何編碼的信息(使用的文本編碼格式),這也稱為字節順序標記(Byte Order Mark,BOM)。可以使用如下方法,獲取BOM

private Encoding GetEncoding(FileStream stream) { //如果當前流不支持檢索就拋出異常 if (!stream.CanSeek) throw new ArgumentException("require a stream that can seek"); Encoding encoding = Encoding.ASCII; //定義緩沖區,這里只是為了將BOM格式寫入到該字節數組中 byte[] bom = new byte[5]; //從流中讀取字節塊,填充bom字節數組的同時,返回讀入緩沖區的總字節數 //注意流可能小於緩沖區。如果沒有更多的字符可用於讀取,Read()方法就返回0,此時沒有數據寫入到緩沖區 int nRead = stream.Read(bom, offset: 0, count: 5); if (bom[0] == 0xff && bom[1] == 0xfe && bom[2] == 0 && bom[3] == 0) { Console.WriteLine("UTF-32"); //將該流的當前位置設置為給定值,從流的開始位置起 stream.Seek(4, SeekOrigin.Begin); return Encoding.UTF32; } else if (bom[0] == 0xff && bom[1] == 0xfe) { Console.WriteLine("UTF-16, little endian"); stream.Seek(2, SeekOrigin.Begin); return Encoding.Unicode; } else if (bom[0] == 0xfe && bom[1] == 0xff) { Console.WriteLine("UTF-16,big endian"); stream.Seek(2, SeekOrigin.Begin); return Encoding.BigEndianUnicode; } else if (bom[0] == 0xef && bom[1] == 0xbb && bom[2] == 0xbf) { Console.WriteLine("UTF-8"); stream.Seek(3, SeekOrigin.Begin); return Encoding.UTF8; } stream.Seek(0, SeekOrigin.Begin); return encoding; }

上述方法中,使用Read()方法將流的前5個字節寫入到字節數組中,接着對5個字節的值進行比對,找出最終的編碼格式,並把流定位在編碼字符后的位置。

讀取流

當創建FileStream對象后,就可以使用Read()方法對文件進行讀取。可以使用循環重復此過程,直到該方法返回0為止。詳細說明見示例中的注釋描述:

private void ReadFileUsingFileStream(string fileName) { //每次讀取的字節數 const int BUFFERSIZE = 4096; using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) { ShowStreamInfomation(stream); Encoding encoding = GetEncoding(stream); byte[] buffer = new byte[BUFFERSIZE]; bool completed = false; do { int nread = stream.Read(buffer, 0, BUFFERSIZE); //如果沒有字節被讀取,就跳出循環 if (nread == 0) completed = true; //如果最終讀取的字節數小於緩沖區的字節數大小 if (nread < BUFFERSIZE) { //Clear()方法用於清除數組中不需要的多余元素 //將buffer數組從索引nread處開始清理,清理的元素個數為BUFFERSIZE - nread //這樣就將未填滿的buffer中,多余的字節給清理調用 Array.Clear(buffer, nread, BUFFERSIZE - nread); } Console.WriteLine($"讀取 {nread} 字節"); //將讀取到緩沖區buffer中的數據,按照指定的編碼格式,轉換為字符串並輸出 //此處可以直接調用encoding.GetString(buffer) string s = encoding.GetString(buffer, 0, nread); Console.WriteLine(s); } while (!completed); } }

寫入流

除了使用FileStream構造方法外,可以使用File.OpenWrite()方法創建一個可以寫入的流。接着可以調用WriteByte()方法寫入一個字節,或者調用Write()方法寫入包含多個字節的字節數組。

下述示例說明了如何進行寫入流的操作,詳細說明見代碼中的注釋描述:

public static void WriteTextFile() { string tempTextFileName = Path.ChangeExtension(Path.GetTempFileName(), "txt"); using (FileStream stream = File.OpenWrite(tempTextFileName)) { //在寫入文本之前,需要先寫入序言信息 //返回使用UTF-8的字節序列,preamble即為編碼為UTF-8的字節序列,它代表文件的序言信息 byte[] preamble = Encoding.UTF8.GetPreamble(); //寫入序言信息為UTF-8到文件流中 stream.Write(preamble, 0, preamble.Length); string hello = "你好,C#!"; //將字符串轉換為字節數組寫入到文件流中 byte[] buffer = Encoding.UTF8.GetBytes(hello); stream.Write(buffer, 0, buffer.Length); Console.WriteLine("文件:" + stream.Name + ",已經寫入"); } }

復制流

可以直接調用Stream類的CopyTo()方法,將一個流寫入到另一個流中。

public static void CopyUsingStream2(string inputFile, string outputFile) { using (var inputStream = File.OpenRead(inputFile)) { using (var outputStream = File.OpenWrite(outputFile)) { //從當前流中讀取字節並將其寫入到另一流中 inputStream.CopyTo(outputStream); } } }

隨機對流進行切塊讀取

為了說明對流進行切塊讀取的操作,先寫入流到一個文件中,寫入時,使用了StreamWriter流(該流特別適合文本格式的文件寫入操作,在之后會詳細講解),代碼如下:

private const string SampleFilePath = "./samplefile.data"; public static async void CreateSampleFileAsync(int nRecords) { //雖然FileStream沒有使用using或者調用釋放方法,但是在StreamWriter銷毀時,StreamWriter會控制所使用的資源,並銷毀流 FileStream stream = File.Create(SampleFilePath); //StreamWriter用於文本流的寫入 using (var writer = new StreamWriter(stream)) { var r = new Random(); var records = Enumerable.Range(0, nRecords).Select(x => new { Number = x, Text = $"Sample text {r.Next(200)}", Date = new DateTime(Math.Abs((long)((r.NextDouble() * 2 - 1) * DateTime.MaxValue.Ticks))) }); foreach (var rec in records) { string date = rec.Date.ToString("d", CultureInfo.InvariantCulture); string s = $"#{rec.Number,8}; {rec.Text,-20}; {date}#{Environment.NewLine}"; await writer.WriteAsync(s); } } }

上述代碼中,隨機獲取日期格式的時間寫入到文件中,調用代碼如下:

FileStreamDemo.CreateSampleFileAsync(1000);

執行完成后,文件中將會寫入1000行數據,寫入的內容如下:

# 988; Sample text 131 ; 06/02/4085# # 989; Sample text 168 ; 02/25/8671# # 990; Sample text 93 ; 07/31/1218# # 991; Sample text 67 ; 03/22/7271# # 992; Sample text 193 ; 04/26/6755# # 993; Sample text 72 ; 09/01/9993# # 994; Sample text 11 ; 05/22/6211# # 995; Sample text 24 ; 01/04/5765# # 996; Sample text 70 ; 08/08/1013# # 997; Sample text 148 ; 04/11/8407# # 998; Sample text 187 ; 07/16/2297# # 999; Sample text 168 ; 01/11/5624#

接着對寫入的文件進行隨機定位讀取,使用Steam類的Seek()方法定位流中的光標。讀取流的代碼如下:

public static void RandomAccessSample() { //每次讀取的字節數 const int RECORDSIZE = 100; try { using (FileStream stream = File.OpenRead(SampleFilePath)) { byte[] buffer = new byte[RECORDSIZE]; do { try { Console.WriteLine("record numer (or 'bye' to end):"); //接收用戶輸入的行號 string line = Console.ReadLine(); if ("BYE".Equals(line)) break; int record; if (int.TryParse(line, out record)) { //設定流的讀取位置 stream.Seek((record - 1) * RECORDSIZE, SeekOrigin.Begin); stream.Read(buffer, 0, RECORDSIZE); string s = Encoding.UTF8.GetString(buffer); Console.WriteLine("record:" + s); } } catch (Exception ex) { Console.WriteLine(ex.Message); } } while (true); } } catch (FileNotFoundException) { Console.WriteLine("文件不存在!"); } }

使用文本格式流StreamReader和StreamWriter

如果是處理文本格式的文件,使用FileStream類讀寫操作時,需要使用字節數組,比較繁瑣。可以直接使用StreamReaderStreamWriter類讀寫FileStream,無須處理字節數組和編碼,會容易很多。

使用這兩個類的StreamReader.ReaderLine()StreamWriter.WriterLine()方法,可以一次讀寫一行文本。在讀取文件時,流會自動確定下一個回車符的位置,並在該處停止讀取。在寫入文件時,流會自動把回車符和換行符追加到文本的末尾。並且,使用StreamWriterStreamReader類,不需要擔心文件中使用的編碼方式。

使用StreamReader類讀取文本格式數據

public static void ReadFileUsingReader(string fileName) { var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read); //該構造方法接收一個Stream對象 using (var reader = new StreamReader(stream)) { //當前流位置是否在流末尾 while (!reader.EndOfStream) { string line = reader.ReadLine(); Console.WriteLine(line); } } }

使用StreamReader類,不需要處理字節數組和編碼。注意:StreamReader默認使用UTF-8編碼。在該類的構造方法的重載版本中,可以指定detectEncodingFromByteOrderMarks參數,用來指定是否可以讓StreamReader使用文件中的序言定義的編碼。也可以使用重載版本中的另一個指定Encoding參數的方法,顯示的指定編碼。還可以使用重載版本中的指定bufferSize參數的方法,用來設置要使用的緩沖區大小。默認為1024字節。此外,還可以指定關閉讀取器時,不應該關閉底層流。默認情況下,關閉讀取器時(使用Dispose()方法),會關閉底層流。

如果不使用實例化的方式構造StreamReader對象,也可以使用File類的OpenText()方法創建。

StreamReader還允許把內容讀入一個字符數組中。這類似於Stream類的Read()方法,但是它讀入的是char字符數組,而不是byte字節數組。注意:char類型使用兩個字節。這適合於16Unicode但不適合於UTF-8(其中,一個字符的長度可以是16個字節)。

使用StreamWriter類寫入文本格式數據

public static void WriteFileUsingWriter(string fileName,string [] lines) { var outputStream = File.OpenWrite(fileName); using(var writer=new StreamWriter(outputStream)) { //獲取UTF-8代表的序言轉換的字節數組 byte[] preable = Encoding.UTF8.GetPreamble(); //先寫入編碼格式 outputStream.Write(preable, 0, preable.Length); //在寫入字符串數組 writer.Write(lines); } }

StreamWriter默認使用UTF-8格式寫入文本內容。通過在構造方法中設置Encoding對象,可以定義替代的內容。另外,類似於StreamReader的構造方法,StreamWriter允許指定緩沖區的大小,以及關閉寫入器時是否不應該關閉底層流。

StreamWriterWriter()方法定義了多個重載版本,允許傳遞字符串和一些.NET數據類型,使用傳遞.NET數據類型的方法,這些都會使用指定的編碼變成字符串。

使用二進制文件流BinaryReader和BinaryWriter將數據以二進制格式寫入到文件中

對於內容是二進制格式的文件,可以使用FileStream流進行讀寫,此時需要使用字節數組執行讀寫操作。除此之外,還可以使用BinaryReaderBinaryWriter對二進制文件進行讀寫,它們不需要使用任何編碼,並且是以二進制格式而不是文本格式寫入到文件中。

使用BinaryWriter將數據以二進制格式寫入到文件中

public static void WriteFileUsingBinaryWriter(string binFile) { var outputStream = File.Create(binFile); using (var writer = new BinaryWriter(outputStream)) { double d = 47.47; int i = 42; long l = 987654321; string s = "sample"; writer.Write(d); writer.Write(i); writer.Write(l); writer.Write(s); } }

BinaryWriter.Write()方法有很多的重載版本,可以將不同類型的數據以二進制的格式寫入到文件中,包括基本數據類型和字節數組等。調用上述方法,將會生成一個二進制格式的文件。可以使用Visual Studio進行打開,打開后的內容如下:
圖片描述

使用BinaryReader將二進制格式的數據從文件中進行讀取

public static void ReadFileUsingBinaryReader(string binFile) { var inputStream = File.Open(binFile, FileMode.Open); using(var reader=new BinaryReader(inputStream)) { //讀取並定位 double d= reader.ReadDouble(); int i = reader.ReadInt32(); long l = reader.ReadInt64(); string s = reader.ReadString(); Console.WriteLine($"d:{d} \t i:{i} \t l:{l} \t s:{s} "); } }

注意:讀取的類型應該和寫入的類型相同才能夠按照正確順序讀取。讀取的結果如下:

d:47.47 i:42 l:987654321 s:sample

讀取文件的順序必須完全匹配寫入的順序,創建自己的二進制格式時,需要知道存儲的內容和方式,並用相應的方式讀取。舊的Word文檔doc使用二進制文件格式,新的docx文件擴展是ZIP文件。

壓縮文件

.NET中壓縮文件常用的類型有以下幾種:

  • DeflateStream:可以使用該類完成文件的壓縮和解壓操作。
  • GZipStream:可以使用該類完成文件的壓縮和解壓操作,它和DeflateStream使用相同的壓縮算法(事實上,GZipStream在后台使用DeflateStream),但GZipStream增加了循環冗余校驗,來檢測數據的損壞情況。使用GZipStream創建的壓縮文件,可以使用第三方工具GZip直接打開。
  • ZipArchive:使用ZipArchive可以創建和讀取ZIP文件。ZIP文件,可以直接在Windows資源管理器中打開查看。

使用DeflateStream壓縮/解壓文件

壓縮示例:

public static void CompressFile(string fileName, string compressedFileName) { //讀取要壓縮的文件 using (FileStream inputStream = File.OpenRead(fileName)) { //創建要寫入的文件 FileStream outputStream = File.OpenWrite(compressedFileName); //創建壓縮流,構造方法指明最終寫入的文件流 using (var compressStream = new DeflateStream(outputStream, CompressionMode.Compress)) { //將讀取的文件流寫入到壓縮流中 inputStream.CopyTo(compressStream); } } }

如上所示,可以將多個流鏈接起來,在將一個文件壓縮到另一個文件中時,需要指明讀取流(inputStream)和寫入流(outputStream),然后創建一個壓縮流DeflateStream對象,在構造方法中,指明了將要寫入的文件流outputStream,同時使用CompressionMode.Compress表示壓縮操作,最后使用了CopyTo()方法,將讀取流inputStream復制到壓縮流中,實現壓縮。除了CopyTo()方法外,還可以使用Write()方法或其他功能寫入inputStream流。

可以使用如下代碼進行調用:

DeflateStreamAndGZipStreamDemo.CompressFile("samplefile.data", "b.ys");

解壓示例:

public static void DecompressFile(string fileName) { //創建文件讀取流 FileStream inputStream = File.OpenRead(fileName); //這里為了直接輸出文件內容使用了MemoryStream,可以換成FileStream用來保存解壓后的文件 using (MemoryStream outputStream = new MemoryStream()) { //解壓讀取的文件 using (var compressStream = new DeflateStream(inputStream, CompressionMode.Decompress)) { //將解壓流寫入到內存流中,以便后續直接輸出 compressStream.CopyTo(outputStream); //定位內存流的當前位置 outputStream.Seek(0, SeekOrigin.Begin); //將內存流的內容使用StreamReader輸出文本 using (var reader = new StreamReader(outputStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 4096, //注意:此參數很有用途 leaveOpen: true)) { string result = reader.ReadToEnd(); Console.WriteLine(result); } } } }

上述代碼中,最重要的是創建DeflateStream對象時,在構造方法中指定的CompressionMode.Decompress,表示解壓縮。其他說明見代碼中的注釋描述。

使用ZipArchive壓縮文件

可以使用ZipArchive創建Zip壓縮文件。ZipArchive包含多個ZipArchiveEntry對象,ZipArchive類不是一個流,但是它使用流進行讀寫。

壓縮示例:

public static void CreateZipFile(string directory, string zipFile) { //將要穿件的壓縮文件對應的寫入流 FileStream zipStream = File.OpenWrite(zipFile); using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create)) { //獲取目錄下的所有文件 IEnumerable<string> files = Directory.EnumerateFiles(directory, "*", SearchOption.TopDirectoryOnly); foreach (var file in files) { //針對每個文件創建ZipArchiveEntry對象 ZipArchiveEntry entry = archive.CreateEntry(Path.GetFileName(file)); //創建每個文件對應的讀取流 using (FileStream inputSream = File.OpenRead(file)) //打開ZipArchiveEntry對象的壓縮流 using (Stream outputStream = entry.Open()) { //將文件寫入到壓縮流中 inputSream.CopyTo(outputStream); } } } }

使用FileSystemWatcher監視文件的更改

使用FileSystemWatcher可以監視文件的更改。事件在創建、重命名、刪除和更改文件時觸發。

示例代碼:

public static void WatchFiles(string path, string filter) { var watcher = new FileSystemWatcher(path, filter) { //是否應該監視指定目錄的子目錄 IncludeSubdirectories = true }; //創建文件或目錄時發生 watcher.Created += OnFileChanged; //更改文件或目錄時發生 watcher.Changed += OnFileChanged; //刪除文件或目錄時觸發 watcher.Deleted += OnFileChanged; //重命名文件或目錄時觸發 watcher.Renamed += OnFileRenamed; //開始啟用監聽 watcher.EnableRaisingEvents = true; Console.WriteLine("文件監視中。。。"); } private static void OnFileChanged(object sender, FileSystemEventArgs e) { Console.WriteLine(e.Name + "\t" + e.ChangeType); } private static void OnFileRenamed(object sender, RenamedEventArgs e) { Console.WriteLine($"舊名稱:{e.OldName} 新名稱:{e.Name} Type:{e.ChangeType}"); }

在當前目錄下監聽所有的文件或文件夾,調用上述代碼:

WatcherFileDemo.WatchFiles("./", "*");

可以在Debug目錄中創建一個文本文件,試着修改文件名和文件內容,可以看到輸出的內容。

使用內存映射的文件

內存映射文件允許訪問文件,或在不同的進程中共享內存。內存映射文件技術有以下幾個場景和特點:

  • 使用文件地圖,快速隨機訪問大文件
  • 在不同的進程或任務之間共享文件或內存
  • 使用訪問器直接從內存位置進行讀寫
  • 基於流進行讀寫

內存映射文件API允許使用物理文件或共享的內存,其中把系統的頁面文件用作后備存儲器。共享的內存可以大於可用的物理內存,所以需要一個后備存儲器。可以為特定的文件或共享的內存創建一個內存映射文件。使用這兩個選項,可以給內存映射指定的名稱。使用名稱,允許不同的進程訪問同一個共享的內存。

創建了內存映射之后,就可以創建一個視圖。視圖用於映射完整內存映射文件的一部分,以訪問它,進行讀寫。

注:【該段引用的文字翻譯的很晦澀,不是很明白】

下面將用一個示例來說明具體的使用,在Run()方法中,啟動多個任務,一個用於創建內存映射文件和寫入數據,另一個任務用於讀取數據。

//創建兩個事件狀態,用來收發信號 private ManualResetEventSlim _mapCreated = new ManualResetEventSlim(initialState: false); private ManualResetEventSlim _dataWritenEvent = new ManualResetEventSlim(initialState: false); //映射名稱 private const string MAPNAME = "SampleMap"; public void Run() { //啟動一個用於創建內存映射文件和寫入數據的任務 Task.Run(() => WriterAsync()); //啟動一個讀取數據的任務 Task.Run(() => Reader()); Console.WriteLine("任務已經啟動..."); Console.Read(); }

注:由於后序代碼會用到事件狀態和內存映射名稱,所以上述代碼在開頭先進行了聲明。

使用訪問器創建內存映射文件

訪問器來自於MemoryMappedFile對象的CreateViewAccessor()方法進行創建,得到MemoryMappedViewAccessor類型的訪問視圖,用於訪問共享的內存。使用視圖訪問器,可以定義這一任務使用的偏移量和大小,可以使用的大小的最大值是內存映射文件的大小。

寫入器:

private async Task WriterAsync() { try { //創建一個基於內存打車內存映射文件 using (MemoryMappedFile mappedFile = MemoryMappedFile.CreateOrOpen( MAPNAME, 10000, MemoryMappedFileAccess.ReadWrite)) { //給事件_mapCrated發出信號,給其他任務提供信息,說明已經創建了內存映射文件,可以打開它了 _mapCreated.Set(); Console.WriteLine("shared memory segment created"); //創建視圖訪問器,用來訪問共享的內存 using (MemoryMappedViewAccessor accessor = mappedFile.CreateViewAccessor( 0, 10000, MemoryMappedFileAccess.Write)) { for (int i = 0, pos = 0; i < 100; i++, pos += 4) { //將數據寫入到共享內存中 accessor.Write(pos, i); Console.WriteLine($"written {i} at position {pos}"); await Task.Delay(10); } //寫入完數據后,給事件發出信號,通知讀取器,現在可以開始讀取了 _dataWritenEvent.Set(); Console.WriteLine("data written"); } } } catch (Exception ex) { Console.WriteLine("writer " + ex.Message); } }

讀取器:

private void Reader() { try { Console.WriteLine("reader"); //讀取器首先等待創建內存映射文件 _mapCreated.Wait(); Console.WriteLine("reader starting"); //打開內存映射文件 using (MemoryMappedFile mappedFile = MemoryMappedFile.OpenExisting( MAPNAME, MemoryMappedFileRights.Read)) { //創建一個視圖訪問器 using (MemoryMappedViewAccessor accessor = mappedFile.CreateViewAccessor( 0, 10000, MemoryMappedFileAccess.Read)) { //等待設置_dataWritenEvent _dataWritenEvent.Wait(); Console.WriteLine("reading can start now"); for (int i = 0; i < 400; i += 4) { int result = accessor.ReadInt32(i); Console.WriteLine($"reading {result} from position {i}"); } } } } catch (Exception ex) { Console.WriteLine("reader " + ex.Message); } }

使用流創建內存映射文件

除了用內存映射文件寫入原始數據類型之外,還可以使用了流。可以使用MemoryMappedFile對象的CreateViewStream()方法創建一個MemoryMappedViewStream類型的流,它映射到內存映射文件的視圖,並具有指定的偏移、大小和訪問類型。以此來代替之前的MemoryMappedViewAccessor

下面的代碼將使用流的方式創建內存映射文件,和之前不同之處在於使用MemoryMappedViewStream替換了MemoryMappedViewAccessor,其他基本類似,具體見代碼注釋說明。

寫入器:

private async Task WriterUsingStreams() { try { using (MemoryMappedFile mappedFile = MemoryMappedFile.CreateOrOpen( MAPNAME, 10000, MemoryMappedFileAccess.ReadWrite)) { _mapCreated.Set(); Console.WriteLine("shared memory segment created"); //在映射內定義一個視圖,注意此處和之前的CreateViewAccessor()的不同 MemoryMappedViewStream stream = mappedFile.CreateViewStream( 0, 10000, MemoryMappedFileAccess.Write); using (var writer = new StreamWriter(stream)) { //為了用每次寫入的內容刷新緩存,此處需要設置為true writer.AutoFlush = true; for (int i = 0; i < 100; i++) { string s = "some data " + i; Console.WriteLine($"writing {s} at {stream.Position}"); //StreamWriter以緩存的方式寫入操作,所以流的位置不是在每個寫入操作中都更新,只在寫入器寫入塊時才更新 //所以,每次寫入的內容都需要刷新緩存,可以手動調用writer.Flush()方法,也可以在寫入之前設置writer.AutoFlush = true await writer.WriteLineAsync(s); } } _dataWritenEvent.Set(); } } catch (Exception ex) { Console.WriteLine("writer " + ex.Message); } }

讀取器:

private async Task ReaderUsingStreams() { try { Console.WriteLine("reader"); _mapCreated.Wait(); Console.WriteLine("reader starting"); using (MemoryMappedFile mappedFile = MemoryMappedFile.OpenExisting( MAPNAME, MemoryMappedFileRights.Read)) { MemoryMappedViewStream stream = mappedFile.CreateViewStream( 0, 10000, MemoryMappedFileAccess.Read); using (var reader = new StreamReader(stream)) { _dataWritenEvent.Wait(); Console.WriteLine("reading can start now"); for (int i = 0; i < 100; i++) { long pos = stream.Position; string s = await reader.ReadLineAsync(); Console.WriteLine($"read {s} from {pos}"); } } } } catch (Exception ex) { Console.WriteLine("reader " + ex.Message); } }

使用管道通信

為了在線程和進程之間通信,在不同的系統之間快速通信,可以使用管道。在.NET中,管道實現為流,因此不僅可以把字節發送到管道,還可以使用流的所有特性,如讀取器和寫入器。

管道實現為不同的類型:一種是命名管道,其中的名稱可用於連接到每一端,另一種是匿名管道。匿名管道不能用於不同系統之間的通信;只能用於一個父子進程之間的通信或不同任務之間的通信。

命名管道可以是雙向的,對應的PipeDirection可以為InOut(見下述代碼示例);而匿名管道只能是單向的。

創建命名管道服務器

通過創建NamedPipeServerStream的一個新實例,來創建服務器。NamedPipeServerStream派生自基類PipeStreamPipeStream派生自Stream基類,因此可以使用了流的所有功能。(例如,可以創建CrytoStreamGZipStream,把加密或壓縮的數據寫入命名管道)。

//創建命名管道服務器 private static void PipesReader(string pipeName) { try { //創建對象,構造函數需要管道的名稱,通過管道通信的多個進程可以使用該管道 //第二個參數定義了管道的方向,此處用於讀取,因此設置為了向內 using (var pipeReader = new NamedPipeServerStream(pipeName, PipeDirection.In)) { //命名管道等待寫入的連接 pipeReader.WaitForConnection(); Console.WriteLine("reader connected"); const int BUFFERSIZE = 256; bool completed = false; while (!completed) { byte[] buffer = new byte[BUFFERSIZE]; //管道服務器把消息讀入緩沖區數組 int nRead = pipeReader.Read(buffer, 0, BUFFERSIZE); //獲取消息內容並打印顯示 string line = Encoding.UTF8.GetString(buffer, 0, nRead); Console.WriteLine(line); if (line == "bye") completed = true; } } Console.WriteLine("completed reading"); Console.ReadLine(); } catch (Exception ex) { Console.WriteLine(ex.Message); } }

在使用構造函數創建NamedPipeServerStream實例時,可以用命名管道配置的其他一些選項如下:

  • 可以把枚舉PipeTransmissionMode設置為ByteMessage。設置為Byte,就發送一個連接的流,設置為Message,就可以檢索每條消息。
  • 使用管道選項,可以指定WriteThrough立即寫入管道,而不緩存。
  • 可以為輸入和輸出配置緩沖區大小。
  • 配置管道安全性,指定誰允許讀寫管道。
  • 可以配置管道句柄的可繼承性,這對與子進程進行通信是很重要的。

因為NamedPipeServerStream是一個流,所以可以使用StreamReader,而不是讀取字節數組,因此上述代碼可簡化為:

//簡化版 private static void PipesReader2(string pipeName) { try { var pipeReader = new NamedPipeServerStream(pipeName, PipeDirection.In); using (var reader = new StreamReader(pipeReader)) { pipeReader.WaitForConnection(); Console.WriteLine("reader connected"); bool completed = false; while (!completed) { string line = reader.ReadLine(); Console.WriteLine(line); if (line == "bye") completed = true; } } Console.WriteLine("completed reading"); Console.ReadLine(); } catch (Exception ex) { Console.WriteLine(ex.Message); } }

創建命名管道客戶端

上述創建的服務器用於讀取消息,下面需要創建客戶端用於寫入數據。

通過實例化一個NamedPipeClientStream對象來創建客戶端。因為命名管道可以在網絡上通信,所以需要服務器名稱、管道的名稱和管道的方向。

public static void PipesWriter(string pipeName) { var pipeWriter = new NamedPipeClientStream("TheRokcs", pipeName, PipeDirection.Out); using (var writer = new StreamWriter(pipeWriter)) { //連接管道 pipeWriter.Connect(); Console.WriteLine("writer connected"); bool completed = false; while (!completed) { string input = Console.ReadLine(); if (input == "bye") completed = true; //把消息發送給服務器,默認情況下,消息不立即發送,而是緩存起來。 writer.WriteLine(input); //調用Flush()把消息推送到服務器上 writer.Flush(); } } Console.WriteLine("completed writing"); }

執行時,需要同時啟動兩個項目,分別代表服務器和客戶端,一個輸入另一個回應。

創建匿名管道

下面示例中,將創建兩個彼此通信的任務,由於該方式使用的不是特別多,所以具體說明自行查閱官方API,此處只是一個簡單的示例。

公共部分:

private string _pipeHandle; //創建一個信號狀態(是否有收到信號) private ManualResetEventSlim _pipeHandleSet; public void Run() { _pipeHandleSet = new ManualResetEventSlim(initialState: false); Task.Run(() => Reader()); Task.Run(() => Writer()); }

服務器部分:

//服務端 private void Writer() { try { //把服務器充當讀取器 var pipeReader = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.None); using (var reader = new StreamReader(pipeReader)) { //獲取管道的客戶端句柄,被轉換為一個字符串后賦予變量_pipeHandle //這個變量以后有充當寫入器的客戶端使用 _pipeHandle = pipeReader.GetClientHandleAsString(); Console.WriteLine("pipe handle:" + _pipeHandle); _pipeHandleSet.Set(); bool end = false; while (!end) { string line = reader.ReadLine(); Console.WriteLine(line); if (line == "end") end = true; } Console.WriteLine("finished reading"); } } catch (Exception ex) { Console.WriteLine(ex.Message); } }

客戶端部分:

//客戶端 private void Reader() { Console.WriteLine("anonymous pipe writer"); //客戶端等到變量_pipeHandleSet發出信號 _pipeHandleSet.Wait(); //收到信號后,就打開由_pipeHandle變量引用的管道句柄 var pipeWriter = new AnonymousPipeClientStream(PipeDirection.Out, _pipeHandle); using (var writer = new StreamWriter(pipeWriter)) { writer.AutoFlush = true; Console.WriteLine("starting writer"); for (int i = 0; i < 5; i++) { writer.WriteLine("Message " + i); Task.Delay(500).Wait(); } Console.WriteLine("end"); } }

基於Windows運行庫使用文件和流(Winfrom/WPF)

  • FileInputStream
  • FileOutputStream
  • RandomAccessStreams
  • DataReaderDataWriter

關於這些類的使用,此處不做詳細介紹,可查詢官方API文檔。

關於流的總結

  • 如果需要將字符串轉換為字節數組,可以調用 Encoding.UTF8.GetBytes(str)方法。
  • FileStream流是基於字節數組進行寫入或讀取的,對應的方法為Write()Read(),除此之外,常常還需要定位流中光標的位置,此時需要調用Seek()方法。

擴展與補充

字符串、字節數組之間的相互轉換:

string str = "你好嗎?好好學習,天天向上!"; //將字符串轉換為字節 byte[] bs = Encoding.UTF8.GetBytes(str); //將字節數組轉換為等效字符串,實際應用中可以存儲該字符串到文件中 string abc = Convert.ToBase64String(bs); Console.WriteLine("將字節數組轉換為字符串:"); Console.WriteLine(abc); //將等效字符串還原為數組 byte[] bs2 = Convert.FromBase64String(abc); //將字節數組轉換為字符串 Console.WriteLine("還原后的結果:"); Console.WriteLine(Encoding.UTF8.GetString(bs2));


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM