Чи повинні функції повертати нульовий або порожній об'єкт?


209

Яка найкраща практика при поверненні даних із функцій. Краще повернути нуль або порожній об’єкт? І чому варто робити одне над іншим?

Врахуйте це:

public UserEntity GetUserById(Guid userId)
{
     //Imagine some code here to access database.....

     //Check if data was returned and return a null if none found
     if (!DataExists)
        return null; 
        //Should I be doing this here instead? 
        //return new UserEntity();  
     else
        return existingUserEntity;
}

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


4
Я думаю, ти маєш на увазі if (!DataExists).
Сара Судас

107
Це архітектурне питання і цілком доречно. Питання ОП є дійсним незалежно від бізнес-проблеми, яку він намагається вирішити.
Джозеф Ферріс

2
На це питання вже достатньо відповіли. Я думаю, що це дуже цікаве питання.
Джон Сципіон

'getUser ()' повинен повернути null. 'getCurrentUserInfo ()' або 'getCurrentPermissions ()', OTOH, будуть більш показовими питаннями - вони повинні повернути об'єкт невідповідних відповідей незалежно від того, хто / чи ввійшов хтось.
Thomas W

2
Ні @Bergi інший - це дублікат. Спершу просили міну, у жовтні, у другій просили 3 місяця пізніше, у грудні. Плюс інший розповідає про колекцію, яка дещо відрізняється.
7wp

Відповіді:


207

Повернення нуля зазвичай є найкращою ідеєю, якщо ви маєте намір вказати, що немає даних.

Порожній об'єкт означає, що дані повернуті, тоді як повернення нуля чітко вказує на те, що нічого не повернуто.

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


21
Ви повинні кинути виняток, не ковтаючи проблему і повертаючи нуль. Як мінімум, ви повинні записати це, а потім продовжити.
Кріс Баланс

130
@Chris: Я не згоден. Якщо в коді чітко зафіксовано нульове значення, то повернути null цілком прийнятно, якщо не буде знайдено жодного результату, який відповідає вашим критеріям. Викидання виключення має бути вашим ОСТАННІМ вибором, а не ПЕРШИМ.
Майк Хофер

12
@Chris: На чому ви вирішили це? Додавання журналу до рівняння, безумовно, здається надмірним. Дозвольте споживчому коду вирішити, що - якщо що-небудь - робити в разі відсутності користувача. Як і у моєму попередньому коментарі, з поверненням значення, яке загальновизначено як "відсутні дані", абсолютно не виникає.
Адам Робінсон,

17
Я трохи збентежений тим, що розробник Microsoft вважає, що "повернення нуля" прирівнюється до "проковтування проблеми". Якщо пам'ять служить, у Framework є тонна методи, де методи повертаються null, якщо немає нічого, що відповідає запиту абонента. Це "проблема ковтання"?
Майк Хофер

5
І останнє, але не менш важливе, було б bool GetUserById(Guid userId, out UserEntity result)- що я вважаю за краще «нульове» значення повернення, і яке не таке екстремальне, як кидання винятку. Це дозволяє красивий null-безкоштовний код, як if(GetUserById(x,u)) { ... }.
Марсель Джекверт

44

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

Чи має сенс повернути нуль, наприклад, "такого користувача немає"?

Або має сенс створити користувача за замовчуванням? Це має найбільш сенс, коли можна сміливо припускати, що якщо користувач НЕ існує, викликовий код має намір існувати, коли він запитує його.

Або має сенс кинути виняток (a la "FileNotFound"), якщо викликовий код вимагає користувача з недійсним ідентифікатором?

Однак - з розгляду проблем / позицій СРП, перші два є правильнішими. А технічно перше - це найправильніше (але лише за волоссям) - GetUserById повинен відповідати лише за одне - отримання користувача. Поводження з власною справою "користувач не існує" шляхом повернення чогось іншого може бути порушенням SRP. Розділення на інший чек - bool DoesUserExist(id)було б доречно, якщо ви вирішите кинути виняток.

На підставі обширних коментарів нижче : якщо це питання дизайну на рівні API, цей метод може бути аналогічним "OpenFile" або "ReadEntireFile". Ми "відкриваємо" користувача з якогось сховища та зволожуємо об'єкт із отриманих даних. Виняток може бути доречним у цьому випадку. Це може бути, але це може бути.

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


Хтось проголосував за вас, і я проголосував за вас, оскільки це не здається мені поганою відповіддю; за винятком: я б ніколи не кидав винятку, коли не знайшов користувача у такому способі, як той, який дає плакат. Якщо пошук користувача не має на увазі недійсний ідентифікатор або якусь подібну проблему, яка має значення для винятків, це повинно траплятися вище - метод метання повинен знати більше про те, звідки цей ідентифікатор, і т. Д.
Jacob Mattison

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

1
Погоджено до останнього моменту. Немає порушення SRP, повертаючи значення, яке загальновизначається як "немає даних". Це як би сказати, що база даних SQL повинна повернути помилку, якщо пункт де не дає результатів. Хоча Виняток - це правильний вибір дизайну (хоча той, який би дратував мене як споживача), він не є "правильнішим", ніж повернення нуля. І ні, я не DV.
Адам Робінсон

@JacobM ми викидаємо винятки, коли вимагаємо шлях до файлової системи, який не існує, не повертає null, але не з баз даних. Тож однозначно обидва підходять, до чого я потрапляю - це просто залежить.
Rex M

2
@Charles: Ви відповідаєте на питання "чи слід викинути виняток у якийсь момент", але питання "чи повинна ця функція кинути виняток". Правильна відповідь - "може", а не "так".
Адам Робінсон

30

Особисто я використовую NULL. Зрозуміло, що даних, які повертати, немає. Але бувають випадки, коли Null Object може бути корисним.


Просто збираюся додати це як відповідь сам. NullObjectPattern або особливий шаблон корпусу. Тоді ви можете реалізувати по одному для кожного випадку, NoUserEntitiesFound, NullUserEntities тощо
David Swindells

27

Якщо ваш тип повернення - це масив, тоді поверніть порожній масив, інакше поверніть null.


Чи 0 елементів у списку такий самий, як список непризначений наразі?
AnthonyWJones

3
0 елементів у списку не те саме, що null. Це дозволяє використовувати його у foreachвисловлюваннях та запитах linq, не турбуючись про це NullReferenceException.
Дарин Димитров,

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

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

Повернення порожнього масиву, коли дані недоступні, просто неправильне . Існує різниця між наявними даними, які не містять елементів, і тими, що не доступні. Повернення порожнього масиву в обох випадках унеможливлює розуміння того, у чому справа. Зробивши це так, щоб ви могли використовувати foreach, не перевіряючи, чи існують дані, виходить за рамки нерозумного - абонент повинен перевірити, чи існують дані, і NullReferenceException, якщо абонент не перевіряє, це добре, оскільки він виявляє помилку.
Відновіть Моніку

12

Вам слід кинути виняток (лише), якщо певний контракт порушено.
У вашому конкретному прикладі, запитуючи UserEntity на основі відомого Id, це залежатиме від того, якщо очікуваний випадок відсутніх (видалених) користувачів. Якщо так, то повернутися , nullале якщо це не так , то очікується , кинути виняток.
Зауважте, що якщо функція була названа, UserEntity GetUserByName(string name)вона, ймовірно, не кидає, а повертає null. В обох випадках повернення порожнього UserEntity було б не корисним.

Для рядків, масивів і колекцій ситуація зазвичай відрізняється. Я пам’ятаю деякі форми керівництва MS, які методи повинні приймати nullяк «порожній» список, але повертати колекції нульової довжини, а не null. Те саме для струн. Зауважте, що ви можете оголосити порожні масиви:int[] arr = new int[0];


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

Рядки, колекції та масиви не відрізняються. Якщо MS каже, що MS неправильно. Існує різниця між порожнім рядком та null та між порожньою колекцією та null. В обох випадках перший представляє наявні дані (розміром 0), а останній - відсутність даних. У деяких випадках відмінність дуже важлива. Наприклад, якщо ви шукаєте запис у кеш-пам'яті, ви хочете знати різницю між кешованими даними, але порожніми, і тими, що не кешуються, так що ви повинні отримати їх з базового джерела даних, де це може бути не порожній.
Відновіть Моніку

1
Ви, здається, сумуєте за суттю та контекстом. .Wher(p => p.Lastname == "qwerty")повинен повернути порожню колекцію, не null.
Хенк Холтерман

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

11

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

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

Якщо з відсутнім користувачем все в порядку, (один із можливих нормальних результатів виклику цієї функції), тоді поверніть нуль ....

РЕДАКТИРУЙТЕ: (щоб звернутися до коментаря Адама в іншій відповіді)

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

Але якщо всі процеси в програмі вимагають від користувача, я б все-таки кинув виняток у цьому методі ...


-1 до низовика, +1 до Чарльза - це цілком бізнес питання, і для цього немає кращої практики.
Остін Салонен

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

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

10

Я особисто повернув би нуль, тому що саме так я очікую, що діятиме шар DAL / Repository.

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

Найголовніше - бути постійним у вашому шарі DAL / Repos, щоб ви не заплутувались у тому, як ним користуватися.


7

Я схильний до

  • return nullякщо ідентифікатор об'єкта не існує, коли заздалегідь невідомо, чи повинен він існувати.
  • throwякщо ідентифікатор об'єкта не існує, коли він повинен існувати.

Я диференціюю ці два сценарії з цими трьома типами методів. Перший:

Boolean TryGetSomeObjectById(Int32 id, out SomeObject o)
{
    if (InternalIdExists(id))
    {
        o = InternalGetSomeObject(id);

        return true;
    }
    else
    {
        return false;
    }
}

Друге:

SomeObject FindSomeObjectById(Int32 id)
{
    SomeObject o;

    return TryGetObjectById(id, out o) ? o : null;
}

Третє:

SomeObject GetSomeObjectById(Int32 id)
{
    SomeObject o;

    if (!TryGetObjectById(id, out o))
    {
        throw new SomeAppropriateException();
    }

    return o;
}

Ви маєте на увазі outніref
Метт Еллен

@Matt: Так, сер, я, безумовно, так і роблю! Виправлено.
Йоганн Герелл

2
Дійсно, це єдина відповідь, що відповідає всім, і, отже, цілком і єдино правда! :) Так, це залежить від припущень, на основі яких застосовується метод ... Тож спочатку очистіть ці припущення, а потім виберіть правильну комбінацію вищезазначених. Довелося прокручувати занадто багато вниз, щоб потрапити сюди :) +100
yair

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

6

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

public void GetUserById(Guid id, UserCallback callback)
{
    // Lookup user
    if (userFound)
        callback(userEntity);  // or callback.Call(userEntity);
}

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

public void GetUserById(Guid id, UserCallback callback, NotFoundCallback notFound)
{
    // Lookup user
    if (userFound)
        callback(userEntity);  // or callback.Call(userEntity);
    else
        notFound(); // or notFound.Call();
}

Той самий підхід із використанням одного об’єкта може виглядати так:

public void GetUserById(Guid id, UserCallback callback)
{
    // Lookup user
    if (userFound)
        callback.Found(userEntity);
    else
        callback.NotFound();
}

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


Цікаво. Коли ви почали говорити про делегатів, я одразу почав цікавитись, чи можна використовувати тут вирази лямбда.
7wp

Так! Як я розумію, лямбда-синтаксис C # 3.0 і вище - це в основному синтаксичний цукор для анонімних делегатів. Аналогічно і в Java, без симпатичного синтаксису лямбда або анонімного делегата, ви можете просто створити анонімний клас. Це трохи потворніше, але може бути дуже зручним. Я вважаю , в ці дні, мій C # приклад міг би використовувати Func <UserEntity> або щось подібне замість імені делегата, але останній проект C # я був ще використовує версію 2.
Марк

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

4

Ми використовуємо CSLA.NET, і він вважає, що невдалий збір даних повинен повернути "порожній" об'єкт. Це насправді дуже дратує, оскільки вимагає конвенції перевірити, чи obj.IsNewпівденніше, ніжobj == null .

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

Особисто я думаю null більш елегантним.

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

Ви можете впоратися з цим:

якщо (User.Exists (id)) {
  this.User = User.Fetch (id);
} else {
  Response.Redirect ("~ / notfound.aspx");
}

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

this.User = User.Fetch (id);

якщо (this.User == null) {
  Response.Redirect ("~ / notfound.aspx");
}

... вимагає лише одного дзвінка.


4

Я вважаю за краще null, оскільки він сумісний з оператором зв'язання з нулем ( ??).


4

Я б сказав, що повернути null замість порожнього об'єкта.

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

Це правило, якого я зазвичай дотримуюся:

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

Чому б ви кидали виняток у будь-якому з цих випадків? Іноді користувачів не існує в базі даних, і ми очікуємо, що цього не станеться. Це не виняткова поведінка.
Сіріде

3

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

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


+1 Я допитувався до тієї самої логіки, яку ви тут говорите, саме тому я опублікував це запитання, щоб побачити, які інші думки щодо цього будуть
7wp

3

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

Редагувати:

Зазвичай такі методи є членами якогось класу "Користувач" і не мають доступу до його членів. У цьому випадку метод повинен бути статичним, інакше потрібно створити екземпляр "User" і потім викликати метод GetUserById, який поверне інший екземпляр "User". Погодьтеся, це заплутано. Але якщо метод GetUserById є членом якогось класу "DatabaseFactory" - немає проблем залишити його як член екземпляра.


Чи можу я запитати, чому я хотів би зробити свій метод статичним? Що робити, якщо я хочу використовувати введення залежності?
7wp

Гаразд, я розумію вашу логіку. Але я дотримуюся схеми сховища, і мені подобається використовувати ін'єкцію залежності для моїх сховищ, тому я не можу використовувати статичні методи. Але +1 за пропозицію повернути нуль :)
7wp

3

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

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


+1 Мені подобається альтернативний погляд на проблему. Отже, ви в основному говорите, який підхід, який я обрав, повинен бути добре, якщо метод є послідовним у всій програмі?
7wp

1
Це моє переконання. Я думаю, що послідовність надзвичайно важлива. Якщо ви робите речі в декількох місцях, це підвищує ризик нових помилок. Ми особисто пішли з об'єктним підходом за замовчуванням, оскільки він добре працює з шаблоном Essence, який ми використовуємо в нашій доменній моделі. У нас є єдиний загальний метод розширення, який ми можемо протестувати проти всіх об'єктів домену, щоб повідомити, чи він заповнений чи ні, щоб ми знали, що будь-який DO може бути протестований за допомогою виклику objectname.IsDefault () - уникаючи будь-яких перевірок рівності безпосередньо .
Джозеф Ферріс

3

У наших бізнес-об'єктах є 2 основні методи отримання:

Щоб все було просто в контексті або ставите під сумнів, вони б:

// Returns null if user does not exist
public UserEntity GetUserById(Guid userId)
{
}

// Returns a New User if user does not exist
public UserEntity GetNewOrExistingUserById(Guid userId)
{
}

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

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


3

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

Використовуючи Java, нас попросять додати assert exists(object) : "You shouldn't try to access an object that doesn't exist";на початку будь-який метод, який міг би повернути нуль, щоб висловити "попередню умову" (я не знаю, що це слово англійською мовою).

IMO це насправді не просто, але це те, що я використовую, чекаючи чогось кращого.


1
Дякую за вашу відповідь. Але мені не подобається ідея перевірити спочатку, чи існує. Причина полягає в тому, що генерується додатковий запит до бази даних. У додатку, до якого мільйони людей звертаються за день, це може призвести до значних втрат продуктивності.
7wp

1
Однією з переваг є те, що перевірка на наявність належно абстрактна: якщо (userExists) трохи читабельніше, ближче до проблемного домену, і менш 'комп'ютерне', ніж: якщо (user == null)
timoxley

І я заперечую, що "if (x == null)" - це десятиліття, коли ви не бачили його раніше, ви не писали код дуже довго (і вам слід звикнути до нього, як є мільйони рядків коду). "Комп'ютерність"? Ми говоримо про доступ до бази даних ...
Ллойд Сарджент

3

Якщо в разі користувач не був знайдений придумує досить часто, і ви хочете мати справу з цим по - різному в залежності від обставин (іноді кидання виключення, іноді замінюючи порожній користувач) ви можете також використовувати що - то близько до F # 'S Optionабо Хаскель Maybeтипу , яка явно відокремлює випадок "без значення" від "щось знайдено!". Код доступу до бази даних може виглядати так:

public Option<UserEntity> GetUserById(Guid userId)
{
 //Imagine some code here to access database.....

 //Check if data was returned and return a null if none found
 if (!DataExists)
    return Option<UserEntity>.Nothing; 
 else
    return Option.Just(existingUserEntity);
}

І використовуйте так:

Option<UserEntity> result = GetUserById(...);
if (result.IsNothing()) {
    // deal with it
} else {
    UserEntity value = result.GetValue();
}

На жаль, всі здаються, що такий тип подібний.


2

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


2

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

Використовуючи шаблон NullObject це:

public UserEntity GetUserById(Guid userId)

{// Уявіть тут якийсь код для доступу до бази даних .....

 //Check if data was returned and return a null if none found
 if (!DataExists)
    return new NullUserEntity(); //Should I be doing this here instead? return new UserEntity();  
 else
    return existingUserEntity;

}

class NullUserEntity: IUserEntity { public string getFirstName(){ return ""; } ...} 

2

Щоб сказати, що інші сказали в пітерії, ...

Винятки становлять виключні обставини

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

З іншого боку, якби я очікував, що мій параметр відобразить первинний ключ, і мені слід повернути лише один рядок, якщо я отримав більше однієї спинки, я би кинув виняток. 0 добре, щоб повернути нуль, 2 - ні.

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

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


2

Пробачте мій псевдо-php / код.

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

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

Скажімо, у мене є функція, яка займає первинний ключ і масив даних, заповнює рядок даними, а потім зберігає отриманий запис у db. Оскільки я маю намір будь-яким чином заповнити об’єкт своїми даними, це може бути величезною перевагою повернути порожній об’єкт від геттера. Таким чином, я можу виконувати однакові операції в будь-якому випадку. Ви використовуєте результат функції getter незалежно від того.

Приклад:

function saveTheRow($prim_key, $data) {
    $row = getRowByPrimKey($prim_key);

    // Populate the data here

    $row->save();
}

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

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

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

Приклад:

function displayData($row_id) {
    // Logging of the error would happen in this function
    $row = getRow($row_id);
    if($row === null) {
        // Handle the error here
    }

    // Do stuff here with data
}

function getRow($row_id) {
 $row = null;
 try{
     if(!$db->connected()) {
   throw excpetion("Couldn't Connect");
  }

  $result = $db->query($some_query_using_row_id);

  if(count($result) == 0 ) {
   throw new exception("Couldn't find a record!");
  }

  $row = $db->nextRow();

 } catch (db_exception) {
  //Log db conn error, alert admin, etc...
  return null; // This way I know that null means an error occurred
 }
 return $row;
}

Це моє загальне правило. Наразі це добре працює.


2

Цікаве запитання, і я думаю, що «правильної» відповіді немає, оскільки це завжди залежить від відповідальності вашого коду. Чи знає ваш метод, якщо знайдені дані не є проблемою чи ні? У більшості випадків відповідь - «ні», і тому повернення нуля і надання можливості обробці абоненту він ідеальний.

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


+1 Мені подобається ідея використовувати уніфіковану схему іменування для передачі програмісту способу використання цієї функції.
7wp

1
Раптом я визнаю, що це робить LINQ: розглянемо First (...) vs. FirstOrDefault (...)
Marc Wittke

2

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

Приклад:

bool IsAdministrator(User user)
{
    var groupsOfUser = GetGroupsOfUser(user);

    // This foreach would cause a run time exception if groupsOfUser is null.
    foreach (var groupOfUser in groupsOfUser) 
    {
        if (groupOfUser.Name == "Administrators")
        {
            return true;
        }
    }

    return false;
}

2

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

Крім того, такі методи, які не можуть повернути результату, повинні вказувати на це через їх назву. Як правило, я ставлю Try або TryGet або TryFind на початку імені методу, щоб вказати, що він може повернути порожній результат (наприклад, TryFindCustomer, TryLoadFile тощо).

Це дозволяє абоненту застосовувати різні методи, як-от конвеєрний конвеєр (див. Збірник трубопроводу Мартіна Фаулера ) на результат.

Ось ще один приклад, коли повернення Option замість null використовується для зменшення складності коду: Як зменшити цикломатичну складність: Варіант функціонального типу


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

1

Більше м'яса для подрібнення: скажімо, мій DAL повертає нуль для GetPersonByID, як радили деякі. Що повинен робити мій (досить тонкий) BLL, якщо він отримує NULL? Передайте цю NULL вгору і дозвольте кінцевому споживачеві хвилюватися з цього приводу (у цьому випадку на сторінці ASP.Net)? Як щодо того, щоб BLL кинула виняток?

BLL може використовуватися ASP.Net та Win App, або іншою бібліотекою класів - я думаю, що несправедливо сподіватися, що кінцевий споживач невластиво "знає", що метод GetPersonByID повертає нуль (якщо не використовуються нульові типи, я думаю ).

Я вважаю, що мій DAL повертає NULL, якщо нічого не знайдено. ДЛЯ ЯКІХ ОБ'ЄКТІВ це нормально - це може бути список речей 0: багато, тому не мати жодних речей (наприклад, список улюблених книг). У цьому випадку мій BLL повертає порожній список. Для більшості речей з окремими особами (наприклад, користувача, облікового запису, рахунку-фактури), якщо я не маю цього, то це, безумовно, проблема і кидання дорогого виключення. Однак, якщо вилучення користувача за допомогою унікального ідентифікатора, який раніше був доданий програмою, завжди повинен повертати користувача, виняток є "належним" винятком, як у винятковому. Кінцевий споживач BLL (ASP.Net, речовина) лише коли-небудь очікує, що речі будуть виправданими, тому обробник Unhandled Exceller буде використовуватися замість того, щоб перетворювати кожен виклик на GetPersonByID у блок пробних випробувань.

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

Мені подобається цей пост, багато хороших пропозицій для сценаріїв "це залежить" :-)


І звичайно, сьогодні я зіткнувся зі сценарієм, коли я збираюся повернути NULL з мого BLL ;-) Це сказав, що я все-таки можу викинути виняток і використовувати спробу / ловити у своєму споживчому класі, Але у мене все ще є проблеми : як мій спожиючий клас знає використовувати спробу / ловити, як вони знають, щоб перевірити наявність NULL?
Майк Кінгскотт

Ви можете задокументувати, що метод видає виняток через доктаг @throw, і ви задокументуєте факт, що він може повернути нуль у доктазі @return.
timoxley

1

Я думаю, що функції не повинні повертатись до нуля, для здоров'я вашої кодової бази. Я можу придумати кілька причин:

Буде велика кількість охоронних застережень, що стосуються нульової посилання if (f() != null) .

Що таке null, це прийнята відповідь чи проблема? Чи нульовим є дійсний стан для конкретного об'єкта? (уявіть, що ви - клієнт коду). Я маю на увазі, що всі типи посилань можуть бути недійсними, але чи повинні вони?

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

Є деякі рішення tester-doer patternабо реалізація option typeфункціонального програмування.


0

Мене здивує кількість відповідей (в усьому Інтернеті), які говорять, що вам потрібні два методи: метод "IsItThere ()" та метод "GetItForMe ()", і це призводить до стану гонки. Що не так з функцією, яка повертає null, призначаючи її змінній та перевіряючи змінну на Null all в одному тесті? Мій колишній код С був переповнений

if (NULL! = (змінний = функція (аргументи ...))) {

Таким чином, ви отримуєте значення (або null) в змінній, а результат все відразу. Чи забули цю ідіому? Чому?


0

Я погоджуюся з більшістю публікацій, які мають тенденцію до цього null .

Моє міркування полягає в тому, що генерування порожнього об'єкта з ненульовими властивостями може спричинити помилки. Наприклад, суб'єкт int IDвласності має початкову вартістьID = 0 , яке є цілком дійсним значенням. Якщо цей об’єкт за якихось обставин потрапить у базу даних, це було б погано.

Для чого-небудь з ітератором я завжди використовував би порожню колекцію. Щось на зразок

foreach (var eachValue in collection ?? new List<Type>(0))

на мій погляд, кодовий запах. Властивості колекції ніколи не повинні бути нульовими.

Крайовий корпус є String. Багато людей кажуть, що String.IsNullOrEmptyце насправді не потрібно, але ви не завжди можете розрізнити порожній рядок від нуля. Крім того, деякі системи баз даних (Oracle) взагалі не розрізняють їх ( ''зберігаються як DBNULL), тому ви змушені поводитися з ними однаково. Причиною тому є те, що більшість значень рядків надходять або з введення користувача, або із зовнішніх систем, тоді як ні текстові поля, ні більшість форматів обміну не мають різних представлень для ''та null. Тож навіть якщо користувач хоче видалити значення, він не може зробити нічого, крім очищення керування введенням. Також розмежування нульового і ненульовогоnvarchar полів баз даних є більш ніж сумнівним, якщо ваша СУБД не оракул - обов'язкове поле, яке дозволяє''дивно, ваш інтерфейс ніколи цього не дозволить, тому ваші обмеження не збігаються. Тож відповідь тут, на мою думку, поводиться з ними однаково, завжди.

Що стосується вашого питання щодо винятків та продуктивності: Якщо ви кидаєте виняток, який ви не можете повністю впоратися з логікою вашої програми, вам доведеться в якийсь момент відмовитися від того, що робить ваша програма, і попросити користувача повторити все, що він щойно робив. У цьому випадку покарання продуктивності A catch- це насправді найменша стурбованість - потрібно запитати у користувача слона в кімнаті (що означає передати весь інтерфейс або надіслати трохи HTML через Інтернет). Тож якщо ви не дотримуєтеся анти-схеми " Потік програми з винятками ", не турбуйтеся, просто киньте її, якщо це має сенс. Навіть у прикордонних випадках, таких як "Виняток з валідації", продуктивність справді не є проблемою, оскільки вам доведеться запитувати користувача знову, в будь-якому випадку.


0

Асинхронний TryGet шаблон:

Щодо синхронних методів, я вважаю , що відповідь @Johann Gerell - це шаблон, який слід використовувати у всіх випадках.

Однак шаблон TryGet з outпараметром не працює з методами Async.

З Tuple Literals C # 7 тепер ви можете це зробити:

async Task<(bool success, SomeObject o)> TryGetSomeObjectByIdAsync(Int32 id)
{
    if (InternalIdExists(id))
    {
        o = await InternalGetSomeObjectAsync(id);

        return (true, o);
    }
    else
    {
        return (false, default(SomeObject));
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.