Чи слід оголошувати методи, використовуючи перевантаження або необов'язкові параметри в 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 чи буде більше сенсу робити тісно пов'язані групи конструкторів і методів єдиним методом з необов'язковими параметрами, чи є вагома причина дотримуватися традиційного механізму багатьох перевантажень?

Відповіді:


122

Я б розглянув наступне:

  • Чи потрібно, щоб ваш код використовувався з мов, які не підтримують необов’язкові параметри? Якщо так, розгляньте можливість включення перевантажень.
  • Чи є у вас члени команди, які жорстоко виступають проти необов’язкових параметрів? (Іноді легше жити з рішенням, яке вам не подобається, ніж аргументувати справу).
  • Ви впевнені, що ваші за замовчуванням не змінюватимуться між збірками вашого коду, або якщо вони можуть, чи будуть ваші абоненти в порядку?

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


21
+1 за мудрість щодо прагматизму: Іноді легше жити з рішенням, яке вам не подобається, ніж аргументувати справу.
legends2k

13
@romkyns: Ні, ефект перевантажень не збігається з пунктом 3. При перевантаженнях, які надають значення за замовчуванням, значення за замовчуванням знаходяться в коді бібліотеки - тому, якщо ви зміните за замовчуванням і надасте нову версію бібліотеки, абоненти побачити новий за замовчуванням без перекомпіляції. Тоді як з необов’язковими параметрами вам потрібно буде перекомпілювати, щоб "побачити" нові за замовчуванням. Багато часу це не важлива відмінність, але це розходження.
Джон Скіт

привіт @JonSkeet, я хотів би знати, чи ми використовуємо обидві функції, тобто необов'язковий параметр, та інші з перевантаженням, який метод буде викликаний ?? наприклад, Add (int a, int b) та Add (int a, int b, int c = 0) та виклик функції say: Add (5,10); який метод буде називатися перевантаженою функцією або додатковою функцією параметрів? спасибі :)
SHEKHAR SHETE

@Shekshar: Ви пробували? Прочитайте специфікацію для деталей, але в основному в переривателі виграє метод, коли компілятору не потрібно було заповнювати будь-які додаткові параметри.
Jon Skeet

@JonSkeet щойно я спробував із вищезазначеним ... перевантаження функції виграє над необов’язковим параметром :)
SHEKHAR SHETE

19

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

Коли метод перевантаження виконує функцію по-різному залежно від її параметрів, тоді перевантаження буде продовжувати використовуватися.

Я використовував необов'язковий ще в дні 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).
Dan Lugg,

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: Якщо перевантаження без параметрів прив'язується до перевантаження параметром, зміна значення "за замовчуванням" цього параметра та повторна компіляція визначення перевантаження змінить значення, яке воно використовує, навіть якщо код виклику не перекомпільований .
суперкот

Правда, але це не робить її більш проблематичною, ніж альтернатива. В одному випадку (перевантаження методу) викличний код не має права говорити про значення за замовчуванням. Це може бути доречним, якщо телефонний код насправді взагалі не цікавиться додатковим параметром та його значенням. В іншому випадку (необов’язковий параметр зі значенням за замовчуванням), попередньо скомпільований викликовий код не вплине на зміну значення за замовчуванням. Це також може бути доречним, коли викличний код насправді піклується про параметр; пропустити це у джерелі - це все одно, що сказати: "пропоноване на даний момент значення за замовчуванням для мене нормально".
stakx - більше не вносить вклад

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

@stakx: Я сказав "уникати використання", а не "ніколи не використовувати". Якщо зміна X означатиме, що наступна перекомпіляція Y змінить поведінку Y, це вимагатиме того, щоб система збірки була налаштована таким чином, щоб кожна перекомпіляція X також перекомпілювала Y (сповільнюючи ситуацію), або створити ризик того, що програміст зміниться X таким чином, що Y порушить наступний раз, коли він буде скомпільований, і виявить таку поломку лише пізніше, коли Y буде змінено з якоїсь абсолютно не пов'язаної причини. Параметри за замовчуванням слід використовувати лише тоді, коли їх переваги перевищують такі витрати.
суперкіт

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
Я чув, що необов’язкові параметри повинні бути останніми, чи не так?
Ілля Риженков

Залежить від вашого дизайну. Можливо, аргумент "старт" зазвичай важливий, крім випадків, коли це не так. Можливо, у вас десь ще є той самий підпис, що означає щось інше. Для надуманого прикладу, загальнодоступний прямокутник (int width, int height, Point innerSquareStart, Point innerSquareEnd) {}
Роберт П

13
З того, що вони сказали в доповіді, необов’язкові параметри повинні бути після необхідних параметрів.
Greg Beech

3

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

Зокрема: зусилля з документації можуть швидко збільшуватися із збільшенням кількості перевантажень, і ви, мабуть, в кінцевому підсумку скопіюєте вже наявні коментарі з існуючих перевантажень. Це досить дратує, оскільки це не приносить жодної цінності і порушує принцип СУХОСТІ ). З іншого боку, з необов’язковим параметром є рівно одне місце, де всі параметри задокументовані, і ви бачите їх значення, а також значення за замовчуванням під час набору тексту.

І останнє, але не менш важливе: якщо ви є споживачем 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це матиме ненавмисний ефект, подія більше не буде розглядатися як критична.

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

Зверніть увагу, що існує безліч форм цієї самої проблеми. Ще одна форма викладена тут .

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

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


0

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

Необов’язковий параметр: доступний лише у .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;
}

Параметр знижки використовується тут для подання оператора 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.