Почему я не могу определить конструктор по умолчанию для структуры в .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 нестандартных Rationals (и я хочу оптимизировать 1000 конструкций), я буду использовать List<Rational>вместо массива.

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

Моти
источник
3
+1 имел подобную проблему однажды, наконец, преобразовал структуру в класс.
Дирк Воллмар
4
Параметры по умолчанию в C # 4 не могут помочь, потому что Rational()вызывает ctor без параметров, а не 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]что никогда не будет вызывать конструкторы структуры.
Джеппе Стиг Нильсен
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.Почему вы ожидаете, что массив вызовет другой конструктор для List для структуры?
mjwills

Ответы:

197

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


РЕДАКТИРОВАТЬ: я отредактировал ответ ниже из-за проникновения Грауенвольфа в CLR.

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

MyStruct[] foo = new MyStruct[1000];

CLR может сделать это очень эффективно, просто выделив соответствующую память и обнулив все это. Если бы ему пришлось запускать конструктор MyStruct 1000 раз, это было бы намного менее эффективно. (На самом деле, это не делает - если вы делаете иметь конструктор без параметров, он не получает работать , когда вы создаете массив, или если у вас есть неинициализированный переменный экземпляр.)

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

РЕДАКТИРОВАТЬ: Чтобы использовать ваш пример, что бы вы хотели случиться, когда кто-то сделал:

Rational[] fractions = new Rational[1000];

Должен ли он проходить через ваш конструктор 1000 раз?

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

РЕДАКТИРОВАТЬ: (отвечая немного больше на вопрос) Конструктор без параметров не создается компилятором. Типы значений не должны иметь конструкторов, поскольку это касается CLR - хотя оказывается, что это возможно, если вы напишите это в IL. Когда вы пишете " new Guid()" в C #, он выделяет другой IL по сравнению с тем, что вы получаете, если вызываете обычный конструктор. Посмотрите этот ТАК вопрос немного больше по этому аспекту.

Я подозреваю, что в структуре нет никаких типов значений с конструкторами без параметров. Несомненно, NDepend мог бы сказать мне, если бы я спросил это достаточно хорошо ... Тот факт, что C # запрещает это, является достаточно большим намеком, чтобы я подумал, что это, вероятно, плохая идея.

Джон Скит
источник
9
Краткое объяснение: в C ++ struct и class были просто двумя сторонами одной медали. Единственная реальная разница в том, что по умолчанию одно было общедоступным, а другое частным. В .Net существует гораздо большая разница между структурой и классом, и это важно понимать.
Джоэл Коухорн
39
@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 вполне можно было бы использовать в приведенном выше примере и вызывать его методы, если они есть, а также манипулировать их полями и свойствами!

user42467
источник
3
Не уверен, почему кто-то проголосовал за тебя. Вы, кажется, самый правильный ответ здесь.
pipTheGeek
12
Поведение в C ++ заключается в том, что если тип имеет конструктор по умолчанию, то он используется, когда такой объект создается без явного конструктора. Это можно было бы использовать в C # для инициализации m2 с помощью конструктора по умолчанию, поэтому этот ответ бесполезен.
Мотти
3
onester: если вы не хотите, чтобы структуры вызывали свой собственный конструктор при объявлении, то не определяйте такой конструктор по умолчанию! :) вот что говорит Мотти
Стефан Монов
8
@Tarik. Я не согласен. Напротив, конструктор без параметров имеет смысл: если я хочу создать структуру "Matrix", в которой в качестве значения по умолчанию всегда используется единичная матрица, как вы могли бы сделать это другими способами?
Эло
1
Я не уверен, что полностью согласен с «Действительно, м2 вполне можно использовать с удовольствием» . Возможно, это было правдой в предыдущем C #, но это ошибка компилятора, чтобы объявить структуру, а не newЭто, а затем попытаться использовать ее члены
Caius Jard
18

Хотя CLR это позволяет, C # не позволяет структурам иметь конструктор по умолчанию без параметров. Причина в том, что для типа значения компиляторы по умолчанию не генерируют конструктор по умолчанию и не генерируют вызов конструктора по умолчанию. Таким образом, даже если вы определили конструктор по умолчанию, он не будет вызван, и это вас только запутает.

Чтобы избежать таких проблем, компилятор C # запрещает определение конструктора по умолчанию пользователем. И поскольку он не генерирует конструктор по умолчанию, вы не можете инициализировать поля при их определении.

Или большая причина в том, что структура является типом значения, а типы значений инициализируются значением по умолчанию, а конструктор используется для инициализации.

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

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

Конструктор по умолчанию (без параметров) для структуры может устанавливать значения, отличные от состояния с нулевым состоянием, что может быть неожиданным поведением. Поэтому среда выполнения .NET запрещает конструкторы по умолчанию для структуры.

Adiii
источник
Этот ответ, безусловно, лучший. В конце концов, весь смысл ограничения состоит в том, чтобы избежать неожиданностей, например, MyStruct s;не вызывая предоставленный вами конструктор по умолчанию.
talles
1
Спасибо за объяснение. Таким образом, это только недостаток компилятора, который должен быть исправлен, нет теоретически веской причины запрещать конструкторы без параметров (как только они могут быть ограничены только доступом к свойствам).
Эло
16

Вы можете создать статическое свойство, которое инициализирует и возвращает «рациональное» число по умолчанию:

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

И используйте это как:

var rat = Rational.One;
AustinWBryan
источник
24
В этом случае Rational.Zeroможет быть немного менее запутанным.
Кевин
13

Краткое объяснение:

В C ++ struct и class были просто двумя сторонами одной медали. Единственная реальная разница в том, что по умолчанию один был общедоступным, а другой - частным.

В .NET существует гораздо большая разница между структурой и классом. Главное, что struct предоставляет семантику типа-значения, а класс - семантику ссылочного типа. Когда вы начинаете думать о последствиях этого изменения, другие изменения начинают приобретать больше смысла, включая поведение конструктора, которое вы описываете.

Джоэл Коухорн
источник
7
Вы должны быть немного более четкими о том, как это подразумевается под разделением значения и ссылочного типа, я не понимаю ...
Motti
Типы значений имеют значение по умолчанию - они не равны NULL, даже если вы не определяете конструктор. Хотя на первый взгляд это не исключает также определения конструктора по умолчанию, фреймворк использует эту функцию внутри, чтобы сделать определенные предположения о структурах.
Джоэл Коухорн
@annakata: Другие конструкторы, вероятно, полезны в некоторых сценариях с использованием Reflection. Кроме того, если бы дженерики когда-либо улучшались, чтобы разрешить параметризованное «новое» ограничение, было бы полезно иметь структуры, которые могли бы им соответствовать.
Суперкат
@annakata Я полагаю, это потому, что в C # есть особые строгие требования, которые newдействительно должны быть написаны для вызова конструктора. В C ++ конструкторы вызываются скрытыми способами, при объявлении или создании экземпляров массивов. В C # либо все указатели, поэтому начинайте с нуля, либо это структура и должны начинаться с чего-то, но когда вы не можете написать new... (например, массив init), это нарушит строгое правило C #.
v.oddou
3

Я не видел эквивалента запоздалого решения, которое я собираюсь дать, так что вот оно.

используйте смещения для перемещения значений по умолчанию 0 в любое значение, которое вам нравится. здесь свойства должны использоваться вместо прямого доступа к полям. (возможно, с возможной функцией c # 7 вы лучше определите поля области свойств, чтобы они оставались защищенными от прямого доступа в коде.)

Это решение работает для простых структур только с типами значений (без типа 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 # вы можете использовать try / catch / finally для обработки ошибок, когда конкретный тип данных не инициализируется конструктором по умолчанию Point2D (). Я думаю, что это неуловимо, как решение некоторых людей на этот ответ. Именно поэтому я и добавляю свои. Использование функциональности get и set в C # позволит вам обойти этот конструктор по умолчанию без смысла и попытаться поймать то, что вы не инициализировали. Для меня это работает нормально, для кого-то еще вы можете добавить некоторые операторы if. Таким образом, в случае, если вы хотите установить Numerator / Denominator, этот код может помочь. Я просто хотел бы повторить, что это решение не выглядит красиво, возможно, работает еще хуже с точки зрения эффективности, но, для кого-то из более старой версии 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;
    }
}
G1xb17
источник
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
Моти