Потоковые возможности .NET - безопасен ли тест CanXXX? - PullRequest
11 голосов
/ 31 июля 2009

В .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(…)
}

Обратите внимание, что нет кода синхронизации, скажем, для какой-либо блокировки объекта потока & mdash; другие потоки могут обращаться к нему или объектам, на которые он ссылается. Также нет кода для перехвата NotSupportedException.

В документации MSDN не указано, что значение свойства не может изменяться со временем. Фактически свойство CanSeek изменяется на false при закрытии потока, демонстрируя динамическую природу этих свойств. Таким образом, нет никакой договорной гарантии, что вызов Read () в приведенном выше фрагменте кода не вызовет исключение NotSupportedException.

Я ожидаю, что существует много кода, который страдает от этой потенциальной проблемы. Интересно, как те, кто выявил эту проблему, обратились к ней. Какие шаблоны дизайна здесь уместны?

Буду также признателен за комментарии о допустимости этого шаблона (пары CanXXX, XXX ()). Для меня, по крайней мере, в случае класса Stream, это представляет класс / интерфейс, который пытается сделать слишком много и должен быть разбит на более фундаментальные части. Отсутствие жесткого документированного контракта делает невозможным тестирование, а внедрение еще сложнее!

Ответы [ 6 ]

4 голосов
/ 01 августа 2009

Хорошо, вот еще одна попытка, которая, будем надеяться, будет более полезной, чем мой другой ответ ...

К сожалению, MSDN не дает каких-либо конкретных гарантий о том, как CanRead / CanWrite / CanSeek может меняться со временем. Я думаю, что было бы разумно предположить, что если поток доступен для чтения, он будет оставаться читаемым до тех пор, пока он не будет закрыт - и то же самое будет сохраняться для других свойств

В некоторых случаях я думаю, что было бы разумно, чтобы поток стал доступным для поиска позже - например, он может буферизовать все, что он читает, до достижения конца базовых данных, а затем разрешить поиск внутри это впоследствии, чтобы позволить клиентам перечитать данные. Однако я думаю, что для адаптера было бы разумно игнорировать эту возможность.

Это должно позаботиться обо всех, кроме самых патологических случаев. (Потоки в значительной степени предназначены для того, чтобы вызывать хаос!) Добавление этих требований к существующей документации является теоретически принципиальным изменением, хотя я подозреваю, что 99,9% реализаций уже будут этому подчиняться. Тем не менее, возможно, стоит предложить Connect .

Теперь, что касается обсуждения между использованием «основанного на возможностях» 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);

    ...
}

Я не знаю, насколько статическая проверка может помочь с этим - или как она справляется с тем фактом, что потоки do становятся нечитаемыми / недоступными для записи, когда они закрыты.

3 голосов
/ 31 июля 2009

Не зная внутренности объекта, вы должны предположить, что свойство "flag" слишком изменчиво, чтобы полагаться на него, когда объект изменяется в нескольких потоках.

Я видел, что этот вопрос чаще задавался о коллекциях только для чтения, чем о потоках, но я чувствую, что это еще один пример того же шаблона проектирования, и применяются те же аргументы.

Для пояснения, интерфейс ICollection в .NET имеет свойство IsReadOnly, которое предназначено для использования в качестве индикатора того, поддерживает ли коллекция методы для изменения своего содержимого. Как и в случае с потоками, это свойство может измениться в любое время и вызвать исключение InvalidOperationException или NotSupportedException.

Дискуссии вокруг этого обычно сводятся к:

  • Почему вместо этого нет интерфейса IReadOnlyCollection?
  • Является ли NotSupportedException хорошей идеей.
  • Плюсы и минусы наличия «режимов» в сравнении с конкретной конкретной функциональностью.

Режимы редко бывают полезными, поскольку вы вынуждены иметь дело с более чем одним «набором» поведения; наличие чего-то, что может переключать режимы в любое время, значительно хуже, так как теперь вашему приложению приходится иметь дело с более чем одним «набором» поведения. Однако то, что можно разбить что-то на более незаметную функциональность, не обязательно означает, что вы всегда должны это делать, особенно если разбивание чего-либо не приводит к снижению сложности выполняемой задачи.

Мое личное мнение заключается в том, что вы должны выбрать модель, наиболее близкую к ментальной модели, которую, как вы понимаете, понимают потребители вашего класса. Если вы единственный потребитель, выберите любую понравившуюся вам модель. В случае Stream и ICollection, я думаю, что одно их определение намного ближе к ментальной модели, построенной годами разработки в подобных системах. Когда вы говорите о потоках, вы говорите о файловых потоках и потоках памяти, а не о том, являются ли они читаемыми или записываемыми. Точно так же, когда вы говорите о коллекциях, вы редко ссылаетесь на них с точки зрения «возможности записи».

Мое эмпирическое правило: всегда ищите способ разбить поведение на более конкретные интерфейсы, вместо того, чтобы использовать «режимы» работы, если это дополняет простую ментальную модель. Если трудно рассматривать отдельные варианты поведения как отдельные вещи, используйте шаблон на основе режима и четко документируйте его очень .

1 голос
/ 18 мая 2013

Буду также признателен за комментарии о допустимости этого паттерна (пары CanXXX, XXX ()).

Когда я вижу экземпляр этого паттерна, я обычно ожидаю этого:

  1. A без параметров CanXXX элемент всегда будет возвращать одно и то же значение, если только…

  2. … при наличии события CanXXXChanged , где без параметров CanXXX может возвращать другое значение до и после возникновения этого события; но это не изменится без запуска события.

  3. A параметризованный CanXXX(…) член может возвращать разные значения для разных аргументов; но для тех же аргументов, он может вернуть то же значение. То есть CanXXX(constValue), вероятно, останется неизменным.

    Я здесь осторожен: если stream.CanWriteToDisk(largeConstObject) возвращает true сейчас, разумно ли предположить, что он всегда вернет true в будущем? Вероятно, нет, поэтому, возможно, это зависит от контекста, будет ли параметризованный CanXXX(…) возвращать то же значение для тех же аргументов или нет.

  4. Вызов XXX(…) может быть успешным, только если CanXXX вернет true.


При этом я согласен, что Stream использование этого шаблона несколько проблематично. По крайней мере, теоретически, если не так много на практике.

1 голос
/ 31 июля 2009

Исходя из вашего вопроса и всех последующих комментариев, я предполагаю, что ваша проблема заключается в ясности и «правильности» заявленного контракта. Указанный договор соответствует тому, что содержится в сетевой документации MSDN.

Что вы указали, так это то, что в документации чего-то не хватает, что заставляет делать предположения о контракте. Более конкретно, поскольку ничего не сказано о волатильности свойства читаемости потока, можно сделать только одно предположение, что возможно для NotSupportedException, который будет выброшен, независимо от того, что значение соответствующего свойства CanRead было за несколько миллисекунд (или более) ранее.

Я думаю, что в этом случае нужно перейти на намерение этого интерфейса, то есть:

  1. Если вы используете более одного потока, все ставки отключены;
  2. до тех пор, пока вы не вызовете что-то на интерфейсе, которое потенциально изменит состояние потока, вы можете смело предполагать, что значение CanRead является инвариантным.

Несмотря на вышесказанное, чтение * методов может потенциально вызвать NotSupportedException.

Тот же аргумент может быть применен ко всем другим свойствам Can *.

1 голос
/ 31 июля 2009

stream.CanRead просто проверяет, имеет ли базовый поток возможность чтения. В нем ничего не говорится о том, возможно ли реальное чтение (например, ошибка диска).

Нет необходимости перехватывать NotImplementedException, если вы использовали какой-либо из классов * Reader, поскольку все они поддерживают чтение. Только * Writer будет иметь CanRead = False и выдавать это исключение. Если вы знаете, что поток поддерживает чтение (например, вы использовали StreamReader), ИМХО нет необходимости делать дополнительную проверку.

Вам все еще нужно перехватывать исключения, поскольку любая ошибка во время чтения приведет к их выбросу (например, ошибка диска).

Также обратите внимание, что любой код, который не задокументирован как поточно-ориентированный, не является поточно-ориентированным. Обычно статические члены являются потокобезопасными, но члены экземпляров - нет, однако необходимо проверить документацию для каждого класса.

0 голосов
/ 31 июля 2009

Это больше похоже на теоретическую проблему, чем на практическую. Я не могу вспомнить ни одной ситуации, в которой поток стал бы нечитаемым / не записываемым другим , чем из-за его закрытия.

Вполне могут быть и угловые случаи, но я бы не ожидал, что они вообще будут часто появляться. Я не думаю, что подавляющее большинство кода должно беспокоиться об этом.

Хотя это интересная философская проблема.

РЕДАКТИРОВАТЬ: Обращаясь к вопросу о том, полезны ли CanRead и т. Д., Я считаю, что они все еще - в основном для проверки аргументов. Например, просто потому, что метод принимает поток, который он захочет прочитать в какой-то момент, не означает, что он хочет прочитать его прямо в начале метода, но именно здесь в идеале должна выполняться проверка аргумента. Это на самом деле ничем не отличается от проверки, является ли параметр нулевым, и выдачи ArgumentNullException вместо ожидания выдачи NullReferenceException, когда вы впервые разыменовываете его.

Кроме того, CanSeek немного отличается: в некоторых случаях ваш код вполне может справиться как с поисковыми, так и с неисследуемыми потоками, но с большей эффективностью в случае с поиском.

Это зависит от того, что «поиск» и т. Д. Остается неизменным - но, как я уже сказал, это похоже на правду в реальной жизни.


Хорошо, давайте попробуем изложить это по-другому ...

Если вы не читаете / ищите в памяти и уже убедились, что данных достаточно, или вы записываете в предварительно выделенный буфер, то всегда может привести к ошибкам. Диски выходят из строя или заполняются, сети разрушаются и т. Д. Такие вещи do случаются в реальной жизни, поэтому вам всегда нужно кодировать таким образом, чтобы пережить сбой (или сознательно выбрать игнорировать проблему, когда это не действительно важно).

Если ваш код может правильно действовать в случае сбоя диска, скорее всего, он сможет пережить FileStream, превратившись из записываемого в недоступный для записи.

Если бы у Stream были твердые контракты, они должны были бы быть невероятно слабыми - вы не могли бы использовать статическую проверку, чтобы доказать, что ваш код всегда будет работать. Лучшее, что вы можете сделать, это доказать, что он поступил правильно перед лицом неудачи.

Я не верю, что Stream скоро изменится. Хотя я, конечно, признаю, что это может быть лучше задокументировано, я не принимаю идею, что это «полностью сломано». Было бы больше сломано, если бы мы не могли использовать его в реальной жизни ... и если бы оно могло быть более сломанным, чем сейчас, логически оно не полностью сломано.

У меня гораздо большие проблемы с фреймворком, такие как относительно плохое состояние API даты / времени. Они стали на много лучше в последних двух версиях, но им все еще не хватает многих функций (скажем) Joda Time . Отсутствие встроенных неизменяемых коллекций, плохая поддержка неизменяемости в языке и т. Д. - это реальные проблемы, которые вызывают у меня настоящих головных болей. Я бы предпочел увидеть их адресованными, чем потратить целую вечность на Stream, что мне кажется несколько неразрешимой теоретической проблемой, которая вызывает мало вопросов в реальной жизни.

...