Чому я не можу визначити конструктор за замовчуванням для структури в .NET?


261

У .NET тип значення (C # struct) не може мати конструктор без параметрів. Відповідно до цієї посади, це визначається специфікацією CLI. Що трапляється, що для кожного типу значень створюється конструктор за замовчуванням (компілятором?), Який ініціалізує всіх членів до нуля (абоnull ).

Чому заборонено визначати такий конструктор за замовчуванням?

Одне тривіальне використання для раціональних чисел:

public struct Rational {
    private long numerator;
    private long denominator;

    public Rational(long num, long denom)
    { /* Todo: Find GCD etc. */ }

    public Rational(long num)
    {
        numerator = num;
        denominator = 1;
    }

    public Rational() // This is not allowed
    {
        numerator = 0;
        denominator = 1;
    }
}

Використовуючи поточну версію C #, Rational за замовчуванням є 0/0 це не так круто.

PS : Чи допоможуть параметри за замовчуванням вирішити це для C # 4.0 чи буде викликано CLR-конструктор за замовчуванням?


Джон Скіт відповів:

Щоб використовувати ваш приклад, що ви хотіли б статися, коли хтось зробив:

 Rational[] fractions = new Rational[1000];

Чи повинен він пробігти ваш конструктор 1000 разів?

Звичайно, це слід, тому я написав конструктор за замовчуванням в першу чергу. CLR повинен використовувати конструктор занулення за замовчуванням, коли не визначений чіткий конструктор за замовчуванням; таким чином ви платите лише за те, що використовуєте. Тоді, якщо я хочу контейнер з 1000 не за замовчуванням Rational(і хочу оптимізувати 1000 конструкцій), я буду використовувати List<Rational>скоріше масив.

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


3
+1 колись мала подібну проблему, нарешті перетворила структуру в клас.
Дірк Волмар

4
Параметри за замовчуванням у C # 4 не можуть допомогти, оскільки Rational()викликає безпараметричний ctor, а не the Rational(long num=0, long denom=1).
LaTeX

6
Зауважте, що в C # 6.0, який постачається з Visual Studio 2015, буде дозволено записувати конструктори екземплярів з нульовими параметрами для структур. Так new Rational()буде викликати конструктор, якщо він існує, однак якщо його не існує, new Rational()буде еквівалентним default(Rational). У будь-якому випадку вам рекомендується використовувати синтаксис, default(Rational)коли хочете "нульове значення" вашої структури (що є "поганим" числом із запропонованою вами конструкції Rational). Типове значення типу типу Tзавжди є default(T). Так new Rational[1000]ніколи не буде посилатися на конструктори структури.
Jeppe Stig Nielsen

6
Щоб вирішити цю конкретну проблему, можна зберегти denominator - 1всередині структури, щоб значення за замовчуванням стало 0/1
miniBill

3
Then if I want a container of 1000 non-default Rationals (and want to optimize away the 1000 constructions) I will use a List<Rational> rather than an array.Чому ви очікуєте, що масив викликає інший конструктор до списку для структури?
mjwills

Відповіді:


197

Примітка: відповідь нижче була написана задовго до C # 6, який планує запровадити можливість оголошення структур без параметрів у структурах - але вони все одно не будуть викликані у всіх ситуаціях (наприклад, для створення масиву) (врешті-решт, для створення масиву) ця функція не додана до C # 6 ).


РЕДАКТИРУЙТЕ: Я відповів редакцією нижче, оскільки Грауенвольф розумів CLR.

CLR дозволяє типам значень мати конструктори без параметрів, але C # ні. Я вважаю, це тому, що це ввело б сподівання, що конструктор буде викликаний, коли він цього не зробить. Наприклад, врахуйте це:

MyStruct[] foo = new MyStruct[1000];

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

Основне правило в C # - "значення за замовчуванням для будь-якого типу не може покладатися на будь-яку ініціалізацію". Тепер вони могли дозволити визначення конструкторів без параметрів, але тоді вони не вимагали виконання цього конструктора у всіх випадках - але це призвело б до більше плутанини. (Або принаймні, тому я вважаю, що аргумент іде.)

EDIT: Щоб використовувати ваш приклад, що б ви хотіли статися, коли хтось зробив:

Rational[] fractions = new Rational[1000];

Чи повинен він пробігти ваш конструктор 1000 разів?

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

EDIT: (Відповідаючи на трохи більше запитання) Компілятор не створює безпараметричний конструктор. Типи значень не повинні мати конструкторів, наскільки CLR стурбований - хоча виявляється , що це може , якщо записати його в IL. Коли ви пишете " new Guid()" в C #, це випромінює інший ІЛ до того, що ви отримуєте, якщо ви викликаєте звичайний конструктор. Дивіться це питання ТА ще трохи про цей аспект.

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


9
Коротше пояснення: у C ++ структура та клас були лише двома сторонами однієї монети. Єдина реальна різниця: одна була за замовчуванням публічною, а інша - приватною. У .Net є набагато більша різниця між структурою та класом, і важливо це зрозуміти.
Джоел Куехорн

38
@Joel: Однак це не зовсім пояснює це обмеження, чи не так?
Джон Скіт

6
CLR дозволяє типів значень мати конструктори без параметрів. І так, він запустить його для кожного елемента в масиві. C # вважає, що це погана ідея і не дозволяє, але ви можете написати .NET мову, яка це робить.
Джонатан Аллен

2
Вибачте, я трохи плутаюсь із наступним. Чи Rational[] fractions = new Rational[1000];також витрачається велике навантаження, якщо Rationalце клас замість структури? Якщо так, то чому в класах є ctor за замовчуванням?
поцілуй мою пахву

5
@ FifaEarthCup2014: Вам доведеться бути більш конкретними щодо того, що ви маєте на увазі під "марнуванням навантаження". Але в будь-якому випадку, конструктор не зателефонує 1000 разів. Якщо Rationalце клас, ви отримаєте масив з 1000 нульових посилань.
Джон Скіт

48

Структура - це тип значення, а тип значення повинен мати значення за замовчуванням, як тільки воно оголошено.

MyClass m;
MyStruct m2;

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


3
Не впевнений, чому хтось проголосував за вас. Ви, здається, є найбільш правильною відповіддю тут.
pipTheGeek

12
Поведінка в C ++ полягає в тому, що якщо тип має конструктор за замовчуванням, то він використовується, коли такий об'єкт створюється без явного конструктора. Це могло бути використано в C # для ініціалізації m2 з конструктором за замовчуванням, тому ця відповідь не корисна.
Мотті

3
oneter: якщо ви не хочете, щоб структури оголошували власний конструктор при оголошенні, тоді не визначайте такого конструктора за замовчуванням! :) ось висловлювання
Мотті

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

1
Я не впевнений, що повністю згоден з "Дійсно м2 можна було б із задоволенням використати .." . Можливо, це було правдою в попередньому C #, але помилка компілятора оголосила структуру, а не newЦе, тоді спробуйте використовувати її членів
Caius Jard

18

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

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

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

Не потрібно інстанціювати структуру newключовим словом. Натомість він працює як int; Ви можете безпосередньо отримати доступ до нього.

Структури не можуть містити явних конструкторів без параметрів. Члени структури автоматично ініціалізуються до значень за замовчуванням.

Конструктор за замовчуванням (без параметрів) для структури може встановлювати різні значення, ніж стан, що дорівнює нулю, що було б несподіваною поведінкою. Отже, час виконання .NET забороняє конструктори за замовчуванням для struct.


Ця відповідь, безумовно, найкраща. Зрештою, вся суть обмеження полягає у тому, щоб уникнути сюрпризів, щоб MyStruct s;не викликати створений вами конструктор за замовчуванням.
talles

1
Дякую за пояснення. Отже, лише недолік компілятора повинен був би бути вдосконалений, немає теоретично вагомих причин забороняти конструктори без параметрів (як тільки вони можуть бути обмежені лише властивостями доступу).
Ело

16

Ви можете зробити статичну властивість, яка ініціалізує та повертає "раціональне" число за замовчуванням:

public static Rational One => new Rational(0, 1); 

І використовувати його так:

var rat = Rational.One;

24
У цьому випадку це Rational.Zeroможе бути трохи менш заплутано.
Кевін

13

Коротше пояснення:

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

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


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

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

@annakata: Інші конструктори, ймовірно, корисні в деяких сценаріях, що стосуються Reflection. Крім того, якщо б generics коли-небудь було вдосконалено, щоб дозволити параметризоване "нове" обмеження, було б корисно мати структури, які могли б відповідати їм.
supercat

@annakata Я вважаю, що це тому, що в C # є особлива вимога, яку newпотрібно писати, щоб викликати конструктор. У C ++ конструктори викликаються прихованими способами, при оголошенні або інстанції масивів. У C # або все є вказівником, тому почніть з нуля, або це структура, і вона повинна починатися з чогось, але коли ви не можете написати new... (наприклад, масив init), це порушило б сильне правило C #.
v.oddou

3

Я не бачив рівноцінного пізньому рішенню, яке збираюся дати, тому ось воно.

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

Це рішення працює для простих структур із лише типовими значеннями (без типу ref чи змінної структури).

public struct Tempo
{
    const double DefaultBpm = 120;
    private double _bpm; // this field must not be modified other than with its property.

    public double BeatsPerMinute
    {
        get => _bpm + DefaultBpm;
        set => _bpm = value - DefaultBpm;
    }
}

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

Приклад із перерахунками як поле.

public struct Difficaulty
{
    Easy,
    Medium,
    Hard
}

public struct Level
{
    const Difficaulty DefaultLevel = Difficaulty.Medium;
    private Difficaulty _level; // this field must not be modified other than with its property.

    public Difficaulty Difficaulty
    {
        get => _level + DefaultLevel;
        set => _level = value - DefaultLevel;
    }
}

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


Це хороше рішення для прикладу, який я дав, але це було насправді лише прикладом, питання загальним.
Мотті

2

Просто особливий випадок. Якщо ви бачите чисельник 0 і знаменник 0, зробіть вигляд, ніби він має значення, які ви дійсно хочете.


5
Мені особисто не хотілося б, щоб мої заняття / структури мали таку поведінку. Невдача мовчки (або відновлення у тому, як найкращі здогадки для вас) - це шлях до неприхованих помилок.
Борис Калленс

2
+1 Це хороша відповідь, тому що для типів значень потрібно враховувати їх значення за замовчуванням. Це дозволить вам "встановити" значення за замовчуванням своєю поведінкою.
IllidanS4 хоче, щоб Моніка повернулася

Саме так вони реалізують такі класи, як Nullable<T>(наприклад int?).
Джонатан Аллен

Це дуже погана ідея. 0/0 завжди повинен бути недійсним дробом (NaN). Що робити, якщо хтось дзвонить, new Rational(x,y)де x і y стають 0?
Майк Рософт

Якщо у вас є фактичний конструктор, ви можете кинути виняток, не даючи справжньому 0/0. Або якщо ви хочете, щоб це сталося, вам потрібно додати додатковий пуль, щоб розрізняти типовий і 0/0.
Джонатан Аллен

2

Що я використовую, це оператор з’єднання нуля (??) у поєднанні з полем резервного копіювання, як це:

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

  public SomeRefType MyRefVariable {
    get { return m_MyRefVariableBackingField ?? (m_MyRefVariableBackingField = new SomeRefType()); }
  }
}

Сподіваюся, це допомагає;)

Примітка: присвоєння нульового приєднання на даний момент є функцією пропозиції для C # 8.0.


1

Ви не можете визначити конструктор за замовчуванням, оскільки ви використовуєте C #.

Конструктори можуть мати конструктори за замовчуванням у .NET, хоча я не знаю жодної конкретної мови, яка це підтримує.


У C # класи та структури семантично відрізняються. Структура - це тип значення, тоді як клас - тип посилання.
Том Сардуй

-1

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

public struct Point2D {
    public static Point2D NULL = new Point2D(-1,-1);
    private int[] Data;

    public int X {
        get {
            return this.Data[ 0 ];
        }
        set {
            try {
                this.Data[ 0 ] = value;
            } catch( Exception ) {
                this.Data = new int[ 2 ];
            } finally {
                this.Data[ 0 ] = value;
            }
        }
    }

    public int Z {
        get {
            return this.Data[ 1 ];
        }
        set {
            try {
                this.Data[ 1 ] = value;
            } catch( Exception ) {
                this.Data = new int[ 2 ];
            } finally {
                this.Data[ 1 ] = value;
            }
        }
    }

    public Point2D( int x , int z ) {
        this.Data = new int[ 2 ] { x , z };
    }

    public static Point2D operator +( Point2D A , Point2D B ) {
        return new Point2D( A.X + B.X , A.Z + B.Z );
    }

    public static Point2D operator -( Point2D A , Point2D B ) {
        return new Point2D( A.X - B.X , A.Z - B.Z );
    }

    public static Point2D operator *( Point2D A , int B ) {
        return new Point2D( B * A.X , B * A.Z );
    }

    public static Point2D operator *( int A , Point2D B ) {
        return new Point2D( A * B.Z , A * B.Z );
    }

    public override string ToString() {
        return string.Format( "({0},{1})" , this.X , this.Z );
    }
}

ігноруючи факт, у мене є статична структура під назвою null, (Примітка: Це лише для всіх позитивних квадрантів), використовуючи get; set; у C # ви можете спробувати / catch / нарешті, для вирішення помилок, коли певний тип даних не ініціалізований конструктором за замовчуванням Point2D (). Я думаю, що це невловиме як рішення для деяких людей щодо цієї відповіді. Це, головним чином, я додаю моє. Використання функцій getter та setter в C # дозволить вам обійти цей конструктор за замовчуванням безглуздо і поставити спробу перехопити те, що ви не ініціалізували. Для мене це добре працює, для когось іншого ви можете додати деякі, якщо заяви. Отже, у випадку, коли ви хочете встановити чисельник / знаменник, цей код може допомогти. Я хотів би ще раз зазначити, що це рішення не виглядає приємно, можливо, працює навіть гірше з точки зору ефективності, але, для тих, хто походить із старішої версії C #, використання типів даних масиву надає вам цю функціональність. Якщо ви просто хочете щось, що працює, спробуйте:

public struct Rational {
    private long[] Data;

    public long Numerator {
        get {
            try {
                return this.Data[ 0 ];
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                return this.Data[ 0 ];
            }
        }
        set {
            try {
                this.Data[ 0 ] = value;
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                this.Data[ 0 ] = value;
            }
        }
    }

    public long Denominator {
        get {
            try {
                return this.Data[ 1 ];
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                return this.Data[ 1 ];
            }
        }
        set {
            try {
                this.Data[ 1 ] = value;
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                this.Data[ 1 ] = value;
            }
        }
    }

    public Rational( long num , long denom ) {
        this.Data = new long[ 2 ] { num , denom };
        /* Todo: Find GCD etc. */
    }

    public Rational( long num ) {
        this.Data = new long[ 2 ] { num , 1 };
        this.Numerator = num;
        this.Denominator = 1;
    }
}

2
Це дуже поганий код. Чому у структурі є посилання на масив? Чому ви просто не маєте координати X і Y як поля? І використовувати винятки для контролю потоку - погана ідея; як правило, слід написати свій код таким чином, щоб NullReferenceException ніколи не виникала. Якщо вам це справді потрібно - хоча така конструкція краще підходить для класу, а не для структури - тоді вам слід використовувати ліниву ініціалізацію. (А технічно ви - абсолютно непотрібні в кожному, але першому встановленні координати - встановлення кожної координати двічі.)
Майк Рософт,

-1
public struct Rational 
{
    private long numerator;
    private long denominator;

    public Rational(long num = 0, long denom = 1)   // This is allowed!!!
    {
        numerator   = num;
        denominator = denom;
    }
}

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