Чому для локальних змінних потрібна ініціалізація, а поля - ні?


140

Якщо я створюю bool у своєму класі, просто щось подібне bool check, воно за замовчуванням відповідає false.

Коли я створюю той самий bool у своєму методі, bool check(замість класу), я отримую помилку "використання непризначеної перевірки локальної змінної". Чому?


Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Martijn Pieters

14
Питання неясне. Чи прийнятною відповіддю буде "тому, що специфікація так говорить"?
Ерік Ліпперт

4
Тому що саме так було зроблено на Java, коли вони її скопіювали. : P
Елвін Томпсон,

Відповіді:


177

Відповіді Юваля та Девіда в основному правильні; Підводячи підсумки:

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

Учасник відповіді Девіда запитує, чому неможливо виявити використання непризначеного поля за допомогою статичного аналізу; це я хочу розгорнути у цій відповіді.

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

bool x;
if (M()) x = true;
Console.WriteLine(x);

Питання "призначено х?" еквівалентно "чи повертає М () істину?" Тепер, припустимо, M () повертає істину, якщо Остання теорема Ферма відповідає правилам для всіх цілих чисел, менших за одинадцять гаджліонів, а помилка - інакше. Для того, щоб визначити, чи точно визначено х, компілятор повинен по суті створити доказ останньої теореми Ферма. Компілятор не такий розумний.

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

bool x;
if (N() * 0 == 0) x = true;
Console.WriteLine(x);

Припустимо, N () повертає ціле число. Ви і я знаємо, що N () * 0 буде 0, але компілятор цього не знає. (Примітка: компілятор C # 2.0 це знав, але я зняв цю оптимізацію, оскільки специфікація не говорить про те, що компілятор це знає.)

Гаразд, так що ми знаємо поки що? Місцеві жителі недоцільно отримувати точну відповідь, але ми можемо недооцінити недосвідченість дешево і отримати досить хороший результат, який помиляється на стороні "змусить виправити свою незрозумілу програму". Добре. Чому б не зробити те ж саме для полів? Тобто зробити певну перевірку завдань, яка завищує недорого?

Ну, скільки існує способів ініціалізації локального? Її можна призначити в тексті методу. Його можна призначити в межах лямбда в тексті методу; що лямбда ніколи не може бути використана, тому ці призначення не є актуальними. Або він може бути переданий як "поза" іншому методу, і тоді ми можемо вважати, що він призначений, коли метод повертається нормально. Це дуже чіткі моменти, за якими призначається локальний, і вони знаходяться там саме тим же методом, що і місцевий оголошений . Визначення певного призначення місцевим жителям вимагає лише місцевого аналізу . Методи, як правило, короткі - набагато менше мільйона рядків коду в методі - і тому аналіз всього методу досить швидкий.

А що робити з полями? Поля можуть бути ініціалізовані в конструкторі звичайно. Або польовий ініціалізатор. Або конструктор може викликати метод екземпляра, який ініціалізує поля. Або конструктор може викликати віртуальний метод, який ініціалізує поля. Або конструктор може викликати метод в іншому класі , який може бути в бібліотеці , який ініціалізує поля. Статичні поля можуть бути ініціалізовані в статичних конструкторах. Статичні поля можуть бути ініціалізовані іншими статичними конструкторами.

По суті, ініціалізатор поля може бути в будь-якій точці всієї програми , включаючи всередині віртуальних методів, які будуть оголошені в бібліотеках, які ще не були записані :

// Library written by BarCorp
public abstract class Bar
{
    // Derived class is responsible for initializing x.
    protected int x;
    protected abstract void InitializeX(); 
    public void M() 
    { 
       InitializeX();
       Console.WriteLine(x); 
    }
}

Чи помилка при складанні цієї бібліотеки? Якщо так, як BarCorp повинен виправити помилку? Призначивши значення x за замовчуванням до x? Але це вже робить компілятор.

Припустимо, ця бібліотека легальна. Якщо FooCorp пише

public class Foo : Bar
{
    protected override void InitializeX() { } 
}

це що помилка? Як компілятор повинен це зрозуміти? Єдиний спосіб - зробити цілий аналіз програми, який відстежує статику ініціалізації кожного поля на кожному можливому шляху через програму , включаючи шляхи, що передбачають вибір віртуальних методів під час виконання . Ця проблема може бути довільно важкою ; це може включати модельоване виконання мільйонів контурів управління. Аналіз локальних потоків управління займає мікросекунди і залежить від розміру методу. Аналіз глобальних потоків управління може зайняти години, оскільки це залежить від складності кожного методу в програмі та всіх бібліотек .

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

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

А як щодо занять? Ну як ви знаєте, що конструктор ініціалізував поле ? Ctor міг викликати віртуальний метод для ініціалізації полів, і тепер ми знову в тому ж становищі, в якому були раніше. У структур немає похідних класів; класи можуть. Чи потрібна бібліотека, що містить абстрактний клас, щоб містити конструктор, який ініціалізує всі його поля? Як абстрактний клас знає, до яких значень поля слід ініціалізувати?

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

  • Зробити загальні, безпечні, часто використовувані ідіоми програмування незаконними.
  • Зробіть дорогий аналіз всієї програми, що змушує компіляцію займати години, щоб шукати помилок, яких, мабуть, немає.
  • Покладайтеся на автоматичну ініціалізацію до значень за замовчуванням.

Команда дизайнерів обрала третій варіант.


1
Чудова відповідь, як завжди. Однак у мене виникає питання: чому б автоматично не призначити значення за замовчуванням також локальним змінним? Іншими словами, чому б не зробити bool x;рівнозначним bool x = false; навіть усередині методу ?
durron597

8
@ durron597: Оскільки досвід показав, що забути призначити значення локальному, ймовірно, помилка. Якщо це, ймовірно, помилка, і це дешево і легко виявити, то є хороший стимул зробити поведінку або незаконною, або попередженням.
Ерік Ліпперт

27

Коли я створюю той самий bool в рамках свого методу, bool check (замість класу), я отримую помилку "використання непризначеної перевірки локальної змінної". Чому?

Тому що компілятор намагається не допустити помилки.

Чи ініціалізація вашої змінної falseзмінить що-небудь у цьому конкретному шляху виконання? Напевно, ні, вважаючи default(bool)це помилковим, але це змушує вас усвідомлювати, що це відбувається. Середовище .NET заважає вам отримувати доступ до «сміттєвої пам’яті», оскільки вона ініціалізує будь-яке значення до їх дефолту. Але все ж, уявіть, що це був еталонний тип, і ви передасте неініціалізоване (null) значення методу, що очікує ненульового значення, і отримаєте NRE під час виконання. Компілятор просто намагається запобігти цьому, приймаючи той факт, що іноді це може призвести до bool b = falseтверджень.

Про це Ерік Ліпперт розповідає в публікації в блозі :

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

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


"уявіть, що це тип посилання, і ви передасте цей неініціалізований об'єкт методу, що очікує ініціалізований". Ви мали на увазі: "уявіть, що це тип посилання, і ви передавали за замовчуванням (null) замість посилання на об’єкт "?
Дедуплікатор

@ Дедуплікатор Так. Метод, що очікує ненульового значення. Редагував цю частину. Сподіваюся, зараз зрозуміліше.
Юваль Ітчаков

Я не думаю, що це через накреслену лінію. Кожен клас передбачає наявність конструктора, принаймні конструктора за замовчуванням. Отже, коли ви будете дотримуватися конструктора за замовчуванням, ви отримаєте значення за замовчуванням (тихо прозорі). Визначаючи конструктор, від вас очікується чи слід знати, що ви робите в ньому, і які поля ви хочете ініціалізувати яким чином, включаючи знання значень за замовчуванням.
Пітер

Навпаки: поле в межах методу може за допомогою оголошених і присвоєних значень різним шляхам виконання. Можуть бути винятки, які легко контролювати, поки ви не ознайомтеся з документацією рамки, яку ви можете використовувати або навіть в інших частинах коду, які ви не можете підтримувати. Це може ввести дуже складний шлях виконання. Тому компілятори натякають.
Пітер

@Peter Я не дуже зрозумів ваш другий коментар. Щодо першого, то немає ніякої вимоги ініціалізувати будь-які поля всередині конструктора. Це звичайна практика . Завдання компіляторів не полягає у застосуванні такої практики. Ви не можете покладатися на будь-яку реалізацію запущеного конструктора і сказати "добре, всі поля добре пройти". У своїй відповіді Ерік багато розробив про те, як можна ініціалізувати поле класу, і показує, як потрібно дуже багато часу, щоб обчислити всі логічні способи ініціалізації.
Юваль Ітчаков

25

Чому для локальних змінних потрібна ініціалізація, а поля - ні?

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

Чому для локальних змінних потрібна ініціалізація?

Це не більше ніж дизайнерське рішення мови C #, як пояснив Ерік Ліпперт . CLR та середовище .NET цього не вимагають. Наприклад, VB.NET буде чудово компілювати неініціалізовані локальні змінні, а насправді CLR ініціалізує всі неініціалізовані змінні до значень за замовчуванням.

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

Чому поля не потребують ініціалізації?

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

Що з видами колекцій?

Застосування ініціалізації локальної змінної C # обмежене, що часто вилучає розробників. Розглянемо наступні чотири рядки коду:

string str;
var len1 = str.Length;
var array = new string[10];
var len2 = array[0].Length;

Другий рядок коду не компілюється, оскільки він намагається прочитати неініціалізовану змінну рядків. Четвертий рядок коду складається добре, хоча, як arrayбуло ініціалізовано, але лише зі значеннями за замовчуванням. Оскільки значення рядка за замовчуванням є нульовим, ми отримуємо виняток під час виконання. Кожен, хто провів тут час на переповнюванні стека, буде знати, що ця явна / неявна непослідовність ініціалізації призводить до значної кількості "Чому я отримую помилку" Посилання на об'єкт не встановлено на екземпляр об'єкта "?" питання.


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

@JohnKugelman, розглянемо простий випадок public interface I1 { string str {get;set;} }і метод int f(I1 value) { return value.str.Length; }. Якщо це існує в бібліотеці, компілятор не може знати, з чим буде пов’язана ця бібліотека, таким чином, чи setбуде закликане заздалегідь поле get, поле резервного копіювання може бути не явно ініціалізовано, але він повинен скомпілювати такий код.
Девід Арно

Це правда, але я б не очікував, що помилка буде створена під час компіляції f. Він створюється при складанні конструкторів. Якщо ви залишите конструктор із полем, можливо, неініціалізованим, це буде помилкою. Можливо, також повинні бути обмеження щодо виклику методів класу та getters, перш ніж ініціалізувати всі поля.
Джон Кугельман

@JohnKugelman: Я опублікую відповідь, в якій обговорюватиму поставлене вами питання.
Ерік Ліпперт

4
Це не чесно. Тут ми намагаємося мати розбіжність!
Джон Кугельман

10

Хороші відповіді вище, але я подумав, що опублікую набагато простішу / коротшу відповідь, щоб люди лінувалися прочитати довгу (як я).

Клас

class Foo {
    private string Boo;
    public Foo() { /** bla bla bla **/ }
    public string DoSomething() { return Boo; }
}

Властивість Booможе або не може бути ініціалізована в конструкторі. Тож коли він знаходить return Boo;, не передбачає, що він ініціалізований. Це просто пригнічує помилку.

Функція

public string Foo() {
   string Boo;
   return Boo; // triggers error
}

У { }символах визначають обсяг блоку коду. Компілятор ходить гілками цих { }блоків, відслідковуючи речі. Це легко може сказати, що Booне було ініціалізовано. Потім помилка спрацьовує.

Чому існує помилка?

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

public string Foo() {
   string Boo;
   /* bla bla bla */
   if(Boo == null) {
      return "";
   }
   return Boo;
}

З посібника:

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

Довідка: https://msdn.microsoft.com/en-us/library/4y7h161d.aspx

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