为什么我不能在.NET中为结构定义默认构造函数?

时间:2008-12-02 12:39:25

标签: c# .net struct

在.NET中,值类型(C#struct)不能包含没有参数的构造函数。根据{{​​3}},这是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定义的默认构造函数?


this post回答:

  

要使用您的示例,当有人执行时您希望发生什么:

 Rational[] fractions = new Rational[1000];
     

它应该通过你的构造函数运行1000次吗?

当然应该,这就是我首先编写默认构造函数的原因。当没有定义显式默认构造函数时,CLR应该使用默认清零构造函数;这样你只需支付你使用的费用。然后,如果我想要一个1000个非默认Rational的容器(并希望优化1000个结构),我将使用List<Rational>而不是数组。

在我看来,这个原因并不足以阻止默认构造函数的定义。

11 个答案:

答案 0 :(得分:177)

注意:下面的答案是在C#6之前很长一段时间写的,它计划引入在结构中声明无参数构造函数的能力 - 但它们仍然不会被调用在所有情况下(例如阵列创建)(最后这个特征was not added to C# 6)。


编辑:由于Grauenwolf洞察CLR,我编辑了下面的答案。

CLR允许值类型具有无参数构造函数,但C#不支持。我相信这是因为它会引入一种期望,即构造函数不会被调用。例如,考虑一下:

MyStruct[] foo = new MyStruct[1000];

CLR能够通过分配适当的内存并将其归零来非常有效地完成此任务。如果它必须运行MyStruct构造函数1000次,那将效率低得多。 (事实上​​,它没有 - 如果你有一个无参数构造函数,它就不会在你创建一个数组时运行,或者你有一个未初始化的实例变量。)

C#中的基本规则是“任何类型的默认值都不能依赖于任何初始化”。现在他们可以允许定义无参数构造函数,但是不需要在所有情况下执行构造函数 - 但这会导致更多的混淆。 (或至少,所以我相信这个论点。)

编辑:要使用你的例子,当有人做的时候你会想要发生什么:

Rational[] fractions = new Rational[1000];

它应该通过你的构造函数运行1000次吗?

  • 如果没有,我们最终会得到1000个无效的理由
  • 如果确实如此,那么如果我们要用真实值填充数组,我们可能会浪费大量的工作。

编辑:(回答更多问题)无参数构造函数不是由编译器创建的。就CLR而言,值类型不必具有构造函数 - 尽管如果您在IL中编写它,可以。当您在C#中编写“new Guid()”时,如果调用普通构造函数,则会发出不同的IL。有关该方面的更多信息,请参阅this SO question

怀疑在框架中没有无参数构造函数的任何值类型。毫无疑问,NDepend可以告诉我,如果我说得很好...... C#禁止它的事实足以让我认为这可能是一个坏主意。

答案 1 :(得分:42)

结构是一种值类型,值类型一旦声明就必须具有默认值。

MyClass m;
MyStruct m2;

如果你声明上面的两个字段而没有实例化,那么打破调试器,m将为null,但m2将不会。鉴于此,无参数构造函数没有任何意义,实际上结构上的所有构造函数都是赋值,事物本身只是通过声明它就已存在。实际上m2可以很高兴地用在上面的例子中,如果有的话,调用它的方法,并操纵它的字段和属性!

答案 2 :(得分:14)

您可以创建一个初始化并返回默认“有理”数字的静态属性:

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

并使用它:

var rat = Rational.One;

答案 3 :(得分:14)

虽然CLR允许它,但C#不允许结构具有默认的无参数构造函数。原因是,对于值类型,编译器默认情况下既不生成默认构造函数,也不生成对默认构造函数的调用。因此,即使您碰巧定义了默认构造函数,也不会调用它,这只会让您感到困惑。

为避免此类问题,C#编译器不允许用户定义默认构造函数。并且因为它不生成默认构造函数,所以在定义字段时无法初始化字段。

或者最大的原因是结构是值类型,值类型由默认值初始化,构造函数用于初始化。

您不必使用new关键字来实例化您的结构。它反过来像一个int;你可以直接访问它。

结构不能包含显式无参数构造函数。 Struct成员会自动初始化为默认值。

结构的默认(无参数)构造函数可以设置与全零状态不同的值,这将是意外行为。因此,.NET运行时禁止结构的默认构造函数。

答案 4 :(得分:13)

更短的解释:

在C ++中,struct和class只是同一枚硬币的两面。唯一真正的区别是,一个是默认公开,另一个是私人。

.NET中,结构和类之间存在更大的差异。主要的是struct提供了值类型语义,而class提供了引用类型语义。当您开始考虑此更改的含义时,其他更改也会开始变得更有意义,包括您描述的构造函数行为。

答案 5 :(得分:3)

只是特例。如果你看到0的分子和0的分母,假装它有你真正想要的值。

答案 6 :(得分:1)

您无法定义默认构造函数,因为您使用的是C#。

结构可以在.NET中有默认的构造函数,但我不知道任何支持它的特定语言。

答案 7 :(得分: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()未初始化特定数据类型的错误。我想这是一个难以捉摸的解决方案。这主要是为什么我加入我的。使用C#中的getter和setter功能将允许您绕过此默认构造函数,并尝试捕获您未初始化的内容。对我来说这很好,对于其他人你可能想要添加一些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;
    }
}

答案 8 :(得分:1)

我没有看到相当于我将要给出的后期解决方案,所以在这里。

使用偏移将值从默认值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;
    }
}

这是不同的than这个答案,这种方法不是特别的套管,但它的使用偏移量适用于所有范围。

将枚举作为字段的示例。

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

正如我所说,这个技巧在所有情况下可能都不起作用,即使struct只有值字段,只有你知道它是否适用于你的情况。试试看。但你得到了一般的想法。

答案 9 :(得分:1)

我使用的是null-coalescing operator (??)和这样的后备字段:

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

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

希望这会有所帮助;)

注意:null coalescing assignment当前是C#8.0的功能建议。

答案 10 :(得分:0)

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

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