Переваги використання const замість змінних усередині методів


77

Щоразу, коли в методі є локальні змінні, ReSharper пропонує перетворити їх у константи:

// instead of this:
var s = "some string";
var flags = BindingFlags.Public | BindingFlags.Instance;

// ReSharper suggest to use this:
const string s = "some string";
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;

Враховуючи, що це справді постійні значення (а не змінні), я розумію, що ReSharper пропонує змінити їх на const.

Але крім цього, чи існує якась інша перевага при використанні const (наприклад, краща продуктивність), яка виправдовує використання const BindingFlagsзамість зручного та читабельного varключового слова?

BTW: Я щойно знайшов тут подібне запитання: Resharper завжди пропонує мені зробити const string замість string , але я думаю, що це більше стосується полів класу, де моє запитання стосується локальної змінної / consts.

Відповіді:


87

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

Також, як правило, невелика користь від продуктивності використання констант проти змінних. Це пов’язано з тим, як вони компілюються до MSIL відповідно до цього журналу MSDN Q&A :

Тепер, де б не було посилання на myInt у коді, замість того, щоб робити "ldloc.0", щоб отримати значення зі змінної, MSIL просто завантажує значення константи, яке жорстко закодовано, у MSIL. Таким чином, використання констант зазвичай має невелику перевагу в продуктивності та пам’яті. Однак для того, щоб використовувати їх, ви повинні мати значення змінної під час компіляції, і будь-які посилання на цю константу під час компіляції, навіть якщо вони знаходяться в іншій збірці, матимуть заміну.

Константи, безумовно, є корисним інструментом, якщо ви знаєте значення під час компіляції. Якщо ви цього не зробите, але хочете переконатись, що ваша змінна встановлена ​​лише один раз, ви можете використовувати ключове слово readonly у C # (яке відображається лише для ініціалізації в MSIL), щоб вказати, що значення змінної можна встановити лише в конструкторі; після цього помилкою є його зміна. Це часто використовується, коли поле допомагає визначити ідентичність класу, і часто встановлюється рівним параметру конструктора.


6
Для локальних змінних при використанні немає переваг щодо продуктивності const. Це не дає компілятору жодної додаткової інформації. Перевірте цю відповідь, щоб дізнатися більше.
Drew Noakes

Принаймні для моно це не завжди вірно. Перевірено за допомогою mono 5.14.0.177. Місцеві consts спричиняють, коли consts використовує для обчислення деякого значення (fe const int a = 10; const int b = a + 20;) різні оператори. ldc.r4 110 замість ldc.i4.s та інші операції для фактичного обчислення значень. (протестовано з налагодженням як цільовим, все ще вважають це доречним, особливо для високопродуктивного коду; для низької продуктивності різниця може бути не такою актуальною)
Даніель Бішар,

2
@SACO, якщо ви стурбовані питаннями, вам слід перевірити збірки випусків. Навіть якщо компілятор Mono виконує операцію додавання, JIT, швидше за все, застосує основну константу згину до машинного коду. Рослін у C # 7.3 робить правильно .
Дрю

@Drew Noakes приємно! І чудове посилання, яким ви поділилися.
Даніель Бішар,

@SACO також не забудьте ознайомитися з переглядом JIT Asm .
Drew

28

tl; dr для локальних змінних з буквальними значеннями, constвзагалі не має різниці.


Ваше розмежування "внутрішніх методів" дуже важливо. Давайте подивимось на це, а потім порівняємо з constполями.

Локальні змінні Const

Тільки перевагою constлокальної змінної є те , що значення не може бути перепризначений.

Однак constобмежується примітивними типами ( int, double, ...) і string, що обмежує його застосування.

Відступ: Є пропозиції для компілятора C # дозволити більш загальне поняття "лише для читання" місцевих жителів ( тут ), яке поширило б цю перевагу на інші сценарії. Вони , ймовірно , не розглядатиметься як constніби, і, ймовірно , мають інше ключове слово для таких декларацій (тобто letабо readonly varабо що - щось подібне).

Розглянемо ці два методи:

private static string LocalVarString()
{
    var s = "hello";
    return s;
}

private static string LocalConstString()
{
    const string s = "hello";
    return s;
}

Побудований в Releaseрежимі, ми бачимо наступний (скорочений) ІЛ:

.method private hidebysig static string LocalVarString() cil managed 
{
    ldstr        "hello"
    ret          
}

.method private hidebysig static string LocalConstString() cil managed 
{
    ldstr        "hello"
    ret          
}

Як бачите, вони обидва виробляють абсолютно однаковий ІЛ. Незалежно від місцевих sIS constчи ні , не має ніякого впливу.

Те саме стосується примітивних типів. Ось приклад використання int:

private static int LocalVarInt()
{
    var i = 1234;
    return i;
}

private static int LocalConstInt()
{
    const int i = 1234;
    return i;
}

І знову Іллінойс:

.method private hidebysig static int32 LocalVarInt() cil managed
{
    ldc.i4       1234
    ret          
}

.method private hidebysig static int32 LocalConstInt() cil managed
{
    ldc.i4       1234
    ret     
}

Тож знову ми не бачимо різниці. Тут не може бути різниці в продуктивності чи пам’яті. Єдина відмінність полягає в тому, що розробник не може повторно призначити символ.

Поля const

Порівнюючи constполе зі змінним полем є іншим. Поле, що не є const, має бути прочитане під час виконання. Отже, ви закінчуєте IL таким чином:

// Load a const field
ldc.i4       1234

// Load a non-const field
ldsfld       int32 MyProject.MyClass::_myInt

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

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

Const вирази

Розглянемо ці дві декларації:

const int i = 1 + 2;
int i = 1 + 2;

Для constформи додавання має бути обчислене під час компіляції, тобто число 3 зберігається в ІЛ.

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

Компілятор С # 7.3 випромінює ldc.i4.3опкод для обох наведених вище виразів.


Не можу погодитися з вашим твердженням, що "для локальних змінних з буквальними значеннями const взагалі не має значення". Вони мають сенс, якщо їх використовувати в методі з ранніми поверненнями та умовними операторами. Наприклад, якщо ви оголошуєте змінну на початку свого методу і використовуєте її лише в if (), тоді const не прийме жодного байта з пам'яті, поки ви не введете оператор if (тоді як звичайна змінна все одно з'їсть деякі байти).
Юрій Козлов

1
Якщо припустити, що ви здійснювали мікрооптимізацію, чи мало би сенс визначати константи, що використовуються в локальних методах, як поля в класі, а потім посилатися на них у методах? Я думаю, це було б за рахунок читабельності, а також зробило б ці методи менш "самостійними" (інкапсульовані).
Ден

1
@YuryKozlov немає різниці взагалі. Погляньте на ІЛ.
Дрю

@DanDiplo, якщо його const не має значення, де ви його визначаєте. Значення const в кінцевому підсумку виражається безпосередньо в IL.
Дрю

15

На моє розуміння, значення Const не існують під час виконання - тобто у вигляді змінної, що зберігається в якомусь місці пам'яті - вони вбудовуються в код MSIL під час компіляції. І, отже, матиме вплив на продуктивність. Більше за час виконання не потрібно буде виконувати будь-яке домашнє обслуговування (перевірки конверсії / збір сміття тощо) на них, де, оскільки змінні вимагають цих перевірок.


3
плюс 1 за вказівку на перевірки конверсії / збір сміття.
Крістіан Марк

Для коду, наведеного в оригінальному запитанні, немає різниці у згенерованому ІЛ, а отже, і різниці в пам’яті, перевірці, вивозі сміття тощо
Дрю Ноукс

Значення const не існують під час виконання , це трохи вводить в оману. Вони існують, інакше ви не змогли б ними скористатися.
Ліам

@Liam - погодьтеся, я намагався пояснити це в наступному реченні. "Значення const не існують під час виконання - тобто у вигляді змінної, що зберігається в якомусь місці пам'яті "
YetA AnotherUser

4

const - це константа часу компіляції - це означає, що весь ваш код, що використовує змінну const, скомпільований, щоб містити вираз константи, який містить змінна const - випущений ІЛ міститиме саме це значення константи.

Це означає, що розмір пам'яті менший для вашого методу, оскільки константа не вимагає виділення пам’яті під час виконання.


1
Я не вважаю твердженням про зменшення пам’яті тут правдою. constпрацює лише з примітивами (результатом яких є IL, що завантажує константи в стек оцінки, як ви говорите) та рядки (які виділяють однакову кількість пам'яті незалежно від того, є вони const чи ні).
Дрю Ноукс

1
Провів деякі тести, і немає різниці під час виконання пам’яті чи чогось іншого. Створений код ідентичний.
Drew Noakes

4

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

  1. Я повинен ініціалізувати це зі значенням прямо зараз, я не можу зробити це в іншому місці.
  2. Я ніде не можу змінити його значення.

У коді все про читабельність та спілкування.


2

Значення const також є "спільним" між усіма екземплярами об'єкта. Це також може призвести до зменшення використання пам'яті.

Як приклад:

public class NonStatic
{
    int one = 1;
    int two = 2;
    int three = 3;
    int four = 4;
    int five = 5;
    int six = 6;
    int seven = 7;
    int eight = 8;
    int nine = 9;
    int ten = 10;        
}

public class Static
{
    static int one = 1;
    static int two = 2;
    static int three = 3;
    static int four = 4;
    static int five = 5;
    static int six = 6;
    static int seven = 7;
    static int eight = 8;
    static int nine = 9;
    static int ten = 10;
}

Споживання пам’яті в .Net складно, і я не буду робити вигляд, що розумію точніші деталі цього, але якщо ви створили екземпляр списку з мільйоном "статичних", швидше за все, буде використано значно менше пам'яті, ніж якщо цього не зробити.

    static void Main(string[] args)
    {
        var maxSize = 1000000;
        var items = new List<NonStatic>();
        //var items = new List<Static>();

        for (var i=0;i<maxSize;i++)
        {
            items.Add(new NonStatic());
            //items.Add(new Static());
        }

        Console.WriteLine(System.Diagnostics.Process.GetCurrentProcess().WorkingSet64);
        Console.Read();
    }

При використанні "NonStatic" робочий набір становить 69 398 528, порівняно з лише 32 423 936, коли використовується статичний.


Єдиний довідковий тип, з яким можна використовувати const- це string. І рядок const ( const string s = "foo";), і літеральний рядок ( var s = "foo";) будуть інтерновані. Отже, тут немає різниці у споживанні пам’яті. Усі інші значення const передаються за значенням.
Дрю Ноукс

@DrewNoakes - Я не впевнений, що цілком розумію, але я розширив свою відповідь. Будь ласка, повідомте мене, якщо я допустив помилку або пропустив вашу думку.
Роб П.

Статистика відрізняється від const і не має нічого спільного з вихідним запитанням, вибачте. Я думаю, що ваше редагування не допомогло вашій відповіді. Ви не можете досягти перевірки N елементів за допомогою const, оскільки в результаті ви отримаєте один екземпляр у пам’яті, так само, як і з тимчасовим рядком компіляції. Наприклад, строковий літерал "hello"буде інтернований, тому два методи, які визначаються "hello"як локальні змінні, в кінцевому підсумку мають однакові посилання (на основі того, що рядки є незмінними). Однак "hello".ToUpper()це значення часу виконання, тому кілька викликів дадуть кілька екземплярів.
Drew Noakes

У цій відповіді я надав більше інформації .
Drew Noakes,

1

Ключове слово const повідомляє компілятору, що його можна повністю оцінити під час компіляції. У цьому є перевага в продуктивності та пам’яті, але вона невелика.


1
Насправді, у випадку локальної змінної немає продуктивності чи різниці пам’яті. Дивіться мою відповідь .
Drew Noakes,

1

Константи в C # надають іменоване розташування в пам'яті для зберігання значення даних. Це означає, що значення змінної буде відоме під час компіляції і буде зберігатися в одному місці.

Коли ви заявляєте про це, це начебто "жорстко закодовано" в Microsoft Intermediate Language (MSIL).

Хоча трохи, але це може покращити продуктивність вашого коду. Якщо я оголошую змінну і можу зробити це const, я це завжди роблю. Не тільки тому, що це може покращити продуктивність, але й тому, що це ідея констант. Інакше, чому вони існують?

Відбивач може бути дуже корисним у таких ситуаціях. Спробуйте оголосити змінну, а потім зробіть її константою і подивіться, який код генерується в IL . Тоді все, що вам потрібно зробити, це побачити різницю в інструкціях і подивитися, що ці інструкції означають.


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