Укажите необязательные имена параметров, даже если они не обязательны?

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

Нечто похожее на статью выше было бы здорово.

JᴀʏMᴇᴇ
источник
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)
NiklasJ
источник
Я склонен согласиться; и, как правило, привыкли использовать перечисления в моем новом коде, если есть какой-то смутный шанс, что условия будут расширены в будущем. Замена bool на enum, когда что-то превращается в три-состояние, в итоге создает действительно шумные различия и делает обвинительный код намного более утомительным; когда вам в конечном итоге придется снова модифицировать свой код год спустя для третьего варианта.
Дэн Нили,
3
Мне нравится enumподход, но я не большой поклонник микширования, Includeи Excludeв то же enumвремя мне сложнее понять, что происходит. Если вы хотите, чтобы это было действительно расширяемым, вы можете сделать это a [Flags] enum, сделать их все IncludeXxxи предоставить значение, IncludeAllкоторое установлено в Int32.MaxValue. Затем вы можете предоставить 2 ReturnEmployeeIdsперегрузки - одну, которая возвращает всех сотрудников, и другую, которая принимает WhatToIncludeпараметр фильтра, который может быть комбинацией флагов enum.
LeopardSkinPillBoxHat
@ LeopardSkinPillBoxHat Хорошо, я согласен на исключение / включение. Так что это немного надуманный пример, но я мог бы назвать его IncludeAllButManagement. С другой вашей точки зрения, я не согласен - я думаю, что [Flags]это реликвия, и ее следует использовать только из соображений наследия или производительности. Я думаю, что ISet<WhatToInclude>это гораздо лучшая концепция (к сожалению, в C # не хватает синтаксического сахара, чтобы его было удобно использовать).
NiklasJ
@NiklasJ - мне нравится Flagsподход, потому что он позволяет вызывающей стороне передавать WhatToInclude.Managers | WhatToInclude.Cleanersи побитовый или точно выражает, как вызывающая сторона будет обрабатывать его (т. Е. Менеджеры или уборщики). Интуитивно понятный синтаксический сахар :-)
LeopardSkinPillBoxHat
@ LeopardSkinPillBoxHat Точно, но это единственный плюс этого метода. Недостатки включают, например, ограниченное количество вариантов и несовместимость с другими понятиями, подобными linq.
NiklasJ
20

имеет ли смысл писать:

 var ids=ReturnEmployeeIds(includeManagement: true);

Это спорно, если это «хороший стиль», но ИМХО это, как минимум, не плохой стиль, и полезно, пока строка кода не становится «слишком долго». Без именованных параметров это также общий стиль для введения объясняющей переменной:

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

Этот стиль, вероятно, более распространен среди программистов C #, чем вариант именованных параметров, поскольку именованные параметры не были частью языка до версии 4.0. Влияние на читаемость практически одинаково, просто нужна дополнительная строка кода.

Поэтому моя рекомендация: если вы думаете, что использование enum или двух функций с разными именами - это «перебор», или вы не хотите или не можете изменить сигнатуру по другой причине, продолжайте и используйте названный параметр.

Док Браун
источник
Могу заметить, что вы используете дополнительную память во втором примере
Alvaro
10
@ Alvaro Это очень маловероятно, чтобы быть правдой в таком тривиальном случае (где переменная имеет постоянное значение). Даже если это и так, вряд ли стоит упоминать. В наши дни большинство из нас не используют 4 КБ ОЗУ.
Крис Хейс
3
Поскольку это C #, вы можете пометить его includeManagementкак локальный constи вообще не использовать дополнительную память.
Джейкоб Кралл
8
Ребята, я не собираюсь добавлять в свой ответ какие-либо вещи, которые могут только отвлечь от того, что я считаю важным здесь.
Док Браун
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.

РЕДАКТИРОВАТЬ Как было отмечено в комментариях, случай, когда эти два метода, вероятно, будут иметь общие функциональные возможности, и что общие функциональные возможности, вероятно, лучше всего будут обернуты в частную функцию, которая будет нуждаться в булевом параметре для управления некоторым аспектом того, что он делает ,

В этом случае, следует ли указывать имя параметра, даже если компилятору это не нужно? Я бы предположил, что нет простого ответа на этот вопрос, и это решение необходимо в каждом конкретном случае:

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

Является ли исходный файл и методы маленькими и понятными? Если да, то названный параметр, вероятно, составляет шум. Если нет, это может быть очень полезно. Поэтому применяйте правило в соответствии с обстоятельствами.

Дэвид Арно
источник
12
Я слышу, что вы говорите, но вы упустили из виду то, что я спрашиваю. Предполагая , что сценарий , при котором она является наиболее подходящей для использования дополнительных параметров (эти сценарии действительно существует), это нормально , чтобы назвать параметры , даже если в этом нет необходимости?
JᴀʏM 11:
4
Все верно, но вопрос был о вызове такого метода. Кроме того, параметр flag вообще не гарантирует переход в методе. Это может быть просто передано на другой слой.
Робби Ди
Это не так, но упрощенно. В реальном коде вы обнаружите public List<Guid> ReturnEmployeeIds(bool includeManagement) { calculateSomethingHereForBothBranches; if (includeManagement) return something else return something else }, что разделение на два простых метода, вероятно, будет означать, что эти два метода будут нуждаться в результате первого вычисления в качестве параметра.
Док Браун
4
Я думаю, что все мы говорим, что публичное лицо вашего класса должно, вероятно, выставлять два метода (каждый с одной ответственностью), но внутренне каждый из этих методов может разумно переслать запрос к одному частному методу с параметром booling. ,
MetaFight
1
Я могу представить 3 случая для поведения, которое отличается между двумя функциями. 1. Есть предварительная обработка, которая отличается. Публичные функции должны предварительно обработать аргументы в приватную функцию. 2. Существует постобработка, которая отличается. Публичные функции постобработают результат от частной функции. 3. Существует промежуточное поведение, которое отличается. Это может быть передано в качестве лямбды частной функции, если ваш язык поддерживает это. Часто это приводит к новой многократно используемой абстракции, которая не приходила вам в голову раньше.
jpmc26
1

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

Да, верно. Самодокументированный код - прекрасная вещь. Как указывалось в других ответах, существуют способы устранить неоднозначность вызова ReturnEmployeeIds, используя перечисление, имена других методов и т. Д. Однако иногда вы не можете избежать случая, но не хотите уходить и использовать они повсюду (если вам не нравится многословие Visual Basic).

Например, это может помочь уточнить один вызов, но не обязательно другой.

Именованный аргумент может быть полезен здесь:

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 могут быть перечислены в любом порядке). Кроме того, я использую их только когда я предоставляю статическое значение inline, в противном случае, если я передаю переменную, я пытаюсь использовать хорошие имена переменных.

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
В любом случае, маловероятно, что мы напишем ReturnEmployeeName (57) в реальной программе. Гораздо более вероятно, что мы напишем ReturnEmployeeName (employeeId). Почему мы должны жестко закодировать идентификатор сотрудника? Если это не идентификатор программиста и не происходит какое-то мошенничество, например, добавить немного к зарплате этого сотрудника. :-)
Джей
0

Да, я думаю, что это хороший стиль.

Это имеет смысл для логических и целочисленных констант.

Если вы передаете переменную, имя переменной должно прояснить смысл. Если это не так, вы должны дать переменной лучшее имя.

Если вы передаете строку, смысл часто ясен. Например, если я вижу вызов GetFooData («Орегон»), я думаю, читатель может догадаться, что этот параметр является именем состояния.

Конечно, не все строки очевидны. Если я вижу GetFooData («M»), то, если я не знаю функцию, я не знаю, что означает «M». Если это какой-то код, такой как M = "сотрудники уровня управления", то, вероятно, у нас должно быть перечисление, а не литерал, и имя перечисления должно быть ясным.

И я полагаю, что строка может вводить в заблуждение. Может быть, «Орегон» в моем примере выше относится не к государству, а к продукту, клиенту или чему-то еще.

Но GetFooData (true) или GetFooData (42) ... это может означать что угодно. Именованные параметры - замечательный способ сделать самодокументирование. Я использовал их именно для этой цели несколько раз.

сойка
источник
0

Не существует каких-либо очень четких причин, почему вообще стоит использовать этот тип синтаксиса.

В общем, старайтесь не иметь логических аргументов, которые на расстоянии могут выглядеть произвольно. includeManagement(скорее всего) сильно повлияет на результат. Но аргумент выглядит так, как будто он имеет «небольшой вес».

Обсуждение использования enum не только будет выглядеть как аргумент «имеет больший вес», но и обеспечит масштабируемость метода. Однако это может быть не лучшим решением во всех случаях, так как ваш ReturnEmployeeIds-метод должен масштабироваться вместе с WhatToInclude-enum (см. Ответ NiklasJ ). Это может вызвать у вас головную боль позже.

Учтите это: если вы масштабируете WhatToInclude-enum, но не ReturnEmployeeIds-method. Затем он может выбросить 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может означать абсолютно все . Но я бы постарался избежать этого аргумента.

умереть
источник