Лучшие практики: выбрасывание исключений из свойств

111

Когда уместно генерировать исключение из получателя или установщика свойства? Когда это не подходит? Зачем? Ссылки на внешние документы по этой теме были бы полезны ... Google оказался на удивление мало.

Джон Сейгел
источник
См. Также: stackoverflow.com/questions/633944/…
Джон Б.
Также по теме: stackoverflow.com/questions/1488322/…
Брайан Расмуссен
1
Я прочитал оба этих вопроса, но ни один не ответил на этот вопрос полностью, ИМО.
Jon Seigel
По мере необходимости. Как предыдущие вопросы, так и ответы показывают, что разрешено и полезно вызывать исключения из геттера или сеттера, так что вы можете просто «быть умным».
Лекс Ли,

Ответы:

135

У Microsoft есть свои рекомендации по проектированию свойств на http://msdn.microsoft.com/en-us/library/ms229006.aspx.

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

Для индексаторов Microsoft указывает, что как геттеры, так и сеттеры допустимо генерировать исключения. Фактически, это делают многие индексаторы в библиотеке .NET. Наиболее частым исключением является ArgumentOutOfRangeException.

Есть несколько довольно веских причин, по которым вы не хотите создавать исключения в методах получения свойств:

  • Поскольку свойства «кажутся» полями, не всегда очевидно, что они могут вызвать исключение (по умолчанию); в то время как с помощью методов программисты обучаются ожидать и исследовать, являются ли исключения ожидаемым следствием вызова метода.
  • Геттеры используются во многих инфраструктурах .NET, таких как сериализаторы и привязка данных (например, в WinForms и WPF) - обработка исключений в таких контекстах может быстро стать проблематичной.
  • Получатели свойств автоматически оцениваются отладчиками, когда вы просматриваете или проверяете объект. Исключение здесь может сбивать с толку и замедлять ваши усилия по отладке. По тем же причинам нежелательно выполнять другие дорогостоящие операции со свойствами (например, доступ к базе данных).
  • Свойства часто используются в соглашении о цепочках: obj.PropA.AnotherProp.YetAnother- при таком синтаксисе становится проблематичным решить, куда вводить операторы исключения исключения.

В качестве примечания следует помнить, что то, что свойство не предназначено для создания исключения, не означает, что этого не произойдет; это может легко вызвать код, который это делает. Даже простое выделение нового объекта (например, строки) может привести к исключениям. Вы всегда должны писать свой код с осторожностью и ожидать исключений от всего, что вы вызываете.

Л.Бушкин
источник
41
Если вы столкнулись с фатальным исключением, например «нехватка памяти», не имеет значения, получаете ли вы исключение в свойстве или где-то еще. Если вы не получили его в свойстве, вы бы просто получили его через пару наносекунд от следующего объекта, который выделяет память. Вопрос не в том, "может ли свойство вызвать исключение?" Почти весь код может вызвать исключение из-за фатального состояния. Вопрос в том, должно ли свойство по умолчанию генерировать исключение как часть своего конкретного контракта.
Эрик Липперт,
1
Я не уверен, что понимаю аргумент в этом ответе. Например, в отношении привязки данных - и WinForms, и WPF специально написаны для правильной обработки исключений, создаваемых свойствами, и для обработки их как ошибок проверки - что является прекрасным (некоторые даже считают, что это лучший) способ обеспечить проверку модели предметной области. .
Павел Минаев
6
@Pavel - хотя и WinForms, и WPF могут корректно восстанавливаться после исключений в средствах доступа к свойствам, не всегда легко выявить такие ошибки и исправить их. В некоторых случаях (например, когда в WPF установщик шаблонов элементов управления выдает исключение) исключение автоматически проглатывается. Это может привести к болезненным сеансам отладки, если вы никогда раньше не сталкивались с такими случаями.
LBushkin
1
@ Стивен: Тогда насколько этот класс был вам полезен в исключительном случае? Если после этого вам пришлось написать защитный код для обработки всех этих исключений из-за одного сбоя и, предположительно, предоставить подходящие значения по умолчанию, почему бы не указать эти значения по умолчанию в вашем улове? В качестве альтернативы, если исключения свойств передаются пользователю, почему бы просто не выбросить исходное «InvalidArgumentException» или подобное, чтобы они могли предоставить недостающий файл настроек?
Zhaph - Бен Дугид
6
Есть причина, по которой это рекомендации, а не правила; никакие рекомендации не охватывают все безумные крайние случаи. Я бы, наверное, сам создал эти методы, а не свойства, но это суждение.
Эрик Липперт,
34

Нет ничего плохого в том, чтобы генерировать исключения из сеттеров. В конце концов, какой лучший способ указать, что значение недействительно для данного свойства?

Для геттеров это обычно не одобряется, и это можно довольно легко объяснить: геттер свойства, как правило, сообщает текущее состояние объекта; таким образом, единственный случай, когда геттер имеет смысл выбросить, - это когда состояние недействительно. Но также обычно считается хорошей идеей проектировать свои классы таким образом, чтобы просто невозможно было изначально получить недопустимый объект или перевести его в недопустимое состояние обычными средствами (т.е. всегда обеспечивать полную инициализацию в конструкторах и попробуйте сделать методы безопасными в отношении исключений в отношении допустимости состояния и инвариантов класса). Пока вы придерживаетесь этого правила, ваши геттеры никогда не должны попадать в ситуацию, когда они должны сообщать о недопустимом состоянии и, следовательно, никогда не бросать.

Есть одно известное мне исключение, и оно на самом деле довольно серьезное: реализация любого объекта IDisposable. Disposeспециально предназначен как способ привести объект в недопустимое состояние, и в этом случае даже есть специальный класс исключения ObjectDisposedException. Совершенно нормально бросать ObjectDisposedExceptionиз любого члена класса, включая средства получения свойств (и исключая его Disposeсамого), после того, как объект был удален.

Павел Минаев
источник
4
Спасибо, Павел. Этот ответ переходит в «почему» вместо того, чтобы просто повторять, что генерировать исключение из свойств - не лучшая идея.
SolutionYogi,
1
Мне не нравится идея о том, что абсолютно все члены IDisposableдолжны стать бесполезными после a Dispose. Если для вызова члена потребуется использовать ресурс, который Disposeстал недоступным (например, член будет читать данные из потока, который был закрыт), член должен выдавать, ObjectDisposedExceptionа не утечку, например ArgumentException, но если у кого-то есть форма со свойствами, которые представляют значений в определенных полях, было бы гораздо полезнее разрешить чтение таких свойств после удаления (с получением последних введенных значений), чем требовать ...
supercat 08
1
... это Disposeдолжно быть отложено до тех пор, пока не будут прочитаны все такие свойства. В некоторых случаях, когда один поток может использовать блокирующее чтение объекта, в то время как другой его закрывает, и когда данные могут поступать в любое время до Disposeэтого, может быть полезно Disposeотключить входящие данные, но разрешить чтение ранее полученных данных. Не следует навязывать искусственное различие между ситуациями Closeи Disposeв тех случаях, когда в противном случае ничего не существовало бы.
supercat 08
Понимание причины правила позволяет узнать, когда его нарушать (Раймонд Чен). В этом случае мы видим, что если есть неисправимая ошибка любого типа, вы не должны скрывать ее в геттере, поскольку в таких случаях приложение должно завершить работу как можно скорее.
Бен
Я пытался подчеркнуть, что ваши методы получения свойств обычно не должны содержать логики, допускающей неисправимые ошибки. Если это так, может быть, лучше использовать Get...метод. Исключением является ситуация, когда вам необходимо реализовать существующий интерфейс, который требует от вас предоставления свойства.
Павел Минаев
24

Это почти никогда не подходит для геттера, а иногда подходит для сеттера.

Лучшим источником ответов на подобные вопросы является «Руководство по разработке каркаса» Квалины и Абрамса; он доступен в виде переплетенной книги, и большие его части также доступны в Интернете.

Из раздела 5.2: Дизайн недвижимости

ИЗБЕГАЙТЕ исключения исключений из методов получения свойств. Получатели свойств должны быть простыми операциями и не должны иметь предварительных условий. Если получатель может генерировать исключение, его, вероятно, следует переделать в метод. Обратите внимание, что это правило не применяется к индексаторам, где мы ожидаем исключений в результате проверки аргументов.

Обратите внимание, что это правило применимо только к получателям свойств. Можно генерировать исключение в установщике свойств.

Эрик Липперт
источник
2
Хотя (в целом) я согласен с такими руководящими принципами, было бы полезно предоставить дополнительную информацию о том, почему им следует следовать - и какие последствия могут возникнуть, если их игнорировать.
LBushkin
3
Как это относится к одноразовым объектам и рекомендациям, которые вы должны рассмотреть ObjectDisposedExceptionпосле того, как объект был Dispose()вызван и что-то впоследствии запрашивает значение свойства? Похоже, что руководство должно быть таким: «Избегайте генерирования исключений из методов получения свойств, если только объект не был удален, и в этом случае вам следует рассмотреть возможность создания исключения ObjectDisposedExcpetion».
Скотт Дорман,
4
Дизайн - это искусство и наука поиска разумных компромиссов перед лицом противоречивых требований. Любой способ кажется разумным компромиссом; Я не удивлюсь, если удаленный объект добавит свойство; и не удивлюсь, если это не так. Поскольку использование удаленного объекта - ужасная практика программирования, было бы неразумно ожидать чего-либо.
Эрик Липперт,
1
Другой сценарий, в котором совершенно допустимо генерировать исключения из геттеров, - это когда объект использует инварианты класса для проверки своего внутреннего состояния, которое необходимо проверять всякий раз, когда осуществляется открытый доступ, независимо от того, является ли это метод или свойство
Ловушка
2

Один из хороших подходов к Исключениям - использовать их для документирования кода для себя и других разработчиков следующим образом:

Исключения должны быть для исключительных состояний программы. Это значит, что их можно писать где угодно!

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

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

Сообщения об исключениях - это очень грубый способ сообщения об ошибках - они не могут собираться массово и действительно содержат только строку. Это делает их непригодными для сообщения о проблемах во входных данных. При нормальной работе сама система не должна переходить в состояние ошибки. В результате сообщения в них должны быть предназначены для программистов, а не для пользователей - ошибки во входных данных могут быть обнаружены и переданы пользователям в более подходящих (настраиваемых) форматах.

Исключение (ха-ха!) Из этого правила - это такие вещи, как ввод-вывод, где исключения не находятся под вашим контролем и не могут быть проверены заранее.

JonnyRaa
источник
2
Как этот правильный и актуальный ответ был отклонен? В StackOverflow не должно быть политики, и если этот ответ не попал в цель, добавьте комментарий по этому поводу. Голосование против неуместных или неправильных ответов.
дебатер
1

Все это задокументировано в MSDN (как указано в других ответах), но вот общее практическое правило ...

В установщике, если ваше свойство должно быть проверено выше и выше типа. Например, свойство с именем PhoneNumber, вероятно, должно иметь проверку регулярного выражения и должно выдавать ошибку, если формат недействителен.

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

Дэвида Стрэттона
источник
0

Это очень сложный вопрос, и ответ зависит от того, как используется ваш объект. Как показывает практика, методы получения и установки свойств, которые являются «поздним связыванием», не должны генерировать исключения, в то время как свойства с исключительно «ранним связыванием» должны генерировать исключения, когда возникает необходимость. Кстати, инструмент анализа кода Microsoft, на мой взгляд, слишком узко определяет использование свойств.

"позднее связывание" означает, что свойства обнаруживаются посредством отражения. Например, атрибут «Serializeable» используется для сериализации / десериализации объекта с помощью его свойств. Вызов исключения во время такого рода ситуации приводит к катастрофическим сбоям в работе и не является хорошим способом использования исключений для создания более надежного кода.

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

Состояние объекта с внутренними атрибутами определяется значениями этих атрибутов. Свойства, выражающие атрибуты, которые осведомлены о внутреннем состоянии объекта и чувствительны к нему, не должны использоваться для позднего связывания. Например, допустим, у вас есть объект, который нужно открыть, получить к нему доступ, а затем закрыть. В этом случае доступ к свойствам без предварительного вызова open должен привести к исключению. Предположим в этом случае, что мы не генерируем исключение и разрешаем коду доступ к значению, не генерируя исключения? Код будет казаться счастливым, даже если он получил значение от бессмысленного получателя. Теперь мы поместили код, который вызывал геттер, в плохую ситуацию, поскольку он должен знать, как проверить значение, чтобы убедиться, что оно не имеет смысла. Это означает, что код должен делать предположения о значении, которое он получил от средства получения свойства, чтобы его проверить. Так пишется плохой код.

Джек Д. Менендес
источник
0

У меня был этот код, в котором я не был уверен, какое исключение нужно выбросить.

public Person
{
    public string Name { get; set; }
    public boolean HasPets { get; set; }
}

public void Foo(Person person)
{
    if (person.Name == null) {
        throw new Exception("Name of person is null.");
        // I was unsure of which exception to throw here.
    }

    Console.WriteLine("Name is: " + person.Name);
}

Я запретил модели иметь свойство равным нулю в первую очередь, заставив его в качестве аргумента в конструкторе.

public Person
{
    public Person(string name)
    {
        if (name == null) {
            throw new ArgumentNullException(nameof(name));
        }
        Name = name;
    }

    public string Name { get; private set; }
    public boolean HasPets { get; set; }
}

public void Foo(Person person)
{
    Console.WriteLine("Name is: " + person.Name);
}
Фред
источник