該文章綜合了幾本書的內容.程序員
某咖啡店供應咖啡, 客戶買咖啡的時候能夠添加若干調味料, 最後要求算出總價錢.windows
Beverage是全部咖啡飲料的抽象類, 裏面的cost方法是抽象的. description變量在每一個子類裏面都須要設置(表示對咖啡的描述).數組
每一個子類實現cost方法, 表示咖啡的價格.緩存
除了這些類以外, 還有調味品:安全
問題是調味品太多了, 若是使用繼承來作的話, 各類組合簡直是類的爆炸.服務器
並且還有其餘的問題, 若是牛奶的價格上漲了怎麼辦? 若是再加一種焦糖調料呢?網絡
父類裏面有調味料的變量(bool), 而且在父類裏面直接實現cost方法(經過是否有某種調味料來計算價格).app
子類override父類的cost方法, 可是也調用父類的cost方法, 這樣就能夠把子類這個咖啡的價格和父類裏計算出來的調味料的價格加到一塊兒算出最終的價格了.異步
下面就是:socket
看起來不錯, 那麼, 問題來了:
類應該對擴展開放 而對修改關閉.
使用裝飾模式, 咱們能夠購買一個咖啡, 而且在運行時使用調味料對它進行裝飾.
大約步驟以下:
到目前我知道了這些:
動態的對某個對象進行擴展(附加額外的職責), 裝飾器是除了繼承以外的另一種爲對象擴展功能的方法.
下面看看該模式的類圖:
這個就很好理解了, 父類都是Beverage(飲料), 左邊是四種具體實現的咖啡, 右邊上面是裝飾器的父類, 下面是具體的裝飾器(調味料).
這裏須要注意的是, 裝飾器和咖啡都繼承於同一個父類只是由於須要它們的類型匹配而已, 並非要繼承行爲.
Beverage:
namespace DecoratorPattern.Core { public abstract class Beverage { public virtual string Description { get; protected set; } = "Unknown Beverage"; public abstract double Cost(); } }
CondimentDecorator:
namespace DecoratorPattern.Core { public abstract class CondimentDecorator : Beverage { public abstract override string Description { get; } } }
Espresso 濃咖啡:
using DecoratorPattern.Core; namespace DecoratorPattern.Coffee { public class Espresso : Beverage { public Espresso() { Description = "Espresso"; } public override double Cost() { return 1.99; } } }
using DecoratorPattern.Core; namespace DecoratorPattern.Coffee { public class HouseBlend : Beverage { public HouseBlend() { Description = "HouseBlend"; } public override double Cost() { return .89; } } }
Mocha:
using DecoratorPattern.Core; namespace DecoratorPattern.Condiments { public class Mocha : CondimentDecorator { private readonly Beverage beverage; public Mocha(Beverage beverage) => this.beverage = beverage; public override string Description => $"{beverage.Description}, Mocha"; public override double Cost() { return .20 + beverage.Cost(); } } }
Whip:
using DecoratorPattern.Core; namespace DecoratorPattern.Condiments { public class Whip : CondimentDecorator { private readonly Beverage beverage; public Whip(Beverage beverage) => this.beverage = beverage; public override string Description => $"{beverage.Description}, Whip"; public override double Cost() { return .15 + beverage.Cost(); } } }
Program:
using System; using DecoratorPattern.Coffee; using DecoratorPattern.Condiments; using DecoratorPattern.Core; namespace DecoratorPattern { class Program { static void Main(string[] args) { var beverage = new Espresso(); Console.WriteLine($"{beverage.Description} $ {beverage.Cost()}"); Beverage beverage2 = new HouseBlend(); beverage2 = new Mocha(beverage2); beverage2 = new Mocha(beverage2); beverage2 = new Whip(beverage2); Console.WriteLine($"{beverage2.Description} $ {beverage2.Cost()}"); } } }
運行結果:
首先須要知道, System.IO命名空間是低級I/O功能的大本營.
.NET Core裏面的Stream主要是三個概念: 存儲(backing stores 我不知道怎麼翻譯比較好), 裝飾器, 適配器.
backing stores是讓輸入和輸出發揮做用的端點, 例如文件或者網絡鏈接. 就是下面任意一點或兩點:
程序員能夠經過Stream類來發揮backing store的做用. Stream類有一套方法, 能夠進行讀取, 寫入, 定位等操做. 個數組不一樣的是, 數組是把全部的數據都一同放在了內存裏, 而stream則是順序的/連續的處理數據, 要麼是一次處理一個字節, 要麼是一次處理特定大小(不能太大, 可管理的範圍內)的數據.
因而, stream能夠用比較小的固定大小的內存來處理不管多大的backing store.
中間的那部分就是裝飾器Stream. 它符合裝飾模式.
從圖中能夠看到, Stream又分爲兩部分:
裝飾器Stream有以下結構性的優勢(參考裝飾模式):
backing store和裝飾器stream都是按字節進行處理的. 儘管這很靈活和高效, 可是程序通常仍是採用更高級別的處理方式例如文字或者xml.
適配器經過使用特殊化的方法把類裏面的stream進行包裝成特殊的格式. 這就彌合了上述的間隔.
例如 text reader有一個ReadLine方法, XML writer又WriteAttributes方法.
注意: 適配器包裝了stream, 這點和裝飾器同樣, 可是不同的是, 適配器自己並非stream, 它通常會把全部針對字節的方法都隱藏起來. 因此本文就不介紹適配器了.
總結一下:
backing store stream 提供原始數據, 裝飾器stream提供透明的轉換(例如加密); 適配器提供方法來處理高級別的類型例如字符串和xml.
想要連成串的話, 秩序把對象傳遞到另外一個對象的構造函數裏.
Stream抽象類是全部Stream的基類.
它的方法和屬性主要分三類基本操做: 讀, 寫, 尋址(Seek); 和管理操做: 關閉(close), 衝(flush)和設定超時:
這些方法都有異步的版本, 加async, 返回Task便可.
一個例子:
using System; using System.IO; namespace Test { class Program { static void Main(string[] args) { // 在當前目錄建立按一個 test.txt 文件 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); // 寫 5 字節 Console.WriteLine(s.Length); // 7 Console.WriteLine(s.Position); // 7 s.Position = 0; // 回到開頭位置 Console.WriteLine(s.ReadByte()); // 101 Console.WriteLine(s.ReadByte()); // 102 // 從block數組開始的地方開始read: Console.WriteLine(s.Read(block, 0, block.Length)); // 5 // 假設最後一次read返回 5, 那就是在文件結尾, 因此read會返回0: Console.WriteLine(s.Read(block, 0, block.Length)); // 0 } } } }
運行結果:
異步例子:
using System; using System.IO; using System.Threading.Tasks; namespace Test { class Program { static void Main(string[] args) { Task.Run(AsyncDemo).GetAwaiter().GetResult(); } async static Task AsyncDemo() { using (Stream s = new FileStream("test.txt", FileMode.Create)) { byte[] block = { 1, 2, 3, 4, 5 }; await s.WriteAsync(block, 0, block.Length); s.Position = 0; Console.WriteLine(await s.ReadAsync(block, 0, block.Length)); } } } }
異步版本比較適合慢的stream, 例如網絡的stream.
CanRead和CanWrite屬性能夠判斷Stream是否能夠讀寫.
Read方法把stream的一塊數據寫入到數組, 返回接受到的字節數, 它老是小於等於count這個參數. 若是它小於count, 就說明要麼是已經讀取到stream的結尾了, 要麼stream給的數據塊過小了(網絡stream常常這樣).
一個讀取1000字節stream的例子:
// 假設s是某個stream byte[] data = new byte[1000]; // bytesRead 的結束位置確定是1000, 除非stream的長度不足1000 int bytesRead = 0; int chunkSize = 1; while (bytesRead < data.Length && chunkSize > 0) bytesRead += chunkSize = s.Read(data, bytesRead, data.Length - bytesRead);
ReadByte方法更簡單一些, 一次就讀一個字節, 若是返回-1表示讀取到stream的結尾了. 返回類型是int.
Write和WriteByte就是相應的寫入方法了. 若是沒法寫入某個字節, 那就會拋出異常.
上面方法簽名裏的offset參數, 表示的是緩衝數組開始讀取或寫入的位置, 而不是指stream裏面的位置.
CanSeek爲true的話, Stream就能夠被尋址. 能夠查詢和修改可尋址的stream(例如文件stream)的長度, 也能夠隨時修改讀取和寫入的位置.
Position屬性就是所須要的, 它是相對於stream開始位置的.
Seek方法就容許你移動到當前位置或者stream的尾部.
注意改變FileStream的Position會花去幾微秒. 若是是在大規模循環裏面作這個操做的話, 建議使用MemoryMappedFile類.
對於不可尋址的Stream(例如加密Stream), 想知道它的長度只能是把它讀完. 並且你要是想讀取前一部分的話必須關閉stream, 而後再開始一個全新的stream才能夠.
Stream用完以後必須被處理掉(dispose)來釋放底層資源例如文件和socket處理. 一般使用using來實現.
關閉裝飾器stream的時候會同時關閉裝飾器和它的backing store stream.
針對一連串的裝飾器裝飾的stream, 關閉最外層的裝飾器就會關閉全部.
有些stream從backing store讀取/寫入的時候有一個緩存機制, 這就減小了實際到backing store的往返次數以達到提升性能的目的(例如FileStream).
這就意味着你寫入數據到stream的時候可能不會當即寫入到backing store; 它會有延遲, 直到緩衝被填滿.
Flush方法會強制內部緩衝的數據被當即的寫入. Flush會在stream關閉的時候自動被調用. 因此你不須要這樣寫: s.Flush(); s.Close();
若是CanTimeout屬性爲true的話, 那麼該stream就能夠設定讀或寫的超時.
網絡stream支持超時, 而文件和內存stream則不支持.
支持超時的stream, 經過ReadTimeout和WriteTimeout屬性能夠設定超時, 單位毫秒. 0表示無超時.
Read和Write方法經過拋出異常的方式來表示超時已經發生了.
stream並非線程安全的, 也就是說兩個線程同時讀或寫一個stream的時候就會報錯.
Stream經過Synchronized方法來解決這個問題. 該方法接受stream爲參數, 返回一個線程安全的包裝結果.
這個包裝結果在每次讀, 寫, 尋址的時候會得到一個獨立鎖/排他鎖, 因此同一時刻只有一個線程能夠執行操做.
實際上, 這容許多個線程同時爲同一個數據追加數據, 而其餘類型的操做(例如同讀)則須要額外的鎖來保證每一個線程能夠訪問到stream相應的部分.
文件流
構建一個FileStream:
FileStream fs1 = File.OpenRead("readme.bin"); // Read-only FileStream fs2 = File.OpenWrite(@"c:\temp\writeme.tmp"); // Write-only FileStream fs3 = File.Create(@"c:\temp\writeme.tmp"); // Read/write
OpenWrite和Create對於已經存在的文件來講, 它的行爲是不一樣的.
Create會把現有文件的內容清理掉, 寫入的時候從頭開寫.
OpenWrite則是完整的保存着現有的內容, 而stream的位置定位在0. 若是寫入的內容比原來的內容少, 那麼OpenWrite打開並寫完以後的內容是原內容和新寫入內容的混合體.
直接構建FileStream:
var fs = new FileStream ("readwrite.tmp", FileMode.Open); // Read/write
其構造函數裏面還能夠傳入其餘參數, 具體請看文檔.
File類的快捷方法:
下面這些靜態方法會一次性把整個文件讀進內存:
下面的方法直接寫入整個文件:
還有一個靜態方法叫File.ReadLines: 它有點想ReadAllLines, 可是它返回的是一個懶加載的IEnumerable<string>. 這個實際上效率更高一些, 由於沒必要一次性把整個文件都加載到內存裏. LINQ很是適合處理這個結果. 例如:
int longLines = File.ReadLines ("filePath").Count (l => l.Length > 80);
指定的文件名:
能夠是絕對路徑也能夠是相對路徑.
可已修改靜態屬性Environment.CurrentDirectory的值來改變當前的路徑. (注意: 默認的當前路徑不必定是exe所在的目錄)
AppDomain.CurrentDomain.BaseDirectory會返回應用的基目錄, 它一般是包含exe的目錄.
指定相對於這個目錄的地址最好使用Path.Combine方法:
string baseFolder = AppDomain.CurrentDomain.BaseDirectory; string logoPath = Path.Combine(baseFolder, "logo.jpg"); Console.WriteLine(File.Exists(logoPath));
經過網絡對文件讀寫要使用UNC路徑:
例如: \\JoesPC\PicShare \pic.jpg 或者 \\10.1.1.2\PicShare\pic.jpg.
FileMode:
全部的FileStream的構造器都會接收一個文件名和一個FileMode枚舉做爲參數. 若是選擇FileMode請看下圖:
其餘特性仍是須要看文檔.
MemoryStream在隨機訪問不可尋址的stream時就有用了.
若是你知道源stream的大小能夠接受, 你就能夠直接把它複製到MemoryStream裏:
var ms = new MemoryStream(); sourceStream.CopyTo(ms);
能夠經過ToArray方法把MemoryStream轉化成數組.
GetBuffer方法也是一樣的功能, 可是由於它是直接把底層的存儲數組的引用直接返回了, 因此會更有效率. 不過不幸的是, 這個數組一般比stream的真實長度要長.
注意: Close和Flush 一個MemoryStream是可選的. 若是關閉了MemoryStream, 你就不再能對它讀寫了, 可是仍然能夠調用ToArray方法來獲取其底層的數據.
Flush則對MemoryStream毫無用處.
PipeStream經過Windows Pipe 協議, 容許一個進程(process)和另外一個進程通訊.
分兩種:
pipe很適合一個電腦上的進程間交互(IPC), 它並不依賴於網絡傳輸, 這也意味着沒有網絡開銷, 也不在意防火牆.
注意: pipe是基於Stream的, 一個進程等待接受一串字符的同時另外一個進程發送它們.
PipeStream是抽象類.
具體的實現類有4個:
匿名pipe:
命名Pipe:
命名Pipe
命名pipe的雙方經過同名的pipe進行通訊. 協議規定了兩個角色: 服務器和客戶端. 按照下述方式進行通訊:
而後雙方就能夠讀寫stream來進行通訊了.
例子:
using System; using System.IO; using System.IO.Pipes; using System.Threading.Tasks; namespace Test { class Program { static void Main(string[] args) { Console.WriteLine(DateTime.Now.ToString()); using (var s = new NamedPipeServerStream("pipedream")) { s.WaitForConnection(); s.WriteByte(100); // Send the value 100. Console.WriteLine(s.ReadByte()); } Console.WriteLine(DateTime.Now.ToString()); } } }
using System; using System.IO.Pipes; namespace Test2 { class Program { static void Main(string[] args) { Console.WriteLine(DateTime.Now.ToString()); using (var s = new NamedPipeClientStream("pipedream")) { s.Connect(); Console.WriteLine(s.ReadByte()); s.WriteByte(200); // Send the value 200 back. } Console.WriteLine(DateTime.Now.ToString()); } } }
命名的PipeStream默認狀況下是雙向的, 因此任意一方均可以進行讀寫操做, 這也意味着服務器和客戶端必須達成某種協議來協調它們的操做, 避免同時進行發送和接收.
還須要協定好每次傳輸的長度.
在處理長度大於一字節的信息的時候, pipe提供了一個信息傳輸的模式, 若是這個啓用了, 一方在調用read的時候能夠經過檢查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(); }
注意: 針對PipeStream不能夠經過Read返回值是0的方式來它是否已經完成讀取消息了. 這是由於它和其餘的Stream不一樣, pipe stream和network stream沒有肯定的終點. 在兩個信息傳送動做之間, 它們就乾等着.
這樣啓用信息傳輸模式, 服務器端 :
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))); }
客戶端:
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); }
匿名pipe:
匿名pipe提供父子進程間的單向通訊. 流程以下:
由於匿名pipe是單向的, 因此服務器必須建立兩份pipe來進行雙向通訊
例子:
server:
using System; using System.Diagnostics; using System.IO; using System.IO.Pipes; using System.Text; using System.Threading.Tasks; namespace Test { class Program { static void Main(string[] args) { string clientExe = @"D:\Projects\Test2\bin\Debug\netcoreapp2.0\win10-x64\publish\Test2.exe"; HandleInheritability inherit = HandleInheritability.Inheritable; using (var tx = new AnonymousPipeServerStream(PipeDirection.Out, inherit)) using (var rx = new AnonymousPipeServerStream(PipeDirection.In, inherit)) { string txID = tx.GetClientHandleAsString(); string rxID = rx.GetClientHandleAsString(); var startInfo = new ProcessStartInfo(clientExe, txID + " " + rxID); startInfo.UseShellExecute = false; // Required for child process Process p = Process.Start(startInfo); tx.DisposeLocalCopyOfClientHandle(); // Release unmanaged rx.DisposeLocalCopyOfClientHandle(); // handle resources. tx.WriteByte(100); Console.WriteLine("Server received: " + rx.ReadByte()); p.WaitForExit(); } } } }
client:
using System; using System.IO.Pipes; namespace Test2 { class Program { static void Main(string[] args) { string rxID = args[0]; // Note we're reversing the string txID = args[1]; // receive and transmit roles. 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); } } } }
最好發佈一下client成爲獨立運行的exe:
dotnet publish --self-contained --runtime win10-x64
運行結果:
匿名pipe不支持消息模式, 因此你必須本身來爲傳輸的長度制定協議. 有一種作法是: 在每次傳輸的前4個字節裏存放一個整數表示消息的長度, 可使用BitConverter類來對整型和長度爲4的字節數組進行轉換.
BufferedStream對另外一個stream進行裝飾或者說包裝, 讓它擁有緩衝的能力.它也是衆多裝飾stream類型中的一個.
緩衝確定會經過減小往返backing store的次數來提高性能.
下面這個例子是把一個FileStream裝飾成20k的緩衝stream:
// 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 } }
經過預讀緩衝, 底層的stream會在讀取1字節後, 直接預讀了20000字節, 這樣咱們在另外調用ReadByte 19999次以後, 纔會再次訪問到FileStream.
這個例子是把BufferedStream和FileStream耦合到一塊兒, 實際上這個例子裏面的緩衝做用有限, 由於FileStream有一個內置的緩衝. 這個例子也只能擴大一下緩衝而已.
關閉BufferedStream就會關閉底層的backing store stream..
先寫到這裏, 略微有點跑題了, 可是.NET Core的Stream這部分沒寫完, 另開一篇文章再寫吧.