欧美vvv,亚洲第一成人在线,亚洲成人欧美日韩在线观看,日本猛少妇猛色XXXXX猛叫

新聞資訊

    近一直在弄D3D游戲的透視,被他弄得焦頭爛額,很多時間都花在了解決

    代碼錯誤的問題上。不過功夫不負有心人我取得了一些成果,在編寫代碼的過程

    當中參閱了大量的DirectX編程方面的資料。同時也在國外N多論壇做了一個名

    副其實的伸手黨,了解和掌握了D3D程序實現透視的一些基礎知識。在這里我把

    我自己在研究過程中的心得體會和一些經驗大體整理了一下,只是希望能夠找到

    對D3D感興趣的朋友能夠共同研究,話不多說了,下面開始咱們的教程。(說明

    一下,下面的內容都是我一個字一個字敲出來的,沒有復制粘貼)。

    在看這篇教程之前,你最好有一些基本的C++的知識,此外還需要一些基本的D

    irectX編程的基礎。我的教程呢分為兩個部分,第一部分呢主要講一些透視原

    理。第二部分主要講透視dll的制作過程。

    首先來看第一部分,主要是講解一些透視的原理,

    在講透視之前首先來說一下什么是Z軸緩沖

    Z軸緩沖就是DirectX中的成像元素之一 ,他是一個深度緩沖,用來決定

    渲染對象的遮蔽關系 ,在計算機圖形學中, 有時候通過硬件完成,有時候通過

    軟件完成。 在觀察被渲染對象的時候就出現這樣一個問題, 如果一個對象在另

    一個對象的后面,并且距離觀察者的距離較遠那么我們是看不到他的。 Z軸緩

    沖也就是深度緩沖。

    上面說的可能有點抽象我就直白的說一下吧: Z軸緩沖就是要告訴攝像機,如

    果他在一個對象的后面,就不用顯示它了 ,如果他在這個對象前面,就得顯示

    他。

    現在根據上面所說的,你可能要說把Z軸緩沖禁用來實現透視就是了。但事實情

    況并不是這樣。你會注意到,你會看到所有的東西甚至還有場景之外的東西也就

    是說你必須找到某一個對象,再選這個對象的時候來禁用Z軸緩沖。這就是:

    Strides(數據流中每個頂點所占內存的大小), NumVertices(渲染的頂點索

    引的跨度),和 PrimitiveCounts(渲染的圖元個數)。

    上面這三個就決定了透視到墻(物體)后你想看到的東西,在大部分情況下就是

    人物模型(也就是想弄人物透視)

    如果直接禁用Z軸緩沖的話,那就是全圖透視了,玩過AVA

    的都知道全圖透視在冰凍工廠是可以使用的,不過在其他地圖,或者可以說在其

    他游戲里,全圖透視效果是很差的,所以要實現人物透視就必須找到游戲中的人

    物模型表示。例如,CF的人物模型表示為44和40,AVA(戰地之王的人物模型

    標識為32),所以我們說的人物透視是必須找到人物模

    型的表示然后再禁用Z軸緩沖

    下面我們來禁用Z軸緩沖了。首先把人物模型標識來定義一下

    #define PlayerBody ( Stride == 44 || Stride == 40 )// 注意這個 " ||

    " 是“或”的意思。意思就是說"PlayerBody 等價于 44或者是40。

    //這里的Stride即為游戲中的人物模型標識,

    好了,上面的代碼是對人物模型的標識進行了定義,下面就要開始D3D9中的Dr

    awIndexedPrimitive函數來修改游戲中人物模型的渲染狀態

    下面的的代碼就是在hook the DrawIndexedPrimitive 這函數之中

    復制代碼

    1.

    LPDIRECT3DVERTEXBUFFER8 Stream_Data;

    UINT Stride = 0;

    if (m_pD3DDev->GetStreamSource(0, &Stream_Data, &Stride) == D3D_OK)

    Stream_Data->Release();

    if(Chams)//這里的chams是透視開關 if(Player)

    {

    {

    //一旦游戲中Stride的值等于所設定的值,就開始執行以下代碼: DrawIndexedPrim

    itive(Device, Type, MinIndex, NumVertices, StartIndex, PrimitiveCoun

    t);

    //在墻后面就是綠色 Device->SetRenderState( D3DRS_ZENABLE,false );

    Device->SetTexture( 0, texGreen );

    //禁用Z軸緩沖 DrawIndexedPrimitive(Device, Type, MinIndex, NumVerti

    ces, StartIndex, PrimitiveCount);

    //游戲模型在墻前面的話(就是可以看見) Device->SetRenderState( D3DRS_ZEN

    ABLE, true );

    //打開Z軸緩沖,并填充為藍色 Device->SetTexture( 0, texBlue);

    DrawIndexedPrimitive(Device, Type, MinIndex, NumVertices, Start

    Index, PrimitiveCount);

    }

    }

    我們繼續:

    如果你想知道透視上色的代碼是如何運行的,它是通過消息隊列進行操作的。.

    下面就是他的工作流程:

    如果人物要被顯示:

    給人物上色為紅色.

    啟用Z軸緩沖, 在物體前面的時候為紅色。

    禁用Z軸緩沖

    在物體后面的時候就為黃色

    Disable stencil shaders.(這個我暫時還沒搞懂)

    然后這樣不斷重復。說到重復,有人就會問了,什么是重復。我簡單地說一下吧。

    游戲在渲染人物模型的時候,是一幀一幀的進行渲染的,放在顯存中的中的每幅

    圖像從后備緩沖區到前臺緩沖區并呈現到屏幕上。打個形象的比喻,好比放幻燈

    片。速度慢了,可以看成是一張一張的,當速度很快時,就可以看成連續的畫面。

    所以上面的渲染過程至少每秒要執行24次,要不然畫面就會很卡。


    本章介紹 .NET 中輸入和輸出的基本類型,重點介紹以下主題:

    • .NET 流體系結構及其如何為跨各種 I/O 類型的讀取和寫入提供一致的編程接口
    • 用于操作磁盤上的文件和目錄的類
    • 用于壓縮、命名管道和內存映射文件的專用流。

    本章重點介紹 System.IO 命名空間(較低級別 I/O 功能的所在地)中的類型。

    流體系結構

    .NET 流體系結構以三個概念為中心:后備存儲、修飾器和適配器,如圖 所示。

    是使輸入和輸出有用的終結點,例如文件或網絡連接。準確地說,它是以下一種或兩種:

    • 可從中按順序讀取字節的源
    • 可以按順序寫入字節的目標

    但是,除非向程序員公開,否則后備存儲是沒有用的。流是用于此目的的標準 .NET 類;它公開了一組用于讀取、寫入和定位的標準方法。與所有后備數據同時存在于內存中的數組不同,流以串行方式處理數據 — 一次處理一個字節或以可管理大小的塊處理數據。因此,流可以使用少量的固定內存,而不管其后備存儲的大小如何。


    流分為兩類:

    支持存儲流

    這些硬連接到特定類型的后備存儲,例如 文件流 或 網絡流 。

    裝飾器流

    這些饋送來自另一個流,以某種方式轉換數據,例如 放氣流 or 加密流 .

    修飾器流具有以下體系結構優勢:

    • 它們將后備存儲流從需要自己實現壓縮和加密等功能中解放出來。
    • 流在裝飾時不會發生界面更改。
    • 在運行時連接裝飾器。
    • 您可以將裝飾器鏈接在一起(例如,壓縮器后跟加密)。

    后備存儲流和裝飾器流都以字節為單位專門處理。盡管這既靈活又高效,但應用程序通常在更高級別(如文本或 XML)上工作。通過使用類型化為特定格式的專用方法將流包裝在類中來彌合此差距。例如,文本讀取器公開 ReadLine 方法;XML 編寫器公開 WriteAttributes 方法。

    注意

    適配器包裝流,就像裝飾器一樣。但是,與裝飾器不同,適配器不是流;它通常完全隱藏面向字節的方法。

    總而言之,后備存儲流提供原始數據;裝飾器流提供透明的二進制轉換,例如加密;適配器提供類型化方法,用于處理更高級別的類型(如字符串和 XML)。 說明了它們的關聯。要組成鏈,只需將一個對象傳遞到另一個對象的構造函數中即可。

    使用流

    抽象 Stream 類是所有流的基礎。它定義了三個基本操作的方法和屬性:、和,以及關閉、刷新和配置超時等管理任務(請參閱)。

    流類成員

    類別

    成員

    讀數

    公共摘要布爾 CanRead { get; }


    公共抽象 int 讀取 (byte[] 緩沖區, int 偏移量, int count)


    public virtual int ReadByte();

    寫作

    public abstract bool CanWrite { get; }


    公共抽象 void 寫入 (byte[] buffer, int offset, int count);


    公共虛擬空寫字節(字節值);

    尋求

    public abstract bool CanSeek { get; }


    公共摘要多頭倉位 { get; set; }


    公共抽象空集長度(長值);


    公共摘要長長度 { 得到; }


    公共摘要長尋道(長偏移,尋源原點);

    關閉/沖洗

    公共虛擬無效 關閉();


    公共無效處置();


    公共抽象無效同花順();

    超時

    公共虛擬布爾值 CanTimeout { get; }


    public virtual int ReadTimeout { get; set; }


    public virtual int WriteTimeout { get; set; }

    其他

    公共靜態只讀流空;“空”流


    公共靜態流同步(流流);

    還有異步版本的讀取和寫入方法,它們都返回 Task s 并選擇性地接受取消令牌,以及處理 Span<T> 和 Memory<T> 類型的重載,我們在中描述。

    在以下示例中,我們使用文件流來讀取、寫入和查找:

    using System;
    using System.IO;
    
    // Create a file called test.txt in the current directory:
    using (Stream s = new FileStream ("test.txt", FileMode.Create))
    {
      Console.WriteLine (s.CanRead);       // True
      Console.WriteLine (s.CanWrite);      // True
      Console.WriteLine (s.CanSeek);       // True
    
      s.WriteByte (101);
      s.WriteByte (102);
      byte[] block = { 1, 2, 3, 4, 5 };
      s.Write (block, 0, block.Length);     // Write block of 5 bytes
    
      Console.WriteLine (s.Length);         // 7
      Console.WriteLine (s.Position);       // 7
      s.Position = 0;                       // Move back to the start
    
      Console.WriteLine (s.ReadByte());     // 101
      Console.WriteLine (s.ReadByte());     // 102
    
      // Read from the stream back into the block array:
      Console.WriteLine (s.Read (block, 0, block.Length));   // 5
    
      // Assuming the last Read returned 5, we'll be at
      // the end of the file, so Read will now return 0:
      Console.WriteLine (s.Read (block, 0, block.Length));   // 0
    }

    異步讀取或寫入只是調用 ReadAsync / WriteAsync 而不是 讀/寫 的問題,并等待表達式(我們還必須將 async 關鍵字添加到調用方法中,如所述):

    async static void AsyncDemo()
    {
      using (Stream s = new FileStream ("test.txt", FileMode.Create))
      {
        byte[] block = { 1, 2, 3, 4, 5 };
        await s.WriteAsync (block, 0, block.Length);    // Write asychronously
    
        s.Position = 0;                       // Move back to the start
    
        // Read from the stream back into the block array:
        Console.WriteLine (await s.ReadAsync (block, 0, block.Length));   // 5
      }
    }

    異步方法可以輕松編寫響應迅速且可縮放的應用程序,這些應用程序可以處理可能較慢的流(尤其是網絡流),而不會占用線程。

    注意

    為了簡潔起見,我們將繼續對本章中的大多數示例使用同步方法;但是,我們建議在涉及網絡 I/O 的大多數方案中最好使用異步讀/寫操作。

    閱讀和寫作

    流可以支持讀取和/或寫入。如果 CanWrite 返回 false ,則流是只讀的;如果 CanRead 返回 false ,則流是只寫的。

    讀取將流中的數據塊接收到數組中。它返回接收的字節數,該字節數始終小于或等于 count 參數。如果它小于計數,則表示流的末尾已到達,或者流以較小的塊形式為您提供數據(網絡流通常就是這種情況)。無論哪種情況,數組中的字節余額都將保持未寫入狀態,并保留其先前的值。

    注意

    使用 Read ,只有當該方法返回 0 時,您才能確定您已到達流的末尾。因此,如果你有一個 1,000 字節的流,以下代碼可能無法將其全部讀入內存:

    // Assuming s is a stream:
    byte[] data = new byte [1000];
    s.Read (data, 0, data.Length);

    Read 方法可以讀取 1 到 1,000 個字節之間的任何內容,使流的余額保持未讀取狀態。

    以下是讀取 1,000 字節流的正確方法:

    byte[] data = new byte [1000];
    
    // bytesRead will always end up at 1000, unless the stream is
    // itself smaller in length:
    
    int bytesRead = 0;
    int chunkSize = 1;
    while (bytesRead < data.Length && chunkSize > 0)
      bytesRead +=
        chunkSize = s.Read (data, bytesRead, data.Length - bytesRead);

    注意

    幸運的是,BinaryReader 類型提供了一種更簡單的方法來實現相同的結果:

    byte[] data = new BinaryReader (s).ReadBytes (1000);

    如果流的長度小于 1,000 字節,則返回的字節數組將反映實際的流大小。如果流是可搜索的,您可以通過將 1000 替換為 (int)s.Length 來讀取其全部內容。

    我們在中進一步描述了 BinaryReader 類型。

    ReadByte 方法更簡單:它只讀取一個字節,返回 -1 以指示流的結束。ReadByte 實際上返回一個 int 而不是一個字節,因為后者不能返回 ?1。

    Write 和 WriteByte 方法將數據發送到流。如果它們無法發送指定的字節,則會引發異常。

    注意

    在 Read 和 Write 方法中,offset 參數引用緩沖區數組中開始讀取或寫入的索引,而不是流中的位置。

    尋求

    如果 CanSeek 返回 true ,則流是可搜索的。使用可搜索的流(例如文件流),您可以查詢或修改其長度(通過調用 SetLength ),并隨時更改正在讀取或寫入的位置。屬性相對于流的開頭;但是,Seek 方法允許您相對于流的當前位置或末尾移動。

    注意

    更改文件流上的位置通常需要幾微秒。如果你在一個循環中執行數百萬次,MemoryMappedFile 類可能是比 FileStream 更好的選擇(請參閱)。

    對于不可搜索的流(例如加密流),確定其長度的唯一方法是完全讀取它。此外,如果您需要重讀上一節,則必須關閉流并從新流重新開始。

    關閉和沖洗

    流必須在使用后釋放,以釋放基礎資源,例如文件和套接字句柄。保證這一點的一種簡單方法是使用塊實例化流。通常,流遵循標準處置語義:

    • “處置”和“關閉”在功能上是相同的。
    • 重復釋放或關閉流不會導致錯誤。

    關閉修飾器流會關閉裝飾器及其后備存儲流。對于裝飾器鏈,關閉最外層的裝飾器(在鏈的頭部)會關閉整個批次。

    某些流在內部緩沖進出后備存儲的數據,以減少往返,從而提高性能(文件流就是一個很好的例子)。這意味著寫入流的數據可能不會立即到達后備存儲;當緩沖區填滿時,它可能會延遲。Flush 方法強制立即寫入任何內部緩沖數據。關閉流時會自動調用刷新,因此您無需執行以下操作:

    s.Flush(); s.Close();

    超時

    流支持讀取和寫入超時,如果 CanTimeout 返回 true。網絡流支持超時;文件和內存流則不然。對于支持超時的流,“讀取超時”和“寫入超時”屬性確定所需的超時(以毫秒為單位),其中 0 表示沒有超時。讀取和寫入方法通過引發異常來指示發生超時。

    異步讀取異步/寫入異步方法不支持超時;相反,您可以將取消令牌傳遞到這些方法中。

    線程安全

    通常,流不是線程安全的,這意味著兩個線程不能同時讀取或寫入同一流而不會出錯。Stream 類通過靜態同步方法提供了一個簡單的解決方法。此方法接受任何類型的流并返回線程安全包裝器。包裝器的工作原理是在每個讀取、寫入或查找周圍獲取獨占鎖,確保一次只有一個線程可以執行此類操作。實際上,這允許多個線程同時將數據追加到同一流 - 其他類型的活動(如并發讀取)需要額外的鎖定,以確保每個線程訪問流的所需部分。我們將在第 中全面討論線程安全性。

    注意

    從 .NET 6 開始,可以使用 RandomAccess 類執行高性能線程安全文件 I/O 操作。隨機訪問還允許您傳入多個緩沖區以提高性能。

    支持存儲流

    顯示了 .NET 提供的密鑰后備存儲流。“空流”也可以通過流的靜態空字段獲得。空流在編寫單元測試時很有用。


    在以下部分中,我們將描述 文件流 和 內存流 ;在本章的最后一節中,我們描述了 隔離存儲流 。在第中,我們將介紹 網絡流 。

    文件流

    在本節前面,我們演示了 FileStream 讀取和寫入數據字節的基本用法。現在讓我們檢查一下此類的特殊功能。

    注意

    如果您使用的是通用 Windows 平臺 (UWP),您還可以使用 Windows.Storage 中的類型執行文件 I/O。我們在 的在線增刊中對此進行了描述。

    構造文件流

    實例化 FileStream 的最簡單方法是在 File 類上使用以下靜態外觀方法之一:

    FileStream fs1 = File.OpenRead  ("readme.bin");            // Read-only
    FileStream fs2 = File.OpenWrite ("writeme.tmp");           // Write-only
    FileStream fs3 = File.Create    ("readwrite.tmp");         // Read/write

    如果文件已存在,則 OpenWrite 和 Create 的行為會有所不同。創建截斷任何現有內容;OpenWrite 保留現有內容不變,流定位為零。如果您寫入的字節數比以前在文件中的字節少,OpenWrite 會混合使用新舊內容。

    您也可以直接實例化 文件流 。它的構造函數提供對每個功能的訪問,允許您指定文件名或低級文件句柄、文件創建和訪問模式以及用于共享、緩沖和安全性的選項。下面打開一個現有文件進行讀/寫訪問,而不會覆蓋它(using 關鍵字確保在 fs 退出作用域時釋放它):

    using var fs = new FileStream ("readwrite.tmp", FileMode.Open);

    我們稍后會更仔細地研究文件模式。

    指定文件名

    文件名可以是絕對的(例如,,或者在Unix中,)或相對于當前目錄(例如,或)。可以通過靜態 Environment.CurrentDirectory 訪問或更改當前目錄。

    注意

    當程序啟動時,當前目錄可能與程序可執行文件的目錄一致,也可能不一致。因此,切勿依賴當前目錄來查找與可執行文件一起打包的其他運行時文件。

    AppDomain.CurrentDomain.BaseDirectory 返回目錄,在正常情況下,該目錄是包含程序可執行文件的文件夾。要指定相對于此目錄的文件名,可以調用 Path.Comb:

    string baseFolder = AppDomain.CurrentDomain.BaseDirectory;
    string logoPath = Path.Combine (baseFolder, "logo.jpg");
    Console.WriteLine (File.Exists (logoPath));

    文件類上的快捷方式方法

    以下靜態方法在一個步驟中將整個文件讀入內存:

    • File.ReadAllText (返回一個字符串)
    • File.ReadAllLines(返回字符串數組)
    • File.ReadAllBytes(返回一個字節數組)

    以下靜態方法一步寫入整個文件:

    • 文件.全部寫入文本
    • File.WriteAllLines
    • File.WriteAllBytes
    • File.AppendAllText(非常適合附加到日志文件)

    還有一個名為File.ReadLines的靜態方法:這類似于ReadAllLines,只是它返回一個延遲計算的IEnumerable<string>。這更有效,因為它不會一次將整個文件加載到內存中。LINQ 非常適合使用結果;下面計算長度大于 80 個字符的行數:

    int longLines = File.ReadLines ("filePath")
                        .Count (l => l.Length > 80);

    您可以通過通用命名約定 (UNC) 路徑(如 \\pic.jpg 或 \)跨 Windows 網絡進行讀寫。(若要從 macOS 或 Unix 訪問 Windows 文件共享,請按照特定于操作系統的說明將其掛載到文件系統,然后使用 C# 中的普通路徑打開它。

    指定文件模式

    所有接受文件名的 FileStream 構造函數也需要 FileMode 枚舉參數。 顯示了如何選擇 FileMode,這些選擇產生的結果類似于在 File 類上調用靜態方法。


    注意

    File.Create 和 FileMode.Create 如果在隱藏文件上使用,將引發異常。要覆蓋隱藏文件,必須刪除并重新創建它:

    File.Delete ("hidden.txt");
    using var file = File.Create ("hidden.txt");
    ...

    僅使用文件名和文件模式構造文件流會為您提供(只有一個例外)可讀/可寫流。如果您還提供了 FileAccess 參數,則可以請求降級:

    [Flags]
    public enum FileAccess { Read = 1, Write = 2, ReadWrite = 3 }

    下面返回一個只讀流,等效于調用 File.OpenRead:

    using var fs = new FileStream ("x.bin", FileMode.Open, FileAccess.Read);
    ...

    FileMode.Append是一個奇怪的:使用這種模式,你會得到一個流。若要追加讀寫支持,必須改為使用 FileMode.Open 或 FileMode.OpenOrCreate,然后查找流的末尾:

    using var fs = new FileStream ("myFile.bin", FileMode.Open);
    
    fs.Seek (0, SeekOrigin.End);
    ...

    高級文件流功能

    以下是構造文件流時可以包含的其他可選參數:

    • 一個文件共享枚舉,描述在您完成之前授予想要浸入同一文件的其他進程多少訪問權限(無、讀取 [默認]、讀寫或寫入)。
    • 內部緩沖區的大小(以字節為單位)(當前默認值為 4 KB)。
    • 指示是否遵從操作系統進行異步 I/O 的標志。
    • FileOptions 標記枚舉,用于請求操作系統加密(加密)、關閉臨時文件時自動刪除 (DeleteOnClose ) 和優化提示(隨機訪問和順序掃描)。還有一個直寫標志,要求操作系統禁用后寫緩存;這適用于事務文件或日志。基礎操作系統不支持的標志將被靜默忽略。

    使用 FileShare.ReadWrite 打開文件允許其他進程或用戶同時讀取和寫入同一文件。為了避免混亂,你們都可以同意在讀取或寫入之前鎖定文件的指定部分,使用以下方法:

    // Defined on the FileStream class:
    public virtual void Lock   (long position, long length);
    public virtual void Unlock (long position, long length);

    如果請求的部分或全部文件部分已被鎖定,則 Lock 將引發異常。

    內存流

    MemoryStream 使用數組作為后備存儲。這部分違背了擁有流的目的,因為整個后備存儲必須立即駐留在內存中。當您需要隨機訪問不可搜索的流時,MemoryStream 仍然很有用。如果您知道源流的大小可管理,則可以將其復制到內存流中,如下所示:

    var ms = new MemoryStream();
    sourceStream.CopyTo (ms);

    您可以通過調用 ToArray 將 MemoryStream 轉換為字節數組。GetBuffer 方法通過返回對基礎存儲陣列的直接引用來更有效地完成相同的工作;不幸的是,這個數組通常比流的實際長度長。

    注意

    關閉和刷新內存流是可選的。如果關閉 MemoryStream ,則無法再讀取或寫入它,但仍允許調用 ToArray 以獲取底層數據。刷新對內存流絕對不執行任何操作。

    您可以在第 的”和“中找到更多 MemoryStream 示例。

    管道流

    PipeStream 提供了一種簡單的方法,通過這種方法,一個進程可以通過操作系統的協議與另一個進程進行通信。管道有兩種:

    匿名管道(更快)

    允許同一臺計算機上的父進程和子進程之間進行單向通信

    命名管道(更靈活)

    允許網絡上的任意進程或不同計算機上的任意進程之間進行雙向通信

    管道適用于單臺計算機上的進程間通信 (IPC):它不依賴于網絡傳輸,這意味著沒有網絡協議開銷,并且防火墻沒有問題。

    注意

    管道是基于流的,因此一個進程等待接收一系列字節,而另一個進程發送它們。另一種方法是讓進程通過共享內存塊進行通信;我們在中描述了如何執行此操作。

    PipeStream 是一個抽象類,具有四個具體的子類型。兩個用于匿名管道,另外兩個用于命名管道:

    匿名管道

    AnonymousPipeServerStream 和 AnonymousPipeClientStream

    命名管道

    NamedPipeServerStream 和 NamedPipeClientStream

    命名管道更易于使用,因此我們首先介紹它們。

    命名管道

    使用命名管道,各方通過同名管道進行通信。該協議定義了兩個不同的角色:客戶端和服務器。客戶端和服務器之間的通信發生如下:

    • 服務器實例化 NamedPipeServerStream,然后調用 WaitForConnection 。
    • 客戶端實例化 NamedPipeClientStream,然后調用 Connect(具有可選的超時)。

    然后,雙方讀取和寫入流以進行通信。

    下面的示例演示發送單個字節 (100) 然后等待接收單個字節的服務器:

    using var s = new NamedPipeServerStream ("pipedream");
    
    s.WaitForConnection();
    s.WriteByte (100);                // Send the value 100.
    Console.WriteLine (s.ReadByte());

    下面是相應的客戶端代碼:

    using var s = new NamedPipeClientStream ("pipedream");
    
    s.Connect();
    Console.WriteLine (s.ReadByte());
    s.WriteByte (200);                 // Send the value 200 back.

    默認情況下,命名管道流是雙向的,因此任何一方都可以讀取或寫入其流。這意味著客戶端和服務器必須就某些協議達成一致來協調他們的操作,這樣雙方就不會同時發送或接收。

    還需要就每次傳輸的長度達成一致。在這方面,我們的例子是微不足道的,因為我們在每個方向上只反彈了一個字節。為了幫助處理長度超過一個字節的消息,管道提供了傳輸模式(僅限 Windows)。如果啟用此功能,則調用 Read 的一方可以通過檢查 IsMessageComplete 屬性來知道消息何時完成。為了演示,我們首先編寫一個幫助程序方法,該方法從啟用消息的 PipeStream 讀取整條消息 — 換句話說,讀取直到 IsMessageComplete 為真:

    static byte[] ReadMessage (PipeStream s)
    {
      MemoryStream ms = new MemoryStream();
      byte[] buffer = new byte [0x1000];      // Read in 4 KB blocks
    
      do    { ms.Write (buffer, 0, s.Read (buffer, 0, buffer.Length)); }
      while (!s.IsMessageComplete);
    
      return ms.ToArray();
    }

    (要使此異步,請將“s.Read ”替換為“await s.ReadAsync ”。

    注意

    您無法僅通過等待 Read 返回 0 來確定 PipeStream 是否已完成消息的讀取。這是因為,與大多數其他流類型不同,管道流和網絡流沒有明確的終點。相反,它們在消息傳輸之間暫時“干涸”。

    現在我們可以激活消息傳輸模式。在服務器上,這是通過在構造流時指定 PipeTransmission Mode.Message 來完成的:

    using var s = new NamedPipeServerStream ("pipedream", PipeDirection.InOut,
                                              1, PipeTransmissionMode.Message);
    
    s.WaitForConnection();
    
    byte[] msg = Encoding.UTF8.GetBytes ("Hello");
    s.Write (msg, 0, msg.Length);
    
    Console.WriteLine (Encoding.UTF8.GetString (ReadMessage (s)));

    在客戶端上,我們通過在調用 Connect 后設置 ReadMode 來激活消息傳輸模式:

    using var s = new NamedPipeClientStream ("pipedream");
    
    s.Connect();
    s.ReadMode = PipeTransmissionMode.Message;
    
    Console.WriteLine (Encoding.UTF8.GetString (ReadMessage (s)));
    
    byte[] msg = Encoding.UTF8.GetBytes ("Hello right back!");
    s.Write (msg, 0, msg.Length);

    注意

    消息模式僅在 Windows 上受支持。其他平臺拋出 PlatformNotSupportedException 。

    匿名管道

    匿名管道在父進程和子進程之間提供單向通信流。匿名管道不使用系統范圍的名稱,而是通過專用句柄進行調入。

    與命名管道一樣,存在不同的客戶端和服務器角色。但是,通信系統略有不同,其過程如下:

    1. 服務器實例化一個匿名管道服務器流,提交到In或Out的PipeDirection。
    2. 服務器調用 GetClientHandleAsString 來獲取管道的標識符,然后將其傳遞給客戶端(通常在啟動子進程時作為參數)。
    3. 子進程實例化一個 AnonymousPipeClientStream,指定相反的 PipeDirection。
    4. 服務器通過調用 DisposeLocalCopyOfClientHandle 釋放在步驟 2 中生成的本地句柄。
    5. 父進程和子進程通過閱讀/寫流進行通信。

    由于匿名管道是單向的,因此服務器必須創建兩個管道進行雙向通信。下面的Console程序創建兩個管道(輸入和輸出),然后啟動一個子進程。然后,它向子進程發送一個字節,并接收一個字節作為返回:

    class Program
    {
      static void Main (string[] args)
      {
        if (args.Length == 0)
          // No arguments signals server mode
          AnonymousPipeServer();
        else
          // We pass in the pipe handle IDs as arguments to signal client mode
          AnonymousPipeClient (args [0], args [1]);
      }
    
      static void AnonymousPipeClient (string rxID, string txID)
      {
        using (var rx = new AnonymousPipeClientStream (PipeDirection.In, rxID))
        using (var tx = new AnonymousPipeClientStream (PipeDirection.Out, txID))
        {
          Console.WriteLine ("Client received: " + rx.ReadByte ());
          tx.WriteByte (200);
        }
      }
    
      static void AnonymousPipeServer ()
      {
        using var tx = new AnonymousPipeServerStream (
                         PipeDirection.Out, HandleInheritability.Inheritable);
        using var rx = new AnonymousPipeServerStream (
                         PipeDirection.In, HandleInheritability.Inheritable);
    
        string txID = tx.GetClientHandleAsString ();
        string rxID = rx.GetClientHandleAsString ();
    
        // Create and start up a child process.
        // We'll use the same Console executable, but pass in arguments:
        string thisAssembly = Assembly.GetEntryAssembly().Location;
        string thisExe = Path.ChangeExtension (thisAssembly, ".exe");
        var args = $"{txID} {rxID}";
        var startInfo = new ProcessStartInfo (thisExe, args);
    
        startInfo.UseShellExecute = false;       // Required for child process
        Process p = Process.Start (startInfo);
    
        tx.DisposeLocalCopyOfClientHandle ();    // Release unmanaged
        rx.DisposeLocalCopyOfClientHandle ();    // handle resources.
    
        tx.WriteByte (100);    // Send a byte to the child process
    
        Console.WriteLine ("Server received: " + rx.ReadByte ());
    
        p.WaitForExit ();
      }
    }

    與命名管道一樣,客戶端和服務器必須協調它們的發送和接收,并就每次傳輸的長度達成一致。遺憾的是,匿名管道不支持消息模式,因此您必須實現自己的消息長度協議協議。一種解決方案是在每次傳輸的前四個字節中發送一個整數值,該值定義要遵循的消息的長度。類提供了在整數和四個字節的數組之間進行轉換的方法。

    緩沖流

    BufferedStream 裝飾或包裝具有緩沖功能的另一個流,它是 .NET 中的許多修飾器流類型之一,所有這些類型如圖 所示。



    緩沖通過減少到后備存儲的往返次數來提高性能。以下是我們如何將文件流包裝在 20 KB 的緩沖流中:

    // Write 100K to a file:
    File.WriteAllBytes ("myFile.bin", new byte [100000]);
    
    using FileStream fs = File.OpenRead ("myFile.bin");
    using BufferedStream bs = new BufferedStream (fs, 20000);  //20K buffer
    
    bs.ReadByte();
    Console.WriteLine (fs.Position);         // 20000

    在此示例中,由于預讀緩沖,基礎流在僅讀取一個字節后前進 20,000 字節。我們可以在FileStream再次被擊中之前再調用ReadByte19,999次。

    將 BufferedStream 耦合到 FileStream (如本例所示)的價值有限,因為 FileStream 已經具有內置的緩沖功能。它的唯一用途可能是擴大已經構建的文件流上的緩沖區。

    關閉緩沖流會自動關閉基礎后備存儲流。

    流適配器

    流僅以字節為單位處理;若要讀取或寫入字符串、整數或 XML 元素等數據類型,必須插入適配器。下面是 .NET 提供的功能:

    文本適配器(用于字符串和字符數據)

    文本閱讀器 , 文本編寫器

    StreamReader , StreamWriter

    StringReader , StringWriter

    二進制適配器(用于基元類型,如 int、bool、string 和 float)

    BinaryReader , BinaryWriter

    XML 適配器(中介紹))

    XmlReader , XmlWriter

    說明了這些類型之間的關系。


    文本適配器

    TextReader 和 TextWriter 是專門處理字符和字符串的適配器的抽象基類。每個在 .NET 中都有兩個常規用途實現:

    流閱讀器 / 流寫入器

    使用 Stream 作為其原始數據存儲,將流的字節轉換為字符或字符串

    StringReader / StringWriter

    使用內存中的字符串實現文本閱讀器/文本編寫器

    按類別列出了 TextReader 的成員。Peek 返回流中的下一個字符,而不前進位置。如果流的末尾,Peek 和 Read 的零參數版本都返回 ?1;否則,它們返回一個可以直接轉換為 char 的整數。接受 char[] 緩沖區的 Read 重載在功能上與 ReadBlock 方法相同。ReadLine 讀取,直到依次到達 CR(字符 13)或 LF(字符 10)或 CR+LF 對。然后,它返回一個字符串,丟棄 CR/LF 字符。

    文本閱讀器成員

    類別

    成員

    讀取一個字符

    public virtual int Peek();將結果轉換為字符


    公共虛擬 int 讀取();將結果轉換為字符

    讀取許多字符

    公共虛擬 int 讀取 (char[] 緩沖區, int index, int count);


    public virtual int ReadBlock (char[] buffer, int index, int count);


    公共虛擬字符串 ReadLine();


    公共虛擬字符串 ReadToEnd();

    關閉

    公共虛擬無效 關閉();


    公共無效處置();與關閉相同

    其他

    公共靜態只讀文本讀取器空;


    公共靜態文本閱讀器同步(文本閱讀器閱讀器);

    注意

    Environment.NewLine 返回當前操作系統的換行符序列。

    在Windows上,這是“\r\n”(想想“ReturN”),并且松散地模仿機械打字機:CR(字符13)后跟LF(字符10)。顛倒順序,你會得到兩個新行或沒有!

    在 Unix 和 macOS 上,它只是 “\n” .

    TextWriter具有類似的寫入方法,如所示。此外,還會重載 Write 和 WriteLine 方法,以接受每個基元類型以及對象類型。這些方法只是在傳入的任何內容上調用 ToString 方法(可選地通過調用方法或構造 TextWriter 時指定的 IFormatProvider)。

    文本編寫器成員

    類別

    成員

    寫一個字符

    公共虛擬空 寫入(字符值);

    編寫許多字符

    公共虛擬空 寫入(字符串值);


    public virtual void Write (char[] buffer, int index, int count);


    public virtual void Write (string format, params object[] arg);


    公共虛擬空寫行(字符串值);

    關閉和沖洗

    公共虛擬無效 關閉();


    公共無效處置();與關閉相同


    公共虛擬虛空同花順();

    格式化和編碼

    public virtual IFormatProvider FormatProvider { get; }


    公共虛擬字符串換行符 { get; set; }


    公共摘要編碼編碼 { get; }

    其他

    公共靜態只讀文本編寫器空;


    公共靜態文本編寫器同步(文本編寫器編寫器);

    WriteLine 只是將給定的文本附加到 Environment.NewLine 。您可以通過 NewLine 屬性更改此設置(這對于與 Unix 文件格式的互操作性非常有用)。

    注意

    與Stream一樣,TextReader和TextWriter提供了基于任務的異步版本的讀/寫方法。

    StreamReader 和 StreamWriter

    在下面的示例中,StreamWriter 將兩行文本寫入文件,然后 StreamReader 讀回該文件:

    using (FileStream fs = File.Create ("test.txt"))
    using (TextWriter writer = new StreamWriter (fs))
    {
      writer.WriteLine ("Line1");
      writer.WriteLine ("Line2");
    }
    
    using (FileStream fs = File.OpenRead ("test.txt"))
    using (TextReader reader = new StreamReader (fs))
    {
      Console.WriteLine (reader.ReadLine());       // Line1
      Console.WriteLine (reader.ReadLine());       // Line2
    }

    由于文本適配器經常與文件耦合,因此 File 類提供了靜態方法 CreateText 、AppendText 和 OpenText 來縮短該過程:

    using (TextWriter writer = File.CreateText ("test.txt"))
    {
      writer.WriteLine ("Line1");
      writer.WriteLine ("Line2");
    }
    
    using (TextWriter writer = File.AppendText ("test.txt"))
      writer.WriteLine ("Line3");
    
    using (TextReader reader = File.OpenText ("test.txt"))
      while (reader.Peek() > -1)
        Console.WriteLine (reader.ReadLine());     // Line1
                                                   // Line2
                                                   // Line3

    這也說明了如何測試文件的結尾(即讀取器。躲貓貓() )。另一種選擇是閱讀直到閱讀器。讀取行返回空值。

    您還可以讀取和寫入其他類型的類型(如整數),但由于 TextWriter 在您的類型上調用 ToString,因此在讀回字符串時必須對其進行分析:

    using (TextWriter w = File.CreateText ("data.txt"))
    {
      w.WriteLine (123);          // Writes "123"
      w.WriteLine (true);         // Writes the word "true"
    }
    
    using (TextReader r = File.OpenText ("data.txt"))
    {
      int myInt = int.Parse (r.ReadLine());     // myInt == 123
      bool yes = bool.Parse (r.ReadLine());     // yes == true
    }

    字符編碼

    TextReader 和 TextWriter 本身只是抽象類,與流或后備存儲沒有連接。但是,StreamReader 和 StreamWriter 類型連接到面向字節的基礎流,因此它們必須在字符和字節之間進行轉換。它們通過 命名空間中的編碼類執行此操作,該類可在構造 StreamReader 或 StreamWriter 時選擇。如果選擇“無”,則使用默認的 UTF-8 編碼。

    注意

    如果顯式指定編碼,默認情況下,StreamWriter 會將前綴寫入流的開頭以標識編碼。這通常是不可取的,您可以通過按如下方式構造編碼來防止它:

    var encoding = new UTF8Encoding (
      encoderShouldEmitUTF8Identifier:false,
      throwOnInvalidBytes:true);

    第二個參數告訴 StreamWriter(或 StreamReader )在遇到沒有有效字符串轉換的字節時引發異常,如果未指定編碼,則與其默認行為匹配。

    最簡單的編碼是 ASCII,因為每個字符由一個字節表示。ASCII 編碼將 Unicode 集的前 127 個字符映射到其單個字節中,涵蓋您在美式鍵盤上看到的內容。大多數其他字符(包括專用符號和非英語字符)無法表示,并轉換為□字符。默認的 UTF-8 編碼可以映射所有分配的 Unicode 字符,但它更復雜。前 127 個字符編碼為單個字節,以實現 ASCII 兼容性;其余字符編碼為可變數量的字節(最常見的是兩個或三個)。請考慮以下事項:

    using (TextWriter w = File.CreateText ("but.txt"))    // Use default UTF-8
      w.WriteLine ("but-");                               // encoding.
    
    using (Stream s = File.OpenRead ("but.txt"))
      for (int b; (b = s.ReadByte()) > -1;)
        Console.WriteLine (b);

    單詞“but”后面不是股票標準連字符,而是更長的長破折號 (—) 字符 U+2014。這是不會給您的圖書編輯器帶來麻煩的!讓我們檢查一下輸出:

    98     // b
    117    // u
    116    // t
    226    // em dash byte 1       Note that the byte values
    128    // em dash byte 2       are >= 128 for each part
    148    // em dash byte 3       of the multibyte sequence.
    13     // <CR>
    10     // <LF>

    由于長破折號位于 Unicode 集的前 127 個字符之外,因此以 UTF-8 編碼需要多個字節(在本例中為三個)。UTF-8 對西方字母表很有效,因為大多數常用字符只占用一個字節。它還可以通過忽略 127 以上的所有字節輕松降級到 ASCII。它的缺點是在流中查找很麻煩,因為字符的位置與其在流中的字節位置不對應。另一種方法是 UTF-16(在編碼類中僅標記為“Unicode”)。以下是我們如何用 UTF-16 編寫相同的字符串:

    using (Stream s = File.Create ("but.txt"))
    using (TextWriter w = new StreamWriter (s, Encoding.Unicode))
      w.WriteLine ("but-");
    
    foreach (byte b in File.ReadAllBytes ("but.txt"))
      Console.WriteLine (b);

    這是輸出:

    255    // Byte-order mark 1
    254    // Byte-order mark 2
    98     // 'b' byte 1
    0      // 'b' byte 2
    117    // 'u' byte 1
    0      // 'u' byte 2
    116    // 't' byte 1
    0      // 't' byte 2
    20     // '--' byte 1
    32     // '--' byte 2
    13     // <CR> byte 1
    0      // <CR> byte 2
    10     // <LF> byte 1
    0      // <LF> byte 2

    從技術上講,UTF-16 每個字符使用兩個或四個字節(分配或保留了近一百萬個 Unicode 字符,因此兩個字節并不總是足夠的)。但是,由于 C# 字符類型本身只有 16 位寬,因此 UTF-16 編碼將始終為每個 .NET 字符使用兩個字節。這樣可以輕松跳轉到流中的特定字符索引。

    UTF-16 使用雙字節前綴來標識字節對是按“小端序”還是“大端序”順序(最低有效字節在前還是最高有效字節在前)寫入。默認的小端順序是基于 Windows 的系統的標準順序。

    StringReader 和 StringWriter

    StringReader 和 StringWriter 適配器根本不包裝流;相反,它們使用字符串或 StringBuilder 作為基礎數據源。這意味著不需要字節轉換 — 事實上,這些類不會執行任何您無法通過字符串或 StringBuilder 與索引變量結合使用來實現的操作。不過,它們的優勢在于它們與StreamReader / StreamWriter共享一個基類。例如,假設我們有一個包含XML的字符串,并希望使用XmlReader解析它。方法接受下列方法之一:

    • 一個 URI
    • 一個流
    • 文本閱讀器

    那么,我們如何對字符串進行XML解析呢?因為StringReader是TextReader的一個子類,我們很幸運。我們可以實例化并傳入一個 StringReader,如下所示:

    XmlReader r = XmlReader.Create (new StringReader (myString));

    二進制適配器

    BinaryReader 和 BinaryWriter 讀寫原生數據類型:布爾、字節、字符、十進制、浮點數、雙精度、短精度、整數、長整型、字節數、ushort 、uint 和 ulong,以及原始數據類型的字符串和數組。

    與StreamReader和StreamWriter不同,二進制適配器有效地存儲基元數據類型,因為它們在內存中表示。因此,int 使用四個字節;雙精度使用八個字節。字符串是通過文本編碼寫入的(如StreamReader和StreamWriter),但以長度為前綴,以便可以在不需要特殊分隔符的情況下讀回一系列字符串。

    假設我們有一個簡單的類型,定義如下:

    public class Person
    {
      public string Name;
      public int    Age;
      public double Height;
    }

    我們可以將以下方法添加到 Person 中,以使用二進制適配器將其數據保存到/從流中加載:

    public void SaveData (Stream s)
    {
      var w = new BinaryWriter (s);
      w.Write (Name);
      w.Write (Age);
      w.Write (Height);
      w.Flush();         // Ensure the BinaryWriter buffer is cleared.
                         // We won't dispose/close it, so more data
    }                    // can be written to the stream.
    
    public void LoadData (Stream s)
    {
      var r = new BinaryReader (s);
      Name   = r.ReadString();
      Age    = r.ReadInt32();
      Height = r.ReadDouble();
    }

    BinaryReader 還可以讀取字節數組。下面讀取可搜索流的全部內容:

    byte[] data = new BinaryReader (s).ReadBytes ((int) s.Length);

    這比直接從流中讀取更方便,因為它不需要循環來確保已讀取所有數據。

    關閉和釋放流適配器

    拆卸下游適配器時,您有四種選擇:

    1. 僅關閉適配器。
    2. 關閉適配器,然后關閉流。
    3. (作家)刷新適配器,然后關閉流。
    4. (致讀者)僅關閉流。

    注意

    關閉和處置是適配器的同義詞,就像它們與流一樣。

    選項 1 和 2 在語義上是相同的,因為關閉適配器會自動關閉基礎流。每當使用語句嵌套時,您都會隱式采用選項 2:

    using (FileStream fs = File.Create ("test.txt"))
    using (TextWriter writer = new StreamWriter (fs))
      writer.WriteLine ("Line");

    由于嵌套從內向外處置,因此首先關閉適配器,然后關閉流。此外,如果在適配器的構造函數中引發異常,流仍會關閉。嵌套的 using 語句很難出錯!

    注意

    切勿在關閉或刷新流的寫入器之前關閉流 - 您將截斷適配器中緩沖的任何數據。

    選項 3 和 4 之所以有效,是因為適配器屬于一次性對象的不尋常類別。您可能選擇不釋放適配器的一個示例是,當您完成適配器,但您希望將基礎流保持打開狀態以供后續使用時:

    using (FileStream fs = new FileStream ("test.txt", FileMode.Create))
    {
      StreamWriter writer = new StreamWriter (fs);
      writer.WriteLine ("Hello");
      writer.Flush();
    
      fs.Position = 0;
      Console.WriteLine (fs.ReadByte());
    }

    在這里,我們寫入文件,重新定位流,然后在關閉流之前讀取第一個字節。如果我們處置了 StreamWriter,它也會關閉底層的 FileStream,導致后續讀取失敗。附帶條件是我們調用 Flush 來確保 StreamWriter 的緩沖區被寫入底層流。

    注意

    流適配器(及其可選的處置語義)不實現終結器調用 Dispose 的擴展處置模式。這允許廢棄的適配器在垃圾回收器趕上時逃避自動處置。

    StreamReader / StreamWriter上還有一個構造函數,指示它在處置后保持流打開。因此,我們可以重寫前面的示例,如下所示:

    using (var fs = new FileStream ("test.txt", FileMode.Create))
    {
      using (var writer = new StreamWriter (fs, new UTF8Encoding (false, true),
                                           0x400, true))
        writer.WriteLine ("Hello");
    
      fs.Position = 0;
      Console.WriteLine (fs.ReadByte());
      Console.WriteLine (fs.Length);
    }

    壓縮流

    System.IO.Compression 命名空間中提供了兩個通用壓縮流: DeflateStream 和 GZipStream 。兩者都使用類似于 ZIP 格式的流行壓縮算法。它們的不同之處在于GZipStream在開始和結束時編寫了一個額外的協議 - 包括一個CRC來檢測錯誤。GZipStream還符合其他軟件認可的標準。

    .NET還包括BrotliStream,它實現了壓縮算法。BrotliStream比DeflateStream和GZipStream慢10倍以上,但實現了更好的壓縮比。(性能影響僅適用于壓縮 — 解壓縮性能非常好。

    所有三個流都允許讀取和寫入,但有以下條件:

    • 壓縮流。
    • 解壓縮時,您始終從流。

    DeflateStream、GZipStream 和 BrotliStream 是裝飾器;它們壓縮或解壓縮您在構造中提供的另一個流中的數據。在下面的示例中,我們使用 FileStream 作為后備存儲來壓縮和解壓縮一系列字節:

    using (Stream s = File.Create ("compressed.bin"))
    using (Stream ds = new DeflateStream (s, CompressionMode.Compress))
      for (byte i = 0; i < 100; i++)
        ds.WriteByte (i);
    
    using (Stream s = File.OpenRead ("compressed.bin"))
    using (Stream ds = new DeflateStream (s, CompressionMode.Decompress))
      for (byte i = 0; i < 100; i++)
        Console.WriteLine (ds.ReadByte());     // Writes 0 to 99

    使用DeflateStream,壓縮文件是102字節:比原始文件略大(BrotliStream會將其壓縮為73字節)。壓縮對于“密集”、非重復的二進制數據效果不佳(最糟糕的是加密數據,它在設計上缺乏規律性)。它適用于大多數文本文件;在下一個示例中,我們使用 算法壓縮和解壓縮由從一個小句子中隨機選擇的 1,000 個單詞組成的文本流。這還演示了鏈接后備存儲流、裝飾器流和適配器( 中本章開頭所述)以及異步方法的使用:

    string[] words = "The quick brown fox jumps over the lazy dog".Split();
    Random rand = new Random (0);   // Give it a seed for consistency
    
    using (Stream s = File.Create ("compressed.bin"))
    using (Stream ds = new BrotliStream (s, CompressionMode.Compress))
    using (TextWriter w = new StreamWriter (ds))
      for (int i = 0; i < 1000; i++)
        await w.WriteAsync (words [rand.Next (words.Length)] + " ");
    
    Console.WriteLine (new FileInfo ("compressed.bin").Length);      // 808
    
    using (Stream s = File.OpenRead ("compressed.bin"))
    using (Stream ds = new BrotliStream (s, CompressionMode.Decompress))
    using (TextReader r = new StreamReader (ds))
      Console.Write (await r.ReadToEndAsync());  // Output below:
    
    lazy lazy the fox the quick The brown fox jumps over fox over fox The
    brown brown brown over brown quick fox brown dog dog lazy fox dog brown
    over fox jumps lazy lazy quick The jumps fox jumps The over jumps dog...

    在這種情況下,BrotliStream 可以有效地壓縮到 808 字節,每個字不到一個字節。(為了進行比較,DeflateStream 將相同的數據壓縮到 885 字節。

    在內存中壓縮

    有時,您需要完全壓縮內存。以下是使用MemoryStream來實現此目的的方法:

    byte[] data = new byte[1000];          // We can expect a good compression
                                           // ratio from an empty array!
    var ms = new MemoryStream();
    using (Stream ds = new DeflateStream (ms, CompressionMode.Compress))
      ds.Write (data, 0, data.Length);
    
    byte[] compressed = ms.ToArray();
    Console.WriteLine (compressed.Length);       // 11
    
    // Decompress back to the data array:
    ms = new MemoryStream (compressed);
    using (Stream ds = new DeflateStream (ms, CompressionMode.Decompress))
      for (int i = 0; i < 1000; i += ds.Read (data, i, 1000 - i));

    圍繞 DeflateStream 的 using 語句以教科書的方式關閉它,刷新過程中任何未寫入的緩沖區。這也會關閉它包裝的 MemoryStream,這意味著我們必須調用 ToArray 來提取它的數據。

    下面是避免關閉 MemoryStream 并使用異步讀取和寫入方法的替代方法:

    byte[] data = new byte[1000];
    
    MemoryStream ms = new MemoryStream();
    using (Stream ds = new DeflateStream (ms, CompressionMode.Compress, true))
      await ds.WriteAsync (data, 0, data.Length);
    
    Console.WriteLine (ms.Length);             // 113
    ms.Position = 0;
    using (Stream ds = new DeflateStream (ms, CompressionMode.Decompress))
      for (int i = 0; i < 1000; i += await ds.ReadAsync (data, i, 1000 - i));

    發送到 DeflateStream 的構造函數的附加標志指示它不要遵循通常的協議,即處理底層流。換句話說,MemoryStream保持打開狀態,允許我們將其定位回零并重新讀取。

    Unix gzip 文件壓縮

    GZipStream的壓縮算法在Unix系統上作為一種文件壓縮格式很流行。每個源文件都壓縮到具有擴展名的單獨目標文件中。

    以下方法執行 Unix 命令行 gzip 和 gunzip 實用程序的工作:

    async Task GZip (string sourcefile, bool deleteSource = true)
    {
      var gzipfile = $"{sourcefile}.gz";
      if (File.Exists (gzipfile))
        throw new Exception ("Gzip file already exists");
    
      // Compress
      using (FileStream inStream = File.Open (sourcefile, FileMode.Open))
      using (FileStream outStream = new FileStream (gzipfile, FileMode.CreateNew))
      using (GZipStream gzipStream = 
        new GZipStream (outStream, CompressionMode.Compress))
        await inStream.CopyToAsync (gzipStream); 
      
      if (deleteSource) File.Delete(sourcefile);
    }
    
    async Task GUnzip (string gzipfile, bool deleteGzip = true)
    {
      if (Path.GetExtension (gzipfile) != ".gz") 
        throw new Exception ("Not a gzip file");
    
      var uncompressedFile = gzipfile.Substring (0, gzipfile.Length - 3);
      if (File.Exists (uncompressedFile)) 
        throw new Exception ("Destination file already exists");
    
      // Uncompress
      using (FileStream uncompressToStream = 
             File.Open (uncompressedFile, FileMode.Create))
      using (FileStream zipfileStream = File.Open (gzipfile, FileMode.Open))
      using (var unzipStream = 
             new GZipStream (zipfileStream, CompressionMode.Decompress))
        await unzipStream.CopyToAsync (uncompressToStream);
        
      if (deleteGzip) File.Delete (gzipfile);
    }

    下面壓縮文件:

    await GZip ("/tmp/myfile.txt");      // Creates /tmp/myfile.txt.gz

    下面解壓縮它:

    await GUnzip ("/tmp/myfile.txt.gz")  // Creates /tmp/myfile.txt

    使用 ZIP 文件

    System.IO.Compression 中的 ZipArchive 和 ZipFile 類支持 ZIP 壓縮格式。ZIP格式相對于DeflateStream和GZipStream的優勢在于它充當多個文件的容器,并與使用Windows資源管理器創建的ZIP文件兼容。

    注意

    ZipArchive 和 ZipFile 在 Windows 和 Unix 中都可以工作;但是,該格式在 Windows 中最流行。在Unix中,格式作為多個文件的容器更受歡迎。您可以使用第三方庫(如 SharpZipLib)讀取/寫入文件。

    ZipArchive 處理流,而 ZipFile 解決處理文件的更常見情況。(ZipFile 是 ZipArchive 的靜態幫助程序類。

    ZipFile 的 CreateFromDirectory 方法將指定目錄中的所有文件添加到一個 ZIP 文件中:

    ZipFile.CreateFromDirectory (@"d:\MyFolder", @"d:\archive.zip");

    ExtractToDirectory 執行相反的操作,將 ZIP 文件解壓縮到目錄中:

    ZipFile.ExtractToDirectory (@"d:\archive.zip", @"d:\MyFolder");

    壓縮時,您可以指定是否優化文件大小或速度,以及是否在存檔中包含源目錄的名稱。在我們的示例中啟用后一個選項將在存檔中創建一個名為 的子目錄,壓縮文件將進入該子目錄。

    ZipFile 有一個用于讀取/寫入單個條目的 Open 方法。這將返回一個 ZipArchive 對象(您也可以通過使用 Stream 對象實例化 ZipArchive 來獲取該對象)。調用 Open 時,必須指定文件名并指示是要讀取、創建還是更新存檔。然后,您可以通過 Entries 屬性枚舉現有條目,或者通過調用 來查找特定文件:

    using (ZipArchive zip = ZipFile.Open (@"d:\zz.zip", ZipArchiveMode.Read))
    
      foreach (ZipArchiveEntry entry in zip.Entries)
        Console.WriteLine (entry.FullName + " " + entry.Length);

    ZipArchiveEntry 還有一個 Delete 方法、一個 ExtractToFile 方法(這實際上是 ZipFileExtensions 類中的一個擴展方法)和一個返回可讀/可寫 Stream 的 Open 方法。您可以通過在 ZipArchive 上調用 CreateEntry(或 CreateEntryFromFile 擴展方法)來創建新條目。下面在歸檔文件中名為 的目錄結構下創建歸檔 ,并向其添加 :

    byte[] data = File.ReadAllBytes (@"d:\foo.dll"); 
    using (ZipArchive zip = ZipFile.Open (@"d:\zz.zip", ZipArchiveMode.Update))
      zip.CreateEntry (@"bin\X64\foo.dll").Open().Write (data, 0, data.Length);

    您可以通過使用 MemoryStream 構建 ZipArchive 來完全在內存中執行相同的操作。

    文件和目錄操作

    System.IO 命名空間提供了一組用于執行“實用程序”文件和目錄操作的類型,例如復制和移動、創建目錄以及設置文件屬性和權限。對于大多數功能,您可以在兩個類之間進行選擇,一個提供靜態方法,另一個提供實例方法:

    靜態類

    文件和目錄

    實例方法類(使用文件或目錄名稱構造)

    文件信息和目錄信息

    此外,還有一個名為 路徑 的靜態類 .這對文件或目錄沒有任何作用;相反,它為文件名和目錄路徑提供了字符串操作方法。路徑還有助于處理臨時文件。

    文件類

    File 是一個靜態類,其方法都接受文件名。文件名可以是相對于當前目錄的文件名,也可以是目錄的完全限定名。以下是它的方法(所有公共和靜態):

    bool Exists (string path);      // Returns true if the file is present
    
    void Delete  (string path);
    void Copy    (string sourceFileName, string destFileName);
    void Move    (string sourceFileName, string destFileName);
    void Replace (string sourceFileName, string destinationFileName,
                                         string destinationBackupFileName);
    
    FileAttributes GetAttributes (string path);
    void SetAttributes           (string path, FileAttributes fileAttributes);
    
    void Decrypt (string path);
    void Encrypt (string path);
    
    DateTime GetCreationTime   (string path);      // UTC versions are
    DateTime GetLastAccessTime (string path);      // also provided.
    DateTime GetLastWriteTime  (string path);
    
    void SetCreationTime   (string path, DateTime creationTime);
    void SetLastAccessTime (string path, DateTime lastAccessTime);
    void SetLastWriteTime  (string path, DateTime lastWriteTime);
    
    FileSecurity GetAccessControl (string path);
    FileSecurity GetAccessControl (string path,
                                   AccessControlSections includeSections);
    void SetAccessControl (string path, FileSecurity fileSecurity);

    如果目標文件已存在,Move 將引發異常;替換不。這兩種方法都允許重命名文件以及移動到另一個目錄。

    如果文件標記為只讀,則刪除會引發未經授權的訪問異常;你可以通過調用 GetAttributes 來提前判斷這一點。如果操作系統拒絕對進程執行該文件的刪除權限,它還會引發該異常。以下是 GetAttributes 返回的 FileAttribute 枚舉的所有成員:

    Archive, Compressed, Device, Directory, Encrypted,
    Hidden, IntegritySystem, Normal, NoScrubData, NotContentIndexed, 
    Offline, ReadOnly, ReparsePoint, SparseFile, System, Temporary

    此枚舉中的成員是可組合的。下面介紹了如何切換單個文件屬性而不打亂其余屬性:

    string filePath = "test.txt";
    
    FileAttributes fa = File.GetAttributes (filePath);
    if ((fa & FileAttributes.ReadOnly) != 0)
    {
        // Use the exclusive-or operator (^) to toggle the ReadOnly flag
        fa ^= FileAttributes.ReadOnly;
        File.SetAttributes (filePath, fa);
    }
    
    // Now we can delete the file, for instance:
    File.Delete (filePath);

    注意

    FileInfo 提供了一種更簡單的方法來更改文件的只讀標志:

    new FileInfo ("test.txt").IsReadOnly = false;

    壓縮和加密屬性

    注意

    此功能僅適用于 Windows,需要 NuGet 包 System.Management 。

    “壓縮”和“加密”文件屬性對應于 Windows 資源管理器中文件或目錄的“屬性”對話框中的壓縮和加密復選框。這種類型的壓縮和加密是,因為操作系統在后臺完成所有工作,允許您讀取和寫入純數據。

    不能使用 SetAttributes 更改文件的壓縮或加密屬性 - 如果嘗試,它將以靜默方式失敗!在后一種情況下,解決方法很簡單:改為在類中調用 Encrypt() 和 Decrypt() 方法。使用壓縮,它更復雜;一種解決方案是在System.Management中使用Windows管理規范(WMI)API。以下方法壓縮目錄,如果成功,則返回 0(如果成功,則返回 WMI 錯誤代碼):File

    static uint CompressFolder (string folder, bool recursive)
    {
      string path = "Win32_Directory.Name='" + folder + "'";
      using (ManagementObject dir = new ManagementObject (path))
      using (ManagementBaseObject p = dir.GetMethodParameters ("CompressEx"))
      {
        p ["Recursive"] = recursive;
        using (ManagementBaseObject result = dir.InvokeMethod ("CompressEx",
                                                                 p, null))
          return (uint) result.Properties ["ReturnValue"].Value;
      }
    }

    要解壓縮,請將 CompressEx 替換為 解壓縮Ex 。

    透明加密依賴于從登錄用戶的密碼中設定的密鑰。系統對經過身份驗證的用戶執行的密碼更改具有魯棒性,但如果通過管理員重置密碼,則加密文件中的數據將無法恢復。

    注意

    透明加密和壓縮需要特殊的文件系統支持。NTFS(最常在硬盤驅動器上使用)支持這些功能;CDFS(在 CD-ROM 上)和 FAT(在可移動介質卡上)則不然。

    您可以確定卷是否支持使用 Win32 互操作進行壓縮和加密:

    using System;
    using System.IO;
    using System.Text;
    using System.ComponentModel;
    using System.Runtime.InteropServices;
    
    class SupportsCompressionEncryption
    {
      const int SupportsCompression = 0x10;
      const int SupportsEncryption = 0x20000;
    
      [DllImport ("Kernel32.dll", SetLastError = true)]
      extern static bool GetVolumeInformation (string vol, StringBuilder name,
        int nameSize, out uint serialNum, out uint maxNameLen, out uint flags,
        StringBuilder fileSysName, int fileSysNameSize);
    
      static void Main()
      {
        uint serialNum, maxNameLen, flags;
        bool ok = GetVolumeInformation (@"C:\", null, 0, out serialNum,
                                        out maxNameLen, out flags, null, 0);
        if (!ok)
          throw new Win32Exception();
    
        bool canCompress = (flags & SupportsCompression) != 0;
        bool canEncrypt = (flags & SupportsEncryption) != 0;
      }
    }

    文件安全

    注意

    此功能僅適用于 Windows,需要 NuGet 包 System.IO.FileSystem.AccessControl。

    FileSecurity 類允許您查詢和更改分配給用戶和角色的操作系統權限(命名空間 System.Security.AccessControl)。

    在此示例中,我們列出文件的現有權限,然后將寫入權限分配給“用戶”組:

    using System;
    using System.IO;
    using System.Security.AccessControl;
    using System.Security.Principal;
    
    void ShowSecurity (FileSecurity sec)
    {
      AuthorizationRuleCollection rules = sec.GetAccessRules (true, true,
                                                           typeof (NTAccount));
      foreach (FileSystemAccessRule r in rules.Cast<FileSystemAccessRule>()
        .OrderBy (rule => rule.IdentityReference.Value))
      {
        // e.g., MyDomain/Joe
        Console.WriteLine ($"  {r.IdentityReference.Value}");
        // Allow or Deny: e.g., FullControl
        Console.WriteLine ($"    {r.FileSystemRights}: {r.AccessControlType}");
      }
    }
    
    var file = "sectest.txt";
    File.WriteAllText (file, "File security test.");
    
    var sid = new SecurityIdentifier (WellKnownSidType.BuiltinUsersSid, null);
    string usersAccount = sid.Translate (typeof (NTAccount)).ToString();
    
    Console.WriteLine ($"User: {usersAccount}");
    
    FileSecurity sec = new FileSecurity (file,
                              AccessControlSections.Owner |
                              AccessControlSections.Group |
                              AccessControlSections.Access);
    
    Console.WriteLine ("AFTER CREATE:");
    ShowSecurity(sec); // BUILTIN\Users doesn't have Write permission
    
    sec.ModifyAccessRule (AccessControlModification.Add,
        new FileSystemAccessRule (usersAccount, FileSystemRights.Write, 
                                  AccessControlType.Allow),
        out bool modified);
    
    Console.WriteLine ("AFTER MODIFY:");
    ShowSecurity (sec); // BUILTIN\Users has Write permission

    稍后,我們在中再舉一個例子。

    目錄類

    靜態 Directory 類提供了一組類似于 File 類中的方法 — 用于檢查目錄是否存在 ( 存在 ), 移動目錄 ( 移動 ), 刪除目錄 ( 刪除 ), 獲取/設置創建或上次訪問時間,以及獲取/設置安全權限。此外,目錄公開了以下靜態方法:

    string GetCurrentDirectory ();
    void   SetCurrentDirectory (string path);
    
    DirectoryInfo CreateDirectory  (string path);
    DirectoryInfo GetParent        (string path);
    string        GetDirectoryRoot (string path);
    
    string[] GetLogicalDrives(); // Gets mount points on Unix
    
    // The following methods all return full paths:
    
    string[] GetFiles             (string path);
    string[] GetDirectories       (string path);
    string[] GetFileSystemEntries (string path);
    
    IEnumerable<string> EnumerateFiles             (string path);
    IEnumerable<string> EnumerateDirectories       (string path);
    IEnumerable<string> EnumerateFileSystemEntries (string path);

    注意

    最后三種方法可能比 Get* 變體更有效,因為它們是延遲計算的 - 在枚舉序列時從文件系統獲取數據。它們特別適合 LINQ 查詢。

    枚舉* 和 Get* 方法被重載,以便也接受搜索模式(字符串)和搜索選項(枚舉)參數。如果指定“搜索選項”。搜索所有子目錄 ,執行遞歸子目錄搜索。方法將 *Files 與 *Directory 的結果組合在一起。

    如果目錄尚不存在,請按以下步驟創建目錄:

    if (!Directory.Exists (@"d:\test"))
      Directory.CreateDirectory (@"d:\test");

    文件信息和目錄信息

    文件和目錄上的靜態方法便于執行單個文件或目錄操作。如果需要在一行中調用一系列方法,FileInfo 和 DirectoryInfo 類將提供一個對象模型,使作業更容易。

    FileInfo 以實例形式提供了大多數 File 的靜態方法,以及一些附加屬性,如擴展名、長度、IsReadOnly 和 Directory,用于返回 DirectoryInfo 對象。例如:

    static string TestDirectory => 
      RuntimeInformation.IsOSPlatform (OSPlatform.Windows)
        ? @"C:\Temp" 
        : "/tmp"; 
    
    Directory.CreateDirectory (TestDirectory);
    
    FileInfo fi = new FileInfo (Path.Combine (TestDirectory, "FileInfo.txt"));
    
    Console.WriteLine (fi.Exists);         // false
    
    using (TextWriter w = fi.CreateText())
      w.Write ("Some text");
    
    Console.WriteLine (fi.Exists);         // false (still)
    fi.Refresh();
    Console.WriteLine (fi.Exists);         // true
    
    Console.WriteLine (fi.Name);           // FileInfo.txt
    Console.WriteLine (fi.FullName);       // c:\temp\FileInfo.txt (Windows)
                                           // /tmp/FileInfo.txt (Unix)
    Console.WriteLine (fi.DirectoryName);  // c:\temp (Windows)
                                           // /tmp (Unix)
    Console.WriteLine (fi.Directory.Name); // temp
    Console.WriteLine (fi.Extension);      // .txt
    Console.WriteLine (fi.Length);         // 9
    
    fi.Encrypt();
    fi.Attributes ^= FileAttributes.Hidden;   // (Toggle hidden flag)
    fi.IsReadOnly = true;
    
    Console.WriteLine (fi.Attributes);    // ReadOnly,Archive,Hidden,Encrypted
    Console.WriteLine (fi.CreationTime);  // 3/09/2019 1:24:05 PM
    
    fi.MoveTo (Path.Combine (TestDirectory, "FileInfoX.txt")); 
    
    DirectoryInfo di = fi.Directory;
    Console.WriteLine (di.Name);             // temp or tmp
    Console.WriteLine (di.FullName);         // c:\temp or /tmp
    Console.WriteLine (di.Parent.FullName);  // c:\ or /
    di.CreateSubdirectory ("SubFolder");

    下面介紹如何使用 DirectoryInfo 枚舉文件和子目錄:

    DirectoryInfo di = new DirectoryInfo (@"e:\photos");
    
    foreach (FileInfo fi in di.GetFiles ("*.jpg"))
      Console.WriteLine (fi.Name);
    
    foreach (DirectoryInfo subDir in di.GetDirectories())
      Console.WriteLine (subDir.FullName);

    路徑

    靜態 Path 類定義用于處理路徑和的方法和字段。

    假設此設置代碼

    string dir  = @"c:\mydir";    // or /mydir
    string file = "myfile.txt";
    string path = @"c:\mydir\myfile.txt";    // or /mydir/myfile.txt
    
    Directory.SetCurrentDirectory (@"k:\demo");    // or /demo

    我們可以使用以下表達式演示 Path 的方法和字段:

    表達

    結果(Windows,然后是Unix)

    Directory.GetCurrentDirectory()

    k:\demo\ or /demo

    Path.IsPathRoot (file)

    Path.IsPathRoot (path)

    Path.GetPathRoot (path)

    c:\或/

    Path.GetDirectoryName (path)

    c:\mydir 或 /mydir

    Path.GetFileName (path)

    我的文件.txt

    Path.GetFullPath (file)

    k:\demo\myfile.txt 或

    Path.Combined (dir, file)

    c:\mydir\myfile.txt or /

    文件擴展名:


    Path.HasExtension (file)

    Path.GetExtension (file)

    。.txt

    Path.GetFileNameWithoutExtension (file)

    我的文件

    Path.ChangeExtension (file, “.log”)

    我的文件.log

    分隔符和字符:


    Path.DirectorySeparatorChar

    \或/

    Path.AltDirectorySeparatorChar

    /

    路徑.路徑分隔符

    ;或:

    路徑.卷分隔符字符

    :或/

    Path.GetInvalidPathChars()

    字符 0 到 31 和“<>|或 0

    Path.GetInvalid文件名字符()

    字符 0 到 31 和“<>|:*?\/ 或 0 和 /

    臨時文件:


    Path.GetTempPath()

    \ 臨時或

    Path.GetRandomFileName()

    d2dwuzjf.dnp

    Path.GetTempFileName()

    \ Temp \ 或 /.tmptmp14B.tmp

    合并特別有用:它允許您組合一個目錄和文件名(或兩個目錄),而無需首先檢查是否存在尾隨路徑分隔符,并且它會自動為操作系統使用正確的路徑分隔符。它提供最多接受四個目錄和/或文件名的重載。

    GetFullPath 將相對于當前目錄的路徑轉換為絕對路徑。它接受諸如 .。

    GetRandomFileName 返回一個真正唯一的 8.3 個字符的文件名,而不實際創建任何文件。GetTempFileName 使用每 65,000 個文件重復一次的自動遞增計數器生成一個臨時文件名。然后,它在本地臨時目錄中創建此名稱的零字節文件。

    警告

    完成后,必須刪除由 GetTemp文件名生成的文件;否則,它最終將引發異常(在您第 65,000 次調用 GetTempFileName 之后)。如果這是一個問題,您可以改為將 GetTempPath 與 GetRandomFile Name 組合在一起。只是要小心不要填滿用戶的硬盤驅動器!

    特殊文件夾

    路徑和目錄中缺少的一件事是查找文件夾(”、“”、“”等)的方法。這由 System.Environment 類中的 GetFolderPath 方法提供:

    string myDocPath = Environment.GetFolderPath
      (Environment.SpecialFolder.MyDocuments);

    Environment.SpecialFolder是一個枚舉,其值包含Windows中的所有特殊目錄,例如AdminTools,ApplicationData,Fonts,History,SendTo,StartMenu等。此處介紹了除 .NET 運行時目錄之外的所有內容,您可以按如下方式獲取該目錄:

    System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()

    注意

    大多數特殊文件夾在 Unix 沒有分配路徑。以下路徑在 Ubuntu Linux 18.04 桌面上:ApplicationData 、CommonApplicationData 、Desktop 、DesktopDirectory 、LocalApplicationData 、MyDocuments 、MyMusic 、MyPictures 、MyVideos、Templates 和 。

    在Windows系統上特別有價值的是應用程序數據 :在這里,您可以存儲與用戶一起通過網絡傳輸的設置(如果在網絡域上啟用了漫游配置文件), LocalApplicationData ,用于非漫游數據(特定于登錄用戶)和 CommonApplicationData ,由計算機的每個用戶共享。將應用程序數據寫入這些文件夾被認為比使用 Windows 注冊表更可取。在這些文件夾中存儲數據的標準協議是使用應用程序的名稱創建一個子目錄:

    string localAppDataPath = Path.Combine (
      Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData),
      "MyCoolApplication");
    
    if (!Directory.Exists (localAppDataPath))
      Directory.CreateDirectory (localAppDataPath);

    使用CommonApplicationData時有一個可怕的陷阱:如果用戶使用管理提升啟動程序,然后程序在CommonApplicationData中創建文件夾和文件,則該用戶可能缺少以后在受限Windows登錄下運行時替換這些文件的權限。(在受限權限帳戶之間切換時也存在類似的問題。您可以通過創建所需的文件夾(將權限分配給每個人)作為設置的一部分來解決此問題。

    將配置和日志文件寫入的另一個位置是將配置和日志文件寫入應用程序的基目錄,您可以使用 AppDomain.CurrentDomain.BaseDirectory 獲取該目錄。但是,不建議這樣做,因為操作系統可能會拒絕應用程序在初始安裝后寫入此文件夾的權限(無管理)。

    查詢卷信息

    您可以使用 DriveInfo 類查詢計算機上的驅動器:

    DriveInfo c = new DriveInfo ("C");       // Query the C: drive.
                                             // On Unix: /
    
    long totalSize = c.TotalSize;            // Size in bytes.
    long freeBytes = c.TotalFreeSpace;       // Ignores disk quotas.
    long freeToMe  = c.AvailableFreeSpace;   // Takes quotas into account.
    
    foreach (DriveInfo d in DriveInfo.GetDrives())  // All defined drives.
                                                    // On Unix: mount points
    {
      Console.WriteLine (d.Name);             // C:\
      Console.WriteLine (d.DriveType);        // Fixed
      Console.WriteLine (d.RootDirectory);    // C:\
    
      if (d.IsReady)   // If the drive is not ready, the following two
                       // properties will throw exceptions:
      {
        Console.WriteLine (d.VolumeLabel);    // The Sea Drive
        Console.WriteLine (d.DriveFormat);    // NTFS
      }
    }

    靜態 GetDrives 方法返回所有映射的驅動器,包括 CD-ROM、媒體卡和網絡連接。驅動器類型是具有以下值的枚舉:

    Unknown, NoRootDirectory, Removable, Fixed, Network, CDRom, Ram

    捕獲文件系統事件

    類允許您監視目錄(以及可選的子目錄)的活動。文件系統觀察器具有在創建、修改、重命名和刪除文件或子目錄時以及當其屬性更改時觸發的事件。無論執行更改的用戶或進程如何,都會觸發這些事件。下面是一個示例:

    Watch (GetTestDirectory(), "*.txt", true);
    
    void Watch (string path, string filter, bool includeSubDirs)
    {
      using (var watcher = new FileSystemWatcher (path, filter))
      {
        watcher.Created += FileCreatedChangedDeleted;
        watcher.Changed += FileCreatedChangedDeleted;
        watcher.Deleted += FileCreatedChangedDeleted;
        watcher.Renamed += FileRenamed;
        watcher.Error   += FileError;
    
        watcher.IncludeSubdirectories = includeSubDirs;
        watcher.EnableRaisingEvents = true;
    
        Console.WriteLine ("Listening for events - press <enter> to end");
        Console.ReadLine();
      }
      // Disposing the FileSystemWatcher stops further events from firing.
    }
    
    void FileCreatedChangedDeleted (object o, FileSystemEventArgs e)
      => Console.WriteLine ("File {0} has been {1}", e.FullPath, e.ChangeType);
    
    void FileRenamed (object o, RenamedEventArgs e)
      => Console.WriteLine ("Renamed: {0}->{1}", e.OldFullPath, e.FullPath);
    
    void FileError (object o, ErrorEventArgs e)
      => Console.WriteLine ("Error: " + e.GetException().Message);
    
    string GetTestDirectory() =>
      RuntimeInformation.IsOSPlatform (OSPlatform.Windows)
        ? @"C:\Temp"
        : "/tmp"; 

    注意

    由于 FileSystemWatcher 在單獨的線程上引發事件,因此必須對事件處理代碼進行異常處理,以防止錯誤導致應用程序關閉。更多信息請參閱

    錯誤事件不會通知您文件系統錯誤;相反,它表示文件系統觀察器的事件緩沖區溢出,因為它被更改、創建、刪除或重命名的事件淹沒。可以通過 InternalBufferSize 屬性更改緩沖區大小。

    包含子目錄以遞歸方式應用。因此,如果在 上創建一個 FileSystemWatcher,并且 IncludeSubdirectory true ,當文件或目錄在硬盤驅動器上的任何位置發生更改時,其事件將觸發。

    警告

    使用 FileSystemWatcher 的一個陷阱是在文件完全填充或更新之前打開并讀取新創建或更新的文件。如果您正在與創建文件的其他一些軟件配合使用,則可能需要考慮一些策略來緩解此問題,例如創建具有未監視擴展名的文件,然后在完全寫入后重命名它們。

    操作系統安全性

    所有應用程序都受操作系統限制的約束,具體取決于用戶的登錄權限。這些限制會影響文件 I/O 以及其他功能,例如對 Windows 注冊表的訪問。

    在Windows和Unix中,有兩種類型的帳戶:

    • 對訪問本地計算機不施加任何限制的管理/超級用戶帳戶
    • 限制管理功能和其他用戶數據的可見性的受限權限帳戶

    在Windows上,稱為用戶帳戶控制(UAC)的功能意味著管理員在登錄時會收到兩個令牌或“帽子”:管理帽子和普通用戶帽子。默認情況下,程序戴著普通用戶帽(權限受限)運行,除非程序請求。然后,用戶必須在顯示的對話框中批準請求。

    在Unix上,用戶通常使用受限帳戶登錄。對于管理員來說,減少無意中損壞系統的可能性也是如此。當用戶需要運行需要提升權限的命令時,他們在命令前面加上 sudo(超級用戶 do 的縮寫)。

    ,應用程序將使用受限的用戶權限運行。這意味著您必須執行以下操作之一:

    • 編寫應用程序,使其無需管理權限即可運行。
    • 在應用程序清單中請求管理提升(僅限 Windows),或檢測缺少所需權限并提醒用戶以管理員/超級用戶身份重新啟動應用程序。

    第一個選項對用戶來說更安全、更方便。在大多數情況下,將程序設計為在沒有管理權限的情況下運行很容易。

    您可以按方式了解您是否在管理帳戶下運行:

    [DllImport("libc")]
    public static extern uint getuid();
    
    static bool IsRunningAsAdmin()
    {
      if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
      {
        using var identity = WindowsIdentity.GetCurrent();
        var principal = new WindowsPrincipal (identity);
        return principal.IsInRole (WindowsBuiltInRole.Administrator);
      }
      return getuid() == 0;
    }

    在 Windows 上啟用 UAC 后,僅當當前進程具有管理提升時,才會返回 true。在 Linux 上,僅當當前進程以超級用戶身份運行時,它才返回 true(例如,)。

    在標準用戶帳戶中運行

    以下是標準用戶帳戶中執行的關鍵操作:

    • 寫入以下目錄:
      • 操作系統文件夾(通常為 或 /)和子目錄
      • 程序文件文件夾( 或 /)和子目錄
      • 操作系統驅動器的根目錄(例如, 或 /)
    • 寫入注冊表的HKEY_LOCAL_MACHINE分支 (Windows)
    • 讀取性能監視 (WMI) 數據 (Windows)

    此外,作為普通 Windows 用戶(甚至管理員),您可能會被拒絕訪問屬于其他用戶的文件或資源。Windows 使用訪問控制列表 (ACL) 系統來保護此類資源 — 您可以通過 System.Security.AccessControl 中的類型查詢和斷言您在 ACL 中的權限。ACL 也可以應用于跨進程等待句柄,如所述。

    如果由于操作系統安全性而拒絕訪問任何內容,CLR 將檢測到故障并引發 UnauthorizedAccessException(而不是以靜默方式失敗)。

    在大多數情況下,您可以按如下方式處理標準用戶限制:

    • 將文件寫入其建議的位置。
    • 避免將注冊表用于可存儲在文件中的信息(HKEY_CURRENT_USER配置單元除外,你只能在 Windows 上對其具有讀/寫訪問權限)。
    • 在安裝過程中注冊 ActiveX 或 COM 組件(僅限 Windows)。

    用戶文檔的推薦位置是 SpecialFolder.MyDocuments :

    string docsFolder = Environment.GetFolderPath
                        (Environment.SpecialFolder.MyDocuments);
    
    string path = Path.Combine (docsFolder, "test.txt");

    用戶可能需要在應用程序外部修改的配置文件的建議位置是 SpecialFolder.ApplicationData(僅限當前用戶)或 SpecialFolder.CommonApplicationData(所有用戶)。您通常根據組織和產品名稱在這些文件夾中創建子目錄。

    管理提升和虛擬化

    使用,可以請求 Windows 在運行程序時提示用戶進行管理提升(Linux 忽略此請求):

    <?xml version="1.0" encoding="utf-8"?>
    <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
      <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
        <security>
          <requestedPrivileges>
            <requestedExecutionLevel level="requireAdministrator" />
          </requestedPrivileges>
        </security>
      </trustInfo>
    </assembly>

    (我們將在第 中更詳細地描述應用程序清單。

    如果將 requireAdministrator 替換為 asInvoker ,它會指示 Windows 不需要管理提升。其效果幾乎與根本沒有應用程序清單相同,只是。虛擬化是隨 Windows Vista 引入的一項臨時措施,可幫助舊應用程序在沒有管理權限的情況下正確運行。缺少具有 requestExecutionLevel 元素的應用程序清單將激活此向后兼容性功能。

    當應用程序寫入或 目錄或注冊表的HKEY_LOCAL_MACHINE區域時,虛擬化將發揮作用。更改不會引發異常,而是重定向到硬盤上不會影響原始數據的單獨位置。這可以防止應用程序干擾操作系統或其他行為良好的應用程序。

    內存映射文件

    提供兩個關鍵功能:

    • 高效隨機訪問文件數據
    • 在同一臺計算機上的不同進程之間共享內存的能力

    內存映射文件的類型駐留在 System.IO.MemoryMappedFiles 命名空間中。在內部,它們通過包裝操作系統的 API 來存儲內存映射文件。

    內存映射文件和隨機文件 I/O

    盡管普通文件流允許隨機文件 I/O(通過設置流的 Position 屬性),但它針對順序 I/O 進行了優化。作為粗略的經驗法則:

    • 對于順序 I/O,FileStream 大約比內存映射文件快 10 倍。
    • 對于隨機 I/O,內存映射文件大約比 FileStream 快 10 倍。

    更改文件流的位置可能會花費幾微秒 - 如果在循環中完成,則會加起來。FileStream 也不適合多線程訪問,因為它的位置會隨著讀取或寫入而變化。

    要創建內存映射文件:

    1. 像往常一樣獲取文件流。
    2. 實例化一個 MemoryMappedFile ,傳入文件流。
    3. 在內存映射文件對象上調用 CreateViewAccessor。

    最后一步為您提供了一個 MemoryMappedViewAccessor 對象,該對象提供了用于隨機讀取和寫入簡單類型、結構和數組的方法(在中對此進行了詳細介紹)。

    The following creates a one million-byte file and then uses the memory-mapped file API to read and then write a byte at position 500,000:

    File.WriteAllBytes ("long.bin", new byte [1000000]);
    
    using MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile ("long.bin");
    using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor();
    
    accessor.Write (500000, (byte) 77);
    Console.WriteLine (accessor.ReadByte (500000));   // 77

    您還可以在調用 創建從文件 時指定映射名稱和容量。指定非空映射名稱允許內存塊與其他進程共享(請參閱下一節);指定容量會自動將文件放大到該值。以下內容將創建一個 1,000 字節的文件:

    File.WriteAllBytes ("short.bin", new byte [1]);
    using (var mmf = MemoryMappedFile.CreateFromFile
                     ("short.bin", FileMode.Create, null, 1000))
      ...

    內存映射文件和共享內存 (Windows)

    在 Windows 下,還可以使用內存映射文件作為在同一臺計算機上的進程之間共享內存的方法。一個進程通過調用 MemoryMappedFile.CreateNew 來創建共享內存塊,然后其他進程通過調用具有相同名稱的 MemoryMappedFile.Open存在來訂閱相同的內存塊。盡管它仍稱為內存映射“文件”,但它完全駐留在內存中,不存在磁盤。

    以下代碼創建一個 500 字節的共享內存映射文件,并將整數 12345 寫入位置 0:

    using (MemoryMappedFile mmFile = MemoryMappedFile.CreateNew ("Demo", 500))
    using (MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor())
    {
      accessor.Write (0, 12345);
      Console.ReadLine();   // Keep shared memory alive until user hits Enter.
    }

    以下代碼打開該內存映射文件并讀取該整數:

    // This can run in a separate executable:
    using (MemoryMappedFile mmFile = MemoryMappedFile.OpenExisting ("Demo"))
    using (MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor())
      Console.WriteLine (accessor.ReadInt32 (0));   // 12345

    跨平臺進程間共享內存

    Windows和Unix都允許多個進程對同一個文件進行內存映射。您必須小心確保適當的文件共享設置:

    static void Writer()
    {
      var file = Path.Combine (TestDirectory, "interprocess.bin");
      File.WriteAllBytes (file, new byte [100]);
    
      using FileStream fs = 
        new FileStream (file, FileMode.Open, FileAccess.ReadWrite, 
                        FileShare.ReadWrite);
                        
      using MemoryMappedFile mmf = MemoryMappedFile
        .CreateFromFile (fs, null, fs.Length, MemoryMappedFileAccess.ReadWrite,
                         HandleInheritability.None, true);
      using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor();
        
      accessor.Write (0, 12345);
    
      Console.ReadLine();   // Keep shared memory alive until user hits Enter.
    
      File.Delete (file);
    }
    
    static void Reader()
    {
      // This can run in a separate executable:
      var file = Path.Combine (TestDirectory, "interprocess.bin");
      using FileStream fs = 
        new FileStream (file, FileMode.Open, FileAccess.ReadWrite, 
                        FileShare.ReadWrite);
      using MemoryMappedFile mmf = MemoryMappedFile
        .CreateFromFile (fs, null, fs.Length, MemoryMappedFileAccess.ReadWrite, 
                          HandleInheritability.None, true);
      using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor();
      
      Console.WriteLine (accessor.ReadInt32 (0));   // 12345
    }
      
    static string TestDirectory =>
      RuntimeInformation.IsOSPlatform (OSPlatform.Windows)
        ?  @"C:\Test"
        : "/tmp";

    使用視圖訪問器

    在 MemoryMappedFile 上調用 CreateViewAccessor 會給你一個視圖訪問器,讓你在隨機位置讀/寫值。

    Read* / Write* 方法接受數值類型、布爾值和字符,以及包含值類型元素或字段的數組和結構。禁止引用類型(以及包含引用類型的數組或結構),因為它們無法映射到非托管內存。因此,如果要編寫字符串,則必須將其編碼為字節數組:

    byte[] data = Encoding.UTF8.GetBytes ("This is a test");
    accessor.Write (0, data.Length);
    accessor.WriteArray (4, data, 0, data.Length);

    請注意,我們首先編寫了長度。這意味著我們知道稍后要讀回多少字節:

    byte[] data = new byte [accessor.ReadInt32 (0)];
    accessor.ReadArray (4, data, 0, data.Length);
    Console.WriteLine (Encoding.UTF8.GetString (data));   // This is a test

    下面是讀取/寫入結構的示例:

    struct Data { public int X, Y; }
    ...
    var data = new Data { X = 123, Y = 456 };
    accessor.Write (0, ref data);
    accessor.Read (0, out data);
    Console.WriteLine (data.X + " " + data.Y);   // 123 456

    讀取和寫入方法出奇地慢。通過指針直接訪問基礎非托管內存,可以獲得更好的性能。繼上一個示例之后:

    unsafe
    {
      byte* pointer = null;
      try
      {
        accessor.SafeMemoryMappedViewHandle.AcquirePointer (ref pointer);
        int* intPointer = (int*) pointer;
        Console.WriteLine (*intPointer);               // 123
      }
      finally
      {
        if (pointer != null)
          accessor.SafeMemoryMappedViewHandle.ReleasePointer();
      }
    }

    必須將項目配置為允許不安全的代碼。您可以通過編輯 .csproj 文件來執行此操作:

      <PropertyGroup>
        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
      </PropertyGroup>

    指針的性能優勢在處理大型結構時更加明顯,因為它們允許您直接處理原始數據,而不是使用讀/寫在托管和非托管內存之間數據。我們將在第章中進一步探討這一點。

網站首頁   |    關于我們   |    公司新聞   |    產品方案   |    用戶案例   |    售后服務   |    合作伙伴   |    人才招聘   |   

友情鏈接: 餐飲加盟

地址:北京市海淀區    電話:010-     郵箱:@126.com

備案號:冀ICP備2024067069號-3 北京科技有限公司版權所有