在.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>
而不是数组。
在我看来,这个原因并不足以阻止默认构造函数的定义。
答案 0 :(得分:177)
注意:下面的答案是在C#6之前很长一段时间写的,它计划引入在结构中声明无参数构造函数的能力 - 但它们仍然不会被调用在所有情况下(例如阵列创建)(最后这个特征was not added to C# 6)。
CLR允许值类型具有无参数构造函数,但C#不支持。我相信这是因为它会引入一种期望,即构造函数不会被调用。例如,考虑一下:
MyStruct[] foo = new MyStruct[1000];
CLR能够通过分配适当的内存并将其归零来非常有效地完成此任务。如果它必须运行MyStruct构造函数1000次,那将效率低得多。 (事实上,它没有 - 如果你做有一个无参数构造函数,它就不会在你创建一个数组时运行,或者你有一个未初始化的实例变量。)
C#中的基本规则是“任何类型的默认值都不能依赖于任何初始化”。现在他们可以允许定义无参数构造函数,但是不需要在所有情况下执行构造函数 - 但这会导致更多的混淆。 (或至少,所以我相信这个论点。)
编辑:要使用你的例子,当有人做的时候你会想要发生什么:
Rational[] fractions = new Rational[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;
}
}