使用C# (.NET Core) 實現裝飾模式 (Decorator Pattern) 並介紹 .NET/Core的Stream

solenovex發表於2018-04-03

 

該文章綜合了幾本書的內容.

某咖啡店專案的解決方案

某咖啡店供應咖啡, 客戶買咖啡的時候可以新增若干調味料, 最後要求算出總價錢.

Beverage是所有咖啡飲料的抽象類, 裡面的cost方法是抽象的. description變數在每個子類裡面都需要設定(表示對咖啡的描述).

每個子類實現cost方法, 表示咖啡的價格.

除了這些類之外, 還有調味品:

問題是調味品太多了, 如果使用繼承來做的話, 各種組合簡直是類的爆炸.

而且還有其他的問題, 如果牛奶的價格上漲了怎麼辦? 如果再加一種焦糖調料呢?

另一種解決方案

父類裡面有調味料的變數(bool), 並且在父類裡面直接實現cost方法(通過是否有某種調味料來計算價格).

子類override父類的cost方法, 但是也呼叫父類的cost方法, 這樣就可以把子類這個咖啡的價格和父類裡計算出來的調味料的價格加到一起算出最終的價格了.

下面就是:

看起來不錯, 那麼, 問題來了:

  • 調味料價格變化的話就不得不修改現有程式碼了
  • 如果有新的調味料那麼久必須在父類裡面新增新的方法, 並且修改cost方法了.
  • 可能某些調味料根本不適用於某些咖啡
  • 如果某個客戶想要兩個摩卡(調味料)怎麼辦?

設計原則

類應該對擴充套件開放 而對修改關閉.

裝飾模式

使用裝飾模式, 我們可以購買一個咖啡, 並且在執行時使用調味料對它進行裝飾.

大約步驟如下:

  1. 買某種咖啡
  2. 使用mocha調味料裝飾它
  3. 使用whip調味料裝飾它
  4. 呼叫cost方法, 並且使用委託來計算調味料的總價格

 

 

到目前我知道了這些:

  • 裝飾器的父類和它所要裝飾的物件的父類是一樣的
  • 可以使用多個裝飾器來裝飾某物件
  • 既然裝飾器和被裝飾物件的父類是一樣的, 那傳遞的時候就傳遞被裝飾過的物件就好了.
  • 裝飾器會在委託給它要裝飾的物件之前和/或之後新增自己的行為以便來完成餘下的工作.
  • 物件可以在任意時刻被裝飾, 所以可以在執行時使用任意多個裝飾器對物件進行裝飾.

裝飾模式定義

 動態的對某個物件進行擴充套件(附加額外的職責), 裝飾器是除了繼承之外的另外一種為物件擴充套件功能的方法.

下面看看該模式的類圖:

重新設計

這個就很好理解了, 父類都是Beverage(飲料), 左邊是四種具體實現的咖啡, 右邊上面是裝飾器的父類, 下面是具體的裝飾器(調味料).

這裡需要注意的是, 裝飾器和咖啡都繼承於同一個父類只是因為需要它們的型別匹配而已, 並不是要繼承行為.

.NET Core 程式碼實現

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;
        }
    }
}

 

HouseBlend:
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()}");
        
        }
    }
}

 

執行結果:

 

.NET Core裡面的裝飾模式例子: Streams 和 I/O

首先需要知道, System.IO名稱空間是低階I/O功能的大本營.

Stream的結構

.NET Core裡面的Stream主要是三個概念: 儲存(backing stores 我不知道怎麼翻譯比較好), 裝飾器, 介面卡.

backing stores是讓輸入和輸出發揮作用的端點, 例如檔案或者網路連線. 就是下面任意一點或兩點:

  • 一個源, 從它這裡位元組可以被順序的讀取
  • 一個目的地, 位元組可以被連續的寫入.

程式設計師可以通過Stream類來發揮backing store的作用. Stream類有一套方法, 可以進行讀取, 寫入, 定位等操作. 個陣列不同的是, 陣列是把所有的資料都一同放在了記憶體裡, 而stream則是順序的/連續的處理資料, 要麼是一次處理一個位元組, 要麼是一次處理特定大小(不能太大, 可管理的範圍內)的資料.

於是, stream可以用比較小的固定大小的記憶體來處理無論多大的backing store.

中間的那部分就是裝飾器Stream. 它符合裝飾模式.

從圖中可以看到, Stream又分為兩部分:

  • Backing Store Streams: 硬連線到特定型別的backing store, 例如FileStream和NetworkStream
  • Decorator Streams 裝飾器Stream: 使用某種方式把資料進行了轉化, 例如DeflateStream和CryptoStream.

裝飾器Stream有如下結構性的優點(參考裝飾模式):

  • 無需讓backing store stream去實現例如壓縮, 加密等功能.
  • 裝飾的時候介面(interface)並沒有變化
  • 可以在執行時進行裝飾
  • 可以串聯裝飾(先後進行多個裝飾)

backing store和裝飾器stream都是按位元組進行處理的. 儘管這很靈活和高效, 但是程式一般還是採用更高階別的處理方式例如文字或者xml.

介面卡通過使用特殊化的方法把類裡面的stream進行包裝成特殊的格式. 這就彌合了上述的間隔.

例如 text reader有一個ReadLine方法, XML writer又WriteAttributes方法.

注意: 介面卡包裝了stream, 這點和裝飾器一樣, 但是不一樣的是, 介面卡本身並不是stream, 它一般會把所有針對位元組的方法都隱藏起來. 所以本文就不介紹介面卡了.

總結一下:

backing store stream 提供原始資料, 裝飾器stream提供透明的轉換(例如加密); 介面卡提供方法來處理高階別的型別例如字串和xml.

想要連成串的話, 秩序把物件傳遞到另一個物件的建構函式裡.

使用Stream

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裡面的位置.

定址 Seek

CanSeek為true的話, Stream就可以被定址. 可以查詢和修改可定址的stream(例如檔案stream)的長度, 也可以隨時修改讀取和寫入的位置.

Position屬性就是所需要的, 它是相對於stream開始位置的.

Seek方法就允許你移動到當前位置或者stream的尾部.

注意改變FileStream的Position會花去幾微秒. 如果是在大規模迴圈裡面做這個操作的話, 建議使用MemoryMappedFile類.

對於不可定址的Stream(例如加密Stream), 想知道它的長度只能是把它讀完. 而且你要是想讀取前一部分的話必須關閉stream, 然後再開始一個全新的stream才可以.

關閉和Flush

Stream用完之後必須被處理掉(dispose)來釋放底層資源例如檔案和socket處理. 通常使用using來實現.

  • Dispose和Close方法功能上是一樣的.
  • 重複close和flush一個stream不會報錯.

關閉裝飾器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相應的部分.

Backing Store Stream

FileStream

檔案流

構建一個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.ReadAllText(返回string)
  • File.ReadAllLines(返回string陣列) 
  • File.ReadAllBytes(返回byte陣列)

下面的方法直接寫入整個檔案:

  • File.WriteAllText
  • File.WriteAllLines
  • File.WriteAllBytes
  • File.AppendAllText (很適合附加log檔案)

還有一個靜態方法叫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

MemoryStream在隨機訪問不可定址的stream時就有用了.

如果你知道源stream的大小可以接受, 你就可以直接把它複製到MemoryStream裡:

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

 

可以通過ToArray方法把MemoryStream轉化成陣列.

GetBuffer方法也是同樣的功能, 但是因為它是直接把底層的儲存陣列的引用直接返回了, 所以會更有效率. 不過不幸的是, 這個陣列通常比stream的真實長度要長.

注意: Close和Flush 一個MemoryStream是可選的. 如果關閉了MemoryStream, 你就再也不能對它讀寫了, 但是仍然可以呼叫ToArray方法來獲取其底層的資料.

Flush則對MemoryStream毫無用處.

PipeStream

PipeStream通過Windows Pipe 協議, 允許一個程式(process)和另一個程式通訊.

分兩種:

  • 匿名程式(快一點), 允許同一個電腦內的父子程式單向通訊.
  • 命名程式(更靈活), 允許同一個電腦內或者同一個windows網路內的不同電腦間的任意兩個程式間進行雙向通訊

pipe很適合一個電腦上的程式間互動(IPC), 它並不依賴於網路傳輸, 這也意味著沒有網路開銷, 也不在乎防火牆.

注意: pipe是基於Stream的, 一個程式等待接受一串字元的同時另一個程式傳送它們.

PipeStream是抽象類.

具體的實現類有4個:

匿名pipe:

  • AnonymousePipeServerStream
  • AnonymousePipeClientStream

命名Pipe:

  • NamedPipeServerStream
  • NamePipeClientStream

命名Pipe

命名pipe的雙方通過同名的pipe進行通訊. 協議規定了兩個角色: 伺服器和客戶端. 按照下述方式進行通訊:

  • 伺服器例項化一個NamedPipeServerStream然後呼叫WaitForConnection方法.
  • 客戶端例項化一個NamedPipeClientStream然後呼叫Connect方法(可以設定超時).

然後雙方就可以讀寫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提供父子程式間的單向通訊. 流程如下:

  • 伺服器例項化一個AnonymousPipeServerStream, 並指定PipeDirection是In還是Out
  • 伺服器呼叫GetClientHandleAsString方法來獲取一個pipe的標識, 然後會把它傳遞給客戶端(通常是啟動子程式的引數 argument)
  • 子程式例項化一個AnonymousePipeClientStream, 指定相反的PipeDirection
  • 伺服器通過呼叫DisposeLocalCopyOfClientHandle釋放步驟2的本地處理, 
  • 父子程式間通過讀寫stream進行通訊

因為匿名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

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這部分沒寫完, 另開一篇文章再寫吧.

相關文章