Викиньте виняток або нехай код не вдасться


52

Мені цікаво, чи є плюси і мінуси проти цього стилю:

private void LoadMaterial(string name)
{
    if (_Materials.ContainsKey(name))
    {
        throw new ArgumentException("The material named " + name + " has already been loaded.");
    }

    _Materials.Add(
        name,
        Resources.Load(string.Format("Materials/{0}", name)) as Material
    );
}

Цей метод для кожного nameслід застосовувати лише один раз. _Materials.Add()викине виняток, якщо він буде викликаний кілька разів для одного і того ж name. Чи є мій охоронець, як наслідок, зовсім зайвим, чи є якісь менш очевидні переваги?

Це C #, Єдність, якщо когось цікавить.


3
Що станеться, якщо ви два рази завантажуєте матеріал без охорони? Є чи _Materials.Addкинути виняток?
користувач253751

2
Насправді я вже згадував у викидах виняток: P
async

9
Бічні примітки: 1) Розгляньте можливість використання string.Formatконкатенації рядків для побудови повідомлення про виключення. 2) використовувати лише у тому asвипадку, якщо ви очікуєте, що виступ буде невдалим, і перевірте результат null. Якщо ви очікуєте, що завжди отримаєте балончик Material, використовуйте ролі типу (Material)Resources.Load(...). Виняток чіткого виступу є простішим для виправлення помилок, ніж нульове виняткове посилання, яке відбувається згодом.
CodesInChaos

3
У цьому конкретному випадку ваші абоненти можуть також вважати корисним супутника LoadMaterialIfNotLoadedабо ReloadMaterial(ця назва може використовувати поліпшення).
jpmc26

1
Ще одна примітка: Цей шаблон нормальний для того, щоб іноді додавати елемент до списку. Однак не використовуйте цю схему для заповнення всього списку, оскільки ви зіткнетесь із ситуацією O (n ^ 2), коли з великими списками ви зіткнетеся з проблемами продуктивності. Ви не помітите його при 100 записах, але при 1000 записах ви, безумовно, і при 10 000 записів це буде головним вузьким місцем.
Пітер Б

Відповіді:


109

Перевага полягає в тому, що ваш "спеціальний" виняток містить повідомлення про помилку, яке має значення для тих, хто називає цю функцію, не знаючи, як вона реалізована (якою в майбутньому ви можете бути!).

Зрозуміло, що в цьому випадку вони, ймовірно, зможуть здогадатися, що означає "стандартний" виняток, але ви все одно даєте зрозуміти, що вони порушили ваш контракт, а не натрапили на якусь дивну помилку у вашому коді.


3
Погодьтеся. Майже завжди краще кинути виняток за порушення попередньої умови.
Френк Хілеман

22
"Перевага полягає в тому, що ваш" спеціальний "виняток містить повідомлення про помилку, яке має значення для тих, хто називає цю функцію, не знаючи, як вона реалізована (якою в майбутньому ви можете бути!)." Топ-рада.
асинхрон

2
"Явне краще, ніж неявне." - Zen of Python
jpmc26

4
Навіть якщо _Materials.Addпомилка, яку передає помилка, не є помилкою, яку ви хочете передати абоненту, я вважаю, що цей метод відновлення помилки неефективний. Для кожного успішного дзвінка LoadMaterial(нормальної роботи) ви зробили один і той же тест на розумність двічі , LoadMaterialзнову і знову _Materials.Add. Якщо більше шарів загорнуте, кожен з яких використовує один і той же стиль, ви навіть можете мати багато разів один і той же тест.
Марк ван Левенен

15
... Замість цього подумайте про те, щоб зануритися у _Materials.Addбеззастережну ситуацію , тоді вловите потенційну помилку і в обробнику киньте іншу. Додаткова робота зараз виконується лише в помилкових випадках, винятковий шлях виконання, коли ви зовсім не турбуєтесь про ефективність.
Марк ван Левенен

100

Я згоден з відповіддю Іксрека . Однак ви можете розглянути третю альтернативу: зробити функцію idempotent. Іншими словами, повертайтеся рано, а не кидаючи ArgumentException. Це часто бажано, якщо в іншому випадку вас змусять перевірити, чи він уже завантажений, перш ніж дзвонити LoadMaterial. Чим менше у вас передумов, тим менше когнітивне навантаження на програміста.

Кидати виняток було б краще, якщо це справді помилка програміста, коли він повинен бути очевидним і відомим під час компіляції, якщо матеріал уже завантажений, не перевіряючи час виконання перед викликом.


2
З іншого боку, беззвучне функціонування функції може призвести до несподіваної поведінки, внаслідок чого виявити помилки, які виникають пізніше.
Філіп

30
@Philipp функція не вийде з ладу. Бути бездумним означає, що запускати його багато разів має такий же ефект, як і запустити його один раз. Іншими словами, якщо матеріал уже завантажений, то наша специфікація ("матеріал повинен бути завантажений") вже задоволена, тому нам нічого не потрібно робити. Ми можемо просто повернутися.
Варбо

8
Мені це більше подобається, насправді. Звичайно, залежно від обмежень, заданих вимогами додатків, це може бути неправильним. Знову ж таки, я абсолютно любитель ідентичних методів.
FP

6
@Philipp, якщо ми думаємо про це, LoadMaterialмає такі обмеження: "Після виклику його матеріал завжди завантажується, а кількість завантажених матеріалів не зменшується". Закидання винятку для третього обмеження "Матеріал не можна додавати двічі" здається мені дурним і контрсуєсним. Здійснення функції idempotent зменшує залежність коду від порядку виконання та стану програми. Мені здається, що це подвійна перемога.
Бенджамін Груенбаум

7
Мені подобається ця ідея, але пропоную одну незначну зміну: поверніть бульне значення, що відображає, чи щось завантажено. Якщо абонент дійсно піклується про логіку програми, він може перевірити це та викинути виняток.
user949300

8

Основне питання, яке ви повинні задати, це: Яким ви хочете бути інтерфейс для вашої функції? Зокрема, чи є умовою _Materials.ContainsKey(name)передумовою функції?

Якщо це не є передумовою, функція повинна забезпечувати чітко визначений результат для всіх можливих долей name. У цьому випадку виняток, викинутий якщо nameне є частиною, _Materialsстає частиною інтерфейсу функції. Це означає, що вона повинна бути частиною документації на інтерфейс, і якщо ви коли-небудь вирішите змінити цей виняток у майбутньому, це буде невід'ємною зміною інтерфейсу .

Більш цікаве питання - що відбувається, якщо це є передумовою. У цьому випадку сама передумова стає частиною інтерфейсу функції, але спосіб поведінки функції при порушенні цієї умови не обов'язково є частиною інтерфейсу.

У цьому випадку підхід, який ви опублікували, перевірка наявності попередніх умов та повідомлення про помилку, називається оборонним програмуванням . Захисне програмування добре тим, що воно сповіщає користувача рано, коли він помилився і викликав функцію з хибним аргументом. Це погано тим, що він значно збільшує навантаження на технічне обслуговування, оскільки код користувача може залежати від того, що функція певним чином обробляє порушення умов. Зокрема, якщо ви виявите, що перевірка часу виконання коли-небудь стане вузьким місцем у майбутньому (навряд чи для такого простого випадку, але досить поширеного для складніших передумов), ви більше не зможете його видалити.

Виявляється, ці недоліки можуть бути дуже істотними, що дало оборонному програмуванню вид поганої репутації в певних колах. Однак початкова мета все ще діє: Ми хочемо, щоб користувачі нашої функції помітили рано, коли вони помилилися.

Тому сьогодні багато розробників пропонують дещо інший підхід до подібних питань. Замість того, щоб кидати виняток, вони використовують механізм, схожий на затвердження, для перевірки передумов. Тобто, передумови можуть перевірятися в налагодженнях для налагодження, щоб допомогти користувачам на початку помилок, але вони не є частиною інтерфейсу функцій. Різниця може здатися тонкою на перший погляд, але вона може змінити величезну практику.

Технічно викликати функцію з порушеними передумовами - це невизначена поведінка. Але реалізація може вирішити виявити ці випадки та повідомити користувача одразу, якщо це станеться. На жаль, винятки не є хорошим інструментом для цього, оскільки код користувача може реагувати на них і, таким чином, може почати покладатися на їх присутність.

Детальне пояснення проблем із класичним оборонним підходом, а також можлива реалізація перевірок передумов у стилі ствердження див. У розмові « Оборонне програмування» Джона Лакоса, виконане справа від CppCon 2014 ( Слайди , відео ).


4

Тут вже є кілька відповідей, але ось відповідь, яка враховує Unity3D (відповідь дуже специфічна для Unity3D, я б зробив більшість цього зовсім по-різному в більшості контекстів):

Загалом, Unity3D традиційно не використовує винятків. Якщо ви викинете виняток у Unity3D, він не схожий на ваш середній додаток .NET, а саме він не зупинить програму, t більшість ви можете налаштувати редактор на паузу. Він просто зареєструється. Це може легко поставити гру в недійсний стан і створити ефект каскаду, що ускладнює відстеження помилок. Тому я б сказав, що у випадку Unity, дозволити Addвикинути виняток є особливо небажаним варіантом.

Але при вивченні швидкості виключень в деяких випадках не відбувається передчасної оптимізації через те, як Mono працює в Unity на деяких платформах. Насправді Unity3D на iOS підтримує деякі розширені оптимізації сценаріїв, а виключені виключення * є побічним ефектом одного з них. Це дійсно щось, що слід враховувати, оскільки ці оптимізації виявились дуже цінними для багатьох користувачів, показуючи реалістичний випадок розгляду обмеження на використання винятків у Unity3D. (* керовані винятки з двигуна, а не ваш код)

Я б сказав, що в Unity ви можете скористатися більш спеціалізованим підходом. За іронією долі, на момент написання цього запиту, дуже невдоволена відповідь , показує один із способів, як я можу реалізувати щось подібне конкретно в контексті Unity3D (в інших місцях щось подібне насправді неприйнятно, і навіть в Unity це досить неелегантно).

Ще один підхід, який я вважаю, насправді не вказує на помилку, що стосується абонента, а скоріше використання Debug.LogXXфункцій. Таким чином, ви отримуєте таку саму поведінку, як кидати необроблений виняток (через те, як Unity3D поводиться з ними), не ризикуючи поставити щось у дивному стані вниз. Також врахуйте, чи справді це помилка (намагається завантажити один і той же матеріал двічі, обов'язково помилка у вашому випадку? Або це може бути випадок, коли Debug.LogWarningце більше стосується).

Що стосується використання таких речей, як Debug.LogXXфункції, а не винятки, вам все одно доведеться враховувати, що відбувається, коли виняток буде викинуто з чогось, що повертає значення (наприклад, GetMaterial). Я схильний підходити до цього, передаючи null разом із реєстрацією помилки ( знову ж таки, лише в Unity ). Тоді я використовую нульові перевірки в моїх MonoBehaviors, щоб гарантувати, що будь-яка залежність, наприклад, матеріал не є нульовим значенням, і відключаю MonoBehavior, якщо він є. Приклад простої поведінки, яка вимагає декількох залежностей, є приблизно таким:

    public void Awake()
    {
        _inputParameters = GetComponent<VehicleInputParameters>();
        _rigidbody = GetComponent<Rigidbody>();
        _rigidbodyTransform = _rigidbody.transform;
        _raycastStrategySelector = GetComponent<RaycastStrategySelectionBehavior>();

        _trackParameters =
            SceneManager.InstanceOf.CurrentSceneData.GetValue<TrackParameters>();

        this.DisableIfNull(() => _rigidbody);
        this.DisableIfNull(() => _raycastStrategySelector);
        this.DisableIfNull(() => _inputParameters);
        this.DisableIfNull(() => _trackParameters);
    }

SceneData.GetValue<>подібний вашому прикладу тим, що він викликає функцію у словнику, яка видає виняток. Але замість того, щоб викидати виняток, він використовує, Debug.LogErrorщо дає слід стека, як звичайний виняток, і повертає null. Наступні перевірки * відключать поведінку замість того, щоб продовжувати існувати у недійсному стані.

* чеки виглядають так через маленького помічника, який використовує, який виводить форматоване повідомлення, коли він відключає ігровий об’єкт **. Прості нульові перевірки з ifроботою тут (** перевірки помічників збираються лише у налагодженнях налагодження (наприклад, твердження). Використання лямбдів і виразів, подібних до Unity, може спричинити справність)


1
+1 за додавання справді нової інформації в гру. Я цього не очікував.
Іксрек

3

Мені подобаються дві основні відповіді, але хочу припустити, що назви ваших функцій можна було б покращити. Я звик до Java, тому YMMV, але метод "add" повинен, IMO, не кидати виняток, якщо елемент вже є. Він повинен знову додати елемент , або, якщо пунктом призначення є Набір, нічого не робити. Оскільки це не поведінка Materials.Add, її слід перейменувати в TryPut або AddOnce або AddOrThrow або подібне.

Так само ваш LoadMaterial повинен бути перейменований на LoadIfAbsent або Put або TryLoad або LoadOrThrow (залежно від того, використовуєте ви відповідь №1 або №2) або деякі подібні.

Дотримуйтесь конвенцій про іменування C # Unity, якими б вони не були.

Це буде особливо корисно, якщо у вас є інші функції AddFoo та LoadBar, де дозволено завантажувати одну і ту ж річ двічі. Без чітких імен розробники зірвуться.


Існує різниця між семантикою betweeen C # Dictionary.Add () (див. Msdn.microsoft.com/en-us/library/k7z0zy8k%28v=vs.110%29.aspx ) та семантику Java Collection.add () (див. Документи. oracle.com/javase/8/docs/api/java/util/… ). C # повертає недійсність і кидає виняток. Тоді як Java повертає bool і замінює попередньо збережене значення новим значенням або викидає виняток, коли новий елемент неможливо додати.
Каспер ван ден Берг

Каспер - спасибі, і цікаво. Як ви замінюєте значення у словнику C #? (Еквівалентно Java put ()). На цій сторінці не бачити вбудованого методу.
user949300

гарне запитання, можна зробити Dictionary.Remove () (див. msdn.microsoft.com/en-us/library/bb356469%28v=vs.110%29.aspx ) з подальшим словником.Add () (див. msdn.microsoft .com / en-us / library / bb338565% 28v = vs.110% 29.aspx ), але це здається трохи незручним.
Каспер ван ден Берг

@ user949300 словник [key] = значення замінює запис, якщо він уже існує
Rupe

@ Рупа і, імовірно, кидає щось, якщо цього не зробити. Все здається мені дуже незручно. Більшість клієнтів, які встановлюють дік, не хвилюються, чи був там запис , вони просто дбають про те, щоб після їх налаштування код був там. Оцінка за API Java Collections (IMHO). PHP також незручно диктувати, що помилка C # була помилкою виконувати цей прецедент.
user949300

3

Усі відповіді додають цінні ідеї, я хотів би їх поєднати:

Визначтеся із наміченою та очікуваною семантикою LoadMaterial()операції. Принаймні існують такі варіанти:

  • Обов’язкова умова для nameзавантаженихматеріалів : →

    Коли попередня умова порушується, то ефект LoadMaterial()не визначений (як в відповідь по ComicSansMS ). Це дозволяє отримати найбільшу свободу у здійсненні та майбутніх змінах Росії LoadMaterial(). Або,

  • визначено ефекти виклику за LoadMaterial(name)допомогою nameLoadedMaterials ; або:

Вирішивши семантику, ви повинні вибрати реалізацію. Наступні варіанти та міркування, де пропонується:

  • Киньте призначене для користувача виняток (як запропоновано на Ixrec ) →

    Перевага полягає в тому, що ваш "спеціальний" виняток містить повідомлення про помилку, яке має значення для всіх, хто викликає цю функцію - Ixrec та User16547

    • Щоб уникнути витрат на повторну перевірку nameЗавантаженихматеріалів, ви можете дотримуватися поради Марка ван Левену :

      ... Замість цього подумайте про те, щоб зануритися у _Materials.Add беззастережно, тоді вловити потенційну помилку і в обробник кинути іншу.

  • Нехай Dictionay.Add кине виняток →

    Код, що кидає виняток, є зайвим. - Джон Рейнор

    Хоча більшість виборців більше погоджуються з Ixrec

    Додатковою причиною вибору цієї реалізації є:

    • що абоненти вже можуть обробляти ArgumentExceptionвинятки, і
    • щоб уникнути втрати інформації про стеки.

    Але якщо ці дві причини є важливими, ви також можете отримати власний виняток ArgumentExceptionі використовувати оригінал як прикутий виняток.

  • Зробити LoadMaterial()ідемпотентна в anwser по Карлу Білефельдта і upvoted найчастіше ( в 75 разів).

    Варіанти реалізації такої поведінки:

    • Зверніться до слова.ContainsKey ()

    • Завжди зателефонуйте до словника.Add () ловіть його, який ArgumentExceptionвін кидає, коли ключ для вставки вже існує, і ігноруйте цей виняток. Документуйте, що ігнорування винятку - це те, що ви маєте намір робити і чому.

      • Коли LoadMaterials()(майже) завжди викликається один раз для кожного name, це дозволяє уникнути витрат, які неодноразово перевіряються nameLoadedMaterials пор. Марк ван Левен . Однак,
      • коли LoadedMaterials()його часто називають декількома разів за одне і те ж name, це вимагає (дорогої) вартості закидання ArgumentExceptionта розмотування стека.
    • Я подумав, що існує TryAddаналог -методу TryGet (), який дозволив би вам уникнути закидання дорогого виключення та розмотування стека провальних викликів у Dictionary.Add.

      Але цей TryAdd-метод, схоже, не існує.


1
+1 за найповнішу відповідь. Це, мабуть, виходить далеко за рамки того, що було створено в минулому, але це корисний підсумок всіх варіантів.
Ixrec

1

Код, що кидає виняток, є зайвим.

Якщо ви телефонуєте:

_Materials.Add (ім'я, Resources.Load (string.Format ("Матеріали / {0}", ім'я)) як Матеріал

тим самим ключем він кине a

System.ArgumentException

Повідомлення буде: "Елемент з тим самим ключем уже додано."

ContainsKeyПеревірка є зайвою , оскільки по суті таку ж поведінку досягається без нього. Єдиний елемент, який відрізняється - це фактичне повідомлення за винятком. Якщо справжня вигода від отримання спеціального повідомлення про помилку є, то захисний код має певні заслуги.

В іншому випадку я б, мабуть, відмовився від охорони в цій справі.


0

Я думаю, це скоріше питання щодо дизайну API, ніж питання конвенції кодування.

Який очікуваний результат (контракт) дзвінка:

LoadMaterial("wood");

Якщо абонент може очікувати / гарантується, що після виклику цього методу завантажується матеріал «деревина», то я не бачу причин кидати виняток, коли цей матеріал уже завантажений.

Якщо під час завантаження матеріалу виникає помилка, наприклад, підключення до бази даних не вдалося відкрити, або у сховищі відсутня матеріальна «деревина», тоді правильно викинути виняток, щоб повідомити абонента про цю проблему.


-2

Чому б не змінити метод, щоб він повернув "true", він додає матеріял і "false", якщо він не був?

private bool LoadMaterial(string name)
{
   if (_Materials.ContainsKey(name))

    {

        return false; //already present
    }

    _Materials.Add(
        name,
        Resources.Load(string.Format("Materials/{0}", name)) as Material
    );

    return true;

}

15
Функції, які повертають значення помилок, є саме антидіаграмою, яку винятки повинні застаріти.
Філіп

Це завжди так? Я думав, що це стосується лише напівпрогнозу ( en.wikipedia.org/wiki/Semipredicate_problem ) Оскільки в зразку коду OP невдача додати матеріал до системи не є реальною проблемою. Метод призначений для того, щоб переконатися, що матеріал знаходиться в системі під час його запуску, і саме це робить цей метод. (навіть якщо він не вдається) Булевий показник просто вказує, чи метод вже намагався додати цю матеріальну чи ні. ps: Я не беру до уваги, що може бути декілька матеріялів з одним іменем.
MrIveck

1
Я думаю, що я знайшов рішення сам: en.wikipedia.org/wiki/… Це вказує на те, що відповідь Іксрека є найбільш правильною
MrIveck

@Philipp У випадку, якщо кодер / специфікація вирішив, що помилки не буде, коли ви намагаєтесь Завантажити двічі. Зворотне значення - у цьому сенсі не значення помилки, а інформаційне.
user949300
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.