Почему я не могу определить конструктор по умолчанию для структуры в .NET?

В .NET тип значения (С# 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;
    }
}

Используя текущую версию С#, по умолчанию Rational является 0/0, что не так круто.

PS: параметры по умолчанию помогут решить эту проблему для С# 4.0 или будет вызываться конструктор по умолчанию CLR?


Jon Skeet ответил:

Чтобы использовать ваш пример, что бы вы хотели сделать, когда кто-то сделал:

 Rational[] fractions = new Rational[1000];

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

Конечно, это должно, поэтому я написал конструктор по умолчанию в первую очередь. CLR должен использовать конструктор обнуления по умолчанию, если не определен явный конструктор по умолчанию; таким образом вы платите только за то, что используете. Тогда, если мне нужен контейнер из 1000 нестандартных Rational (и вы хотите оптимизировать 1000 конструкций), я буду использовать List<Rational>, а не массив.

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

Ответ 1

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


EDIT: я отредактировал ответ ниже из-за понимания Grauenwolf в CLR.

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

MyStruct[] foo = new MyStruct[1000];

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

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

EDIT: чтобы использовать ваш пример, что бы вы хотели сделать, когда кто-то сделал:

Rational[] fractions = new Rational[1000];

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

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

EDIT: (Отвечая на несколько вопросов) Конструктор без параметров не создается компилятором. Типы значений не должны иметь конструкторов в отношении CLR, хотя, оказывается, это можно, если вы напишете его в IL. Когда вы пишете "new Guid()" в С#, который испускает другой IL, что вы получаете, если вы вызываете обычный конструктор. См. этот вопрос SO для получения более подробной информации об этом аспекте.

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

Ответ 2

Структура - это тип значения, а тип значения должен иметь значение по умолчанию, как только оно объявлено.

MyClass m;
MyStruct m2;

Если вы объявляете два поля, как указано выше, без создания экземпляра, то отломите отладчик, m будет пустым, но m2 не будет. Учитывая это, безпараллельный конструктор не имеет смысла, ведь любой конструктор в структуре имеет значение присваивания, сама вещь уже существует, просто объявляя его. Действительно, в приведенном выше примере вполне можно было бы использовать m2, и его методы назывались, если они есть, и поля и свойства обрабатывались!

Ответ 3

Более короткое объяснение:

В С++ структура и класс были всего лишь двумя сторонами одной и той же монеты. Единственное реальное отличие состоит в том, что один был общедоступным по умолчанию, а другой был закрытым.

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

Ответ 4

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

public static Rational One { get { return new Rational(0, 1); } }

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

var rat = Rational.One;

Ответ 5

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

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

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

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

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

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

Ответ 6

Просто особый случай. Если вы видите числитель 0 и знаменатель 0, сделайте вид, что он имеет значения, которые вы действительно хотите.

Ответ 7

Вы не можете определить конструктор по умолчанию, потому что используете С#.

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

Ответ 8

Здесь мое решение дилеммы конструктора no default. Я знаю, что это последнее решение, но я думаю, стоит отметить, что это решение.

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

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;
    }
}

Ответ 9

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

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

Это решение работает для простых структур с только значениями типов (нет типа ref или nullable struct).

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;
    }
}

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