Следует ли объявлять методы с использованием перегрузок или необязательных параметров в C # 4.0?


94

Я смотрел рассказ Андерса о C # 4.0 и предварительный просмотр C # 5.0 , и это заставило меня задуматься о том, когда в C # будут доступны дополнительные параметры, каким будет рекомендуемый способ объявления методов, для которых не нужно указывать все параметры?

Например, что-то вроде этого FileStreamкласса имеет около пятнадцати различных конструкторов, которые могут быть разделены на логические «семейства», например, те, которые находятся ниже, из строки, те, которые находятся в an, IntPtrи те, что из a SafeFileHandle.

FileStream(string,FileMode);
FileStream(string,FileMode,FileAccess);
FileStream(string,FileMode,FileAccess,FileShare);
FileStream(string,FileMode,FileAccess,FileShare,int);
FileStream(string,FileMode,FileAccess,FileShare,int,bool);

Мне кажется, что этот тип шаблона можно упростить, если вместо него использовать три конструктора и использовать необязательные параметры для тех, которые могут быть заданы по умолчанию, что сделало бы разные семейства конструкторов более различимыми [примечание: я знаю, что это изменение не будет сделано в BCL, я говорю гипотетически для ситуации такого типа].

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

Ответы:


123

Я бы рассмотрел следующее:

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

Я не проверял, как будут работать значения по умолчанию, но предполагаю, что значения по умолчанию будут записаны в вызывающий код, почти так же, как ссылки на constполя. Обычно это нормально - изменения значения по умолчанию в любом случае довольно значительны, но это то, что нужно учитывать.


21
+1 за мудрость в отношении прагматизма: Иногда легче жить с решением, которое вам не нравится, чем аргументировать свою позицию.
legends2k

13
@romkyns: Нет, эффект перегрузок отличается от пункта 3. С перегрузками, предоставляющими значения по умолчанию, значения по умолчанию находятся в коде библиотеки, поэтому, если вы измените значение по умолчанию и предоставите новую версию библиотеки, вызывающие будут увидеть новое значение по умолчанию без перекомпиляции. Принимая во внимание, что с необязательными параметрами вам нужно будет перекомпилировать, чтобы «увидеть» новые значения по умолчанию. Много времени это не важное различие, но это различие.
Джон Скит

привет @JonSkeet, я хотел бы знать, используем ли мы обе функции, т.е. функцию с необязательным параметром и другие с перегрузкой, какой метод будет вызван ?? например, Add (int a, int b) и Add (int a, int b, int c = 0) и вызов функции говорят: Add (5,10); какой метод будет называться перегруженной функцией или функцией необязательного параметра? спасибо :)
SHEKHAR SHETE

@Shekshar: Вы пробовали? Подробности читайте в спецификации, но в основном в разрешении конфликтов побеждает метод, при котором компилятору не нужно вводить какие-либо дополнительные параметры.
Джон Скит

@JonSkeet только что я пробовал с вышеупомянутым ... перегрузка функции побеждает необязательный параметр :)
SHEKHAR SHETE

19

Когда перегрузка метода обычно выполняет одно и то же с другим числом аргументов, будут использоваться значения по умолчанию.

Когда перегрузка метода выполняет функцию по-разному в зависимости от своих параметров, перегрузка будет продолжать использоваться.

Я использовал optional еще во времена VB6 и с тех пор пропустил его, это уменьшит дублирование XML-комментариев в C #.


11

Я всегда использовал Delphi с необязательными параметрами. Вместо этого я перешел на использование перегрузок.

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

И мне нравится идея, что обычно существует один супер- метод, а остальные представляют собой более простые оболочки для этого.


1
Я полностью согласен с этим, однако есть предостережение, что, когда у вас есть метод, который принимает несколько (3+) параметров, которые по своей природе являются «необязательными» (могут быть заменены на значение по умолчанию), вы можете столкнуться с множеством перестановок сигнатуры метода больше никакой пользы. Рассмотрим Foo(A, B, C)требует Foo(A), Foo(B), Foo(C), Foo(A, B), Foo(A, C), Foo(B, C).
Дэн

7

Я обязательно буду использовать опцию необязательных параметров 4.0. Это избавляет от нелепого ...

public void M1( string foo, string bar )
{
   // do that thang
}

public void M1( string foo )
{
  M1( foo, "bar default" ); // I have always hated this line of code specifically
}

... и помещает значения там, где вызывающий может их видеть ...

public void M1( string foo, string bar = "bar default" )
{
   // do that thang
}

Гораздо более простой и менее подверженный ошибкам. Я действительно видел в этом ошибку в случае перегрузки ...

public void M1( string foo )
{
   M2( foo, "bar default" );  // oops!  I meant M1!
}

Я еще не играл с компилятором 4.0, но я не был бы шокирован, узнав, что компилятор просто генерирует перегрузки для вас.


6

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

Существенным следствием привязки дополнительных параметров на сайте вызова является то, что им будут присвоены значения в зависимости от версии целевого кода, доступной для компилятора. Если в сборке Fooесть метод Boo(int)со значением по умолчанию 5, а сборка Barсодержит вызов Foo.Boo(), компилятор обработает это как Foo.Boo(5). Если значение по умолчанию изменено на 6 и сборка Fooперекомпилирована, Barвызов будет продолжен до тех Foo.Boo(5)пор, пока он не будет перекомпилирован с этой новой версией Foo. Таким образом, следует избегать использования дополнительных параметров для вещей, которые могут измениться.


Re: «Таким образом, следует избегать использования необязательных параметров для вещей, которые могут измениться». Я согласен, что это может быть проблематично, если изменение останется незамеченным клиентским кодом. Однако, та же проблема возникает , когда значение по умолчанию скрыто внутри перегрузки методы: void Foo(int value) … void Foo() { Foo(42); }. Снаружи вызывающий не знает, какое значение по умолчанию используется и когда оно может измениться; для этого нужно будет следить за письменной документацией. Значения по умолчанию для дополнительных параметров можно рассматривать как просто: документация в коде, что такое значение по умолчанию.
stakx - больше не участвует

@stakx: если перегрузка без параметров связана с перегрузкой с параметром, изменение значения «по умолчанию» этого параметра и повторная компиляция определения перегрузки изменит используемое значение, даже если вызывающий код не перекомпилирован .
supercat 01

Верно, но это не делает его более проблематичным, чем альтернатива. В одном случае (перегрузка метода) вызывающий код не имеет значения по умолчанию. Это может быть целесообразно, если вызывающий код действительно не заботится о необязательном параметре и его значении. В другом случае (необязательный параметр со значением по умолчанию) на ранее скомпилированный вызывающий код не повлияет изменение значения по умолчанию. Это также может быть подходящим, когда вызывающий код действительно заботится о параметре; пропустить его в исходном тексте - все равно что сказать: «Предлагаемое в настоящее время значение по умолчанию мне подходит».
stakx - больше не участвует

Я пытаюсь подчеркнуть, что, хотя у любого подхода есть последствия (как вы указали), они не являются по своей сути выгодными или невыгодными. Это также зависит от потребностей и целей вызывающего кода. Судя по этой точке зрения, я нашел вердикт в последнем предложении вашего ответа слишком жестким.
stakx - больше не участвует

@stakx: Я сказал «избегать использования», а не «никогда не использовать». Если изменение X будет означать, что следующая перекомпиляция Y изменит поведение Y, это потребует либо настройки системы сборки так, чтобы каждая перекомпиляция X также перекомпилировала Y (замедляя работу), либо создаст риск того, что программист изменит X таким образом, что Y сломается в следующий раз, когда он скомпилирован, и обнаружит такую ​​поломку только позже, когда Y будет изменен по какой-то совершенно не связанной причине. Параметры по умолчанию следует использовать только тогда, когда их преимущества перевешивают такие затраты.
supercat 02

4

Можно спорить о том, следует ли использовать необязательные аргументы или перегрузки или нет, но, что наиболее важно, у каждого есть своя собственная область, где они незаменимы.

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

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


3

Я с нетерпением жду дополнительных параметров, потому что они сохраняют значения по умолчанию, которые ближе к методу. Таким образом, вместо десятков строк для перегрузок, которые просто вызывают «расширенный» метод, вы просто определяете метод один раз и можете видеть, какие дополнительные параметры используются по умолчанию в сигнатуре метода. Я лучше посмотрю на:

public Rectangle (Point start = Point.Zero, int width, int height)
{
    Start = start;
    Width = width;
    Height = height;
}

Вместо этого:

public Rectangle (Point start, int width, int height)
{
    Start = start;
    Width = width;
    Height = height;
}

public Rectangle (int width, int height) :
    this (Point.Zero, width, height)
{
}

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


7
Я слышал, что необязательные параметры должны быть последними, не так ли?
Илья Рыженков

Зависит от вашего дизайна. Возможно, аргумент «начало» обычно важен, за исключением тех случаев, когда это не так. Возможно, у вас есть такая же подпись где-то еще, что означает что-то другое. В качестве надуманного примера public Rectangle (int width, int height, Point innerSquareStart, Point innerSquareEnd) {}
Роберт П.

13
Из того, что они сказали в разговоре, необязательные параметры должны быть после обязательных параметров.
Грег Бич,

3

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

В частности: объем документации может быстро увеличиваться с увеличением количества перегрузок, и вы, вероятно, в конечном итоге скопируете уже существующие комментарии из существующих перегрузок. Это очень раздражает, так как не дает никакой ценности и нарушает принцип DRY ). С другой стороны, с необязательным параметром есть ровно одно место, где все параметры задокументированы, и вы видите их значение, а также их значения по умолчанию при вводе.

И последнее, но не менее важное: если вы являетесь потребителем API, у вас может даже не быть возможности проверить детали реализации (если у вас нет исходного кода) и, следовательно, у вас нет возможности увидеть, какой супер-метод перегружен. заворачивают. Таким образом, вы застряли в чтении документа и надеетесь, что там перечислены все значения по умолчанию, но это не всегда так.

Конечно, это не ответ, который касается всех аспектов, но я думаю, что он добавляет один, который до сих пор не рассматривался.


1

Хотя это (предположительно?) Два концептуально эквивалентных способа моделирования вашего API с нуля, они, к сожалению, имеют некоторую тонкую разницу, когда вам нужно учитывать обратную совместимость во время выполнения для ваших старых клиентов в дикой природе. Мой коллега (спасибо, Брент!) Указал мне на этот замечательный пост: Проблемы управления версиями с необязательными аргументами . Некоторые цитаты из него:

Причина того, что необязательные параметры были введены в C # 4 в первую очередь, заключалась в поддержке COM-взаимодействия. Вот и все. И теперь мы узнаем обо всех последствиях этого факта. Если у вас есть метод с необязательными параметрами, вы никогда не сможете добавить перегрузку с дополнительными необязательными параметрами из-за страха вызвать критическое изменение во время компиляции. И вы никогда не сможете удалить существующую перегрузку, поскольку это всегда было критическим изменением во время выполнения. Вам в значительной степени нужно относиться к нему как к интерфейсу. В этом случае ваш единственный выход - написать новый метод с новым именем. Так что помните об этом, если вы планируете использовать необязательные аргументы в своих API.


1

Одним из недостатков необязательных параметров является управление версиями, когда рефакторинг имеет непредвиденные последствия. Пример:

Исходный код

public string HandleError(string message, bool silent=true, bool isCritical=true)
{
  ...
}

Предположим, это один из многих вызывающих вышеупомянутый метод:

HandleError("Disk is full", false);

Здесь событие не молчит и трактуется как критическое.

Теперь предположим, что после рефакторинга мы обнаруживаем, что все ошибки все равно запрашивают пользователя, поэтому нам больше не нужен флаг молчания. Итак, убираем.

После рефакторинга

Первый вызов все еще компилируется и, скажем, проходит через рефакторинг без изменений:

public string HandleError(string message, /*bool silent=true,*/ bool isCritical=true)
{
  ...
}

...

// Some other distant code file:
HandleError("Disk is full", false);

Теперь falseбудет иметь непредвиденный эффект, событие больше не будет считаться критическим.

Это может привести к незначительному дефекту, поскольку не будет ошибок компиляции или выполнения (в отличие от некоторых других предупреждений о дополнительных параметрах, таких как this или this ).

Обратите внимание, что существует множество форм этой проблемы. Еще одна форма изложена здесь .

Отметим также , что строго с использованием именованных параметров при вызове метода позволит избежать проблемы, такие , как например: HandleError("Disk is full", silent:false). Однако нецелесообразно предполагать, что все другие разработчики (или пользователи общедоступного API) сделают это.

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


0

Оба параметра Optional и Method overload имеют свои преимущества или недостатки. Выбор между ними зависит от вашего предпочтения.

Необязательный параметр: доступен только в .Net 4.0. необязательный параметр уменьшить размер кода. Вы не можете определить параметры out и ref

перегруженные методы: вы можете определить параметры Out и ref. Размер кода будет увеличиваться, но перегруженные методы легко понять.


0

Во многих случаях для переключения выполнения используются необязательные параметры. Например:

decimal GetPrice(string productName, decimal discountPercentage = 0)
{

    decimal basePrice = CalculateBasePrice(productName);

    if (discountPercentage > 0)
        return basePrice * (1 - discountPercentage / 100);
    else
        return basePrice;
}

Параметр Discount здесь используется для подачи оператора if-then-else. Есть полиморфизм, который не был распознан, а затем он был реализован как оператор if-then-else. В таких случаях гораздо лучше разделить два потока управления на два независимых метода:

decimal GetPrice(string productName)
{
    decimal basePrice = CalculateBasePrice(productName);
    return basePrice;
}

decimal GetPrice(string productName, decimal discountPercentage)
{

    if (discountPercentage <= 0)
        throw new ArgumentException();

    decimal basePrice = GetPrice(productName);

    decimal discountedPrice = basePrice * (1 - discountPercentage / 100);

    return discountedPrice;

}

Таким образом, мы даже защитили класс от звонков с нулевой скидкой. Этот звонок будет означать, что звонящий думает, что есть скидка, но на самом деле скидки нет вообще. Такое недоразумение может легко вызвать ошибку.

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

Ситуация очень похожа на параметры, которые могут быть нулевыми. Это также плохая идея, когда реализация сводится к таким утверждениям, какif (x == null) .

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


0

Чтобы добавить простую задачу, когда использовать перегрузку вместо дополнительных:

Если у вас есть несколько параметров, которые имеют смысл только вместе, не вводите для них дополнительные параметры.

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

Пример:

enum Match {
    Regex,
    Wildcard,
    ContainsString,
}

// Don't: This way, Enumerate() can be called in a way
//         which does not make sense:
IEnumerable<string> Enumerate(string searchPattern = null,
                              Match match = Match.Regex,
                              SearchOption searchOption = SearchOption.TopDirectoryOnly);

// Better: Provide only overloads which cannot be mis-used:
IEnumerable<string> Enumerate(SearchOption searchOption = SearchOption.TopDirectoryOnly);
IEnumerable<string> Enumerate(string searchPattern, Match match,
                              SearchOption searchOption = SearchOption.TopDirectoryOnly);
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.