我花了很多时间熟悉.NET Stream类。通常我通过研究专业的商业级框架的课程设计来学到很多东西,但我不得不说这里的东西闻起来不太好。
System.IO.Stream是一个表示字节序列的抽象类。它有10个抽象方法/属性:Read, Write, Flush, Length, SetLength, Seek, Position, CanRead, CanWrite, CanSeek
。如此多的抽象成员使得派生起来很麻烦,因为你必须覆盖所有这些方法,即使大多数人最终只是抛出NotImplemented
。
Stream类的用户应该调用CanRead
,CanWrite
或CanSeek
来查找Stream的功能,或者我想只需要调用{{1} },Read
或Write
,看看它是否会抛出Seek
。它只是我,还是这个糟糕的设计?
虽然我想用NotImplemented
类设计选择许多尼特,但我想问的主要问题是:为什么他们不使用接口,如{{1} },Stream
,IReadable
,而不是?然后,新的Stream类可以从它支持的接口中优雅地派生。这不是面向对象的做事方式吗?或者我错过了什么?
更新:有人指出,值IWriteable
等可以在运行时更改 - 例如,如果ISeekable
已关闭 - 并且重点是。但是,我仍然不相信这是一个很好的设计。从我来自哪里,尝试从已经关闭的文件中读取是一个错误,或者至少是一个特殊情况。 (因此抛出异常是处理这种情况的一种自然方式。)
这是否意味着我每次从CanRead
开始FileStream
,我应该检查Read
?这是否意味着我应该设置锁定以避免竞争条件,如果值可能在Stream
呼叫和CanRead
呼叫之间的某个时间发生变化?
2010年8月7日更新:这里的共识似乎是Stream设计非常好。但是,让我再一次要求100%肯定:人们每次从流中读取内容时都会写这样的内容吗?
CanRead
答案 0 :(得分:10)
我认为课程设计得很好。我宁愿检查一个属性然后尝试做某事并且必须捕获异常。在具有多个“类型”的流类型的情况下,接口不足。从一个获取可读写流的方法返回什么类型?我同意设计不是真正的面向对象设计,但你真的想以这种方式处理流吗?如果流关闭或其他更改,某些属性可能会更改,在这种情况下会发生什么?
我认为这个问题带来了一个非常有趣的实验,为什么不尝试设计自己的流相关类。在CodePlex或Google Code上发布您的重新设计,这将是一个很好的学习经验,并将导致一个潜在有用的库供其他人使用。
答案 1 :(得分:9)
使用接口意味着无法在运行时更改“CanRead”的值。 “FileStream”类根据文件的当前状态更改“CanRead”属性。
答案 2 :(得分:7)
他们可能没有使用接口,因为当时没有扩展方法。如果您希望每个流都具有默认的ReadByte方法,则需要使用类。
几个月前我写了blog post为什么我不喜欢IO.Stream和我认为应该做的事情。从本质上讲,它归结为流不是非常类型安全的。答案 3 :(得分:5)
接口可能被过度使用,这将是其中一种情况。我认为目前的设计很棒。流可以在运行时更改功能这一事实意味着IReadable / IWritable / ISeekable不会消除对CanRead,CanWrite和CanSeek的需求,因此除了消除少数存根方法和属性之外,您只会增加复杂性而无法获得实际收益。在派生类中。
就个人而言,我更喜欢流类更容易使用而不是更容易编写,因为你将编写一次并多次使用它。
答案 4 :(得分:4)
Class Stream使用可选功能模式,您可以阅读更多here。
答案 5 :(得分:3)
回答这个问题:可能。
我非常同意@ Strilanc的答案,是的,它实施得很差,但我想我会继续发表我的想法。
虽然使用可组合接口和扩展方法(现在它们可用)实现这些东西肯定会更清晰,但.NET 1没有这些功能,所以我可以理解为什么他们选择设计{{3那样。
然而,既然我们有非常有用的构造,如泛型和扩展方法,我认为是时候重新访问很多原始类并用Stream标记它们。当然,您首先需要替换API。
ObsoleteAttribute将允许这些类作为框架的一部分保留,同时阻止它们的使用。然后可以在框架的未来版本中删除这些类,甚至可以保留在框架的不同配置文件中但不可用。
这将适用于所有非泛型集合(不仅仅是ObsoleteAttribute命名空间中的那些)和“奇怪的抽象”。 System.Collections * Collection类是很好的例子,但整个框架中有更多这样的例子。
我知道我的“奇怪的抽象”评论非常模糊,但我想测试石蕊是如果你必须在派生类中抛出NotImplementedExceptions,那么可能有更好的方法。
总而言之,我不喜欢System.Text.RegularExpressions的设计,但除非有更好的API,否则你可能只需处理它。为现有的Stream编写适配器/包装器可能是你现在最好的选择,而这正是我目前所做的。
答案 6 :(得分:1)
您可以采用(Java *)方法编写一个继承基本MyStream
类的大多数空Stream
类,但提供了大多数成员方法(例如,CanSeek()
并使用合理的默认行为来嵌入它们(例如,抛出NotImplemented
)。那么你真正的课程只需扩展你的MyStream
课程,实现你真正需要的两三种方法。
当您重复使用MyStream
课程时,您将节省大量重新发明的轮子。
*这在Java库中称为抽象适配器类。
答案 7 :(得分:0)
我对stream类的问题是Length属性 - 它没有指定如何实现未知,未指定或无限长度的流!
答案 8 :(得分:0)
我认为Stream
类的设计很差,因为它实际上违反了单一职责原则。我认为阅读与写作完全不同。但是Stream类允许读取和写入同一流。而且,如果我们真的想到了一个真正的流,那么我们几乎无法想象如何寻找一个流。实际上,您可以跳过流中的某些字节,但不能向后移动(特别是在写入时)。想象一下,一股流淌着血红色液体的水流。流中的红色液体流入或流出-根据流动的方向而产生或消耗。无法同时在两个方向上查看流。试图同时看向两个方向会导致从.NET已知的Stream
类。更清楚地说-封装一个破坏SRP的类会产生更多的代码味道。只需将BufferedStream
类源代码放入Flush()
方法中即可。
我来自Java世界,对我来说,输入和输出流分离是很自然的事情。但是,您可能喜欢使用文件并来回查找文件-它没有任何问题,除非您意识到文件就像是存储在某个位置的缓冲区,而不是双向流。您可以使用流填充该缓冲区,然后使用另一个流将其读回。这两个流操作仍然在逻辑上明显分开。