Повернути магічне значення, викинути виняток або повернути помилкове на помилку?


83

Іноді мені доводиться писати метод або властивість для бібліотеки класів, на яку не є винятковою не справжня відповідь, а невдача. Щось неможливо визначити, недоступне, не знайдене, наразі неможливо або більше даних немає.

Я думаю, що для такої порівняно невиключної ситуації є три можливі рішення, що свідчать про збій у C # 4:

  • повернути магічне значення, яке не має сенсу інакше (наприклад, nullта -1);
  • кинути виняток (наприклад KeyNotFoundException);
  • повернути falseі надати фактичне значення повернення в outпараметрі (наприклад, Dictionary<,>.TryGetValue).

Тож питання: у якій невиключній ситуації я повинен кинути виняток? І якщо я не повинен кидати: коли повертається магічне значення, яке було зазначено вище, реалізуючи Try*метод з outпараметром ? (Мені outпараметр здається брудним, і його більше працювати правильно.)

Я шукаю фактичні відповіді, такі як відповіді, що містять керівні принципи проектування (я не знаю жодних про Try*методи), зручність використання (оскільки я прошу це про бібліотеку класів), узгодженість з BCL та читабельність.


У бібліотеці базових класів .NET Framework використовуються всі три методи:

Зауважте, що як Hashtableстворено в часи, коли в C # не було дженериків, воно використовує objectі тому може повернутися nullяк магічне значення. Але з дженериками винятки використовуються в Dictionary<,>, і спочатку цього не було TryGetValue. Мабуть, розуміння змінюються.

Очевидно, що подвійність Item- TryGetValueі Parse- TryParseє з якоїсь причини, тому я припускаю, що викидання винятків за надзвичайні невдачі в C # 4 не робиться . Однак Try*методи не завжди існували, навіть коли вони Dictionary<,>.Itemіснували.


6
"винятки означають помилки". запитувати словник для неіснуючого елемента - помилка; попросити потік прочитати дані, коли це EOF, відбувається щоразу, коли ви використовуєте потік. (це підсумовує довгу красиво відформатовану відповідь, яку я не отримав шанс подати :))
KutuluMike

6
Справа не в тому, що я думаю, що ваше запитання занадто широке, це не конструктивне питання. Канонічної відповіді немає, оскільки відповіді вже показані.
Джордж Стокер

2
@GeorgeStocker Якби відповідь була прямою і очевидною, я б не ставив цього питання. Той факт, що люди можуть заперечити, чому даний вибір є кращим з будь-якої точки зору (наприклад, продуктивність, читабельність, зручність використання, ремонтопридатність, рекомендації щодо дизайну чи послідовність), робить його по суті неканонічним, але відповідає моєму задоволенню. На всі питання можна відповісти дещо суб’єктивно. Мабуть, ви очікуєте, що питання має здебільшого подібні відповіді, щоб воно було гарним питанням.
Daniel AA Pelsmaeker

3
@Virtlink: Джордж - модератор, обраний громадою, який приділяє велику кількість свого часу, щоб допомогти підтримувати переповнення стека. Він заявив, чому він закрив це питання, і FAQ задає його.
Ерік Дж.

15
Питання належить тут не тому, що воно суб'єктивне, а тому, що воно концептуальне. Правило великого пальця: якщо ви сидите перед кодуванням IDE, запитайте його на стек переповнення. Якщо ви стоїте перед мозковим штурмом на дошці, запитайте це тут про програмістів.
Роберт Харві

Відповіді:


56

Я не думаю, що ваші приклади справді рівноцінні. Є три різні групи, кожна з яких має своє обґрунтування своєї поведінки.

  1. Магічне значення - це хороший варіант, коли існує умова "до", наприклад, StreamReader.Readабо коли є просте у використанні значення, яке ніколи не буде дійсною відповіддю (-1 для IndexOf).
  2. Викиньте виняток, коли семантика функції полягає в тому, що абонент впевнений, що вона буде працювати. У цьому випадку неіснуючий ключ або неправильний подвійний формат справді винятковий.
  3. Використовуйте параметр «out» та поверніть bool, якщо семантика має пробувати, чи можлива операція чи ні.

Наведені вами приклади є абсолютно зрозумілими для випадків 2 та 3. Щодо магічних значень, можна стверджувати, чи це хороше дизайнерське рішення чи ні у всіх випадках.

NaNПовертається Math.Sqrtє окремим випадком - він відповідає стандарту з плаваючою комою.


10
Не погоджуються з числом 1. Магічні значення ніколи не є хорошим варіантом, оскільки вони вимагають, щоб наступний кодер дізнався про значення магічного значення. Вони також шкодять читабельності. Я не можу придумати жодного випадку, в якому використання магічного значення краще, ніж шаблон Спроби.
0b101010

1
А як же Either<TLeft, TRight>монада?
сара

2
@ 0b101010: просто витратив деякий час на пошук того, як streamreader.read може безпечно повернутися -1 ...
jmoreno

33

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

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

Ось чому, хоч outпараметри іноді нахмурюються, я віддаю перевагу тому методу із Try...синтаксисом. Це канонічний синтаксис .NET і C #. Ви повідомляєте користувачеві API, що вони повинні перевірити повернене значення, перш ніж використовувати результат. Ви також можете включити другий outпараметр із корисним повідомленням про помилку, що знову допомагає при налагодженні. Ось чому я голосую за Try...з outрозчином параметрів.

Ще один варіант - повернути спеціальний об’єкт "результат", хоча я вважаю це набагато більш стомлюючим:

interface IMyResult
{
    bool Success { get; }
    // Only access this if Success is true
    MyOtherClass Result { get; }
    // Only access this if Success is false
    string ErrorMessage { get; }
}

Тоді ваша функція виглядає правильно, оскільки вона має лише вхідні параметри, і вона повертає лише одне. Просто одна річ, яку вона повертає, це вид кортежу.

Насправді, якщо ви займаєтесь подібними речами, ви можете використовувати нові Tuple<>класи, які були введені в .NET 4. Особисто мені не подобається те, що значення кожного поля є менш явним, тому що я не можу дати Item1і Item2корисні назви.


3
Особисто я часто використовую контейнер результатів, як ваша IMyResultпричина, можна повідомити складніший результат, ніж просто trueабо falseабо значення результату. Try*()корисний лише для простих речей, таких як перетворення рядка в int.
this.myself

1
Гарний пост. Для мене я вважаю за краще ідіому структури результату, яку ви окреслили вище, а не розділення між значеннями "return" та "out". Зберігає його акуратно та охайно.
Ocean Airdrop

2
Проблема з параметром другого виходу полягає в тому, що ви набираєте 50 функцій у складній програмі, як ви потім передаєте повідомлення про помилку користувачеві? Викинути виняток набагато простіше, ніж мати шари помилок перевірки. Коли ти дістанеш його, просто кинь його, і не важливо, наскільки ти глибокий.
рулони

@rolls - Коли ми використовуємо вихідні об'єкти, ми припускаємо, що безпосередній абонент зробить все, що завгодно з результатом: обробляє його локально, ігнорує його або піднімає міхур, кидаючи обернені у виняток. Це найкраще з обох слів - абонент може чітко бачити всі можливі результати (із перерахунками тощо), може вирішити, що робити з помилкою, і не потрібно намагатися ловити кожен виклик. Якщо ви здебільшого розраховуєте негайно обробити результат або ігнорувати його, повернути об’єкти простіше. Якщо ви хочете перекинути всі помилки на верхні шари, винятки легші.
дризін

2
Це лише винахід перевірених винятків у C # набагато більш виснажливим, ніж перевірені винятки на Java.
Зима

17

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

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

Коротка відповідь

Дотримуйтесь своїх відчуттів кишечника, за винятком випадків, коли ви розробляєте досить швидкі методи та очікуєте можливості непередбаченого повторного використання.

Довга відповідь

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

  • Як правило, очікуйте, що викидання винятків відбувається в 200 разів повільніше, ніж звичайне повернення (насправді в цьому є значна різниця).

  • Як ще одне головне правило, викидання винятку часто може дозволяти набагато чистіший код порівняно з найсуворішими магічними значеннями, оскільки ви не покладаєтесь на програміста, який переводить код помилки в інший код помилки, оскільки він рухається через кілька шарів коду клієнта до точка, в якій є достатній контекст для її послідовного та адекватного поводження. (Особливий випадок: тут, nullяк правило, краще, ніж за іншими магічними значеннями, через його схильність до автоматичного переведення себе на a NullReferenceExceptionу випадку деяких, але не всіх типів дефектів; зазвичай, але не завжди, досить близько до джерела дефекту. )

То який урок?

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

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

Для дорогої багаторазової функції використовуйте все, що дає чистіший код. Якщо вам завжди доводиться проходити мережевий кругообіг або розбирати XML-файл на диску, накладні витрати на викид виключення, мабуть, незначні. Важливіше не втратити подробиці про будь-яку невдачу, навіть випадково, ніж повернутися з «невиключного невдачі» надзвичайно швидко.

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

Ви перераховуєте чудовий приклад Dictionary<,>.Item, який, слабко кажучи, змінився від повернення nullзначень до перекидання KeyNotFoundExceptionміж .NET 1.1 та .NET 2.0 (лише якщо ви готові вважати Hashtable.Itemйого практичним негенеральним попередником). Причина цієї "зміни" тут не без інтересу. Оптимізація ефективності типів значень (не більше боксу) зробила початкове магічне значення ( null) необов'язковим; outПараметри просто повернуть невелику частину вартості продуктивності. Останнє врахування продуктивності є абсолютно незначним порівняно із накладними витратами на кидання KeyNotFoundException, але дизайн винятків тут все ж перевершує. Чому?

  • Параметри ref / out несуть свої витрати щоразу, а не лише у випадку "відмови"
  • Будь-який, хто дбає, може зателефонувати Containsперед будь-яким викликом до індексатора, і ця модель читається повністю природно. Якщо розробник хоче, але забуває зателефонувати Contains, жодні проблеми з продуктивністю не можуть виникнути; KeyNotFoundExceptionє достатньо гучним, щоб його помітити і зафіксувати.

Я думаю, що в 200 разів оптимістично ставиться до винятків винятків ... див. Розділ blogs.msdn.com/b/cbrumme/archive/2003/10/01/51524.aspx "Ефективність та тенденції" перед коментарями.
gbjbaanb

@gbjbaanb - Ну, можливо. У цій статті використовується коефіцієнт відмов 1%, щоб обговорити тему, яка не є зовсім іншим загальним принципом. Мої власні думки та невиразно запам’ятовані вимірювання виходитимуть із контексту C ++, що впроваджується таблицею (див. Розділ 5.4.1.2 цього звіту , де одна проблема полягає в тому, що перший виняток такого типу, ймовірно, починається із помилки сторінки (кардинальної та змінної). але я амортизував один раз накладні витрати) Але я зроблю і опублікую експеримент із .NET 4 і, можливо, підправлю це значення бального парку. Я вже підкреслюю дисперсію.
Jirka Hanika

Чи висока тоді вартість параметрів відсилки / виходу ? Як так? І дзвінок Containsперед викликом індексатора може спричинити стан перегонів, у якому не обов'язково бути присутнім TryGetValue.
Daniel AA Pelsmaeker

@gbjbaanb - експериментально закінчено. Я був ледачий і використовував моно в Linux. Винятки дали мені ~ 563000 кидків за 3 секунди. Повернення дало мені ~ 10900000 повернень за 3 секунди. Це 1:20, навіть не 1: 200. Я все-таки рекомендую думати 1: 100+ для будь-якого більш реалістичного коду. (Параметр "парам", як я і передбачав, мав незначні витрати - я насправді підозрюю, що тремтіння, ймовірно, повністю оптимізувало виклик на моєму мінімалістичному прикладі, якщо не було викинуто жодного винятку, незалежно від підпису.)
Jirka Hanika

@Virtlink - узгоджено щодо безпеки потоку в цілому, але враховуючи, що ви посилалися на .NET 4, зокрема, використовуйте ConcurrentDictionaryдля багатопотокового доступу та Dictionaryдля однопотокового доступу для досягнення оптимальної продуктивності. Тобто, невикористання `` Містить 'НЕ робить нитку коду безпечною для цього конкретного Dictionaryкласу.
Jirka Hanika

10

Що найкраще робити в такій порівняно не винятковій ситуації, щоб вказати на невдачу, і чому?

Не слід допускати невдачі.

Я знаю, це рукотворне та ідеалістичне, але вислухай мене. Виконуючи дизайн, існує ряд випадків, коли у вас є можливість віддати перевагу версії, яка не має режимів відмов. Замість того, щоб знайти «FindAll», який не вдається, LINQ використовує пункт, де просто повертається порожнє число. Замість того, щоб мати об'єкт, який потрібно ініціалізувати перед використанням, дозвольте конструктору ініціалізувати об’єкт (або ініціалізувати, коли виявлено неініціалізований). Ключовим моментом є видалення гілки відмови у споживчому коді. Це проблема, тому зосередьтеся на ній.

Ще одна стратегія цього - KeyNotFoundsscenario. Майже в кожній кодовій базі, над якою я працював з 3.0, існує щось подібне до цього методу розширення:

public static class DictionaryExtensions {
    public static V GetValue<K, V>(this IDictionary<K, V> arg, K key, Func<K,V> ifNotFound) {
        if (!arg.ContainsKey(key)) {
            return ifNotFound(key);
        }

        return arg[key];
    }
}

Реального режиму відмови для цього немає. ConcurrentDictionaryмає подібний GetOrAddвбудований.

Все, що говорили, завжди знайдуться часи, коли це просто неминуче. Усі троє мають своє місце, але я б віддавав перевагу першому варіанту. Незважаючи на все, що складається з нульової небезпеки, воно добре відоме і вписується у безліч сценаріїв "предмет не знайдено" або "результат не застосований", що становить набір "не винятковий збій". Особливо, коли ви створюєте нульові типи значень, значення "це може вийти з ладу" дуже чітко в коді і важко забути / викрутити.

Другий варіант достатньо хороший, коли ваш користувач робить щось глупо. Надає вам рядок із неправильним форматом, намагається встановити дату на 42 грудня ... щось, що ви хочете, щоб речі підривались швидко та ефектно під час тестування, щоб поганий код був ідентифікований та виправлений.

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

Якщо значення повернення та параметр не є тривіальними, то перевагу Скотта Вітлока щодо результатного об'єкта є кращим (як Matchклас Regex ).


7
Тут домашні улюбленці: outпараметри повністю ортогональні щодо побічних ефектів. Змінення refпараметра є побічним ефектом, а зміна стану об'єкта, який ви передаєте через вхідний параметр, є побічним ефектом, але outпараметр є лише незручним способом змусити функцію повернути більше ніж одне значення. Побічного ефекту немає, просто кілька повернених значень.
Скотт Вітлок

Я говорив, що схильний , через те, як люди ними користуються. Як ви кажете, вони просто кілька повернених значень.
Теластин

Але якщо ви не любите параметри і використовуєте винятки, щоб ефектно підірвати речі, коли формат неправильний ... як ви потім обробляєте випадок, коли ваш формат є введенням користувача? Тоді або користувач може підірвати речі, або хтось несе штрафний викид за кидання, а потім ловить виняток. Правильно?
Daniel AA Pelsmaeker

@virtlink, маючи чіткий метод перевірки. Вам це все одно потрібно для надання належних повідомлень в інтерфейс користувача, перш ніж їх надсилати.
Теластин

1
Існує законний шаблон для параметрів, і це функція, яка має перевантаження, які повертають різні типи. Розв’язання перевантаження не буде працювати для типів повернення, але буде для параметрів.
Роберт Харві

2

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

Зверніть увагу, що Parseі TryParseнасправді це одне і те ж, крім режимів відмов Те, що TryParseтакож може повернути значення, є дещо ортогональним, насправді. Розглянемо ситуацію, коли, наприклад, ти перевіряєш деякий вклад. Тебе насправді не цікавить, яке значення має, доки воно дійсне. І немає нічого поганого в тому, щоб запропонувати певну IsValidFor(arguments)функцію. Але це ніколи не може бути основним режимом роботи.


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

1
Це більш спеціальна потреба, а не загальна справа.
DeadMG

2
Так ви кажете. Але ви вживали слово "завжди". :)
Роберт Харві

@DeadMG, погоджуючись з Робертом Харвеєм, хоча я думаю, що відповідь було занадто проголосовано, якщо вона була змінена, щоб відображати "більшу частину часу", а потім вказати на винятки (не призначені каламбури) із загального випадку, коли мова йде про часто використовувані високі показники закликає розглянути інші варіанти.
Джеральд Девіс

Винятки не дорогі. Лову глибоко викинутих винятків може бути дорогим, оскільки система повинна розмотати стек до найближчої критичної точки. Але це "дороге" порівняно крихітне і його не варто побоюватися навіть у щільних вкладених петлях.
Метью Віт

2

Як зазначали інші, магічне значення (включаючи булеве значення повернення) не є таким чудовим рішенням, хіба що як маркер "кінця діапазону". Причина: семантика не є явною, навіть якщо ви вивчаєте методи об’єкта. Насправді ви повинні прочитати повну документацію на весь об’єкт аж до "о так, якщо він повертається -42, це означає, бла-бла-бла".

Це рішення може використовуватися з історичних причин або з міркувань продуктивності, але слід інакше уникати.

Це залишає два загальних випадки: зондування або винятки.

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

Приклад:

Ви хочете створити файл із заданого шляху.

Ви повинні використовувати об’єкт File, щоб заздалегідь оцінити, чи законний цей шлях для створення файлів або запису файлів.

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

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


0

Я думаю, що Tryмодель є найкращим вибором, коли код просто вказує, що сталося. Я ненавиджу парам і люблю зведений предмет. Я створив наступний клас

public sealed class Bag<TValue>
{
    public Bag(TValue value, bool hasValue = true)
    {
        HasValue = hasValue;
        Value = value;
    }

    public static Bag<TValue> Empty
    {
        get { return new Bag<TValue>(default(TValue), false); }
    }

    public bool HasValue { get; private set; }
    public TValue Value { get; private set; }
}

тому я можу написати наступний код

    public static Bag<XElement> GetXElement(this XElement element, string elementName)
    {
        try
        {
            XElement result = element.Element(elementName);
            return result == null
                       ? Bag<XElement>.Empty
                       : new Bag<XElement>(result);
        }
        catch (Exception)
        {
            return Bag<XElement>.Empty;
        }
    }

Виглядає як нульовий, але не тільки для типу значення

Ще один приклад

    public static Bag<string> TryParseString(this XElement element, string attributeName)
    {
        Bag<string> attributeResult = GetString(element, attributeName);
        if (attributeResult.HasValue)
        {
            return new Bag<string>(attributeResult.Value);
        }
        return Bag<string>.Empty;
    }

    private static Bag<string> GetString(XElement element, string attributeName)
    {
        try
        {
            string result = element.GetAttribute(attributeName).Value;
            return new Bag<string>(result);
        }
        catch (Exception)
        {
            return Bag<string>.Empty;
        }
    }

3
try catchбуде пошкоджувати вашу ефективність, якщо ви дзвоните GetXElement()та не працюєте багато разів.
Роберт Харві

колись це не має значення. Подивіться на Bag call. Дякуємо за ваше спостереження

ваш Bag <T> clas майже ідентичний System.Nullable <T> aka "об'єкт, що
зводиться нанівець

так, майже public struct Nullable<T> where T : structосновна відмінність обмеження. btw остання версія тут github.com/Nelibur/Nelibur/blob/master/Source/Nelibur.Sword/…
GSerjo

0

Якщо вас цікавить маршрут «магічне значення», ще один спосіб вирішити це - перевантажити мету класу «Ледачі». Хоча Ледачий покликаний відкласти інстанціювання, ніщо насправді не заважає вам використовувати, як, можливо, або Варіант. Наприклад:

    public static Lazy<TValue> GetValue<TValue, TKey>(
        this IDictionary<TKey, TValue> dictionary,
        TKey key)
    {
        TValue retVal;
        if (dictionary.TryGetValue(key, out retVal))
        {
            var retValRef = retVal;
            var lazy = new Lazy<TValue>(() => retValRef);
            retVal = lazy.Value;
            return lazy;
        }

        return new Lazy<TValue>(() => default(TValue));
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.