讀取文件原則上非常簡單,但它不是通過FileInfo和DirectoryInfo來完成的,關於FileInfo和DirectoryInfo請參考C# 文件操作系列一,在.Net Framework4.5中可以通過File類來讀寫文件,在.Net Framework2.0推出之前,讀寫文件是相當費勁的,但是在.Net Framework2.0推出之后,它對File類進行了擴展,只要編寫一行代碼,就能對文件進行讀寫,下面通過一個窗體應用程序來展示文件的讀寫功能。
一、文件讀取
1、通過File類的靜態方法ReadAllText()進行文件的讀取。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace FileReadWrite { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { ReadFile(); } protected void ReadFile() { if (string.IsNullOrEmpty(textBox1.Text)) { MessageBox.Show("請輸入文件路徑!"); } else { string res = File.ReadAllText(textBox1.Text,Encoding.Default); textBox2.Text = res; } } } }
2、通過File類的靜態方法ReadAllLines()進行文件的讀取,代碼如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace FileReadWrite { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { ReadFile(); } protected void ReadFile() { if (string.IsNullOrEmpty(textBox1.Text)) { MessageBox.Show("請輸入文件路徑!"); } else { string[] res = File.ReadAllLines(textBox1.Text,Encoding.Default); StringBuilder result = new StringBuilder(); foreach (string re in res) { result.Append(re); } textBox2.Text = result.ToString(); } } } }
3、通過File類的靜態方法ReadAllBytes()進行文件的讀取,代碼如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace FileReadWrite { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { ReadFile(); } protected void ReadFile() { if (string.IsNullOrEmpty(textBox1.Text)) { MessageBox.Show("請輸入文件路徑!"); } else { byte[] res = File.ReadAllBytes(textBox1.Text); StringBuilder sb = new StringBuilder(); foreach (var b in res) { sb.Append(b.ToString()); } textBox2.Text = sb.ToString(); } } } }
二、文件寫入
File提供了ReadAllBytes()、ReadAllLines()、ReadAllText()等讀取文件的方式,所以對應的就會有相對應的寫入文件的方式WriteAllBytes()、WriteAllLines()、WriteAllText()。
這里就不多做闡述.
三、通過流來操作文件
1、流的概念相信大家都不陌生,無論是哪種語言、哪種平台都會有流的存在,流是一個用於傳輸數據的對象,流可以雙向傳輸,分為讀取流和寫入流。
a、讀取流:數據從外部源傳輸到程序中
b、寫入流:數據從程序傳輸到外部源中
外部源通常是一個文件,但也不都是一個文件,它也可能是:
a、網絡上的數據(可通過一些網絡協議進行讀寫)
.Net提供了一個System.Net.Sockets.NetworkStream類,可以通過它來處理網絡數據。
b、讀寫到內存區域上的數據
.Net提供了一個System.IO.MemoryStream類,可以通過它來讀取內存.
c、讀寫到命名管道上的數據
讀寫管道沒有提供基本的流類,但是有一個泛型流類System.IO.Stream,如果要編寫這樣一個類,就可以從這個基類繼承.
d、另一個計算機上發送的數據
e、外部源甚至可以代碼中的一個變量,使用流在變量之間傳輸數據是一個常用的技巧,可以在數據類型之間轉換數據。
使用一個獨立的對象來傳輸數據,比使用FileInfo和DirectoryInfo類更好,應為把傳輸數據(名詞)的概念和特定數據源分離開來,可以更容易的交換數據源。流對象本身包含許多通用的代碼,可以在外部數據源和代碼中的變量之間移動數據,把這些代碼與特定數據源的概念區分開來,可以實現不同環境下代碼的重用(通過繼承).例如像StringReader和StringWriter,StreamReader和StreamWriter一樣,都是同一繼承樹的一部分,這些類的后台一定共享很多的代碼.
2、FileStream類
(1)、FileStream類的作用
a、這個類只要用於讀取二進制文件中的二進制數據,當然也可以使用它讀取任何文件,通常讀取二進制文件要使用FileStream
b、FileStream對象實例表示在磁盤或網絡路徑上指向文件的流,這個類提供了在文件中讀取字節的方法,但是經常使用StreamReader和StreamWriter來執行這些功能,因為FileStream操作的是字節和字節數組,而Stream類操作的是字符數據.
(2)、FileStream的構造方式
雖然.Net 提供了15種方式構建一個FileStream對象實例,但是構建一個FileStream對象實例,主要需要以下4條信息(也就是構造函數的參數):
a、要訪問的文件
b、表示如何打開文件的模式。例如,新建一個文件或者打開一個現有的文件。如果打開一個現有的文件,寫入操作是覆蓋文件,還是追加到文件的末尾。
c、表示文件的訪問方式------是只讀,只寫,還是讀寫?
d、共享訪問------表示訪問是否獨占文件.如果允許其他流同時訪問文件,則這些流是只讀還是只寫還是讀寫文件。
.Net提供的FileStream構造函數主要分為兩類
一類是構造函數的第一個參數是一個文件的完整路徑的字符串,其余的參數大致是FileMode、FileAcess、FileShare等...
一類是構造函數用老式的windows-api分格的windows句柄來處理文件...
本文主要用的是第一類,第一個參數是文件的完整路徑的字符串的這一類的構造函數,構造形式如下:
接下來主要介紹如上構造函數中的三個參數:FileMode、FileAcess、FileShare
a、FileAccess枚舉
注意,該枚舉有一個Flags特性標簽,說明FileAccess的枚舉值可以進行按位的"|"或者按位的"&"運算,關於這個不清楚,請參考C# 特性(Attribute)之Flag特性
b、FileMode枚舉
namespace System.IO { using System; using System.Runtime.InteropServices; [Serializable, ComVisible(true)] public enum FileMode { /* * 創建新文件,此操作需要 System.Security.Permissions.FileIOPermissionAccess.Write權限 * 如果文件已存在,將引發System.IO.IOException */ CreateNew = 1, /* * 指定操作系統打開現有文件,文件一旦打開,就會截斷成零字節大小 * 此操作需要 System.Security.Permissions.FileIOPermissionAccess.Write * 嘗試讀取通過Truncate打開文件將導致異常. */ Truncate = 5, /* * 指定操作系統創建新文件.如果文件已存在,那么它將被覆蓋 * 此操作需要 System.Security.Permissions.FileIOPermissionAccess.Write * FileMode.Create<=>相當於如果文件不存在則使用System.IO.FileMode.CreateNew * 如果文件存在,則使用System.IO.FileMode.Truncate. * 注:如果文件已存在但是為隱藏文件,則將引發System.UnauthorizedAccessException */ Create = 2, /* * 若文件存在,則找到文件並定位到文件結尾.如果文件不存在,則創建一個新文件 * FileMode.Append只能與FileAccess.Write一起使用.嘗試查找文件尾之前的位置會引發System.IO.IOException * 並且任何嘗試讀取的操作都會失敗並引發 System.NotSupportedException */ Append = 6, /* * 指定操作系統打開現有的文件,打開文件的能力取決於System.IO.FileAccess. * 如果文件不存在,則引發System.IO.FileNotFoundException. */ Open = 3, /* * 指定操作系統應打開文件.打開文件的能力取決於 System.IO.FileAccess 所指定的值. * 如果該文件不存在,則引發System.IO.FileNotFoundException. */ OpenOrCreate = 4, } }
c、FileShare枚舉
這個枚舉主要實現的是"文件讀寫鎖"的功能,在開發過程中,我們往往需要大量讀寫文件的操作,在本地往往能完美運行(單線程),但是項目上線后,就會出現一系列的問題.(.Net本身是多線程環境),下面簡單列舉一些在多線程環境中會出現的問題.
i、寫入一些內容到一個文件中,另一個線程/進程要讀取文件的內容時報異常,提示System.IO.IOException:文件真由另一進程使用,因此該進程無法訪問該文件.
ii、和上面i的順序相反,在對一個文件進行讀操作時,此時另一個線程/進程向該文件進行追加內容操作,也會報i中的異常.
iii、對一個文件進行簡單的讀寫操作后,想刪除文件,依然報上述的錯誤.
出現上述異常有兩種可能的原因
第一種是對文件進行完一個操作后,沒有進行資源的釋放
第二種是當進行完一個操作后,可能要到某個時刻才真正結束,亦或是需要指定某個觸發機制,操作才會真正結束
下面開始逐個解析FileShare的每個枚舉值
// Generated by .NET Reflector from C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll namespace System.IO { using System; using System.Runtime.InteropServices; [Serializable, Flags, ComVisible(true)] public enum FileShare { /* * 謝絕共享當前文件.文件關閉前,打開該文件的任何請求(由此進程或者另一進程)都將拋出異常 */ None = 0, /* * 允許隨后打開文件讀取,如果未指定此標記,則文件關閉前,,打開該文件的任何請求(由此進程或者另一進程)都將拋出異常 * 但是,即使指定了該標記,仍可能需要附加權限才能夠訪問該文件. */ Read = 1, /* * 允許隨后打開文件寫入,如果未指定此標記,則文件關閉前,,打開該文件的任何請求(由此進程或者另一進程)都將拋出異常 * 但是,即使指定了該標記,仍可能需要附加權限才能夠訪問該文件. */ Write = 2, /* * 允許隨后打開文件寫入或讀取,如果未指定此標記,則文件關閉前,,打開該文件的任何請求(由此進程或者另一進程)都將拋出異常 * 但是,即使指定了該標記,仍可能需要附加權限才能夠訪問該文件. */ ReadWrite = 3, /* * 允許隨后刪除文件 */ Delete = 4, /* * 使文件可由子進程繼承,Win32不直接支持此功能. */ Inheritable = 0x10 } }
在講解各個枚舉值之前,先提供兩個方法,用於測試,一個是寫文件的方法
static void WriteFile(FileMode filemode,FileAccess fileacess,FileShare fileshare) { var content = "測試"; FileStream fs = new FileStream(FilePath, filemode, fileacess, fileshare); var buffer=Encoding.Default.GetBytes(content); fs.Write(buffer, 0,buffer.Length); fs.Flush(); }
一個是讀文件的方法
static void ReadFile(FileAccess fileaccess,FileShare fileshare) { FileStream fs=default(FileStream); try { fs = new FileStream(FilePath, FileMode.Open, fileaccess, fileshare); var buffer = new byte[fs.Length]; fs.Position = 0; fs.Read(buffer, 0, buffer.Length); Console.WriteLine(Encoding.Default.GetString(buffer)); } catch (Exception ex) { Console.WriteLine(ex); } }
FileShare.Read
允許隨后打開文件讀取,所以有如下示例代碼:
WriteFile(FileMode.Create, FileAccess.Write, FileShare.Read);
ReadFile(FileAccess.Read, FileShare.Read);
Console.ReadKey();
按照FileShare.Read的關法用法說明,出現上述問題只有一個原因,就是"允許隨后打開文件讀取"之前的操作可能不能是WriteFile,亦或只能是ReadFile,修改代碼如下:
ReadFile(FileAccess.Read, FileShare.Read);
ReadFile(FileAccess.Read, FileShare.Read);
Console.ReadKey();
到這一步還不能確認上面的假設,現在修改代碼如下:
ReadFile(FileAccess.Read, FileShare.Read);
WriteFile(FileMode.Create, FileAccess.Write, FileShare.Read);
Console.ReadKey();
ok,通過上面的3個demo說明FileShare.Read(只讀共享)只有在連續讀取文件才有效.
FileShare.Write
允許隨后打開文件寫入,和FileShare.Read一樣,FileShare.Write(只寫共享)只有在連續寫入文件是才有效,代碼如下:
WriteFile(FileMode.Create, FileAccess.Write, FileShare.Write,"測試"); WriteFile(FileMode.Append, FileAccess.Write, FileShare.Write,"追加內容");
注:設置文件的共享方式為Write后,使用windows記事本也無法打開了.
FileShare.ReadWrite
綜合FileShare.Read和FileShare.Write的特性
FileShare.None/FileShare.Delete
有了上面的經驗,相信這兩個你也很容易的就理解了,None則為不允許后續有任何操作,而Delete則是允許你隨后進行刪除操作。
(3)、通過FileInfo構建FileStream
a、通過FileInfo的實例方法OpenRead構建FileStream,代碼如下:
FileInfo fi = new FileInfo(FilePath); FileStream fs = fi.OpenRead(); byte[] bt = new byte[fs.Length]; fs.Read(bt, 0, bt.Length); Console.WriteLine(Encoding.Default.GetString(bt));
查看OpenRead()的源代碼,代碼如下:
public FileStream OpenRead() { return new FileStream(base.FullPath, FileMode.Open, FileAccess.Read, FileShare.Read, 0x1000, false); }
現在一目了然,其實FileInfo的實例方法OpenRead其實是構建了一個FileStream的實例.
b、通過FileInfo的實例方法OpenWrite構建FileStream,代碼如下:
FileInfo fi = new FileInfo(FilePath); FileStream fs = fi.OpenWrite(); byte[] bt = Encoding.Default.GetBytes("測試"); fs.Write(bt, 0, bt.Length);
查看OpenWrite方法的源碼,如下:
public FileStream OpenWrite() { return new FileStream(base.FullPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); }
和OpenRead()一樣,它的內部原理也是構建了一個FileStream實例.
c、通過FileInfo的實例方法Open構建FileStream,代碼如下:
FileInfo fi = new FileInfo(FilePath); FileStream fs = fi.Open(FileMode.Create, FileAccess.Write, FileShare.None); byte[] bt = Encoding.Default.GetBytes("測試"); fs.Write(bt, 0, bt.Length);
和OpenRead()、OpenWrite()一樣,它的內部原理也是構建了一個FileStream實例.但是Open的參數是可配置的,不像OpenRead()、OpenWrite()
Open源碼如下:
public FileStream Open(FileMode mode) { return this.Open(mode, FileAccess.ReadWrite, FileShare.None); } public FileStream Open(FileMode mode, FileAccess access) { return this.Open(mode, access, FileShare.None); } public FileStream Open(FileMode mode, FileAccess access, FileShare share) { return new FileStream(base.FullPath, mode, access, share); }
(4)、關閉流
通過上面的內容了解了構建一個FileStream其實就是構建了一個文件流,通過查閱源碼可以發現FileStream繼承了Stream,而Stream實現了IDisposable接口,所以當我們使用完FileStream,必須顯示的調用Dispose或者使用using語句塊,來釋放資源.
FileStream提供了一個Close實例方法,來釋放資源,和完成GC的回收,Close源碼如下:
public virtual void Close() { this.Dispose(true); GC.SuppressFinalize(this); }
關閉流會釋放與它相關的資源,允許其他應用程序為同一個文件設置流,這個操作也會刷新緩沖區.在打開和關閉流之間,可以讀寫其中的數據.
(4)、通過FileStream的實例方法讀取流
a、ReadByte()
ReadByte()是讀取流數據的最簡單的方式,他從流中讀取一個字節,把結果轉換成0~255之間的整數.如果達到該流的末尾,就返回-1,ReadByte()返回的是下一個流中的下一個字節,代碼如下:
int nextByte = fs.ReadByte();
b、Read()
如果要一次讀取多個字節,就調用Read(),把特定數量的字節讀入到一個數組中,Read()返回實際讀取的字節數,如果這個數是0,就表示達到了流的末尾,代碼如下:
int nBytes=10; int nextByte = fs.Read(bt, 0, nBytes);
(5)、通過FileStream的實例方法寫入流(給文件寫入內容流)
a、WriteByte()
將一個字節寫入流中,代碼如下:
FileStream fs = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.None); byte bt = 100; fs.WriteByte(bt);
b、Write()
將一個字節數組寫入流中,代碼如下:
FileStream fs = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.None); byte[] bytes = Encoding.Default.GetBytes("測試"); fs.Write(bytes, 0, bytes.Length);
(6)、二進制文件讀取器
新建一個windows窗體應用程序,主要是將選中的文件,轉換成二進制形式.
代碼如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace BinaryFileReader { public partial class Form1 : Form { private readonly FileDialog choseOpenFileDialog = new OpenFileDialog(); private string choseFilePath; public Form1() { InitializeComponent(); menuFileOpen.Click += menuFileOpen_Click; choseOpenFileDialog.FileOk += choseOpenFileDialog_FileOk; } private void choseOpenFileDialog_FileOk(object sender, CancelEventArgs e) { choseFilePath = choseOpenFileDialog.FileName; this.Text = Path.GetFileName(choseFilePath); textBox1.Text = choseFilePath; DisplayFileName(); } private void menuFileOpen_Click(object sender, EventArgs e) { choseOpenFileDialog.ShowDialog(); } private void DisplayFileName() { int sizePerRow = 16;//每行顯示的字符數 FileStream fs = new FileStream(choseFilePath, FileMode.Open, FileAccess.Read); long nBytesToRead = fs.Length; if (nBytesToRead > 65536 / 4) nBytesToRead = 65536 / 4;//需要顯示的字符數 int nLines = (int)(nBytesToRead / sizePerRow) + 1;//總行數 string[] lines = new string[nLines]; int nBytesRead = 0; for (int i = 0; i < nLines; i++) { StringBuilder nextLine = new StringBuilder(); nextLine.Capacity = 4 * sizePerRow; for (int j = 0; j < sizePerRow; j++) { int nextByte = fs.ReadByte(); nBytesRead++; if (nextByte == 0 || nBytesRead > 65535) break; char nextChar = (char)nextByte; if (nextChar < 16)//char值小於16的字符都不是可打印字符 nextLine.Append(" x0" + string.Format("{0,1:X}", (int)nextChar)); else if (char.IsLetterOrDigit(nextChar) || char.IsPunctuation(nextChar))//字母數字十進制標點正常顯示 nextLine.Append(" " + nextChar + " "); else nextLine.Append(" x" + string.Format("{0,2:X}", (int)nextChar)); } lines[i] = nextLine.ToString(); } fs.Close(); this.textBox2.Lines = lines; } } }
3、關於流緩存的問題
如果一個C#或者.Net程序需要讀取Windows操作系統下面的一個文件,那么就可以通過文件流的方式,而如果需要讀取文件流中的兩個字節,那么該流則會把請求傳遞給Windows,注意此時Windows不會直接連接文件系統,在定位文件,並完成讀取操作。而是在一次讀取過程中,檢索文件中的一個大塊,並把該塊保存到一個內存區域即緩沖區上。(后面系列的StreamReader和StreamWriter將會用到緩沖區.)以后對流中數據的請求,就會從該緩沖區中讀取,直到讀取完該緩沖區位置。此時windows會從文件中在獲取另一個數據塊.寫入文件的方式與此相同,對於文件,操作系統會自動完成讀寫操作。
注:如果需要編寫一個流類從沒有緩存的設備中讀取數據。如果是這樣,就應從BufferedStream派生一個類,它實現一個緩沖區(但BufferedStream並不適用於應用程序頻繁的切換讀數據和寫數據的情況).