Вкажіть необов'язкові імена параметрів, хоча вони не потрібні?


25

Розглянемо наступний метод:

public List<Guid> ReturnEmployeeIds(bool includeManagement = false)
{

}

І наступний дзвінок:

var ids = ReturnEmployeeIds(true);

Для нового розробника в системі було б досить складно здогадатися, що trueзробив. Перше, що ви зробите, - навести курсор на ім'я методу або перейти до визначення (жодне з яких не є великими завданнями). Але, для читабельності, чи є сенс писати:

var ids = ReturnEmployeeIds(includeManagement: true);

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

У наступній статті йдеться про деякі умови кодування: https://msdn.microsoft.com/en-gb/library/ff926074.aspx

Щось подібне до статті вище було б чудово.


2
Це два досить непов’язаних питання: (1) Чи слід виписати необов'язкові аргументи для наочності? (2) Чи слід використовувати booleanаргументи методу, і як?
Кіліан Фот

5
Все це зводиться до особистих уподобань / стилю / думки. Особисто мені подобаються назви параметрів, коли передане значення є буквальним (змінна може вже передавати достатньо інформації через своє ім'я). Але це моя особиста думка.
MetaFight

1
можливо, включіть це посилання як джерело прикладу у своє запитання?
MetaFight

4
Якщо це щось додає, я запустив його через StyleCop & ReSharper - жоден чіткий погляд на це не мав. ReSharper просто говорить, що названий параметр може бути видалений, а потім продовжує говорити, що названий параметр може бути доданий. Поради є безкоштовними з причини, на яку я думаю ...: - /
Роббі Ді

Відповіді:


28

Я б сказав, що в світі C # перерахунок був би одним із найкращих варіантів.

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

public enum WhatToInclude
{
    IncludeAll,
    ExcludeManagement
}

var ids = ReturnEmployeeIds(WhatToInclude.ExcludeManagement);

Отже, на мою думку тут:

enum> необов'язковий enum> необов'язковий bool

Редагувати: Зважаючи на обговорення з LeopardSkinPillBoxHat нижче щодо [Flags]перерахунку, який у цьому конкретному випадку може бути вдалим (оскільки ми конкретно говоримо про включення / виключення речей), я замість цього пропоную використовувати ISet<WhatToInclude>як параметр.

Це більш "сучасна" концепція з кількома перевагами, яка головним чином випливає з того, що вона входить до сімейства колекцій LINQ, але також [Flags]має 32 групи. Основним недоліком використання ISet<WhatToInclude>є те, як погано підтримуються набори в синтаксисі C #:

var ids = ReturnEmployeeIds(
    new HashSet<WhatToInclude>(new []{ 
        WhatToInclude.Cleaners, 
        WhatToInclude.Managers});

Деякі з них можуть бути пом’якшені допоміжними функціями, як для створення набору, так і для створення "наборів ярликів", таких як

var ids = ReturnEmployeeIds(WhatToIncludeHelper.IncludeAll)

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

3
Мені подобається enumпідхід, але я не великий шанувальник змішування Includeі Excludeв тому ж enum, що важче зрозуміти, що відбувається. Якщо ви хочете, щоб це було по-справжньому розширюваним, ви можете зробити це [Flags] enum, зробити їх усіма IncludeXxxі надати те, на IncludeAllщо встановлено Int32.MaxValue. Тоді ви можете забезпечити 2 ReturnEmployeeIdsперевантаження - одна, яка повертає всіх працівників, і одна, яка приймає WhatToIncludeпараметр фільтра, який може бути комбінацією прапорів enum.
LeopardSkinPillBoxHat

@LeopardSkinPillBoxHat Добре, я згоден на виключення / включення. Отже, це трохи надуманий приклад, але я міг би назвати це IncludeAllButManagement. З іншого боку, я не згоден - я вважаю [Flags], що це реліквія, і його слід використовувати лише у спадщині чи з метою виконання. Я думаю, що a ISet<WhatToInclude>- це набагато краща концепція (на жаль, у C # бракує синтаксичного цукру, щоб зробити його приємним у використанні).
NiklasJ

@NiklasJ - Мені подобається такий Flagsпідхід, оскільки він дозволяє абоненту проходити WhatToInclude.Managers | WhatToInclude.Cleanersі побіжно або виражає, як саме він буде обробляти його (тобто менеджери чи прибиральники). Інтуїтивно зрозумілий синтаксичний цукор :-)
LeopardSkinPillBoxHat

@LeopardSkinPillBoxHat Саме так, але це єдиний перелік цього методу. Недоліки включають, наприклад, обмежену кількість варіантів і не сумісні з іншими поняттями, подібними до linq.
NiklasJ

20

чи є сенс писати:

 var ids=ReturnEmployeeIds(includeManagement: true);

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

 bool includeManagement=true;
 var ids=ReturnEmployeeIds(includeManagement);

Цей стиль, мабуть, більш поширений серед програмістів на C #, ніж названий варіант параметра, оскільки названі параметри не були частиною мови до версії 4.0. Ефект від читабельності майже однаковий, просто потрібен додатковий рядок коду.

Тож моя рекомендація: якщо ви думаєте, що використання enum або двох функцій з різними іменами є "overkill", або ви не хочете або не можете змінити підпис з іншої причини, продовжуйте користуватися названим параметром.


Можна зауважити, що ви використовуєте додаткову пам’ять на другому прикладі
Альваро

10
@Alvaro Це малоймовірно, що це буде правдою в такому тривіальному випадку (де змінна має постійне значення). Навіть якби це було, це навряд чи варто згадувати. Більшість з нас не працює на 4 КБ оперативної пам’яті в наші дні.
Кріс Хейс

3
Оскільки це C #, ви можете позначити його includeManagementяк локальний constі взагалі не використовувати додаткову пам'ять.
Якоб Кралл

8
Хлопці, я не збираюся додавати у свою відповідь будь-які речі, які могли б лише відволіктись від того, що я вважаю важливим тут.
Doc Brown

17

Якщо ви стикаєтесь з таким кодом:

public List<Guid> ReturnEmployeeIds(bool includeManagement = false)
{

}

Ви можете майже гарантувати, що всередині {}заповіту буде щось на кшталт:

public List<Guid> ReturnEmployeeIds(bool includeManagement = false)
{
    if (includeManagement)
        return something
    else
        return something else
}

Іншими словами, булевий використовується для вибору між двома напрямами дії: метод має два обов'язки. Це майже гарантовано кодовий запах . Ми завжди повинні прагнути до того, щоб методи виконували лише одне; вони несуть лише одну відповідальність. Тому я б стверджував, що обидва ваші рішення - це неправильний підхід. Використання необов'язкових параметрів є ознакою того, що метод робить більше, ніж одне. Булеві параметри також є ознакою цього. Маючи обоє, обов'язково слід встановити дзвінок сигналізації. Тому ви повинні зробити це рефакторинг двох різних наборів функціональності на два окремі методи. Дайте назви методів, які дозволяють зрозуміти, що вони роблять, наприклад:

public List<Guid> GetNonManagementEmployeeIds()
{

}

public List<Guid> GetAllEmployeeIds()
{

}

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

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

У такому випадку, чи слід вводити ім'я параметра, навіть якщо компілятор цього не потребує? Я б припустив, що на це немає простої відповіді, і це судження потрібно для кожного конкретного випадку:

  • Аргументом для визначення імені є те, що інші розробники (включаючи себе через півроку) повинні бути основною аудиторією вашого коду. Укладач, безумовно, другорядна аудиторія. Тому завжди пишіть код, щоб іншим людям було легше читати; а не просто постачати те, що потрібно компілятору. Прикладом того, як ми використовуємо це правило щодня, є те, що ми не заповнюємо наш код змінними _1, _2 тощо, ми використовуємо значущі імена (що не означають нічого для компілятора).
  • Протилежним аргументом є те, що приватні методи - це деталі реалізації. Можна з розумом очікувати, що хтось, дивлячись на такий метод, як нижче, простежить через код, щоб зрозуміти призначення булевого параметра, оскільки вони знаходяться всередині внутрішнього коду:
public List<Guid> GetNonManagementEmployeeIds()
{
    return ReturnEmployeeIds(true);
}

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


12
Я чую, що ви говорите, але ви якось пропустили суть того, про що я прошу. Припускаючи , що сценарій , при якому вона є найбільш придатною для використання додаткових параметрів (ці сценарії дійсно існує), це нормально , щоб назвати параметри , навіть якщо в цьому немає необхідності?
JᴀʏMᴇᴇ

4
Все правда, але питання стосувалося заклику до такого методу. Також параметр прапора взагалі не гарантує гілки в методі. Це може просто перейти на інший шар.
Роббі Ді

Це не неправильно, але надмірно спрощує. У реальному коді ви знайдете public List<Guid> ReturnEmployeeIds(bool includeManagement) { calculateSomethingHereForBothBranches; if (includeManagement) return something else return something else }настільки розщеплення, що просто на два способи, ймовірно, означатиме, що ці два методи потребують результату першого обчислення як параметр.
Doc Brown

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

1
Я можу уявити 3 випадки поведінки, які відрізняються між двома функціями. 1. Існує попередня обробка, яка відрізняється. Попросіть загальнодоступні функції попередньо обробити аргументи в приватну функцію. 2. Існує інша післяобробка. Запропонуйте публічним функціям обробляти результат від приватної функції. 3. Існує проміжна поведінка, яка відрізняється. Це може бути передано як лямбда на приватну функцію, якщо ваша мова підтримує її. Часто це призводить до нового абстрагування для багаторазового використання, яке раніше не траплялося вам.
jpmc26

1

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

Так, правда. Самодокументування коду - прекрасна річ. Як вказували інші відповіді, існують способи усунути неоднозначність виклику ReturnEmployeeIdsза допомогою enum, різних назв методів тощо. Однак іноді справді не вдається уникнути справи, але ви не хочете виходити з ладу та використовувати їх скрізь (якщо вам не подобається багатослівність візуального основного).

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

Тут може бути корисний названий аргумент:

var ids = ReturnEmployeeIds(includeManagement: true);

Не додайте додаткової ясності (я думаю, що це розбір перерахунку):

Enum.Parse(typeof(StringComparison), "Ordinal", ignoreCase: true);

Це може насправді знизити ясність (якщо людина не розуміє, що є у списку):

var employeeIds = new List<int>(capacity: 24);

Або де це не має значення, оскільки ви використовуєте хороші імена змінних:

bool includeManagement = true;
var ids = ReturnEmployeeIds(includeManagement);

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

Не AFAIK. Давайте ж розглянемо обидві частини: єдиний час, коли вам потрібно явно використовувати названі параметри, це тому, що компілятор вимагає від вас цього (як правило, це видалити неоднозначність оператора). Тож крім цього, ви можете їх використовувати коли завгодно. У цій статті від MS є кілька пропозицій щодо того, коли ви, можливо, захочете їх використовувати, але це не особливо остаточно https://msdn.microsoft.com/en-us/library/dd264739.aspx .

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

TL; DR

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


0

Якщо параметр очевидний з (повністю кваліфікованого) імені методи , і це існування природно , що метод повинен робити, ви повинні НЕ використовувати іменований синтаксис параметрів. Наприклад, вReturnEmployeeName(57) цьому досить прочесно, що 57це ідентифікатор працівника, тому зайве коментувати його синтаксисом названого параметра.

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

Тепер розглянемо цей третій випадок:

void PrintEmployeeNames(bool includeManagement) {
    foreach (id; ReturnEmployeeIds(includeManagement)) {
        Console.WriteLine(ReturnEmployeeName(id));
    }
}

Синтаксис названого параметра тут не використовується, але оскільки ми передаємо змінну ReturnEmployeeIds, і ця змінна має ім'я, і ​​це ім'я означає те саме, що і параметр, до якого ми передаємо його (це часто так - але не завжди!) - нам не потрібен синтаксис названого параметра, щоб зрозуміти його значення з коду.

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


7
Я не погоджуюсь з тим, ReturnEmployeeName(57)що відразу стає очевидним, що 57це ідентифікатор. GetEmployeeById(57)з іншого боку ...
Гюго Цинк

@HugoZink Я спочатку хотів використати щось подібне для прикладу, але я навмисно змінив це, щоб ReturnEmployeeNameпродемонструвати випадки, коли навіть без явного згадування параметра в імені методу, цілком розумно очікувати, що люди зрозуміють, що це таке. У будь-якому випадку, примітно, що на відміну від цього ReturnEmployeeName, ви не можете розумно перейменувати ReturnEmployeeIdsте, що вказує на значення параметрів.
Ідан Ар'є

1
У будь-якому випадку, навряд чи ми запишемо ReturnE EmployeeName (57) в реальну програму. Набагато ймовірніше, що ми будемо писати ReturnE EmployeeName (EmployId). Чому ми твердо кодуємо ідентифікатор працівника? Якщо це не посвідчення програміста, і там відбувається якесь шахрайство, наприклад, додайте трохи до зарплати цього працівника. :-)
Джей

0

Так, я думаю, що це гарний стиль.

Це має найбільше значення для булевих і цілих констант.

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

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

Звичайно, не всі рядки очевидні. Якщо я бачу GetFooData ("M"), якщо я не знаю функцію, я поняття не маю, що означає "M". Якщо це якийсь код, наприклад, M = "співробітники на рівні управління", то, ймовірно, нам слід мати перерахунок, а не буквальний, а назва enum має бути чітким.

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

Але GetFooData (true) або GetFooData (42) ... це може означати що завгодно. Названі параметри - чудовий спосіб зробити самодокументацію. Я використовував їх саме для цієї мети неодноразово.


0

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

Взагалі намагайтеся уникати булевих аргументів, які на відстані можуть виглядати довільно. includeManagementбуде (швидше за все) сильно впливати на результат. Але аргумент виглядає так, що він несе "невелику вагу".

Про використання перерахунку обговорювалося не тільки те, що це буде виглядати як аргумент "несе більше ваги", але й надасть масштабованість методу. Однак це може бути не найкращим рішенням у всіх випадках, оскільки ваш ReturnEmployeeIds-метод повинен масштабуватись поряд з WhatToInclude-енумом (див. NiklasJ ). Це може призвести до головного болю пізніше.

Врахуйте це: Якщо ви масштабуєте WhatToInclude-enum, але не ReturnEmployeeIds-метод. Потім це може викинути ArgumentOutOfRangeException(найкращий випадок) або повернути щось абсолютно небажане ( nullабо порожнє List<Guid>). Що в деяких випадках може заплутати програміста, особливо якщо вашReturnEmployeeIds перебуваєте в бібліотеці класів, де вихідний код недоступний.

Можна припустити, що WhatToInclude.Traineesспрацює, якщоWhatToInclude.All і є, бачачи, що слухачі є "підмножиною" всіх.

Це (звичайно), залежить від того, як ReturnEmployeeIds реалізовано.


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

Оскільки існує лише два результати для чогось бугельного аргументу. Реалізація двох методів вимагає зовсім небагато додаткових зусиль.

Менше коду, рідше краще.


Зважаючи на це, є деякі випадки, коли явне оголошення аргументу покращує читабельність. Розглянемо, наприклад. GetLatestNews(max: 10). GetLatestNews(10)як і раніше досить зрозумілий, явна декларація maxдопоможе очистити будь-яку плутанину.

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

var ids = ReturnEmployeeIds(includeManagement: true);

.. абсолютно краще і читабельніше, ніж:

var ids = ReturnEmployeeIds(true);

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

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