问题 .NET流功能 - CanXXX测试安全吗?
.NET中使用了一种相当常见的模式来测试类的功能。这里我将使用Stream类作为示例,但该问题适用于使用此模式的所有类。
该模式是提供一个名为CanXXX的布尔属性,以指示该类上的功能XXX可用。例如,Stream类具有CanRead,CanWrite和CanSeek属性,以指示可以调用Read,Write和Seek方法。如果属性值为false,则调用相应的方法将导致抛出NotSupportedException。
从流类的MSDN文档:
根据底层数据源或存储库,流可能仅支持其中一些功能。应用程序可以使用CanRead,CanWrite和CanSeek属性查询流的功能。
和CanRead属性的文档:
在派生类中重写时,获取一个值,指示当前流是否支持读取。
如果从Stream派生的类不支持读取,则对Read,ReadByte和BeginRead方法的调用会抛出NotSupportedException。
我看到很多代码都是按照以下方式编写的:
if (stream.CanRead)
{
stream.Read(…)
}
请注意,没有同步代码,例如,以任何方式锁定流对象 - 其他线程可能正在访问它或它引用的对象。也没有捕获NotSupportedException的代码。
MSDN文档未声明属性值不能随时间更改。实际上,当流关闭时,CanSeek属性会更改为false,从而演示这些属性的动态特性。因此,没有合同保证在上面的代码片段中调用Read()不会抛出NotSupportedException。
我希望有很多代码可以解决这个潜在的问题。我想知道那些发现这个问题的人是如何解决它的。这里适合哪些设计模式?
我也很感激这个模式的有效性的评论(CanXXX,XXX()对)。对我来说,至少在Stream类的情况下,这代表了一个试图做太多的类/接口,应该分成更基本的部分。缺乏紧密的,有文件记录的合同使测试变得不可能,实施更加困难!
2544
2017-07-31 06:04
起源
答案:
在不知道对象内部的情况下,您必须假设“标志”属性太易于在多个线程中修改对象时无法依赖。
我已经看到这个问题更常见于只读集合而不是流,但我觉得这是相同设计模式的另一个例子,并且适用相同的参数。
为了澄清,.NET中的ICollection接口具有属性IsReadOnly,该属性旨在用作指示集合是否支持修改其内容的方法。就像流一样,此属性可以随时更改,并将导致抛出InvalidOperationException或NotSupportedException。
围绕这个问题的讨论通常归结为:
- 为什么没有IReadOnlyCollection接口呢?
- 是否NotSupportedException是一个好主意。
- 具有“模式”与不同具体功能的优缺点。
模式很少是好事,因为你被迫处理不止一套“行为”;有一些可以随时切换模式的东西要糟糕得多,因为你的应用程序现在也必须处理多个“一组”行为。然而,仅仅因为它可能会将某些内容分解为更加谨慎的功能并不一定意味着你总是应该这样做,特别是当它分开时无助于降低手头任务的复杂性。
我个人认为,你必须选择最接近你认为班级消费者会理解的心理模型的模式。如果您是唯一的消费者,请选择您最喜欢的型号。在Stream和ICollection的情况下,我认为对这些进行单一定义更接近于在类似系统中多年开发建立的心理模型。当你谈论流时,你会谈论文件流和内存流,而不是它们是可读还是可写。同样,当你谈论集合时,你很少用“可写性”来引用它们。
我对此的经验法则:总是寻找一种方法将行为分解为更具体的界面,而不是具有“模式”的操作,只要它与简单的心理模型相称。如果很难将单独的行为视为单独的事物,请使用基于模式的模式并对其进行记录 非常 清晰。
3
2017-07-31 13:10
好的,这是另一次尝试,希望比我的其他答案更有用......
遗憾的是,MSDN没有对如何做出任何具体保证 CanRead
/CanWrite
/CanSeek
可能随着时间而改变。我认为假设如果流是可读的,它将继续可读,直到它被关闭是合理的 - 并且对于其他属性也是如此
在某些情况下,我认为流是合理的 成为 可以在以后查找 - 例如,它可以缓冲它读取的所有内容,直到它到达底层数据的末尾,然后允许在其中进行搜索,让客户端重新读取数据。不过,我认为适配器忽略这种可能性是合理的。
这应该照顾除了最病态的病例。 (Streams几乎被设计为造成严重破坏!)将这些要求添加到现有文档中是理论上的重大变化,即使我怀疑99.9%的实现已经遵守它。不过,它可能值得建议 连。
现在,关于是否使用“基于能力”的API(如 Stream
)和基于接口的...我看到的基本问题是.NET不能提供指定变量必须是对多个接口的实现的引用的能力。例如,我不能写:
public static Foo ReadFoo(IReadable & ISeekable stream)
{
}
如果它 没有 允许这样,它可能是合理的 - 但如果没有这个,你最终会爆发出潜在的接口:
IReadable
IWritable
ISeekable
IReadWritable
IReadSeekable
IWriteSeekable
IReadWriteSeekable
我认为这比目前的情况更糟糕 - 虽然我想我 将 支持公正的想法 IReadable
和 IWritable
除了现有的 Stream
类。这将使客户更容易以声明方式表达他们所需的内容。
同 代码合同,API 能够 无可否认地声明他们提供了什么以及他们需要什么:
public Stream OpenForReading(string name)
{
Contract.Ensures(Contract.Result<Stream>().CanRead);
...
}
public void ReadFrom(Stream stream)
{
Contract.Requires(stream.CanRead);
...
}
我不知道静态检查器可以帮助多少 - 或者它如何应对流的事实 做 当它们关闭时变得不可读/不可写。
4
2017-08-01 11:22
stream.CanRead只检查底层流是否有可能读取。它没有说明实际读数是否可行(例如磁盘错误)。
如果您使用任何* Reader类,则无需捕获NotImplementedException,因为它们都支持读取。只有* Writer会有CanRead = False并抛出该异常。如果您知道流支持读取(例如您使用StreamReader),则恕我直言,无需进行额外检查。
您仍然需要捕获异常,因为读取期间的任何错误都会抛出它们(例如磁盘错误)。
另请注意,未记录为线程安全的任何代码都不是线程安全的。通常静态成员是线程安全的,但实例成员不是 - 但是,需要检查每个类的文档。
1
2017-07-31 06:22
从你的问题和随后的所有评论中,我猜你的问题在于所述合同的清晰度和“正确性”。声明的合同是MSDN在线文档中的内容。
你所指出的是文档中缺少一些东西,迫使人们对合同做出假设。更具体地说,因为没有任何关于流的可读性属性的波动性的说法,所以可以做出的唯一假设是它是 可能 为一个 NotSupportedException
抛出,无论相应的CanRead属性的值是几毫秒(或更多)之前的值。
我认为需要继续下去 意图 在这种情况下,这个接口,即:
- 如果您使用多个线程,则所有投注均已关闭;
- 直到你在接口上调用可能会改变流状态的东西,你可以安全地假设它的值
CanRead
是不变的。
尽管如此,Read *方法 可能 可能抛出一个 NotSupportedException
。
相同的参数可以应用于所有其他Can *属性。
1
2017-07-31 11:22
我也很感激这个模式的有效性的评论(CanXXX,XXX()对)。
当我看到这种模式的实例时,我通常会期望这样:
一个 参数少 CanXXX
会员将始终返回相同的值,除非......
......在...的存在 CanXXXChanged
事件,其中一个参数少 CanXXX
可能在该事件发生之前和之后返回不同的值;但如果不触发事件,它就不会改变。
一个 参数 CanXXX(…)
成员可以为不同的参数返回不同的值;但对于相同的参数,它可能会返回相同的值。那是, CanXXX(constValue)
很可能保持不变。
我在这里很谨慎:如果 stream.CanWriteToDisk(largeConstObject)
回报 true
现在,假设它将永远返回是合理的 true
在将来?可能不是,所以也许它取决于上下文是否参数化 CanXXX(…)
将为相同的参数返回相同的值。
打电话给 XXX(…)
只有这样才能成功 CanXXX
回报 true
。
话虽如此,我同意 Stream
使用这种模式有点问题。至少在理论上,如果在实践中可能没那么多。
1
2018-05-18 18:55
这听起来更像是一个理论问题,而不是一个实际问题。我真的不能想到任何流会变得不可读/不可写的情况 其他 而不是因为它被关闭。
可能存在极端情况,但我不希望它们经常出现。我不认为绝大多数代码需要担心这一点。
然而,这是一个有趣的哲学问题。
编辑:解决CanRead等是否有用的问题,我相信它们仍然存在 - 主要用于参数验证。例如,仅仅因为一个方法需要一个它在某个时候想要读取的流并不意味着它想要在方法的开头就读它,但这就是理想情况下应该执行参数验证的地方。这与检查参数是否为null和抛出没有什么不同 ArgumentNullException
而不是等待 NullReferenceException
当你第一次被解除引用时被抛出。
也, CanSeek
略有不同:在某些情况下,您的代码可以很好地处理可搜索和不可搜索的流,但在可搜索的情况下效率更高。
这确实依赖于“可寻找性”等保持一致 - 但正如我所说,这在现实生活中似乎是真的。
好的,我们试着用另一种方式......
除非你在内存中阅读/寻找,并且你已经确定有足够的数据,或者你是在预分配的缓冲区内写的,所以 总是 事情会出错。磁盘发生故障或填满,网络崩溃等等 做 在现实生活中发生,所以你总是需要以一种能够在失败中存活的方式进行编码(或者当它无关紧要时有意识地选择忽略问题)。
如果您的代码在磁盘发生故障的情况下可以做正确的事情,那么它可能会幸免于难 FileStream
从可写转为不可写。
如果 Stream
确实有合同,他们必须是 令人难以置信 弱 - 您无法使用静态检查来证明您的代码将始终有效。你能做的最好的事情就是证明它在失败时做的是正确的。
我不相信 Stream
随时都会改变。虽然我当然接受它可以更好地记录,但我不接受它被“彻底打破”的想法。这将是 更多 如果我们真的无法在现实生活中使用它......如果它可能比现在更加破碎,那么它在逻辑上并非如此 全然 破碎。
我对框架有更大的问题,例如相对较差的日期/时间API状态。他们已成为一个 批量 在过去几个版本中更好,但它们仍然缺少很多功能(例如) 乔达时间。缺乏内置的不可变集合,对语言不变性的支持不足等等 - 这些都是导致我的真正问题 实际 头痛。我宁愿看到他们解决,也不愿花时间 Stream
在我看来,这是一个有点棘手的理论问题,在现实生活中几乎没有问题。
0
2017-07-31 06:46