该文章综合了几本书的内容.程序员
某咖啡店供应咖啡, 客户买咖啡的时候能够添加若干调味料, 最后要求算出总价钱.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这部分没写完, 另开一篇文章再写吧.