Необязательные параметры: хорошо или плохо?
Я пишу и просматриваю множество методов в проекте, с которым я работаю, и столько, сколько я думаю overloads
полезны, я думаю, что, имея простой optional parameter
со значением по умолчанию можно обойти проблему, помогая писать более читабельный, и я думаю, эффективный код.
Теперь я слышу, что использование этих параметров в методах может привести к неприятным побочным эффектам.
Каковы эти побочные эффекты и стоит ли использовать эти параметры для поддержания чистоты кода?
3 ответа
Я начну с того, что предпочитаю свой ответ, сказав, что любая языковая функция может использоваться хорошо или плохо. Необязательные параметры имеют некоторые недостатки, такие как объявление локальных var
делает, или дженерики.
Каковы эти побочные эффекты
Два приходят на ум.
Во-первых, значение по умолчанию для необязательных параметров - это константы времени компиляции, встроенные в получателя метода. Допустим, у меня есть этот класс в AssemblyA:
public class Foo
{
public void Bar(string baz = "cat")
{
//Omitted
}
}
И это в AssemblyB:
public void CallBar()
{
new Foo().Bar();
}
То, что в итоге получается, это сборка B:
public void CallBar()
{
new Foo().Bar("cat");
}
Так что, если вы когда-либо измените значение по умолчанию на Bar
, и AssemblyA, и AssemblyB должны быть перекомпилированы. Из-за этого я склонен не объявлять методы как публичные, если они используют необязательные параметры, скорее внутренние или приватные. Если бы мне нужно было объявить его публичным, я бы использовал перегрузки.
Вторая проблема заключается в том, как они взаимодействуют с интерфейсами и полиморфизмом. Возьми этот интерфейс:
public interface IBar
{
void Foo(string baz = "cat");
}
и этот класс:
public class Bar : IBar
{
public void Foo(string baz = "dog")
{
Console.WriteLine(baz);
}
}
Эти строки будут печатать разные вещи:
IBar bar1 = new Bar();
bar1.Foo(); //Prints "cat"
var bar2 = new Bar();
bar2.Foo(); //Prints "dog"
Это два негатива, которые приходят на ум. Однако есть и положительные стороны. Рассмотрим этот метод:
void Foo(string bar = "bar", string baz = "baz", string yat = "yat")
{
}
Создание методов, которые предлагают все возможные перестановки по умолчанию, будет состоять из нескольких, если не из десятков строк кода.
Вывод: необязательные параметры хороши, а они могут быть плохими. Как и все остальное.
Некромантинг.
Дело в том, что с необязательными параметрами они ПЛОХИЕ, потому что они не интуитивно понятны, то есть они НЕ ведут себя так, как вы этого ожидаете.
И вот почему:
они нарушают совместимость с ABI!
(и, строго говоря, они также нарушают совместимость с API при использовании в конструкторах)
Например:
У вас есть DLL, в которой есть такой код
public void Foo(string a = "dog", string b = "cat", string c = "mouse")
{
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
}
Теперь, что происходит, вы ожидаете, что компилятор сгенерирует этот код за кулисами:
public void Foo(string a, string b, string c)
{
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
}
public void Foo(string a, string b)
{
Foo(a, b, "mouse");
}
public void Foo(string a)
{
Foo(a, "cat", "mouse");
}
public void Foo()
{
Foo("dog", "cat", "mouse");
}
или, возможно, более реалистично, вы ожидаете, что он передаст NULL и сделает
public void Foo(string a, string b, string c)
{
if(a == null) a = "dog";
if(b == null) b = "cat";
if(c == null) c = "mouse";
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
}
так что вы можете изменить аргументы по умолчанию в одном месте.
Но это не то, что делает компилятор C#, потому что тогда вы не могли:
Foo(a:"dog", c:"dogfood");
Вместо этого компилятор C# делает следующее:
Везде, где вы пишете, например
Foo(a:"dog", c:"mouse");
or Foo(a:"dog");
or Foo(a:"dog", b:"bla");
Он заменяет его на
Foo(your_value_for_a_or_default, your_value_for_b_or_default, your_value_for_c_or_default);
Это означает, что если вы добавите другое значение по умолчанию, измените значение по умолчанию, удалите значение, вы не нарушите совместимость API, но нарушите совместимость с ABI.
Это означает, что если вы просто замените DLL из всех файлов, составляющих приложение, вы сломаете каждое приложение, которое использует вашу DLL. Это довольно плохо. Потому что, если ваша DLL содержит плохую ошибку, и я должен ее заменить, мне придется перекомпилировать все свое приложение с вашей последней DLL. В нем может быть много изменений, поэтому я не могу сделать это быстро. У меня также может не оказаться под рукой старый исходный код, и приложение может быть в значительной модификации, не имея представления о том, на каком коммите была скомпилирована старая версия приложения. Так что я, возможно, не смогу перекомпилировать сейчас. Это очень плохо.
А что касается использования его только в ПУБЛИЧНЫХ методах, а не в частных, защищенных или внутренних.
Да, неплохая попытка, но все же можно использовать частные, защищенные или внутренние методы с отражением. Не потому, что хочется, а потому, что иногда это необходимо, потому что другого пути нет. (Пример).
Интерфейсы уже упоминались vcsjones.
Проблема заключается в дублировании кода (что позволяет использовать разные значения по умолчанию или игнорировать значения по умолчанию).
Но настоящий облом заключается в том, что в дополнение к этому теперь вы можете вносить критические изменения API в конструкторы...
Пример:
public class SomeClass
{
public SomeClass(bool aTinyLittleBitOfNewSomething = true)
{
}
}
И теперь везде, где вы используете
System.Activator.CreateInstance<SomeClass>();
теперь вы получите исключение RUNTIME, потому что теперь нет конструктора без параметров...
Компилятор не сможет поймать это во время компиляции.
Спокойной ночи, если у вас многоActivator.CreateInstance
s в вашем коде.
Вы будете облажались, да и плохо облажались.
Бонусные баллы будут присуждаться, если часть кода, который вы должны поддерживать, использует отражение для создания экземпляров класса или использует отражение для доступа к частным / защищенным / внутренним методам...
Не используйте необязательные параметры!
Особенно в конструкторах классов.Думаю, теоретически они подходят для быстрого прототипирования, но только для этого.
Но поскольку прототипы имеют сильную тенденцию быть продуктивными (по крайней мере, в компании, в которой я сейчас работаю), не используйте их для этого.
Я бы сказал, что это зависит от того, насколько отличается метод, когда вы включаете или опускаете этот параметр.
Если поведение метода и внутреннее функционирование очень отличаются без параметра, то сделайте его перегрузкой. Если вы используете необязательные параметры для изменения поведения, НЕ делайте. Вместо того, чтобы иметь метод, который делает одно с одним параметром, и что-то другое, когда вы передаете второй, используйте один метод, который делает одно, и другой метод, который делает другое. Если их поведение сильно отличается, то они, вероятно, должны быть полностью отдельными, а не перегружаться с тем же именем.
Если вам необходимо узнать, был ли параметр указан пользователем или оставлен пустым, рассмотрите возможность сделать его перегрузкой. Иногда вы можете использовать обнуляемые значения, если место, из которого они передаются, не допускает пустые значения, но, как правило, вы не можете исключить вероятность того, что пользователь передал null
, поэтому, если вам нужно знать, откуда пришло значение, а также каково это значение, не используйте необязательные параметры.
Прежде всего, помните, что необязательные параметры должны (вроде как по определению) использоваться для вещей, которые оказывают небольшое, тривиальное или иное неважное влияние на результат метода. Если вы измените значение по умолчанию, любое место, которое вызывает метод без указания значения, все равно должно быть удовлетворено результатом. Если вы измените значение по умолчанию, а затем обнаружите, что какой-то другой фрагмент кода, который вызывает метод с необязательным параметром, оставленным пустым, теперь не работает должным образом, то, вероятно, он не должен был быть необязательным параметром.
Места, где может быть хорошей идеей использовать дополнительные параметры:
- Методы, в которых безопасно просто установить что-либо на значение по умолчанию, если значение не указано. Это в основном охватывает все, что может не знать вызывающий абонент или его не беспокоит значение. Хороший пример - методы шифрования - вызывающий может просто подумать: "Я не знаю, крипто, я не знаю, какое значение R должно быть установлено, я просто хочу, чтобы это было зашифровано", и в этом случае вы устанавливаете значения по умолчанию. разумным ценностям. Часто они начинаются как метод с внутренней переменной, которую вы затем перемещаете для предоставления пользователю. Бессмысленно делать два метода, когда единственное отличие состоит в том, что
var foo = bar;
где-то в начале. - Методы, которые имеют набор параметров, но не все из них необходимы. Это довольно часто встречается с конструкторами; вы увидите перегрузки, каждый из которых устанавливает различные комбинации различных свойств, но если есть три или четыре параметра, которые могут или не могут быть установлены, для этого может потребоваться много перегрузок, чтобы покрыть все возможные комбинации (это в основном рукопожатие проблема), и все эти перегрузки имеют более или менее идентичное поведение внутри. Вы можете решить эту проблему, установив большинство из них, просто установив значения по умолчанию и вызвав тот, который устанавливает все параметры, но для использования необязательных параметров требуется меньше кода.
- Методы, в которых вызывающий их кодер может захотеть установить параметры, но вы хотите, чтобы они знали, что такое "нормальное" значение. Например, метод шифрования, который мы упомянули ранее, может потребовать различных параметров для любой математики, которая происходит внутри. Кодировщик может увидеть, что они могут передавать значения для
workFactor
или жеblockSize
, но они могут не знать, что такое "нормальные" значения для них. Здесь помогут комментирование и документация, а также необязательные параметры - кодер увидит в подписи[workFactor = 24], [blockSize = 256]
что помогает им судить, какие ценности являются разумными. (Конечно, это не повод, чтобы не комментировать и не документировать ваш код должным образом.)
Вы не делаете более читаемый и эффективный код.
Во-первых, подписи вашего метода будут безвозмездно дольше.
Во-вторых, перегрузок не существует для единственной цели использования значений по умолчанию - быстрый взгляд на класс Convert должен показать вам это. Часто перегруженные методы имеют разные пути выполнения, которые станут кодом спагетти в вашем единственном не перегруженном методе.
В-третьих, иногда вам нужно знать, использовалось ли значение в качестве входных данных. Как вы узнаете, передал ли пользователь эти значения, если он использует то же значение, что и значение по умолчанию, которое вы использовали?
Часто я вижу необязательные параметры в C#, например IMyInterface parameter = null
. Особенно, когда я вижу это в конструкторах, я бы даже сказал, что это запах кода. Я знаю, что это жесткий вердикт, но в данном случае он скрывает ваши зависимости, что плохо.
Как сказал vcsjones, вы можете правильно использовать эти языковые функции, но я считаю, что необязательные параметры следует использовать только в некоторых крайних случаях.
мое мнение.