C# 文件讀寫系列二


讀取文件原則上非常簡單,但它不是通過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並不適用於應用程序頻繁的切換讀數據和寫數據的情況).

C# 文件讀寫系列三

 


免責聲明!

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



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