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

В.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> а не массив.

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

14 ответов

Решение

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


РЕДАКТИРОВАТЬ: я отредактировал ответ ниже из-за понимания Grauenwolf в 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 по сравнению с тем, что вы получаете, если вы вызываете нормальный конструктор. Посмотрите этот вопрос SO, чтобы узнать больше об этом.

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

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

MyClass m;
MyStruct m2;

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

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

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

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

var rat = Rational.One;

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

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

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

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

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

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

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

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

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

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

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

используйте смещения для перемещения значений по умолчанию 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;
    }
}

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

Я использую оператор объединения с нулевым значением (??) в сочетании с таким полем поддержки:

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

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

Надеюсь это поможет;)

Примечание: назначение объединения null в настоящее время является предложением функции для C# 8.0.

Я нашел простое решение для этого:

      struct Data
    {
        public int Point { get; set; }
        public HazardMap Map { get; set; }
        public Data Initialize()
        {
            Point = 1; //set anything you want as default
            Map = new HazardMap();
            return this;
        }
    }

В коде просто выполните:

      Data input = new Data().Initialize();

Начиная с C# 10, вы можете определить конструктор без параметров дляstruct.

Например:

      struct S0 { }                   // ok
struct S1 { public S1() { } }   // ok
struct S2 { internal S2() { } } // error: parameterless constructor must be 'public'

Для получения дополнительной информации вы можете посетить эту ссылку: Конструкторы структур без параметров.

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

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

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

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;
    }
}
public struct Rational 
{
    private long numerator;
    private long denominator;

    public Rational(long num = 0, long denom = 1)   // This is allowed!!!
    {
        numerator   = num;
        denominator = denom;
    }
}
Другие вопросы по тегам