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

在. net中,值类型(c# struct)不能有不带形参的构造函数。根据这篇文章,这是CLI规范强制要求的。实际情况是,对于每一个值类型都创建一个默认构造函数(由编译器创建?),该构造函数将所有成员初始化为0(或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定义的默认构造函数将被调用?


Jon双向飞碟回答:

用你的例子来说,如果有人这么做了,你希望发生什么:

 Rational[] fractions = new Rational[1000];

它应该遍历构造函数1000次吗?

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

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

136098 次浏览

下面的答案是在c# 6之前很长时间写的,c# 6计划引入在结构中声明无参数构造函数的能力-但它们仍然不会在所有情况下被调用(例如用于数组创建)(在此特性的最后没有加入到c# 6中)。


编辑:鉴于Grauenwolf对CLR的深刻见解,我编辑了下面的答案。

CLR允许值类型具有无参数的构造函数,但c#不允许。我相信这是因为它会引入一种期望,即在构造函数没有调用时调用它。例如,考虑这个:

MyStruct[] foo = new MyStruct[1000];

CLR能够通过分配适当的内存并将其全部归零来非常有效地做到这一点。如果它必须运行MyStruct构造函数1000次,那么效率将大大降低。(事实上,它不会——如果你有一个无参数的构造函数,当你创建一个数组,或当你有一个未初始化的实例变量时,它不会运行。)

c#的基本规则是“任何类型的默认值都不能依赖于任何初始化”。现在它们可以允许定义无参数构造函数,但不要求在所有情况下都执行该构造函数——但这将导致更多的混乱。(至少我是这么认为的。)

编辑:用你的例子来说,如果有人这样做了,你希望发生什么:

Rational[] fractions = new Rational[1000];

它应该遍历构造函数1000次吗?

  • 否则,我们就会得到1000个无效理性
  • 如果是这样,那么如果我们要用实值填充数组,我们就可能浪费了大量的工作。

EDIT:(回答了更多的问题)无参数构造函数不是由编译器创建的。就CLR而言,值类型不必有构造函数——尽管如果你用IL写它,结果是可以。当你用c#写“new Guid()”时,如果你调用普通构造函数,它会产生不同的IL。有关这方面的更多信息,请参见这个SO问题

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

短的说明:

在c++中,结构体和类只是一枚硬币的两面。唯一真正的区别是,一个默认是公开的,另一个是私有的。

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

struct是一种值类型,值类型在声明后必须具有默认值。

MyClass m;
MyStruct m2;

如果你像上面那样声明了两个字段而没有实例化它们,那么就会中断调试器,m将为空,而m2将不是。鉴于此,无参数构造函数将没有意义,事实上,结构上的所有构造函数都是赋值,只是声明了它本身就已经存在了。实际上,m2可以在上面的例子中非常愉快地使用,并调用它的方法(如果有的话),并操纵它的字段和属性!

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

因为使用的是c#,所以不能定义默认构造函数。

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

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

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

像这样使用它:

var rat = Rational.One;

下面是我对没有默认构造函数困境的解决方案。我知道这是一个很晚的解决方案,但我认为值得注意的是,这是一个解决方案。

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;在c#中,你可以有一个try/catch/finally,用于处理特定数据类型没有被默认构造函数Point2D()初始化的错误。我想对于一些人来说这是一个难以捉摸的答案。这就是为什么我要加上我的。在c#中使用getter和setter功能将允许你绕过这个默认的无意义构造函数,并在你没有初始化的地方放置一个try catch。对我来说,这很好,对其他人来说,你可能想要添加一些if语句。所以,在你想要分子/分母设置的情况下,这段代码可能会有帮助。我只是想重申,这个解决方案看起来不太好,从效率的角度来看可能更糟糕,但是,对于来自旧版本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;
}
}

我还没有看到和后面的解等价的东西,我要给出的就是这个。

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

正如我所说的,这个技巧可能不是在所有情况下都有效,即使struct只有值字段,也只有你知道它在你的情况下是否有效。只是检查。但是你知道大概的意思。

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


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

我使用的是空合并运算符(??)和一个支持字段的组合,就像这样:

public struct SomeStruct {
private SomeRefType m_MyRefVariableBackingField;


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

希望这对你有所帮助;)

注意:空合并赋值目前是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();