Перевірка параметрів конструктора в C # - Найкращі практики


34

Яка найкраща практика для перевірки параметрів конструктора?

Припустимо, простий біт C #:

public class MyClass
{
    public MyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
            throw new ArgumentException("Text cannot be empty");

        // continue with normal construction
    }
}

Чи допустимо було б викинути виняток?

Альтернатива, з якою я стикався, була попередня перевірка, перш ніж ініціювати:

public class CallingClass
{
    public MyClass MakeMyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
        {
            MessageBox.Show("Text cannot be empty");
            return null;
        }
        else
        {
            return new MyClass(text);
        }
    }
}

10
"прийнятно кидати виняток?" Яка альтернатива?
S.Lott

10
@ S.Lott: Не робіть нічого. Встановіть значення за замовчуванням. Роздрукуйте повідомлення в консоль / журнал. Дзвінок дзвони. Блимати світло.
FrustratedWithFormsDesigner

3
@FrustratedWithFormsDesigner: "Не робити нічого?" Як буде задоволено обмеження "текст не порожній"? "Встановити значення за замовчуванням"? Як буде використовуватися значення текстового аргументу? Інші ідеї чудові, але ці дві люди призведуть до об'єкта в стані, що порушує обмеження цього класу.
S.Lott

1
@ S.Lott Побоюючись повторити себе, я був відкритий до можливості існувати якийсь інший підхід, про який я не знав. Я вважаю, що я не все знаю, що багато я точно знаю.
MPelletier

3
@MPelletier, перевірка параметрів достроково призведе до порушення DRY. (Не повторюйте себе) Оскільки вам потрібно буде скопіювати цю попередню перевірку в будь-який код, який намагається інстанціювати цей клас.
CaffGeek

Відповіді:


26

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

if (string.IsNullOrEmpty(text))
    throw new ArgumentException("message", "text");

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

if (string.IsNullOrWhiteSpace(text))
    throw new ArgumentException("message", "text");

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

@FrustratedWithFormsDesigner - я припускаю, що ти мене за це озвучив? Звичайно, ви праві, що я екстраполюю, але очевидно, що цей теоретичний об'єкт повинен бути недійсним, якщо вхідний текст порожній. Ви дійсно хочете зателефонувати, Initщоб отримати якийсь код помилки, коли виняток спрацює так само добре і змусить ваш об'єкт завжди підтримувати дійсний стан?
ChaosPandion

2
Я схильний розділити цей перший випадок на два окремі винятки: той, який перевіряє недійсність і кидає ArgumentNullExceptionта інший, який тестує порожній і кидає an ArgumentException. Дозволяє абоненту бути більш вибагливим, якщо вони хочуть за його виняток обробляти.
Джессі К. Слікер

1
@Jesse - я використав цю техніку, але все ще на огорожі щодо її загальної корисності.
ChaosPandion

3
+1 для незмінних об'єктів! Я також використовую їх там, де це можливо (я особисто завжди знаходжу їх).

22

Дуже багато людей стверджують, що конструктори не повинні кидати винятків. Наприклад, KyleG на цій сторінці робить саме це. Чесно кажучи, я не можу придумати причину, чому ні.

У C ++ викидання виключення з конструктора - погана ідея, оскільки це залишає вам виділену пам’ять, що містить неініціалізований об'єкт, на який ви не маєте посилання (тобто це класичне витоку пам'яті). Ось, мабуть, звідси і походить стигма - купа розробників старої школи C ++ наполовину розробили своє навчання C # і просто застосували до нього те, що вони знали з C ++. На відміну від цього, в Objective-C Apple відокремила етап розподілу від кроку ініціалізації, тому конструктори на цій мові можуть кидати винятки.

C # не може просочити пам'ять від невдалого дзвінка до конструктора. Навіть деякі класи в .NET-рамках викидають винятки у своїх конструкторах.


Ви так зімкнете пір'я з вами коментарем C ++. :)
ChaosPandion

5
Ехх, C ++ є відмінним мовою, але один з моїх улюблених мозолів є люди , які вивчають одну мову добре , а потім написати як то весь час . C # не є C ++.
Мураха

10
Викидання винятків із ctor все ще може бути небезпечним, оскільки ctor, можливо, виділив некеровані ресурси, які потім ніколи не будуть утилізовані. І фіналізатор повинен бути написаний для захисту від сценарію, коли завершується незавершений побудований об'єкт. Але такі сценарії відносно рідкісні. Наступна стаття про C # Dev Center висвітлює ці сценарії та як захисно кодувати навколо них, тому слідкуйте за цим простором для деталей.
Ерік Ліпперт

6
так? кидати виключення з CTOR це єдине , що ви можете зробити в C ++ , якщо ви не можете побудувати об'єкт, і немає ніяких причин , це має викликати витік пам'яті (хоча , очевидно , його до).
jk.

@jk: Хм, ти маєш рацію! Хоча здається, що альтернативою є позначення поганих об'єктів як "зомбі", що, очевидно, STL робить замість викидів винятків: yosefk.com/c++fqa/exceptions.html#fqa-17.2 Рішення Objective-C набагато охайніше.
Мураха

12

Викиньте виняток IFF, клас не може бути приведений у послідовний стан щодо його семантичного використання. Інакше не варто. НІКОЛИ не дозволяйте об’єкту існувати в непослідовному стані. Це включає не надання повних конструкторів (як, наприклад, порожній конструктор + ініціалізація () до того, як ваш об'єкт буде повністю побудований) ... ДОВОЛЬНО ВІДКАЖІТЬ НІ!

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

Слід зазначити, що під "конструктором" я маю на увазі те, що клієнт закликає створити об'єкт. Це так само легко може бути чимось іншим, ніж фактична конструкція, яка має назву "Конструктор". Наприклад, щось подібне в C ++ не порушило б принцип IMNSHO:

struct funky_object
{
  ...
private:
  funky_object();
  bool initialize(std::string);

  friend boost::optional<funky_object> build_funky(std::string);
};
boost::optional<funky_object> build_funky(std::string str)
{
  funky_object fo;
  if (fo.initialize(str)) return fo;
  return boost::optional<funky_object>();
}

Оскільки єдиний спосіб створити A funky_object- це виклик build_funkyпринципу ніколи не допускати існування недійсного об'єкта, залишається неушкодженим, навіть якщо власне "Конструктор" не закінчив завдання.

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


9

У цьому випадку я використовував би заводський метод. В основному, встановіть, що ваш клас має лише приватні конструктори і має заводський метод, який повертає екземпляр вашого об'єкта. Якщо початкові параметри є недійсними, просто поверніть null і попросіть код виклику вирішити, що робити.

public class MyClass
{
    private MyClass(string text)
    {
        //normal construction
    }

    public static MyClass MakeMyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
            return null;
        else
            return new MyClass(text);
    }
}
public class CallingClass
{
    public MyClass MakeMyClass(string text)
    {
        var cls = MyClass.MakeMyClass(text);
        if(cls == null)
             //show messagebox or throw exception
        return cls;
    }
}

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


1
Я не бачу переваги. Чому для тестування результату фабрики на нуль кращим є тест-лов десь, який може зафіксувати виняток з конструктора? Здається, ваш підхід є "нехтуванням винятками. Поверніться до явної перевірки значення повернення з методів на наявність помилок". Ми давно визнали, що винятки, як правило, перевершують необхідність явно перевірити повернення значення кожного методу на помилки, тому ваша порада здається підозрілою. Я хотів би побачити певне обґрунтування того, чому ви вважаєте, що загальне правило тут не застосовується.
ДВ

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

2
  • Конструктор не повинен мати ніяких побічних ефектів.
    • Більше, ніж ініціалізація приватного поля, слід розглядати як побічний ефект.
    • Конструктор із побічними ефектами порушує принцип одиночної відповідальності (SRP) і суперечить духу об'єктно-орієнтованого програмування (ООП).
  • Конструктор повинен бути легким і ніколи не повинен виходити з ладу.
    • Наприклад, я завжди здригаюся, коли бачу блок-спроб всередині конструктора. Конструктор не повинен викидати винятки або помилки журналу.

Можна з розумом поставити під сумнів ці вказівки і сказати: "Але я не дотримуюся цих правил, і мій код працює нормально!" На це я відповів би: "Ну, це може бути правдою, поки цього не буде".

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

0

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

Отже, це залежить від того, наскільки важливим є текст параметру методу - чи має об'єкт сенс з нульовим чи порожнім значенням?


2
Я думаю, що питання передбачає, що клас насправді вимагає, щоб рядок не був порожнім. Це не так у вашому прикладі StringBuilder.
Серхіо Акоста

Тоді відповідь повинна бути так - допустимо кидати виняток. Щоб уникнути необхідності спробувати / вловити цю помилку, тоді ви можете використовувати заводський зразок для обробки створеного для вас об'єкта, який би включав порожній рядок.
Стівен Стрига

0

Моя перевага - встановити за замовчуванням, але я знаю, що у Java є бібліотека "Apache Commons", яка робить щось подібне, і я думаю, що це досить непогана ідея. Я не бачу проблеми з викидом винятку, якщо неправильне значення поставило б об'єкт у непридатний стан; рядок - це не гарний приклад, але що робити, якщо це було для бідного чоловіка? Ви не можете працювати, якщо значенняnull замість ICustomerRepositoryінтерфейсу , скажімо, було передано . У такій ситуації, я б сказав, що викидання виключень - це правильний спосіб поводження з речами.


-1

Коли я працював у c ++, я не використовував, щоб кидати винятки в конструкторі через проблему витоку пам'яті, до якої це може призвести, я навчився цьому важко. Кожен, хто працює з c ++, знає, наскільки важким і проблемним може бути витік пам'яті.

Але якщо ви перебуваєте в c # / Java, то у вас немає такої проблеми, оскільки сміттєзбірник збиратиме пам'ять. Якщо ви використовуєте C #, я думаю, що краще використовувати властивість для приємного та послідовного способу, щоб гарантувати обмеження даних.

public class MyClass
{
    private string _text;
    public string Text 
    {
        get
        {
            return _text;
        } 
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Text cannot be empty");
            _text = value;
        } 
    }

    public MyClass(string text)
    {
        Text = text;
        // continue with normal construction
    }
}

-3

Закидання винятку буде справжнім болем для користувачів вашого класу. Якщо валідація - це проблема, то я, як правило, створюю об'єкт двоступеневим процесом.

1 - Створіть екземпляр

2 - Виклик методу ініціалізації з результатом повернення.

АБО

Викличте метод / функцію, яка створює для вас клас, який може зробити перевірку.

АБО

Майте прапор isValid, який мені особливо не подобається, але деякі люди це роблять.


3
@Dunk, це просто неправильно.
CaffGeek

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

5
ТОЧКА полягає в тому, що після того, як я викликаю конструктор, якщо він не створюється через те, що вхід недійсний, це ВИКОНАННЯ. Дані програми тепер перебувають у непослідовному / недійсному стані, якщо я просто створю об’єкт і встановлюю прапор із написом isValid = false. Тестування після створення класу, якщо він дійсний, це жахливий дизайн, схильний до помилок. І, ви сказали, мати конструктор, а потім викликати ініціалізацію ... Що робити, якщо я не закликаю ініціалізувати? Що тоді? А що робити, якщо Initialize отримує погані дані, я можу зараз кинути виняток? Ваш клас не повинен бути складним у використанні.
CaffGeek

5
@Dunk, якщо ви виявите важкі у використанні винятки, ви неправильно їх використовуєте. Простіше кажучи, поганий вхід був наданий конструктору. Це виняток. Додаток не повинен продовжувати працювати, якщо це не виправлено. Період. Якщо це станеться, у вас виявиться гірша проблема, погані дані. Якщо ви не знаєте, як обробити виняток, ви цього не зробите, ви дозволяєте йому міхувати стеком викликів, поки з ним не може бути оброблено, навіть якщо це означає просто ведення журналу та припинення виконання. Простіше кажучи, вам не потрібно обробляти винятки або перевіряти їх. Вони просто працюють.
CaffGeek

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